Compare commits

..
Author SHA1 Message Date
adminandClaude Opus 4.5 5cb23464e5 Fix nginx deploy: use SCP to copy config files
The production server doesn't have a git repo, so we can't use git
commands to update files. Instead, use appleboy/scp-action to copy
nginx.conf and docker-compose.prod.yml from the runner to the server,
then restart nginx.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 14:02:15 -08:00
3e1919a715 Update auth workflow to deploy nginx config changes (#5412)
- Add nginx/nginx.conf and docker-compose.prod.yml to trigger paths
- Pull latest nginx config and restart nginx during deployment

This ensures nginx config changes (like OAuth callback routes) are
automatically deployed when merged to main.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 13:56:25 -08:00
699bcc684a Store Scala types in ClientTextStore instead of proto (#5407)
* Store Scala types in ClientTextStore instead of proto

This change updates the ClientTextStore layer to store the native Scala
type `GeneratedTextRequestT` in memory instead of the proto type
`GeneratedTextRequest`. Proto conversion now only happens at persistence
boundaries (when saving to/loading from disk).

Key changes:
- ClientText.scala: Changed llmRequest field to use GeneratedTextRequestT
- ClientTextStore.scala: Updated interface to accept Scala types
- ClientTextStoreImpl.scala: Added toProto/fromProto conversion at persistence
- GameController.scala: Removed proto conversion in allNewLlmRequests
- UnrequestedTextHandler.scala: Uses Scala types internally, converts to proto
  only when passing to LlmResolver
- GamesManager.scala: Uses Scala types directly when creating UnrequestedClientText
- Updated BUILD files for new dependencies and visibility

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix tests for ClientTextStore Scala types migration

- Update UnrequestedTextHandlerTest to use Scala types instead of proto
- Add GeneratedTextRequestConverter import for proto conversions in test mocks
- Change test requests from FixedHeroName to LlmRequestT.DivineMessage to properly test LLM resolution path
- Add visibility for test packages in BUILD files
- Remove unused proto dependency from generator_utilities_test

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 13:54:50 -08:00
42fd9c5853 Fix flush marker wait: skip for old instance being replaced (#5413)
During blue-green deployment, the old (active) instance should not wait
for the flush marker - it would be waiting for itself to stop, which
causes a 30s timeout warning.

Now compares deployment timestamp to instance start time:
- If deployment started AFTER instance: skip wait (we're the old instance)
- If deployment started BEFORE instance: wait (we're the new instance)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 13:53:59 -08:00
01aa3dbace Add /oauth/steam/callback to nginx config (#5410)
Steam OpenID 2.0 redirects back to /oauth/steam/callback after
authentication, but nginx was not configured to proxy this path to the
auth service, resulting in 404 errors.

Added the location block alongside the existing /oauth/callback and
/oauth/apple/callback blocks.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 13:49:34 -08:00
a7595e81a2 Add all OAuth providers to admin server login page (#5409)
- Add GitHub, Apple, Steam, and Twitch to handleLoginStart switch
- Add sign-in buttons with icons for all providers to login.html
- Add CSS styles for new provider buttons

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 13:40:33 -08:00
6a61634f37 Add Steam and Twitch sign-in buttons to Unity client (#5404)
* Add Steam and Twitch sign-in buttons to Unity client

- Add steamLoginButton and twitchLoginButton fields
- Add steamProviderIcon and twitchProviderIcon for stored accounts
- Wire up button click handlers in SetupOAuthUI()
- Add icon mapping for stored account display

Note: Unity scene needs Steam/Twitch button GameObjects and icons wired up.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Add Steam and Twitch OAuth button assets

- Add Steam and Twitch (glitch) logo icons for OAuth buttons
- Consolidate Discord icon to OAuth Buttons folder
- Remove unused Google OAuth button variants
- Update Gameplay.unity with new button references

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 13:34:11 -08:00
cfa552270a Add Twitch secrets to auth service deployment workflow (#5408)
The TWITCH_CLIENT_ID and TWITCH_CLIENT_SECRET were added to
docker-compose.prod.yml but not to the GitHub Actions workflow
that deploys the auth service.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 13:32:03 -08:00
09d560208e Add game state guidance tutorials (#5405)
* Add game state guidance tutorials

Add four contextual tutorials that trigger based on game state analysis:

1. guidance_loyalty_danger - November warning when heroes have low loyalty
   Suggests using Feast or Give Gift to boost loyalty before year end

2. guidance_neighbor_danger - Alert when province borders hostile faction
   Suggests Organizing Troops or seeking Diplomacy

3. guidance_recruit_heroes - When stable province has recruitable heroes
   Suggests using Travel, Divine, and Recruit commands

4. guidance_expand - When player has stable province but only one territory
   Suggests using March to claim new provinces

These provide strategic guidance for new players based on their game state.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix compilation errors in guidance trigger checks

- MarchAvailableCommand.OneProvinceCommands to access destination provinces
- FactionRelationshipView.TargetFactionId instead of FactionId
- province.FullInfo.Gold instead of model.CurrentGold
- Net.Eagle0.Eagle.Common.RecruitmentStatus instead of Views namespace

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Use Any() instead of ToList().Count for efficiency

Any() short-circuits on first match rather than materializing entire list.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Simplify recruit heroes gold check

Just check for 100 gold - January was when this would naturally be true,
not an alternative condition.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 13:18:17 -08:00
2188834ddf Fix auth deploy: remove SHARDOK_ADDRESS validation from docker-compose (#5406)
docker-compose validates the entire file even when deploying just auth.
Remove the :? validation since docker_build.yml already validates
SHARDOK_ADDRESS explicitly before deploying Eagle.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 12:45:18 -08:00
eafe5d73ee Add comment about dynamically registered hero row targets (#5402)
Document that WarlordRow, VassalRow1, VassalRow2 are registered dynamically
by HeroesAndBattalionsPanelController when hero rows are created from prefabs.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 12:27:54 -08:00
f16257591b Add Steam and Twitch OAuth support (#5403)
- Steam: OpenID 2.0 authentication (no API key required)
- Twitch: Standard OAuth 2.0 with user:read:email scope
- Add TWITCH_CLIENT_ID and TWITCH_CLIENT_SECRET to deployment config
- Add Steam/Twitch buttons to invitation landing page

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 12:26:53 -08:00
d259282213 Add fromProto converters for GeneratedTextRequest types (#5401)
Add fromProto methods to LlmRequestConverter and GeneratedTextRequestConverter
to enable converting proto types back to Scala types. This is preparation for
storing Scala types in-memory in ClientTextStore, with proto conversion only
happening at persistence boundaries.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 11:22:27 -08:00
de5e6cc65e Fix duplicate stored account buttons and add delete button (#5400)
- Clear all children of storedAccountsContainer and disable them
  immediately before destroying (fixes visual duplication from
  deferred Destroy)
- Add delete button to StoredAccountButton to remove saved accounts
- Wire up delete button to OAuthManager.RemoveStoredAccount

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 11:21:30 -08:00
7da2fb7913 Fix Divine tutorial text to describe actual functionality (#5399)
Divine reveals what you must do to impress a hero and recruit them,
not their stats/abilities.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 10:00:53 -08:00
a485772e66 Consolidate secrets handling: Eagle uses public key only for JWT validation (#5398)
Security improvement: Eagle no longer has access to JWT private keys.

- JwtService.scala: Simplified to validation-only (removed signing methods)
- Loads public key from /etc/eagle0/keys/public.pem (shared volume from auth)
- Falls back to extracting public key from JWT_PRIVATE_KEY for backward compat
- AuthServiceImpl.scala: Local fallbacks throw UNIMPLEMENTED (signing in Go auth)
- docker-compose.prod.yml: Removed JWT_PRIVATE_KEY from Eagle, volume now read-only

Auth service unchanged - still bootstraps PEM files from JWT_PRIVATE_KEY env var.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 09:44:03 -08:00
e0212fa7a2 Fix unit placement deselection during setup phase (#5397)
Clicking a selected unit during setup would trigger a placement action
instead of deselecting the unit, because HasPlacementAction returned
true for the unit's own tile. Now the placement check excludes the
currently selected tile, allowing the deselection logic to run.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 09:32:23 -08:00
6e76ff0fbc Use commit-count based versioning for Mac builds (#5392)
Change version scheme from git describe (which picked up unrelated tags
like busybox-1.35.0) to commit-count based versions like 1.0.9548.

This gives automatic, always-incrementing version numbers that Sparkle
can properly compare for updates.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 09:17:59 -08:00
a37e0af028 Make DiplomacyResolutionLlmRequestGenerator protoless (#5396) (#5396)
* Update DEPROTO_PLAN.md with recent completions

- Mark Phase 9 (Utilities) as complete - MapGenerator now protoless
- Mark Phase 10 (History APIs) as complete - PersistedHistory accepts Scala types
- Add PRs #5373, #5378, #5381, #5390 to recent completions
- Update proto import inventory with current counts
- Update success criteria checklist

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Make DiplomacyResolutionLlmRequestGenerator protoless

Replace proto types with Scala equivalents:
- DiplomacyOfferStatus -> Status
- GeneratedTextRequest -> LlmRequestT
- Proto message types -> LlmRequestT enum cases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 09:05:37 -08:00
d6704413d3 Fix Sparkle codesigning for XPC services (#5393)
- Use ditto instead of cp -R to copy Sparkle.framework (preserves bundle structure)
- Skip individual signing of Sparkle's internal XPC services, apps, and executables
- Use --deep flag when signing Sparkle.framework to handle its internal components
- Verify cached Sparkle.framework has proper symlink structure, re-download if corrupted

Fixes "bundle format unrecognized" error during codesigning.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 09:04:39 -08:00
be59f9775f Add deployment validation for env vars and Shardok connectivity (#5394)
After investigating a bad deployment where Eagle connected to "shardok"
instead of the actual Hetzner address, found the root cause: GitHub
Actions secrets can occasionally fail to load, causing the heredoc to
expand with default values instead of the actual secrets.

Changes:
- Add validation at deploy start to check critical env vars are present
- Add network connectivity check to Shardok before blue-green deployment
  (blocks deployment if Shardok is unreachable)
- Remove the misleading "shardok:40042" default - now SHARDOK_ADDRESS
  must be explicitly set, making configuration errors immediately obvious
- docker-compose.prod.yml now uses :? syntax to require SHARDOK_ADDRESS

The env var validation will abort deployment early with a clear error
message if secrets failed to load, rather than deploying with broken
config.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 09:00:13 -08:00
bebe050352 Add Scala ActionResultT overload to PersistedHistory.apply (#5390)
Add a new apply overload that takes Vector[ActionResultT] directly,
avoiding proto conversion at the call site. The conversion to proto
for storage still happens internally, but callers like NewGameCreation
can now pass Scala types directly.

Removed the unused proto-based apply overload since all callers that
need proto types use the case class constructor directly.

This makes NewGameCreation fully protoless.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 08:36:52 -08:00
77d6fc571b Fix logout button to return to connection panel instead of quitting (#5391)
The logout button was wired to QuitButtonClicked in the Unity scene,
causing the app to exit instead of returning to the connection panel.

- Change scene to wire logout button to OnLogoutClicked
- Make OnLogoutClicked public so it's visible in Unity inspector
- Remove redundant programmatic listener setup

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-17 08:32:46 -08:00
adminandGitHub 65e0ad93cc center the text in the rows in the lobby (#5389) 2026-01-16 17:05:35 -08:00
adminandGitHub 275cb0c563 Enable the Sign In With Apple button and fix the button layouts (#5388)
* add sign in with apple button (disabled)

* and the github icon

* add the apple button

* better stored account button support
2026-01-16 15:44:17 -08:00
d64f35b18a Add loading indicator when switching environments in lobby (#5365)
* Add loading indicator when switching environments in lobby

Shows "Switching environment..." text while waiting for the server
to respond after changing the environment dropdown. Text is cleared
when the lobby data arrives and populates the game lists.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* add lobby connection status and fix 16:10 aspect ratio

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 15:04:07 -08:00
adminandGitHub ac0c03af9b bad cast (#5387) 2026-01-16 08:40:28 -08:00
adminandGitHub 6b0d58a9e7 map and continue (#5386) 2026-01-16 08:25:01 -08:00
adminandGitHub fb56cf0029 wrong match (#5385) 2026-01-16 07:49:07 -08:00
adminandGitHub 70a5a517af more debugging (#5384) 2026-01-16 07:38:06 -08:00
adminandGitHub 6de07a70dc full stack trace in lockedSendLobbyUpdate (#5383) 2026-01-16 07:30:02 -08:00
8e64af7233 Make NewGameCreation protoless (#5381)
* Make NewGameCreation protoless

Convert NewGameCreation to build Scala ActionResultC internally instead of
protobuf ActionResult. Proto conversion now only happens at the boundary
when storing to PersistedHistory.

Changes:
- NewGameCreation.scala: Use ActionResultC, Scala RoundPhase, ChronicleEntry,
  FactionC, FactionRelationship instead of proto equivalents
- StartGameActionResultUtils.scala: Change from proto ActionResult to
  ActionResultC throughout
- GameParametersUtils.scala: Add provinceWithOverrideScala method
- Add withId methods to HeroT/HeroC and BattalionT/BattalionC traits
- Update BUILD.bazel files with visibility for new_game_creation package
- Update test to use Scala types

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* empty check

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 07:20:41 -08:00
ca91d3193e Fix missing newProvinces in ActionResultProtoConverter.toProto (#5382)
The toProto method was not serializing newProvinces, causing provinces
to be lost when persisting game state. This led to stack overflow during
game creation as the phase advancer would loop infinitely with no provinces.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 07:18:52 -08:00
1de12597d7 Export proto types from converters for caller visibility (#5380)
* Export proto types from converters for caller visibility

ActionResultProtoConverter and GameStateConverter return proto types,
so those proto dependencies need to be exported for callers to use
the return values.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Fix missing proto exports and ServerSetupHelpers warnings

- Export shardok_battle_scala_proto from ShardokBattleConverter
- Export game_state_scala_proto and shardok_battle_scala_proto from GamesManager
- Remove unused ManagedChannelBuilder import from ServerSetupHelpers
- Remove unused default parameter from private newShardokInterface method

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 14:17:08 -08:00
36c09cae77 Fix unused parameter warnings in CustomBattleManager (#5379)
- Add @unused annotation to parameters that are intentionally unused
  (userName and name parameters that need future verification)
- Add missing game_state_scala_proto dependency to BUILD.bazel
- Add CustomBattleManagerTest with tests for CustomBattleGameController

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 13:54:53 -08:00
83d159823a Remove proto ActionResult from Engine layer (#5378)
- Change EngineAndResults.results to return Vector[ActionResultT] (Scala) instead of Vector[ActionResult] (proto)
- Update PostResults to use Scala ActionResultT and ShardokBattle types
- Update GameController methods to use Scala types internally
- Add GeneratedTextRequestConverter for Scala-to-proto conversion when needed for LLM requests
- Add recipientFactionIds field to ClientTextVisibilityExtensionT trait

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 11:32:51 -08:00
5ce88f04d2 Remove Apple Sign-In debug logging (#5377)
Remove verbose debug logs that were added during Apple Sign-In debugging:
- Token exchange client_id/redirect_uri
- id_token length
- Parsed user info (id/email)

Error logging is retained for troubleshooting.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 11:15:52 -08:00
1bfcb343d9 Fix Apple id_token claims to handle both string and bool types (#5376)
Apple inconsistently returns email_verified and is_private_email as
either boolean (true/false) or string ("true"/"false"). Added custom
AppleBool type that unmarshals both formats.

Also added missing claims from Apple's documentation:
- nonce, nonce_supported, c_hash, auth_time

See: https://developer.apple.com/forums/thread/121411

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 10:36:46 -08:00
88b628b73f Add logging for Apple id_token parsing (#5375)
Log the id_token length and any parse errors to help debug
"Failed to parse user info" errors.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 10:29:51 -08:00
9004e6887a Fix Apple private key parsing for base64-encoded PEM (#5374)
The key is stored as base64-encoded PEM. The previous code base64
decoded it but then tried to use the result directly as key bytes.
The fix: after base64 decoding, PEM decode to extract the actual
key bytes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 10:23:39 -08:00
c62b3d3166 Make MapGenerator return Scala types (#5373)
MapGenerator now returns Scala ProvinceT instead of proto Province.
GameParametersUtils works with Scala types internally, converting to
proto only in NewGameCreation when needed for ActionResult.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 10:08:35 -08:00
6d54312aec Fix auth deployment using wrong image tag (#5372)
docker-compose ignores AUTH_IMAGE env var for unknown reasons.
Fix with multiple approaches:

1. Tag pulled image as :latest locally (belt)
2. Pass AUTH_IMAGE explicitly on command line (suspenders)
3. Add debug output to diagnose .env file issues
4. Verify by comparing image digests, not tags

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 10:00:54 -08:00
c886ddc769 Make BattalionTypeLoader return Scala types (#5371)
BattalionTypeLoader now returns Scala BattalionType instead of proto.
GamesManager and NewGameCreation updated to use Scala types internally,
with conversion to proto only when needed for GameState updates.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 09:57:28 -08:00
f6a7545533 Fully separate auth and eagle deployments (#5370)
- Exclude auth paths from docker_build.yml triggers so eagle builds
  don't trigger when only auth code changes
- Remove `docker compose up -d auth` from docker_build.yml - auth is
  deployed exclusively by auth_build.yml now
- Delete unused deploy/update-env.sh and deploy/env.template

This prevents docker_build.yml from reverting auth to stale images
when it runs after auth_build.yml.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 09:28:46 -08:00
8d3bc914ed Make GameHistory APIs return Scala types (#5367)
* Make GameHistory APIs return Scala types

The GameHistory trait now returns ActionResultWithResultingState (pure
Scala) instead of ActionWithResultingState (proto-based). Internal
storage in InMemoryHistory and PersistedHistory still uses proto types
for file persistence, but the public API is now protoless.

Changes:
- GameHistory.all, since, sinceDate, last, recentResultsForRound now
  return Vector[ActionResultWithResultingState]
- withNewResults accepts Scala types and converts to proto for storage
- Added toScala() helper in history implementations
- Updated callers: EngineImpl, EagleServiceImpl, ChronicleEventGenerator,
  ActionResultFilter, GamesManager
- SavedGameUtils uses internal ActionWithProtoState for proto reflection
- Fixed Option[Date] comparisons with .forall() pattern

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Delete unused SavedGameUtils

This file was a standalone debugging utility that was never integrated
into the build (BUILD target was commented out). Removing it as cleanup.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 08:45:09 -08:00
844d7d8e50 Fix auth service deployment not updating container (#5368)
The crane+docker-load approach was causing image tagging issues where the
loaded image didn't have the expected registry tag, causing docker-compose
to not find the correct image.

Changes:
- Use simple `docker pull` instead of crane pull + docker load (since
  deploy server is already logged into the registry)
- Add verification that the running container is using the expected image
- Fail the workflow if container is running the wrong image

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 08:28:47 -08:00
cae2d47af0 Add Apple OAuth debugging and GitHub email fetch (#5364)
- Add comprehensive logging to Apple callback handler
- Fetch GitHub email from /user/emails when not in main response

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 07:34:46 -08:00
137 changed files with 5375 additions and 2596 deletions
+42 -6
View File
@@ -11,6 +11,8 @@ on:
- 'src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
- 'ci/BUILD.bazel'
- '.github/workflows/auth_build.yml'
- 'nginx/nginx.conf'
- 'docker-compose.prod.yml'
workflow_dispatch:
inputs:
push_images:
@@ -110,6 +112,8 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_SIGNIN_KEY_ID: ${{ secrets.APPLE_SIGNIN_KEY_ID }}
APPLE_SIGNIN_PRIVATE_KEY: ${{ secrets.APPLE_SIGNIN_PRIVATE_KEY }}
TWITCH_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }}
TWITCH_CLIENT_SECRET: ${{ secrets.TWITCH_CLIENT_SECRET }}
JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}
FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }}
FASTMAIL_FROM_EMAIL: ${{ secrets.FASTMAIL_FROM_EMAIL }}
@@ -118,6 +122,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Copy config files to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.DO_DROPLET_IP }}
username: deploy
key: ${{ secrets.DO_SSH_KEY }}
source: "nginx/nginx.conf,docker-compose.prod.yml"
target: "/opt/eagle0"
strip_components: 0
- name: Deploy auth service to production
uses: appleboy/ssh-action@v1.0.3
with:
@@ -125,7 +139,7 @@ jobs:
username: deploy
key: ${{ secrets.DO_SSH_KEY }}
script_stop: true
envs: AUTH_IMAGE,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GH_OAUTH_CLIENT_ID,GH_OAUTH_CLIENT_SECRET,APPLE_SIGNIN_CLIENT_ID,APPLE_TEAM_ID,APPLE_SIGNIN_KEY_ID,APPLE_SIGNIN_PRIVATE_KEY,JWT_PRIVATE_KEY,FASTMAIL_API_TOKEN,FASTMAIL_FROM_EMAIL,FASTMAIL_FROM_NAME
envs: AUTH_IMAGE,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GH_OAUTH_CLIENT_ID,GH_OAUTH_CLIENT_SECRET,APPLE_SIGNIN_CLIENT_ID,APPLE_TEAM_ID,APPLE_SIGNIN_KEY_ID,APPLE_SIGNIN_PRIVATE_KEY,TWITCH_CLIENT_ID,TWITCH_CLIENT_SECRET,JWT_PRIVATE_KEY,FASTMAIL_API_TOKEN,FASTMAIL_FROM_EMAIL,FASTMAIL_FROM_NAME
script: |
set -x
cd /opt/eagle0
@@ -142,6 +156,8 @@ jobs:
export APPLE_TEAM_ID="${APPLE_TEAM_ID}"
export APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
export APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
export TWITCH_CLIENT_ID="${TWITCH_CLIENT_ID}"
export TWITCH_CLIENT_SECRET="${TWITCH_CLIENT_SECRET}"
export JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
@@ -156,27 +172,47 @@ jobs:
echo "Pulling Auth image..."
docker pull "${AUTH_IMAGE}" || { echo "ERROR: Failed to pull auth image"; exit 1; }
# Only recreate the auth container (not eagle, shardok, etc.)
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate auth
# Tag as :latest locally so any fallback uses correct image
docker tag "${AUTH_IMAGE}" registry.digitalocean.com/eagle0/auth-server:latest
# Debug: check environment and .env file
echo "DEBUG: AUTH_IMAGE=$AUTH_IMAGE"
env | grep AUTH || echo "AUTH_IMAGE not in env output"
if [ -f .env ]; then
echo "DEBUG: .env file contents related to AUTH:"
grep AUTH .env || echo "No AUTH in .env"
fi
# Recreate auth container - pass AUTH_IMAGE explicitly on command line
AUTH_IMAGE="${AUTH_IMAGE}" docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate auth
# Wait for health check
sleep 5
# Verify container is using the correct image
# Note: docker-compose may use :latest tag (which we tagged to the correct image)
echo "=== Verifying auth container image ==="
RUNNING_IMAGE=$(docker inspect auth-server --format '{{.Config.Image}}')
RUNNING_DIGEST=$(docker inspect auth-server --format '{{.Image}}')
EXPECTED_DIGEST=$(docker inspect "${AUTH_IMAGE}" --format '{{.Id}}')
echo "Expected image: ${AUTH_IMAGE}"
echo "Running image: ${RUNNING_IMAGE}"
echo "Expected digest: ${EXPECTED_DIGEST}"
echo "Running digest: ${RUNNING_DIGEST}"
if [ "$RUNNING_IMAGE" != "$AUTH_IMAGE" ]; then
if [ "$RUNNING_DIGEST" != "$EXPECTED_DIGEST" ]; then
echo "ERROR: Container is running wrong image!"
echo "Container details:"
docker inspect auth-server --format '{{.Id}} {{.Config.Image}}'
exit 1
fi
echo "Image digests match - correct image is running"
# Show container status
docker compose -f docker-compose.prod.yml ps auth
# Restart nginx with updated config (copied via SCP step)
echo "=== Restarting nginx ==="
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate nginx
echo "Nginx restarted"
# Cleanup old images
docker image prune -f
+83 -6
View File
@@ -5,9 +5,15 @@ on:
branches: [ "main" ]
paths:
# Note: C++ changes trigger shardok_arm64_build.yml instead
# Note: Auth changes trigger auth_build.yml instead
- 'src/main/go/**'
- '!src/main/go/net/eagle0/authservice/**'
- '!src/main/go/net/eagle0/authcli/**'
- 'src/main/scala/**'
- 'src/main/protobuf/**'
- '!src/main/protobuf/net/eagle0/eagle/api/auth.proto'
- '!src/main/protobuf/net/eagle0/eagle/api/admin/**'
- '!src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
- 'src/main/resources/**'
- 'ci/BUILD.bazel'
- 'MODULE.bazel'
@@ -198,7 +204,7 @@ jobs:
# Create directory structure on remote
ssh -i ~/.ssh/deploy_key deploy@"$DO_DROPLET_IP" bash -s << 'SETUP_DIRS'
set -e
mkdir -p /opt/eagle0/scripts/bin /opt/eagle0/nginx /opt/eagle0/deploy
mkdir -p /opt/eagle0/scripts/bin /opt/eagle0/nginx
rm -f /opt/eagle0/scripts/bin/warmup
SETUP_DIRS
@@ -207,7 +213,6 @@ jobs:
scp -i ~/.ssh/deploy_key nginx/nginx.conf deploy@"$DO_DROPLET_IP":/opt/eagle0/nginx/
scp -i ~/.ssh/deploy_key scripts/deploy-blue-green.sh scripts/warmup-eagle.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/
scp -i ~/.ssh/deploy_key scripts/bin/warmup deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/bin/
scp -i ~/.ssh/deploy_key deploy/env.template deploy/update-env.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/deploy/
- name: Deploy to production droplet
run: |
@@ -215,6 +220,55 @@ jobs:
set -ex
cd /opt/eagle0
# =================================================================
# CRITICAL: Validate environment variables before proceeding
# This catches GitHub Actions secret store hiccups early
# =================================================================
validate_env() {
local var_name="\$1"
local var_value="\$2"
local default_value="\${3:-}"
if [ -z "\${var_value}" ]; then
echo "ERROR: \${var_name} is empty. GitHub Actions secrets may have failed to load."
echo "Please retry the workflow."
return 1
fi
# Check if we got a default value instead of the real secret
if [ -n "\${default_value}" ] && [ "\${var_value}" = "\${default_value}" ]; then
echo "ERROR: \${var_name} has default value '\${default_value}' instead of the actual secret."
echo "This indicates GitHub Actions secrets failed to load. Please retry the workflow."
return 1
fi
return 0
}
echo "Validating critical environment variables..."
# These are the raw values from GitHub Actions (before export)
# We check them before exporting to catch issues early
VALIDATION_FAILED=0
validate_env "SHARDOK_ADDRESS" "${SHARDOK_ADDRESS}" "" || VALIDATION_FAILED=1
validate_env "EAGLE_IMAGE" "${EAGLE_IMAGE}" "" || VALIDATION_FAILED=1
validate_env "JWT_PRIVATE_KEY" "${JWT_PRIVATE_KEY}" "" || VALIDATION_FAILED=1
validate_env "DO_REGISTRY_TOKEN" "${DO_REGISTRY_TOKEN}" "" || VALIDATION_FAILED=1
if [ "\${VALIDATION_FAILED}" -eq 1 ]; then
echo ""
echo "========================================="
echo "DEPLOYMENT ABORTED: Missing critical secrets"
echo "This is likely a transient GitHub Actions issue."
echo "Please retry the workflow."
echo "========================================="
exit 1
fi
echo "All critical environment variables validated successfully."
# =================================================================
# Export environment variables for docker compose
# These are passed via heredoc and exported so child processes (docker compose) can access them
export EAGLE_IMAGE="${EAGLE_IMAGE}"
@@ -236,7 +290,7 @@ jobs:
export APPLE_TEAM_ID="${APPLE_TEAM_ID}"
export APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
export APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
export SHARDOK_ADDRESS="${SHARDOK_ADDRESS:-shardok:40042}"
export SHARDOK_ADDRESS="${SHARDOK_ADDRESS}"
export SHARDOK_AUTH_TOKEN="${SHARDOK_AUTH_TOKEN}"
export SENTRY_DSN="${SENTRY_DSN}"
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
@@ -276,20 +330,43 @@ jobs:
echo "All images pulled successfully"
# =================================================================
# Verify Shardok connectivity before proceeding with deployment
# This catches network/firewall issues early
# =================================================================
echo "Verifying Shardok connectivity..."
SHARDOK_HOST=\$(echo "\${SHARDOK_ADDRESS}" | cut -d: -f1)
SHARDOK_PORT=\$(echo "\${SHARDOK_ADDRESS}" | cut -d: -f2)
# Try to connect to Shardok (timeout after 10 seconds)
if nc -z -w 10 "\${SHARDOK_HOST}" "\${SHARDOK_PORT}" 2>/dev/null; then
echo "Shardok connectivity verified: \${SHARDOK_ADDRESS} is reachable"
else
echo ""
echo "========================================="
echo "ERROR: Cannot reach Shardok at \${SHARDOK_ADDRESS}"
echo "This may indicate:"
echo " - Shardok server is not running on Hetzner"
echo " - Network/firewall issues between DigitalOcean and Hetzner"
echo " - Incorrect SHARDOK_ADDRESS configuration"
echo ""
echo "DEPLOYMENT ABORTED: Shardok must be reachable for battles to work."
echo "========================================="
exit 1
fi
# Stop local shardok container if running (now runs on Hetzner)
docker stop shardok-server 2>/dev/null || true
docker rm shardok-server 2>/dev/null || true
# Deploy Eagle with blue-green (handles eagle, nginx, admin, jfr-sidecar)
# Note: Shardok runs on Hetzner, deployed separately via shardok_arm64_build.yml
# Note: Auth is deployed separately via auth_build.yml - do NOT touch auth here
chmod +x /opt/eagle0/scripts/*.sh
[ -f "/opt/eagle0/scripts/bin/warmup" ] && chmod +x /opt/eagle0/scripts/bin/warmup
GIT_SHA=\$(echo "\${EAGLE_IMAGE}" | sed 's/.*://')
/opt/eagle0/scripts/deploy-blue-green.sh "\${GIT_SHA}"
# Ensure auth is running
docker compose -f docker-compose.prod.yml up -d auth
# Verify
sleep 10
docker compose -f docker-compose.prod.yml ps
+2 -1
View File
@@ -261,8 +261,9 @@ jobs:
echo "$SPARKLE_EDDSA_PRIVATE_KEY" > "$SPARKLE_PRIVATE_KEY_PATH"
chmod 600 "$SPARKLE_PRIVATE_KEY_PATH"
VERSION=$(git describe --tags --always)
# Use commit count for automatic incrementing versions (e.g., 1.0.9548)
BUILD_NUMBER=$(git rev-list --count HEAD)
VERSION="1.0.${BUILD_NUMBER}"
bazel run //src/main/go/net/eagle0/build/mac_build_handler:mac_build_handler -- \
"/tmp/eagle0/eagle0MAC/eagle0.app" \
-40
View File
@@ -1,40 +0,0 @@
# Environment template for production deployment
# This file defines all env vars used by docker-compose.prod.yml
# Workflows should update their specific vars without overwriting others
# Container images (managed by respective build workflows)
# Note: Shardok runs on Hetzner, deployed via shardok_arm64_build.yml
EAGLE_IMAGE=registry.digitalocean.com/eagle0/eagle-server:latest
ADMIN_IMAGE=registry.digitalocean.com/eagle0/admin-server:latest
JFR_SIDECAR_IMAGE=registry.digitalocean.com/eagle0/jfr-sidecar:latest
AUTH_IMAGE=registry.digitalocean.com/eagle0/auth-server:latest
# OpenAI / LLM
OPENAI_API_KEY=
GPT_MODEL_NAME=gpt-4o
# DigitalOcean Spaces (S3-compatible storage)
EAGLE_ENABLE_S3=false
DO_SPACES_ACCESS_KEY=
DO_SPACES_SECRET_KEY=
# JWT authentication
JWT_PRIVATE_KEY=
# OAuth providers
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Shardok connection (Hetzner ARM64 server)
SHARDOK_ADDRESS=
SHARDOK_AUTH_TOKEN=
# Monitoring
SENTRY_DSN=
# Email (Fastmail JMAP)
FASTMAIL_API_TOKEN=
FASTMAIL_FROM_EMAIL=
FASTMAIL_FROM_NAME=
-65
View File
@@ -1,65 +0,0 @@
#!/bin/bash
# Update .env file without losing other variables
# Usage: ./update-env.sh KEY1=value1 KEY2=value2 ...
#
# This script:
# 1. Creates .env from template if it doesn't exist
# 2. Updates only the specified KEY=value pairs
# 3. Preserves all other existing values
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="${ENV_FILE:-/opt/eagle0/.env}"
TEMPLATE_FILE="${TEMPLATE_FILE:-$SCRIPT_DIR/env.template}"
# Create .env from template if it doesn't exist
if [ ! -f "$ENV_FILE" ]; then
if [ -f "$TEMPLATE_FILE" ]; then
echo "Creating .env from template..."
grep -v '^#' "$TEMPLATE_FILE" | grep -v '^$' > "$ENV_FILE"
else
echo "Creating empty .env..."
touch "$ENV_FILE"
fi
chmod 600 "$ENV_FILE"
fi
# Process each KEY=VALUE argument
for arg in "$@"; do
# Skip empty args
[ -z "$arg" ] && continue
# Parse KEY=VALUE
KEY="${arg%%=*}"
VALUE="${arg#*=}"
# Skip if no key
[ -z "$KEY" ] && continue
# Skip setting empty values (keeps existing value)
if [ -z "$VALUE" ]; then
echo "Skipping $KEY (empty value)"
continue
fi
# Remove existing line for this key and add new one
# Use single quotes to avoid issues with special characters in values
# (JSON strings, base64 with /, etc.)
# Escape any single quotes in the value by replacing ' with '\''
ESCAPED_VALUE="${VALUE//\'/\'\\\'\'}"
if grep -q "^${KEY}=" "$ENV_FILE" 2>/dev/null; then
# Key exists, remove old line and add new one (sed with special chars is fragile)
grep -v "^${KEY}=" "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
echo "${KEY}='${ESCAPED_VALUE}'" >> "$ENV_FILE"
echo "Updated $KEY"
else
# Key doesn't exist, add it
echo "${KEY}='${ESCAPED_VALUE}'" >> "$ENV_FILE"
echo "Added $KEY"
fi
done
chmod 600 "$ENV_FILE"
echo "Done updating $ENV_FILE"
+11 -10
View File
@@ -21,7 +21,7 @@ services:
- "--gpt-model-name"
- "${GPT_MODEL_NAME:-gpt-5.1}"
- "--shardok-interface-remote-address"
- "${SHARDOK_ADDRESS:-shardok:40042}"
- "${SHARDOK_ADDRESS}"
- "--auth-service-url"
- "auth:40033"
ports:
@@ -32,9 +32,8 @@ services:
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}"
DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}"
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
# JWT public key for token validation (auth service handles signing)
# Reads from /etc/eagle0/keys/public.pem via shared volume
# Auth token for Shardok on Hetzner (required)
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
# Use persistent volume for save data (users, games, etc.)
@@ -47,7 +46,7 @@ services:
- ./archived:/app/archived # Archived completed games
- ./jfr:/app/jfr # JFR recordings - dump with: docker exec eagle-blue jcmd 1 JFR.dump filename=/app/jfr/profile.jfr
- jvm-tmp:/tmp # Shared with jfr-sidecar for JVM attach socket files
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
- jwt-keys:/etc/eagle0/keys:ro # JWT public key from auth service (read-only)
depends_on:
- auth
restart: unless-stopped
@@ -71,7 +70,7 @@ services:
- "--gpt-model-name"
- "${GPT_MODEL_NAME:-gpt-5.1}"
- "--shardok-interface-remote-address"
- "${SHARDOK_ADDRESS:-shardok:40042}"
- "${SHARDOK_ADDRESS}"
- "--auth-service-url"
- "auth:40033"
ports:
@@ -82,9 +81,8 @@ services:
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}"
DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}"
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
# JWT public key for token validation (auth service handles signing)
# Reads from /etc/eagle0/keys/public.pem via shared volume
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
EAGLE_SAVE_DIR: "/app/saves"
EAGLE_ARCHIVE_DIR: "/app/archived"
@@ -95,7 +93,7 @@ services:
- ./archived:/app/archived # Same archive directory as blue
- ./jfr:/app/jfr # JFR recordings (same as blue)
- jvm-tmp:/tmp # Shared with jfr-sidecar-green for JVM attach socket files
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
- jwt-keys:/etc/eagle0/keys:ro # JWT public key from auth service (read-only)
depends_on:
- auth
restart: "no" # Don't auto-restart during deployment
@@ -140,6 +138,9 @@ services:
APPLE_TEAM_ID: "${APPLE_TEAM_ID:-}"
APPLE_SIGNIN_KEY_ID: "${APPLE_SIGNIN_KEY_ID:-}"
APPLE_SIGNIN_PRIVATE_KEY: "${APPLE_SIGNIN_PRIVATE_KEY:-}"
# Twitch OAuth credentials
TWITCH_CLIENT_ID: "${TWITCH_CLIENT_ID:-}"
TWITCH_CLIENT_SECRET: "${TWITCH_CLIENT_SECRET:-}"
# Server base URL for OAuth callbacks
SERVER_BASE_URL: "${SERVER_BASE_URL:-https://prod.eagle0.net}"
# JWT keys - PEM files in volume, bootstrapped from JWK on first run
+47 -28
View File
@@ -45,9 +45,11 @@
| Availability (`/library/actions/availability/`) | **0** | ✅ **Complete** |
| Command Choice Helpers (`/library/util/command_choice_helpers/`) | **0** | ✅ **Complete** |
| View Filters (`/library/util/view_filters/`) | **0** | ✅ **Complete** (except boundary code) |
| Other Utilities (`/library/util/`) | **0** | ✅ **Complete** |
| NewGameCreation (`/service/new_game_creation/`) | **0** | ✅ **Complete** (except input GameParameters proto) |
| LLM Prompt Generators (`/library/actions/llm_prompt_generators/`) | **56** | ⏳ Remaining work (34 files) |
| Other Utilities (`/library/util/`) | **7** | ⏳ Remaining work (2 files) |
| Root Library (`/library/`) | **8** | Boundary code (3 files) |
| LLM Request Generators (`/library/actions/llm_request_generators/`) | **2** | ⏳ Remaining work (1 file) |
| Root Library (`/library/`) | **7** | Boundary code (4 files) |
**Total: ~65 proto imports remaining** (down from 149)
@@ -66,7 +68,7 @@
| Phase 6c: AI Clients | **Complete** | AIClient, command choosers all protoless |
| Phase 6d: CommandSelection | **Complete** | Renamed ScalaCommandSelection → CommandSelection |
### Recent Completions (PRs #5326, #5330, #5332, #5333, #5334, #5336, #5341, #5342)
### Recent Completions (PRs #5326-#5390)
1. **CommandFactory protoless** - Now accepts/returns Scala `SelectedCommand` types
2. **AI clients protoless** - `AIClient`, `MidGameAIClient`, all command choosers use Scala types
@@ -81,6 +83,10 @@
11. **StatWithConditionUtils & ProvinceEventUtils protoless** (PR #5342) - Deleted unused proto overloads
12. **IncomingArmyUtils protoless** (PR #5348) - Deleted unused `stats()` method
13. **RansomOfferHelpers deleted** (PR #5351) - Deleted unused dead code file
14. **MapGenerator protoless** (PR #5373) - Returns Scala types instead of proto
15. **Remove proto ActionResult from Engine layer** (PR #5378) - Engine uses Scala `ActionResultT`
16. **NewGameCreation protoless** (PR #5381) - Uses Scala types throughout, no proto conversion
17. **PersistedHistory Scala API** (PR #5390) - New `apply` overload accepts `Vector[ActionResultT]` directly
---
@@ -114,29 +120,26 @@ The LLM prompt generators still use proto types for hero/faction/province data:
**Priority**: Medium - These don't block other migrations and are isolated.
### Phase 9: Remaining Utilities (~1 proto import in 1 file)
### Phase 9: Remaining Utilities ✅ Complete
| File | Proto Usage | Migration Path |
|------|-------------|----------------|
| `ProvinceEventUtils.scala` | ✅ **0** | Deleted unused proto overloads (PR #5342) |
| `StatWithConditionUtils.scala` | ✅ **0** | Deleted unused proto overloads (PR #5342) |
| `MapGenerator.scala` | 1 import | Convert to Scala types |
| `MapGenerator.scala` | **0** | Returns Scala types (PR #5373) |
| `IncomingArmyUtils.scala` | ✅ **0** | Deleted unused `stats()` method (PR #5348) |
| `GameStateViewDiffer.scala` | ✅ **0** | Uses Scala diff types, converts at boundary |
### Phase 10: History APIs 🔄 In Progress
### Phase 10: History APIs ✅ Complete
**Completed:**
- `ActionWithResultingState` now has `scalaActionResult` property (lazy converted from proto)
- `ActionResultFilter.includeForPlayer` uses Scala types (`ActionResultT`, `NotificationT`, `ActionResultType`)
- `GameHistory.withNewResultsScala` preserves both Scala `GameState` and `ActionResultT` to avoid re-conversion
- `PersistedHistory.apply` now accepts `Vector[ActionResultT]` directly (PR #5390)
- `NewGameCreation` is fully protoless (PR #5381)
**Remaining:**
```
src/main/scala/net/eagle0/eagle/service/InMemoryHistory.scala
src/main/scala/net/eagle0/eagle/service/PersistedHistory.scala
```
Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versions.
**Note:** History internals still use proto for persistence (expected - proto is good for disk serialization).
---
@@ -163,9 +166,9 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
### Remaining Proto Usage ⏳
- LLM prompt generators (56 imports in 34 files) - Medium priority
- MapGenerator (1 import) - Low priority
- LLM request generators (2 imports in 1 file) - Medium priority
- BattleFilter (1 import) - Keep (boundary)
- History API internals - Low priority
- Root library boundary code (7 imports in 4 files) - Keep (boundary)
---
@@ -174,10 +177,12 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
| Component | Files | Imports | Priority |
|-----------|-------|---------|----------|
| View Filters | 0 | 0 | ✅ Complete |
| Utility files | 0 | 0 | ✅ Complete |
| History APIs | 0 | 0 | ✅ Complete |
| LLM Prompt Generators | 34 | 56 | Medium |
| Utility files | 1 | 1 | Low |
| Root Library (boundary) | 3 | 8 | Keep |
| **Total** | **37** | **~65** | |
| LLM Request Generators | 1 | 2 | Medium |
| Root Library (boundary) | 4 | 7 | Keep |
| **Total** | **~35** | **~65** | |
---
@@ -190,8 +195,10 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
- [x] Zero proto imports in `/library/util/command_choice_helpers/`
- [x] Zero proto imports in `/library/actions/impl/action/`
- [x] Zero proto imports in `/library/util/view_filters/` (except BattleFilter boundary code)
- [x] Zero proto imports in `/library/util/` (except view filters)
- [x] Zero proto imports in `/service/new_game_creation/` (except input GameParameters)
- [ ] Zero proto imports in `/library/actions/llm_prompt_generators/`
- [ ] Zero proto imports in `/library/util/` (except view filters)
- [ ] Zero proto imports in `/library/actions/llm_request_generators/`
### Architecture
- [x] Clear separation: Scala models (internal) vs Proto (boundaries)
@@ -202,6 +209,8 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
- [x] HeroViewFilter returns Scala types
- [x] ProvinceViewFilter returns Scala types
- [x] All view filters return Scala types
- [x] NewGameCreation uses Scala types
- [x] PersistedHistory accepts Scala types for new games
- [ ] LLM layer uses Scala types
- [ ] No "proto creep" into business logic
@@ -219,7 +228,7 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
## Proto Import Inventory (Detailed)
**Current: ~65 proto imports across 37 files** (as of 2026-01-14)
**Current: ~65 proto imports across ~35 files** (as of 2026-01-17)
### Summary by Directory
@@ -230,10 +239,11 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
| `actions/impl/action/` | 0 | 0 | ✅ Clean |
| `actions/impl/command/` | 0 | 0 | ✅ Clean |
| `actions/llm_prompt_generators/` | 34 | 56 | LLM request types |
| `actions/llm_request_generators/` | 1 | 2 | LLM request types |
| `util/command_choice_helpers/` | 0 | 0 | ✅ Clean |
| `util/view_filters/` | 1 | 1 | ✅ Clean (except boundary) |
| `util/` (other) | 1 | 1 | ✅ Clean |
| Root (`library/`) | 3 | 9 | Boundary code |
| `util/view_filters/` | 0 | 0 | ✅ Clean (except boundary) |
| `util/` (other) | 0 | 0 | ✅ Clean |
| Root (`library/`) | 4 | 7 | Boundary code |
### util/view_filters/ (0 imports, except boundary)
@@ -243,19 +253,20 @@ Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versi
| `GameStateViewDiffer.scala` | ✅ **0** | Uses Scala diff types, converts at edge |
| `BattleFilter.scala` | 1 | Battle view boundary (keep proto) |
### util/ other files (1 import, 1 file)
### util/ other files ✅ Complete
| File | Imports | Notes |
|------|---------|-------|
| `MapGenerator.scala` | 1 | Map generation |
| `MapGenerator.scala` | **0** | Returns Scala types (PR #5373) |
### Root library/ (8 imports, 3 files)
### Root library/ (7 imports, 4 files)
| File | Imports | Notes |
|------|---------|-------|
| `ActionResultFilter.scala` | 2 | Action result filtering (boundary, uses Scala types internally) |
| `Engine.scala` | 1 | Trait interface |
| `EngineImpl.scala` | 5 | Returns proto for persistence (boundary) |
| `ActionResultFilter.scala` | 3 | Action result filtering (boundary, uses Scala types internally) |
| `Engine.scala` | 1 | Trait interface (returns `ActionResultView` proto for client) |
| `EngineImpl.scala` | 1 | Returns proto for persistence (boundary) |
| `ActionWithResultingState.scala` | 2 | Stores both proto and Scala (boundary for persistence) |
### actions/llm_prompt_generators/ (56 imports, 34 files)
@@ -263,6 +274,14 @@ These files generate LLM prompts and primarily use `internal.generated_text_requ
**Migration strategy**: Can be migrated to Scala types when convenient, but low priority as they're isolated from core game logic.
### actions/llm_request_generators/ (2 imports, 1 file)
| File | Imports | Notes |
|------|---------|-------|
| `DiplomacyResolutionLlmRequestGenerator.scala` | 2 | Uses `DiplomacyOfferStatus` enum and `GeneratedTextRequest` |
**Migration strategy**: Create Scala `DiplomacyOfferStatus` enum and `GeneratedTextRequest` types.
---
## Migration Pattern
+7
View File
@@ -119,6 +119,13 @@ http {
proxy_set_header X-Real-IP $remote_addr;
}
# Steam OAuth callback (Steam uses OpenID 2.0)
location /oauth/steam/callback {
proxy_pass http://auth:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Invitation landing page (proxied to Go auth service)
location /invite/ {
proxy_pass http://auth:8080;
+25 -6
View File
@@ -39,32 +39,51 @@ find "$APP_PATH" -name "*.bundle" -print0 | while IFS= read -r -d '' item; do
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign XPC services (inside Sparkle framework)
# Sign XPC services (but skip ones inside Sparkle.framework - they're already signed)
find "$APP_PATH" -name "*.xpc" -print0 | while IFS= read -r -d '' item; do
if [[ "$item" == *"Sparkle.framework"* ]]; then
echo "Skipping Sparkle XPC service (pre-signed): $item"
continue
fi
echo "Signing XPC service: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign nested apps (like Sparkle's Updater.app)
# Sign nested apps (but skip ones inside Sparkle.framework - they're already signed)
find "$APP_PATH" -path "*/Frameworks/*.app" -print0 | while IFS= read -r -d '' item; do
if [[ "$item" == *"Sparkle.framework"* ]]; then
echo "Skipping Sparkle nested app (pre-signed): $item"
continue
fi
echo "Signing nested app: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign standalone executables inside frameworks (like Autoupdate)
# Sign standalone executables inside frameworks (but skip Sparkle.framework internals)
find "$APP_PATH" -path "*/Frameworks/*/Versions/*/Autoupdate" -type f -print0 | while IFS= read -r -d '' item; do
if [[ "$item" == *"Sparkle.framework"* ]]; then
echo "Skipping Sparkle executable (pre-signed): $item"
continue
fi
echo "Signing executable: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign all frameworks (after their contents are signed)
# Use --deep for Sparkle.framework to handle its XPC services
find "$APP_PATH" -name "*.framework" -print0 | while IFS= read -r -d '' item; do
echo "Signing framework: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
if [[ "$item" == *"Sparkle.framework" ]]; then
echo "Signing Sparkle framework with --deep: $item"
codesign --deep --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
else
echo "Signing framework: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
fi
done
echo "=== Signing main app bundle ==="
+46 -19
View File
@@ -27,33 +27,59 @@ if [ -z "${SPARKLE_EDDSA_PUBLIC_KEY:-}" ]; then
exit 1
fi
# Download Sparkle if not cached
# Always use a fresh Sparkle download to avoid cache corruption issues
SPARKLE_DIR="$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION"
if [ ! -d "$SPARKLE_DIR/Sparkle.framework" ]; then
echo "=== Downloading Sparkle $SPARKLE_VERSION ==="
mkdir -p "$SPARKLE_CACHE_DIR"
SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
curl -L "$SPARKLE_URL" | tar -xJ -C "$SPARKLE_CACHE_DIR"
mv "$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION" "$SPARKLE_DIR" 2>/dev/null || true
# If the extracted directory doesn't match version pattern, it may just be "Sparkle"
if [ ! -d "$SPARKLE_DIR" ]; then
mkdir -p "$SPARKLE_DIR"
mv "$SPARKLE_CACHE_DIR/Sparkle.framework" "$SPARKLE_DIR/" 2>/dev/null || true
mv "$SPARKLE_CACHE_DIR/bin" "$SPARKLE_DIR/" 2>/dev/null || true
fi
echo "=== Clearing Sparkle cache and downloading fresh copy ==="
rm -rf "$SPARKLE_DIR"
mkdir -p "$SPARKLE_DIR"
SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
echo "Downloading from: $SPARKLE_URL"
curl -L "$SPARKLE_URL" -o /tmp/sparkle.tar.xz
tar -xJf /tmp/sparkle.tar.xz -C "$SPARKLE_DIR"
rm /tmp/sparkle.tar.xz
# Show what was extracted
echo "=== Extracted contents ==="
ls -la "$SPARKLE_DIR/"
# The tarball extracts files directly, not into a subdirectory
# Verify the framework has proper symlink structure
echo "=== Verifying Sparkle.framework structure ==="
ls -la "$SPARKLE_DIR/Sparkle.framework/"
if [ ! -L "$SPARKLE_DIR/Sparkle.framework/Sparkle" ]; then
echo "ERROR: Sparkle.framework/Sparkle is not a symlink"
file "$SPARKLE_DIR/Sparkle.framework/Sparkle"
exit 1
fi
if [ ! -L "$SPARKLE_DIR/Sparkle.framework/Versions/Current" ]; then
echo "ERROR: Sparkle.framework/Versions/Current is not a symlink"
ls -la "$SPARKLE_DIR/Sparkle.framework/Versions/"
exit 1
fi
echo "Sparkle framework structure verified OK"
echo "=== Injecting Sparkle framework ==="
FRAMEWORKS_DIR="$APP_PATH/Contents/Frameworks"
mkdir -p "$FRAMEWORKS_DIR"
# Copy Sparkle framework
cp -R "$SPARKLE_DIR/Sparkle.framework" "$FRAMEWORKS_DIR/"
# Remove any existing Sparkle.framework in the app
rm -rf "$FRAMEWORKS_DIR/Sparkle.framework"
# Also copy the XPC services if present
if [ -d "$SPARKLE_DIR/Sparkle.framework/Versions/B/XPCServices" ]; then
echo "Sparkle XPC services present"
# Copy Sparkle framework (use ditto to preserve symlinks and bundle structure)
ditto "$SPARKLE_DIR/Sparkle.framework" "$FRAMEWORKS_DIR/Sparkle.framework"
# Verify the copied framework still has proper structure
echo "=== Verifying copied Sparkle.framework structure ==="
ls -la "$FRAMEWORKS_DIR/Sparkle.framework/"
if [ ! -L "$FRAMEWORKS_DIR/Sparkle.framework/Sparkle" ]; then
echo "ERROR: Copied framework lost symlink structure"
exit 1
fi
echo "Copied framework structure OK"
echo "=== Updating Info.plist ==="
PLIST_PATH="$APP_PATH/Contents/Info.plist"
@@ -69,8 +95,9 @@ PLIST_PATH="$APP_PATH/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Add :SUEnableAutomaticChecks bool true" "$PLIST_PATH"
# Set bundle version from git for Sparkle version comparison
VERSION=$(git describe --tags --always 2>/dev/null || echo "1.0.0")
# Use commit count for automatic incrementing versions (e.g., 1.0.9548)
BUILD_NUMBER=$(git rev-list --count HEAD 2>/dev/null || echo "1")
VERSION="1.0.${BUILD_NUMBER}"
echo "Setting version: $VERSION (build $BUILD_NUMBER)"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$PLIST_PATH" 2>/dev/null || \
@@ -87,7 +87,6 @@
<Compile Include="Assets/Modern UI Pack/Scripts/Demo/LaunchURL.cs" />
<Compile Include="Assets/Shardok/HexCoordinates.cs" />
<Compile Include="Assets/Eagle/Table Rows/OrganizeTroopsTableRow.cs" />
<Compile Include="Assets/Bluetooth/PickerRowController.cs" />
<Compile Include="Assets/TouchHandler.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/TravelCommandSelector.cs" />
<Compile Include="Assets/Eagle/ConnectionStatusUI.cs" />
@@ -98,7 +97,6 @@
<Compile Include="Assets/HoveringTooltipTextProvider.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/util/KeyModifiedAmount.cs" />
<Compile Include="Assets/HoveringTooltip.cs" />
<Compile Include="Assets/Bluetooth/DiceInterface.cs" />
<Compile Include="Assets/ButtonColors.cs" />
<Compile Include="Assets/Eagle/MapController.cs" />
<Compile Include="Assets/common/DisclosureTriangle.cs" />
@@ -151,10 +149,8 @@
<Compile Include="Assets/Eagle/GeneratedTextUpdater.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/OutlawApprehendedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/HexMetrics.cs" />
<Compile Include="Assets/Bluetooth/OneDiceRollController.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Switch/SwitchManager.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ProvinceHeldDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Bluetooth/DieInfo.cs" />
<Compile Include="Assets/common/ResourceFetcher.cs" />
<Compile Include="Assets/Tutorial/Content/TutorialStep.cs" />
<Compile Include="Assets/Auth/AuthClient.cs" />
@@ -162,12 +158,11 @@
<Compile Include="Assets/Eagle/Notifications/RiotSuppressedNotificationGenerator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Modal Window/ModalWindowManagerEditor.cs" />
<Compile Include="Assets/common/GUIUtils/AutoScrollingText.cs" />
<Compile Include="Assets/Bluetooth/NativeDiceInterfaceImports.cs" />
<Compile Include="Assets/Eagle/Notifications/ProvinceConqueredNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/Table Rows/ExtraTroopsRowController.cs" />
<Compile Include="Assets/Auth/JwtAuthInterceptor.cs" />
<Compile Include="Assets/Eagle/CommandPanelController.cs" />
<Compile Include="Assets/Tutorial/Content/TutorialContentDefinitions.cs" />
<Compile Include="Assets/Eagle/CommandPanelController.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ShatteredArmyDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/TurnHistoryPanelController.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/HeroDepartureDetailsNotificationGenerator.cs" />
@@ -212,7 +207,6 @@
<Compile Include="Assets/Eagle/CommandSelectors/ExileVassalCommandSelector.cs" />
<Compile Include="Assets/Tutorial/TutorialState.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicWithIconEditor.cs" />
<Compile Include="Assets/Bluetooth/DiceVectors.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerProgressBarLoop.cs" />
<Compile Include="Assets/Eagle/MovingArmiesTableController.cs" />
<Compile Include="Assets/ConnectionHandler/ConnectionHandler.cs" />
@@ -233,9 +227,11 @@
<Compile Include="Assets/Eagle/CommandSelectors/DiplomacyCommandSelector.cs" />
<Compile Include="Assets/Tutorial/Triggers/TutorialTriggerRegistry.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ResolveBreakAllianceCommandSelector.cs" />
<Compile Include="Assets/common/WindowFocusManager.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ResolveAllianceCommandSelector.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/WithdrewForTruceDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/MarchCommandSelector.cs" />
<Compile Include="Assets/Tutorial/TutorialTargetRegistry.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/TrainCommandSelector.cs" />
<Compile Include="Assets/Eagle/CustomFileLogger.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/InvitationAcceptedDetailsNotificationGenerator.cs" />
@@ -269,7 +265,6 @@
<Compile Include="Assets/Shardok/AnimationTestController.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/TradeCommandSelector.cs" />
<Compile Include="Assets/Eagle/Table Rows/DominionTableRowController.cs" />
<Compile Include="Assets/Bluetooth/RollPanelController.cs" />
<Compile Include="Assets/Shardok/ChargeAnimator.cs" />
<Compile Include="Assets/Eagle/ClientTextProvider.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManager.cs" />
@@ -304,6 +299,7 @@
<Compile Include="Assets/Shardok/ControlAnimator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Horizontal Selector/HorizontalSelector.cs" />
<Compile Include="Assets/Eagle/Table Rows/OrganizeTroopsResultRow.cs" />
<Compile Include="Assets/Eagle/SparkleUpdater.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/SliderManagerEditor.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SwearBrotherhoodDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/PersistentClientConnection.cs" />
@@ -322,10 +318,10 @@
<Compile Include="Assets/Shardok/ShardokGameModel.cs" />
<Compile Include="Assets/common/GUIUtils/GeneralClickDetector.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerContextMenu.cs" />
<Compile Include="Assets/Eagle/SparkleInitializer.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/TruceAmbassadorImprisonedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicWithIcon.cs" />
<Compile Include="Assets/Tutorial/UI/TutorialHintIndicator.cs" />
<Compile Include="Assets/Bluetooth/UnityDieColors.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/OrganizeTroopsCommandSelector.cs" />
<Compile Include="Assets/Shardok/ExtinguishAnimator.cs" />
<Compile Include="Assets/Eagle/PanelPositions.cs" />
@@ -338,7 +334,6 @@
<Compile Include="Assets/EagleConnection.cs" />
<Compile Include="Assets/GUI Pro Kit Fantasy RPG/Scripts/CtrPanel.cs" />
<Compile Include="Assets/UI/Scripts/SceneLoadTester.cs" />
<Compile Include="Assets/Bluetooth/RollFetcher.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/HandleRiotGiveCommandSelector.cs" />
<Compile Include="Assets/Eagle/ProvinceUtils.cs" />
<Compile Include="Assets/common/GUIUtils/ProvinceStatUtils.cs" />
@@ -372,16 +367,15 @@
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SuppressBeastsFailedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Terrain Hexes/Example Scene/BasicHexArranger.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ManagePrisonersCommandSelector.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/SendSuppliesCommandSelector.cs" />
<Compile Include="Assets/ConnectionHandler/StoredAccountButton.cs" />
<Compile Include="Assets/Tutorial/UI/TutorialUIManager.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/SendSuppliesCommandSelector.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Window/WindowManager.cs" />
<Compile Include="Assets/Auth/InvitationCodeManager.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/CapturedHeroExiledDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SuppressBeastsSucceededNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/ReservesTableController.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/CommandSelector.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/RestCommandSelector.cs" />
<Compile Include="Assets/Bluetooth/DiceConfigurationPanelController.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerDropdown.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerButton.cs" />
<Compile Include="Assets/common/GUIUtils/GUIUtils.cs" />
@@ -87,20 +87,20 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
public Button discordLoginButton;
public Button googleLoginButton;
public Button githubLoginButton;
public Button appleLoginButton;
public Button steamLoginButton;
public Button twitchLoginButton;
public TextMeshProUGUI oauthStatusText;
[Header("Stored Accounts")]
public GameObject storedAccountsContainer;
public GameObject storedAccountButtonPrefab;
public StoredAccountButton storedAccountButtonPrefab;
public Sprite discordProviderIcon;
public Sprite googleProviderIcon;
public Sprite githubProviderIcon;
[Header("Display Name Setup")]
public GameObject displayNamePanel;
public TMP_InputField displayNameField;
public Button setDisplayNameButton;
public TextMeshProUGUI displayNameErrorText;
public Sprite appleProviderIcon;
public Sprite steamProviderIcon;
public Sprite twitchProviderIcon;
[Header("Status Display")]
public TextMeshProUGUI connectionStatusText;
@@ -121,6 +121,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
public Button cancelCustomBattleButton;
public TMP_Dropdown lobbyEnvironmentDropdown;
public TextMeshProUGUI lobbyUserText;
public TextMeshProUGUI lobbyStatusText;
public GameObject runningGamesListArea;
public GameObject runningGamesListItemPrefab;
@@ -147,14 +148,12 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
private CancellationTokenSource _cancellationTokenSource;
private readonly Object pendingReplyLock = new Object();
private int _connectedEnvironmentIndex = -1; // -1 means not connected
private List<GameObject> _storedAccountButtons = new List<GameObject>();
private List<StoredAccountButton> _storedAccountButtons = new();
public ClientPregeneratedText clientPregeneratedText;
const String BaseDomain = "eagle0.net";
const String EnvironmentKey = "environmentKey";
const String NameKey = "nameKey";
const String PasswordKey = "passwordKey";
// Environment options: index 0 = prod., index 1 = qa., index 2 = (none)
private static readonly List<string> EnvironmentOptions = new() { "prod.", "qa.", "" };
@@ -260,15 +259,20 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
if (githubLoginButton != null) {
githubLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Github));
}
if (setDisplayNameButton != null) {
setDisplayNameButton.onClick.AddListener(OnSetDisplayNameClicked);
if (appleLoginButton != null) {
appleLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Apple));
}
if (steamLoginButton != null) {
steamLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Steam));
}
if (twitchLoginButton != null) {
twitchLoginButton.onClick.AddListener(() => OnOAuthLoginClicked(OAuthProvider.Twitch));
}
// Subscribe to OAuthManager events
if (OAuthManager.Instance != null) {
OAuthManager.Instance.OnLoginSuccess += OnOAuthLoginSuccess;
OAuthManager.Instance.OnLoginFailed += OnOAuthLoginFailed;
OAuthManager.Instance.OnNewUserNeedsDisplayName += OnNewUserNeedsDisplayName;
OAuthManager.Instance.OnLogout += OnOAuthLogout;
}
@@ -277,9 +281,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
}
private void ShowAuthPanel() {
// Hide other panels
if (displayNamePanel != null) displayNamePanel.SetActive(false);
// Show OAuth panel
if (oauthPanel != null) oauthPanel.SetActive(true);
@@ -297,9 +298,13 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
}
private void RefreshStoredAccountButtons() {
// Clear existing buttons
foreach (var btn in _storedAccountButtons) {
if (btn != null) Destroy(btn);
// Clear ALL children of the container
// Disable immediately (Destroy is deferred to end of frame)
if (storedAccountsContainer != null) {
foreach (Transform child in storedAccountsContainer.transform) {
child.gameObject.SetActive(false);
Destroy(child.gameObject);
}
}
_storedAccountButtons.Clear();
@@ -316,28 +321,33 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
buttonObj.transform.localScale = Vector3.one;
// Set button text (just display name, icon shows provider)
var buttonText = buttonObj.GetComponentInChildren<TextMeshProUGUI>();
if (buttonText != null) { buttonText.text = account.DisplayName; }
buttonObj.label.text = account.DisplayName;
// Set provider icon (look for child named "ProviderIcon")
var providerIconTransform = buttonObj.transform.Find("ProviderIcon");
if (providerIconTransform != null) {
var providerImage = providerIconTransform.GetComponent<Image>();
if (providerImage != null) {
var providerLower = account.Provider.ToLowerInvariant();
providerImage.sprite = providerLower switch { "google" => googleProviderIcon,
"github" => githubProviderIcon,
_ => discordProviderIcon };
}
var providerImage = buttonObj.icon;
if (providerImage != null) {
var providerLower = account.Provider.ToLowerInvariant();
providerImage.sprite = providerLower switch { "google" => googleProviderIcon,
"github" => githubProviderIcon,
"apple" => appleProviderIcon,
"steam" => steamProviderIcon,
"twitch" => twitchProviderIcon,
_ => discordProviderIcon };
}
// Set click handler
// Set click handler for sign-in
var button = buttonObj.GetComponent<Button>();
if (button != null) {
var accountCopy = account; // Capture for lambda
button.onClick.AddListener(() => OnStoredAccountClicked(accountCopy));
}
// Set click handler for delete button
if (buttonObj.deleteButton != null) {
var accountCopy = account; // Capture for lambda
buttonObj.deleteButton.onClick.AddListener(
() => OnDeleteStoredAccount(accountCopy));
}
_storedAccountButtons.Add(buttonObj);
}
}
@@ -359,8 +369,13 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
}
}
private void OnDeleteStoredAccount(StoredAccount account) {
Debug.Log($"[ConnectionHandler] Deleting stored account: {account.DisplayName}");
OAuthManager.Instance?.RemoveStoredAccount(account);
RefreshStoredAccountButtons();
}
private void SetupLobbyUI() {
if (logoutButton != null) { logoutButton.onClick.AddListener(OnLogoutClicked); }
if (customBattleButton != null) { customBattleButton.onClick.AddListener(CustomBattle); }
if (cancelCustomBattleButton != null) {
cancelCustomBattleButton.onClick.AddListener(CancelCustomBattle);
@@ -411,13 +426,16 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
foreach (Transform row in runningGamesListArea.transform) { Destroy(row.gameObject); }
foreach (Transform row in availableGamesListArea.transform) { Destroy(row.gameObject); }
// Show loading indicator
if (lobbyStatusText != null) { lobbyStatusText.text = "Switching environment..."; }
// Reconnect to new environment
_createConnection();
RequestMaps();
StartListeningForLobbyUpdates();
}
private async void OnLogoutClicked() {
public async void OnLogoutClicked() {
Debug.Log("[ConnectionHandler] Logout button clicked");
// Clear OAuth tokens
@@ -492,35 +510,6 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
});
}
private void OnNewUserNeedsDisplayName(bool isNewUser) {
MainQueue.Q.Enqueue(() => {
// Show display name panel
if (oauthPanel != null) oauthPanel.SetActive(false);
if (displayNamePanel != null) displayNamePanel.SetActive(true);
if (displayNameErrorText != null) displayNameErrorText.text = "";
});
}
private async void OnSetDisplayNameClicked() {
if (displayNameField == null || OAuthManager.Instance == null) return;
var displayName = displayNameField.text.Trim();
if (string.IsNullOrEmpty(displayName)) {
if (displayNameErrorText != null) {
displayNameErrorText.text = "Please enter a display name";
}
return;
}
if (displayNameErrorText != null) { displayNameErrorText.text = "Setting display name..."; }
var success = await OAuthManager.Instance.SetDisplayNameAsync(displayName);
if (!success && displayNameErrorText != null) {
displayNameErrorText.text = "Name already taken or invalid. Try another.";
}
// OnLoginSuccess will be called if successful
}
private void OnOAuthLogout() {
MainQueue.Q.Enqueue(() => {
ShowAuthPanel();
@@ -724,6 +713,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
availableGameItem.JoinCallback = this.JoinGame;
availableGameItem.DropCallback = this.DropGame;
}
// Clear loading indicator now that lobby is populated
if (lobbyStatusText != null) { lobbyStatusText.text = ""; }
});
}
@@ -602,7 +602,7 @@ MonoBehaviour:
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 0.42745098, b: 0.42745098, a: 1}
m_NormalColor: {r: 1, g: 1, b: 1, a: 0}
m_HighlightedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1}
m_PressedColor: {r: 0, g: 0, b: 0, a: 1}
m_SelectedColor: {r: 0.84313726, g: 0.84313726, b: 0.84313726, a: 1}
@@ -1151,7 +1151,7 @@ MonoBehaviour:
m_Right: 0
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 0
m_ChildAlignment: 3
m_Spacing: 0
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
@@ -1256,14 +1256,14 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.RawImage
m_Material: {fileID: 0}
m_Color: {r: 0, g: 0, b: 0, a: 1}
m_Color: {r: 1, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Texture: {fileID: 2800000, guid: 8c998213d53883d4b8fdf7a3eb3c2909, type: 3}
m_Texture: {fileID: 2800000, guid: 88dca061d041a2a4596563e6a38f66c8, type: 3}
m_UVRect:
serializedVersion: 2
x: 0
@@ -0,0 +1,8 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class StoredAccountButton : MonoBehaviour {
public TMP_Text label;
public Image icon;
public Button deleteButton;
}
@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c2ab265dfad44a1aac98c4eaba3969bc
timeCreated: 1768606277
@@ -1,5 +1,62 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &602258008028479859
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8558900591190483184}
- component: {fileID: 5225486432737178588}
m_Layer: 5
m_Name: ProviderIconContainer
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &8558900591190483184
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 602258008028479859}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 6242087783640990751}
m_Father: {fileID: 157686408557551303}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &5225486432737178588
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 602258008028479859}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 80
m_PreferredHeight: 50
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!1 &1262787222422545696
GameObject:
m_ObjectHideFlags: 0
@@ -10,7 +67,6 @@ GameObject:
m_Component:
- component: {fileID: 6242087783640990751}
- component: {fileID: 5444015614922696687}
- component: {fileID: 8871991993173924918}
- component: {fileID: 6965305925514130928}
m_Layer: 5
m_Name: ProviderIcon
@@ -26,17 +82,17 @@ RectTransform:
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1262787222422545696}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 157686408557551303}
m_Father: {fileID: 8558900591190483184}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_SizeDelta: {x: -10, y: -10}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5444015614922696687
CanvasRenderer:
@@ -46,26 +102,6 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1262787222422545696}
m_CullTransparentMesh: 1
--- !u!114 &8871991993173924918
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1262787222422545696}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 80
m_PreferredHeight: 50
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!114 &6965305925514130928
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -86,7 +122,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: b6b68c8c37d5cf1419267a80c426ac83, type: 3}
m_Sprite: {fileID: 21300000, guid: af9f637b3a1f6433f819a0bcdb326453, type: 3}
m_Type: 0
m_PreserveAspect: 1
m_FillCenter: 1
@@ -96,6 +132,62 @@ MonoBehaviour:
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &4773346950019690658
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 9042872047974783334}
- component: {fileID: 1446804990396579382}
m_Layer: 5
m_Name: Spacer
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &9042872047974783334
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4773346950019690658}
m_LocalRotation: {x: -0, y: -0, z: -0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 157686408557551303}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1446804990396579382
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4773346950019690658}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 5
m_PreferredHeight: -1
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!1 &5166012670666351313
GameObject:
m_ObjectHideFlags: 0
@@ -110,6 +202,7 @@ GameObject:
- component: {fileID: 646268409010340088}
- component: {fileID: 3097176747511135857}
- component: {fileID: 7691398338434978342}
- component: {fileID: 369281190448318618}
m_Layer: 5
m_Name: StoredAccountButton
m_TagString: Untagged
@@ -130,13 +223,15 @@ RectTransform:
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 7974514965529143181}
- {fileID: 6242087783640990751}
- {fileID: 8558900591190483184}
- {fileID: 1544713289774970384}
- {fileID: 9042872047974783334}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_SizeDelta: {x: 400, y: 60}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3634401029247236684
CanvasRenderer:
@@ -258,7 +353,7 @@ MonoBehaviour:
m_Top: 0
m_Bottom: 0
m_ChildAlignment: 5
m_Spacing: 0
m_Spacing: 5
m_ChildForceExpandWidth: 0
m_ChildForceExpandHeight: 0
m_ChildControlWidth: 1
@@ -266,6 +361,21 @@ MonoBehaviour:
m_ChildScaleWidth: 0
m_ChildScaleHeight: 0
m_ReverseArrangement: 0
--- !u!114 &369281190448318618
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5166012670666351313}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c2ab265dfad44a1aac98c4eaba3969bc, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::StoredAccountButton
label: {fileID: 6210669843299902677}
icon: {fileID: 6965305925514130928}
deleteButton: {fileID: 5102831562105438405}
--- !u!1 &5617793748733146591
GameObject:
m_ObjectHideFlags: 0
@@ -425,3 +535,232 @@ MonoBehaviour:
m_FlexibleWidth: 1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!1 &5969634697112825658
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 3505386829136548087}
- component: {fileID: 4500436679700087771}
- component: {fileID: 5081311949443699076}
m_Layer: 5
m_Name: Image
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &3505386829136548087
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5969634697112825658}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 1544713289774970384}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &4500436679700087771
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5969634697112825658}
m_CullTransparentMesh: 1
--- !u!114 &5081311949443699076
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5969634697112825658}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 0, b: 0, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300002, guid: 88dca061d041a2a4596563e6a38f66c8, type: 3}
m_Type: 0
m_PreserveAspect: 1
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &7624557558758837467
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 1544713289774970384}
- component: {fileID: 1437169994306891023}
- component: {fileID: 4243156207255156076}
- component: {fileID: 5102831562105438405}
- component: {fileID: 7366748879155754635}
m_Layer: 5
m_Name: Delete
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &1544713289774970384
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7624557558758837467}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 3505386829136548087}
m_Father: {fileID: 157686408557551303}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &1437169994306891023
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7624557558758837467}
m_CullTransparentMesh: 1
--- !u!114 &4243156207255156076
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7624557558758837467}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Image
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 0}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &5102831562105438405
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7624557558758837467}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.Button
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 4243156207255156076}
m_OnClick:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 5166012670666351313}
m_TargetAssemblyTypeName:
m_MethodName:
m_Mode: 1
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName:
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!114 &7366748879155754635
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 7624557558758837467}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier: UnityEngine.UI::UnityEngine.UI.LayoutElement
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: -1
m_PreferredWidth: 40
m_PreferredHeight: 40
m_FlexibleWidth: -1
m_FlexibleHeight: -1
m_LayoutPriority: 1
@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 1fb1f6deb68f9475281c9f7c427a7024
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 8d524cd6fa08d47c99b2462c2686b8d0
guid: 2bbeb692cb0204e0c965a4f08dfeffce
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 7bf9379c6210e47b88bbe9c2a359d54f
guid: 498227fe7963d42e0b240ce5c37f3665
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 1e69ea88b87d34a48bbb45a1fbad81fc
guid: da40d75c7a6fb481d9d2cf483c71ae3a
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: d3ab99e02ad304404a2d663e3d77db5d
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 1850a7332b6e84e1a9b69f5f5b4cf482
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 8bac7b3a2d18d452197ea3aa4e3e4855
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 0
wrapV: 0
wrapW: 0
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: b6b68c8c37d5cf1419267a80c426ac83
TextureImporter:
fileIDToRecycleName: {}
externalObjects: {}
serializedVersion: 5
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -1
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 2
buildTarget: DefaultTexturePlatform
maxTextureSize: 128
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Standalone
maxTextureSize: 128
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: Android
maxTextureSize: 128
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
- serializedVersion: 2
buildTarget: WebGL
maxTextureSize: 128
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: e8701ed65f69e7b4385fc7a2dcf1f924
vertices: []
indices:
edges: []
weights: []
spritePackingTag:
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: e082b4a1a25a044a99b7798107149f77
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1160,7 +1160,7 @@ namespace Shardok {
int currentMouseGridIndex) {}
public void HandlePrimaryClick(int clickedIndex) {
if (HasPlacementAction(clickedIndex)) {
if (_selectedGridIndex != clickedIndex && HasPlacementAction(clickedIndex)) {
PerformPlacementAction(clickedIndex);
_selectedGridIndex = null;
} else if (HasReinforceAction(clickedIndex)) {
@@ -2057,6 +2057,21 @@ MonoBehaviour:
m_Scale: 1
m_AtlasIndex: 0
m_ClassDefinitionType: 0
- m_Index: 130
m_Metrics:
m_Width: 0
m_Height: 0
m_HorizontalBearingX: 0
m_HorizontalBearingY: 0
m_HorizontalAdvance: 54.015625
m_GlyphRect:
m_X: 0
m_Y: 0
m_Width: 0
m_Height: 0
m_Scale: 1
m_AtlasIndex: 0
m_ClassDefinitionType: 0
m_CharacterTable:
- m_ElementType: 1
m_Unicode: 2
@@ -2582,6 +2597,10 @@ MonoBehaviour:
m_Unicode: 8230
m_GlyphIndex: 389
m_Scale: 1
- m_ElementType: 1
m_Unicode: 173
m_GlyphIndex: 130
m_Scale: 1
m_AtlasTextures:
- {fileID: -953588492498898259}
m_AtlasTextureIndex: 0
@@ -24,6 +24,9 @@ namespace Eagle0.Tutorial {
// Register tactical contextual tutorials
RegisterTacticalTutorials(registry);
// Register game state guidance tutorials
RegisterGuidanceTutorials(registry);
}
// ========== ONBOARDING SEQUENCE ==========
@@ -420,8 +423,8 @@ namespace Eagle0.Tutorial {
var divine = CreateCommandPanelTutorial(
"command_divine",
"Divine Heroes",
"Use divination to learn a hero's hidden potential.\n\n" +
"Costs gold but reveals the hero's true abilities and stats. Divine all available heroes at once for efficiency.");
"Pay a soothsayer to divine a hero's hidden desires.\n\n" +
"Reveals the intentions and aspirations of a free hero, showing you what you must do to win them to your cause.");
registry.RegisterTutorial(divine, "command_panel_DivineCommand");
// Issue Orders command
@@ -682,6 +685,47 @@ namespace Eagle0.Tutorial {
registry.RegisterTutorial(charge, "ability_charge_available");
}
// ========== GAME STATE GUIDANCE TUTORIALS ==========
// These trigger based on complex game state analysis, not simple first-encounters.
private static void RegisterGuidanceTutorials(TutorialTriggerRegistry registry) {
// Loyalty danger warning (November)
var loyaltyDanger = CreateSingleStepTutorial(
"guidance_loyalty_danger",
"Loyalty Warning!",
"Some of your heroes are unhappy. At year's end, disloyal heroes may leave your service—or worse, betray you.\n\n" +
"Use <b>Feast</b> to boost loyalty and vigor for all heroes in a province, or <b>Give Gift</b> to greatly increase one hero's loyalty.",
TutorialDisplayMode.Modal);
registry.RegisterTutorial(loyaltyDanger, "guidance_loyalty_danger");
// Neighbor danger warning
var neighborDanger = CreateSingleStepTutorial(
"guidance_neighbor_danger",
"Border Alert!",
"Your province borders hostile territory. An enemy army could march in at any time.\n\n" +
"Consider <b>Organizing Troops</b> for defense, or use <b>Diplomacy</b> to seek a Truce or Alliance with your neighbor.",
TutorialDisplayMode.Modal);
registry.RegisterTutorial(neighborDanger, "guidance_neighbor_danger");
// Recruit heroes guidance
var recruitHeroes = CreateSingleStepTutorial(
"guidance_recruit_heroes",
"Grow Your Court",
"Free heroes are nearby, seeking a lord to serve. More heroes means more armies and more provinces you can manage.\n\n" +
"Use <b>Travel</b> to visit town, then <b>Divine</b> to learn what a hero desires, or <b>Recruit</b> those already willing to join.",
TutorialDisplayMode.Modal);
registry.RegisterTutorial(recruitHeroes, "guidance_recruit_heroes");
// Expand guidance
var expand = CreateSingleStepTutorial(
"guidance_expand",
"Time to Expand",
"Your province is stable. To grow your realm, you'll need to claim new territory.\n\n" +
"Use <b>March</b> to send your Warlord and another hero to a neighboring province. Once there, develop it just like your first province.",
TutorialDisplayMode.Modal);
registry.RegisterTutorial(expand, "guidance_expand");
}
// ========== HELPER METHODS ==========
private static TutorialSequence CreateSingleStepTutorial(
@@ -134,6 +134,9 @@ namespace Eagle0.Tutorial {
// Check for free heroes (recruitment opportunity)
CheckHeroRecruitmentAvailable(model, previousModel);
// Check game state guidance triggers
CheckGuidanceTriggers(model);
}
private void CheckStrategicCommandsAvailable(IGameModel model, IGameModel previousModel) {
@@ -173,6 +176,143 @@ namespace Eagle0.Tutorial {
}
}
// ========== Game State Guidance Triggers ==========
private void CheckGuidanceTriggers(IGameModel model) {
CheckLoyaltyDanger(model);
CheckNeighborDanger(model);
CheckRecruitHeroesGuidance(model);
CheckExpandGuidance(model);
}
/// <summary>
/// Check if it's November and any heroes have low loyalty.
/// </summary>
private void CheckLoyaltyDanger(IGameModel model) {
if (_manager.State.HasCompletedTutorial("guidance_loyalty_danger")) return;
// Only warn in November (month 11)
if (model.CurrentDate?.Month != 11) return;
// Get the player's faction ID
var playerId = model.PlayerId;
if (playerId == null) return;
// Check all heroes belonging to the player's faction
var hasLowLoyaltyHero = model.Heroes?.Values.Any(
h => h.FactionId == playerId && h.Loyalty != null && h.Loyalty.Stat < 70);
if (hasLowLoyaltyHero == true) { OnGameEvent("guidance_loyalty_danger"); }
}
/// <summary>
/// Check if player has provinces adjacent to hostile factions.
/// </summary>
private void CheckNeighborDanger(IGameModel model) {
if (_manager.State.HasCompletedTutorial("guidance_neighbor_danger")) return;
var playerId = model.PlayerId;
if (playerId == null) return;
// Get player's faction relationships
if (!model.ActiveFactions.TryGetValue(playerId.Value, out var playerFaction)) return;
// Check each province the player owns
foreach (var province in model.Provinces.Values) {
if (province.RulingFactionId != playerId) continue;
// Check available march destinations for hostile neighbors
if (model.AvailableCommandsByProvince == null) continue;
if (!model.AvailableCommandsByProvince.TryGetValue(province.Id, out var commands))
continue;
var marchCommand = commands.Commands.FirstOrDefault(c => c.MarchCommand != null);
if (marchCommand?.MarchCommand == null) continue;
// Check all one-province march commands for adjacent hostile provinces
foreach (var oneProvinceCmd in marchCommand.MarchCommand.OneProvinceCommands) {
foreach (var dest in oneProvinceCmd.AvailableDestinationProvinces) {
var destProvince = model.Provinces[dest.ProvinceId];
if (destProvince.RulingFactionId == null ||
destProvince.RulingFactionId == playerId)
continue;
// Check if we have a truce or alliance with this faction
var relationship = playerFaction.FactionRelationships.FirstOrDefault(
fr => fr.TargetFactionId == destProvince.RulingFactionId);
if (relationship == null ||
relationship.RelationshipLevel ==
Net.Eagle0.Eagle.Views.FactionRelationshipView.Types
.RelationshipLevel.Hostile) {
// Found a hostile neighbor!
OnGameEvent("guidance_neighbor_danger");
return;
}
}
}
}
}
/// <summary>
/// Check if player should be recruiting heroes.
/// </summary>
private void CheckRecruitHeroesGuidance(IGameModel model) {
if (_manager.State.HasCompletedTutorial("guidance_recruit_heroes")) return;
var playerId = model.PlayerId;
if (playerId == null) return;
// Check for a province with support >= 40 that has free heroes
foreach (var province in model.Provinces.Values) {
if (province.RulingFactionId != playerId) continue;
if (province.FullInfo?.Support?.Stat < 40) continue;
// Need at least 100 gold in the province to divine
if (province.FullInfo?.Gold < 100) continue;
// Check for free heroes in this province
var freeHeroes = province.FullInfo?.UnaffiliatedHeroes;
if (freeHeroes == null || freeHeroes.Count == 0) continue;
// Check if any are divineable or ready to join
var recruitableHeroes = freeHeroes.Where(
uh => uh.RecruitmentInfo?.Status ==
Net.Eagle0.Eagle.Common.RecruitmentStatus.WouldJoin ||
uh.RecruitmentInfo?.Status ==
Net.Eagle0.Eagle.Common.RecruitmentStatus.NotDivined);
if (recruitableHeroes.Any()) {
OnGameEvent("guidance_recruit_heroes");
return;
}
}
}
/// <summary>
/// Check if player should consider expanding to new provinces.
/// </summary>
private void CheckExpandGuidance(IGameModel model) {
if (_manager.State.HasCompletedTutorial("guidance_expand")) return;
var playerId = model.PlayerId;
if (playerId == null) return;
// Check if player has any province with support >= 40
var stableProvince = model.Provinces.Values.FirstOrDefault(
p => p.RulingFactionId == playerId && p.FullInfo?.Support?.Stat >= 40);
if (stableProvince == null) return;
// Check if player only has one province (hasn't expanded yet)
var playerProvinceCount =
model.Provinces.Values.Count(p => p.RulingFactionId == playerId);
if (playerProvinceCount > 1) return;
// Player has one stable province but hasn't expanded - suggest expansion
OnGameEvent("guidance_expand");
}
/// <summary>
/// Called when a province is selected.
/// </summary>
@@ -26,6 +26,8 @@ namespace Eagle0.Tutorial {
[Header("Heroes Panel")]
[Tooltip("The Resident Heroes panel")]
public RectTransform HeroesPanel;
// Note: WarlordRow, VassalRow1, VassalRow2 are registered dynamically
// by HeroesAndBattalionsPanelController when hero rows are created
[Header("Command Panel")]
[Tooltip("The command selector panel (parent of individual command selectors)")]
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7240f78031ac24c57a64dd5911bb6b51
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f30ffc2982a6b4c98a935d17858d81f5
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1 @@
@import url("unity-theme://default");
@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: 21a93c7d741154ceab05bac2703c07f9
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 12388, guid: 0000000000000000e000000000000000, type: 0}
disableValidation: 0
unsupportedSelectorAction: 0
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: e345257eb90ab41f7b4719389942adc7
@@ -2299,10 +2299,6 @@ func handleLoginPage(w http.ResponseWriter, r *http.Request) {
func handleLoginStart(w http.ResponseWriter, r *http.Request) {
provider := r.URL.Query().Get("provider")
if provider != "discord" && provider != "google" {
http.Error(w, "Invalid provider", http.StatusBadRequest)
return
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
@@ -2313,6 +2309,17 @@ func handleLoginStart(w http.ResponseWriter, r *http.Request) {
pbProvider = authpb.OAuthProvider_OAUTH_PROVIDER_DISCORD
case "google":
pbProvider = authpb.OAuthProvider_OAUTH_PROVIDER_GOOGLE
case "github":
pbProvider = authpb.OAuthProvider_OAUTH_PROVIDER_GITHUB
case "apple":
pbProvider = authpb.OAuthProvider_OAUTH_PROVIDER_APPLE
case "steam":
pbProvider = authpb.OAuthProvider_OAUTH_PROVIDER_STEAM
case "twitch":
pbProvider = authpb.OAuthProvider_OAUTH_PROVIDER_TWITCH
default:
http.Error(w, "Invalid provider", http.StatusBadRequest)
return
}
// Build the return URL for the OAuth callback to redirect back to admin console
@@ -26,6 +26,30 @@
</svg>
Sign in with Google
</a>
<a href="/login/start?provider=github" class="login-btn github">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z"/>
</svg>
Sign in with GitHub
</a>
<a href="/login/start?provider=apple" class="login-btn apple">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"/>
</svg>
Sign in with Apple
</a>
<a href="/login/start?provider=steam" class="login-btn steam">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.188.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 11.999-5.373 11.999-12S18.605 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.455-.397.957-1.497 1.41-2.454 1.012H7.54zm11.415-9.303c0-1.662-1.353-3.015-3.015-3.015-1.665 0-3.015 1.353-3.015 3.015 0 1.665 1.35 3.015 3.015 3.015 1.663 0 3.015-1.35 3.015-3.015zm-5.273-.005c0-1.252 1.013-2.266 2.265-2.266 1.249 0 2.266 1.014 2.266 2.266 0 1.251-1.017 2.265-2.266 2.265-1.253 0-2.265-1.014-2.265-2.265z"/>
</svg>
Sign in with Steam
</a>
<a href="/login/start?provider=twitch" class="login-btn twitch">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
<path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"/>
</svg>
Sign in with Twitch
</a>
</div>
<footer>
@@ -115,6 +139,42 @@
background-color: #f8f8f8;
}
.login-btn.github {
background-color: #24292e;
color: white;
}
.login-btn.github:hover {
background-color: #1b1f23;
}
.login-btn.apple {
background-color: #000000;
color: white;
}
.login-btn.apple:hover {
background-color: #1a1a1a;
}
.login-btn.steam {
background-color: #171a21;
color: white;
}
.login-btn.steam:hover {
background-color: #1b2838;
}
.login-btn.twitch {
background-color: #9146FF;
color: white;
}
.login-btn.twitch:hover {
background-color: #7d3bdb;
}
.login-card footer {
margin-top: 1.5rem;
text-align: center;
@@ -12,6 +12,7 @@ go_library(
"main.go",
"oauth.go",
"sendgrid.go",
"steam_auth.go",
"users.go",
],
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/authservice",
@@ -27,13 +27,38 @@ type AppleTokenResponse struct {
IDToken string `json:"id_token"`
}
// AppleBool handles Apple's inconsistent boolean fields that may be
// either a JSON boolean (true/false) or a string ("true"/"false")
type AppleBool bool
func (b *AppleBool) UnmarshalJSON(data []byte) error {
// Try boolean first
var boolVal bool
if err := json.Unmarshal(data, &boolVal); err == nil {
*b = AppleBool(boolVal)
return nil
}
// Try string
var strVal string
if err := json.Unmarshal(data, &strVal); err == nil {
*b = AppleBool(strVal == "true")
return nil
}
return fmt.Errorf("cannot unmarshal %s into AppleBool", string(data))
}
// AppleIDTokenClaims represents the claims in Apple's id_token
// See: https://developer.apple.com/documentation/sign_in_with_apple/sign_in_with_apple_rest_api/authenticating_users_with_sign_in_with_apple
type AppleIDTokenClaims struct {
jwt.RegisteredClaims
Email string `json:"email,omitempty"`
EmailVerified string `json:"email_verified,omitempty"`
IsPrivateEmail string `json:"is_private_email,omitempty"`
RealUserStatus int `json:"real_user_status,omitempty"`
Email string `json:"email,omitempty"`
EmailVerified AppleBool `json:"email_verified,omitempty"`
IsPrivateEmail AppleBool `json:"is_private_email,omitempty"`
RealUserStatus int `json:"real_user_status,omitempty"`
Nonce string `json:"nonce,omitempty"`
NonceSupported bool `json:"nonce_supported,omitempty"`
CHash string `json:"c_hash,omitempty"`
AuthTime int64 `json:"auth_time,omitempty"`
}
// GenerateAppleClientSecret generates a JWT client_secret for Apple Sign-In
@@ -48,14 +73,19 @@ func GenerateAppleClientSecret() (string, error) {
}
// Parse the private key
// Key may be: raw PEM, or base64-encoded PEM
block, _ := pem.Decode([]byte(privateKeyPEM))
if block == nil {
// Try base64 decoding if not PEM format
// Try base64 decoding (key might be base64-encoded PEM)
keyBytes, err := base64.StdEncoding.DecodeString(privateKeyPEM)
if err != nil {
return "", fmt.Errorf("failed to decode private key: %v", err)
}
block = &pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes}
// Now PEM decode the base64-decoded content
block, _ = pem.Decode(keyBytes)
if block == nil {
return "", fmt.Errorf("failed to PEM decode private key after base64 decode")
}
}
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
@@ -112,8 +142,6 @@ func (s *OAuthService) ExchangeAppleCode(config OAuthProviderConfig, code string
data.Set("code", code)
data.Set("redirect_uri", callbackURL)
log.Printf("Apple token exchange: client_id=%s redirect_uri=%s", config.ClientID, callbackURL)
req, err := http.NewRequest("POST", config.TokenEndpoint, strings.NewReader(data.Encode()))
if err != nil {
return nil, err
@@ -259,6 +287,7 @@ func (s *OAuthService) HandleAppleCallback(w http.ResponseWriter, r *http.Reques
// Parse user info from id_token
userInfo, err := ParseAppleIDToken(tokenResp.IDToken)
if err != nil {
log.Printf("Apple parse user info failed: %v", err)
s.completeWithError(state, err.Error())
http.Error(w, "Failed to parse user info", http.StatusInternalServerError)
return
@@ -335,6 +335,10 @@ func providerToString(p auth.OAuthProvider) string {
return "github"
case auth.OAuthProvider_OAUTH_PROVIDER_APPLE:
return "apple"
case auth.OAuthProvider_OAUTH_PROVIDER_STEAM:
return "steam"
case auth.OAuthProvider_OAUTH_PROVIDER_TWITCH:
return "twitch"
default:
return "unknown"
}
@@ -350,6 +354,10 @@ func stringToProvider(s string) auth.OAuthProvider {
return auth.OAuthProvider_OAUTH_PROVIDER_GITHUB
case "apple":
return auth.OAuthProvider_OAUTH_PROVIDER_APPLE
case "steam":
return auth.OAuthProvider_OAUTH_PROVIDER_STEAM
case "twitch":
return auth.OAuthProvider_OAUTH_PROVIDER_TWITCH
default:
return auth.OAuthProvider_OAUTH_PROVIDER_UNSPECIFIED
}
@@ -165,10 +165,14 @@ func (h *InvitationHTTPHandler) handleLandingPage(w http.ResponseWriter, r *http
GoogleAuthURL string
GitHubAuthURL string
AppleAuthURL string
SteamAuthURL string
TwitchAuthURL string
ShowDiscord bool
ShowGoogle bool
ShowGitHub bool
ShowApple bool
ShowSteam bool
ShowTwitch bool
}{
Code: code,
Valid: valid,
@@ -185,6 +189,8 @@ func (h *InvitationHTTPHandler) handleLandingPage(w http.ResponseWriter, r *http
data.GoogleAuthURL = fmt.Sprintf("/invite/%s/auth/google", code)
data.GitHubAuthURL = fmt.Sprintf("/invite/%s/auth/github", code)
data.AppleAuthURL = fmt.Sprintf("/invite/%s/auth/apple", code)
data.SteamAuthURL = fmt.Sprintf("/invite/%s/auth/steam", code)
data.TwitchAuthURL = fmt.Sprintf("/invite/%s/auth/twitch", code)
// Check which providers are configured
if _, _, err := h.oauthService.GetAuthURL("discord", "", ""); err == nil {
@@ -199,6 +205,12 @@ func (h *InvitationHTTPHandler) handleLandingPage(w http.ResponseWriter, r *http
if _, _, err := h.oauthService.GetAuthURL("apple", "", ""); err == nil {
data.ShowApple = true
}
if _, _, err := h.oauthService.GetAuthURL("steam", "", ""); err == nil {
data.ShowSteam = true
}
if _, _, err := h.oauthService.GetAuthURL("twitch", "", ""); err == nil {
data.ShowTwitch = true
}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
@@ -765,6 +777,14 @@ var landingPageTemplate = template.Must(template.New("landing").Parse(`<!DOCTYPE
background: black;
color: white;
}
.oauth-steam {
background: #171a21;
color: white;
}
.oauth-twitch {
background: #9146FF;
color: white;
}
.expires {
color: #856404;
background: #fff3cd;
@@ -845,6 +865,22 @@ var landingPageTemplate = template.Must(template.New("landing").Parse(`<!DOCTYPE
Continue with Apple
</a>
{{end}}
{{if .ShowSteam}}
<a href="{{.SteamAuthURL}}" class="oauth-button oauth-steam">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.188.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 11.999-5.373 11.999-12S18.605 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.455-.397.957-1.497 1.41-2.454 1.012H7.54zm11.415-9.303c0-1.662-1.353-3.015-3.015-3.015-1.665 0-3.015 1.353-3.015 3.015 0 1.665 1.35 3.015 3.015 3.015 1.663 0 3.015-1.35 3.015-3.015zm-5.273-.005c0-1.252 1.013-2.266 2.265-2.266 1.249 0 2.266 1.014 2.266 2.266 0 1.251-1.017 2.265-2.266 2.265-1.253 0-2.265-1.014-2.265-2.265z"/>
</svg>
Continue with Steam
</a>
{{end}}
{{if .ShowTwitch}}
<a href="{{.TwitchAuthURL}}" class="oauth-button oauth-twitch">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M11.571 4.714h1.715v5.143H11.57zm4.715 0H18v5.143h-1.714zM6 0L1.714 4.286v15.428h5.143V24l4.286-4.286h3.428L22.286 12V0zm14.571 11.143l-3.428 3.428h-3.429l-3 3v-3H6.857V1.714h13.714Z"/>
</svg>
Continue with Twitch
</a>
{{end}}
</div>
<div class="instructions">
@@ -147,6 +147,7 @@ func main() {
// Start HTTP server for OAuth callbacks and invitation pages
http.HandleFunc("/oauth/callback", oauthSvc.HandleCallback)
http.HandleFunc("/oauth/apple/callback", oauthSvc.HandleAppleCallback) // Apple uses POST
http.HandleFunc("/oauth/steam/callback", oauthSvc.HandleSteamCallback) // Steam uses OpenID 2.0
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "OK")
@@ -198,5 +199,24 @@ func getOAuthConfigs() map[string]OAuthProviderConfig {
Scopes: []string{"name", "email"},
CallbackPath: "/oauth/apple/callback", // Apple uses POST callback
},
"steam": {
// Steam uses OpenID 2.0, not OAuth 2.0
// No ClientID/ClientSecret needed - handled specially in GetAuthURL
ClientID: "steam", // Marker to indicate Steam is "configured"
ClientSecret: "steam", // Marker (not actually used)
AuthorizationEndpoint: "https://steamcommunity.com/openid/login",
TokenEndpoint: "", // Not used for OpenID 2.0
UserInfoEndpoint: "", // Not used for OpenID 2.0
Scopes: []string{},
CallbackPath: "/oauth/steam/callback",
},
"twitch": {
ClientID: os.Getenv("TWITCH_CLIENT_ID"),
ClientSecret: os.Getenv("TWITCH_CLIENT_SECRET"),
AuthorizationEndpoint: "https://id.twitch.tv/oauth2/authorize",
TokenEndpoint: "https://id.twitch.tv/oauth2/token",
UserInfoEndpoint: "https://api.twitch.tv/helix/users",
Scopes: []string{"user:read:email"},
},
}
}
+43 -2
View File
@@ -106,8 +106,8 @@ func (s *OAuthService) GetAuthURL(provider string, returnURL string, invitationC
if config.ClientID == "" {
return "", "", fmt.Errorf("provider %s not configured (missing client ID)", provider)
}
// Apple generates client_secret dynamically, so we don't check it here
if provider != "apple" && config.ClientSecret == "" {
// Apple generates client_secret dynamically, Steam uses OpenID 2.0 (no secret needed)
if provider != "apple" && provider != "steam" && config.ClientSecret == "" {
return "", "", fmt.Errorf("provider %s not configured (missing client secret)", provider)
}
@@ -121,6 +121,19 @@ func (s *OAuthService) GetAuthURL(provider string, returnURL string, invitationC
log.Printf("OAuth: created state=%s for provider=%s returnURL=%s hasInvitationCode=%v", state, provider, returnURL, invitationCode != "")
// Steam uses OpenID 2.0, not OAuth 2.0
if provider == "steam" {
// Get base URL for realm
baseURL := s.callbackURL
if idx := strings.LastIndex(baseURL, "/oauth/callback"); idx > 0 {
baseURL = baseURL[:idx]
}
callbackURL := baseURL + "/oauth/steam/callback"
steamAuth := NewSteamOpenID(baseURL, callbackURL)
authURL = steamAuth.GetAuthURL(state)
return authURL, state, nil
}
// Use custom callback path if configured, otherwise use default
callbackURL := s.callbackURL
if config.CallbackPath != "" {
@@ -325,6 +338,11 @@ func (s *OAuthService) fetchUserInfo(provider string, config OAuthProviderConfig
req.Header.Set("Authorization", "Bearer "+accessToken)
req.Header.Set("Accept", "application/json")
// Twitch requires Client-ID header in addition to Bearer token
if provider == "twitch" {
req.Header.Set("Client-Id", config.ClientID)
}
resp, err := s.httpClient.Do(req)
if err != nil {
return nil, err
@@ -461,6 +479,29 @@ func (s *OAuthService) parseUserInfo(provider string, data []byte) (*ProviderUse
AvatarURL: avatarURL,
}, nil
case "twitch":
// Twitch returns {data: [{id, login, display_name, email, profile_image_url}]}
dataArr, ok := result["data"].([]interface{})
if !ok || len(dataArr) == 0 {
return nil, fmt.Errorf("invalid Twitch response: missing data array")
}
user, ok := dataArr[0].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid Twitch response: invalid user data")
}
id, _ := user["id"].(string)
displayName, _ := user["display_name"].(string)
email, _ := user["email"].(string)
profileImageURL, _ := user["profile_image_url"].(string)
return &ProviderUserInfo{
ID: id,
Username: displayName,
Email: email,
AvatarURL: profileImageURL,
}, nil
default:
return nil, fmt.Errorf("unsupported provider: %s", provider)
}
@@ -0,0 +1,212 @@
package main
import (
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"time"
)
// SteamOpenID handles Steam's OpenID 2.0 authentication
// Steam uses OpenID 2.0, not OAuth 2.0, so it requires different handling
type SteamOpenID struct {
realm string // Our server URL (e.g., https://auth.eagle0.net)
callbackURL string // Where Steam redirects back
httpClient *http.Client
}
const (
steamOpenIDURL = "https://steamcommunity.com/openid/login"
// Steam ID pattern in claimed_id: https://steamcommunity.com/openid/id/76561198012345678
steamIDPattern = `^https://steamcommunity\.com/openid/id/(\d+)$`
)
var steamIDRegex = regexp.MustCompile(steamIDPattern)
// NewSteamOpenID creates a new Steam OpenID handler
func NewSteamOpenID(realm, callbackURL string) *SteamOpenID {
return &SteamOpenID{
realm: realm,
callbackURL: callbackURL,
httpClient: &http.Client{
Timeout: 10 * time.Second,
},
}
}
// GetAuthURL generates the Steam login URL for OpenID 2.0
func (s *SteamOpenID) GetAuthURL(state string) string {
params := url.Values{}
params.Set("openid.ns", "http://specs.openid.net/auth/2.0")
params.Set("openid.mode", "checkid_setup")
params.Set("openid.return_to", s.callbackURL+"?state="+state)
params.Set("openid.realm", s.realm)
params.Set("openid.identity", "http://specs.openid.net/auth/2.0/identifier_select")
params.Set("openid.claimed_id", "http://specs.openid.net/auth/2.0/identifier_select")
return steamOpenIDURL + "?" + params.Encode()
}
// HandleCallback validates the OpenID response and extracts Steam ID
// Returns the Steam ID (e.g., "76561198012345678") on success
func (s *SteamOpenID) HandleCallback(r *http.Request) (steamID string, err error) {
// Get OpenID parameters from the callback
query := r.URL.Query()
// Check for errors
if mode := query.Get("openid.mode"); mode == "error" || mode == "cancel" {
return "", fmt.Errorf("Steam login was cancelled or failed")
}
// Validate the response signature with Steam
if err := s.ValidateResponse(query); err != nil {
return "", fmt.Errorf("Steam response validation failed: %v", err)
}
// Extract Steam ID from claimed_id
claimedID := query.Get("openid.claimed_id")
matches := steamIDRegex.FindStringSubmatch(claimedID)
if len(matches) != 2 {
return "", fmt.Errorf("invalid Steam claimed_id format: %s", claimedID)
}
return matches[1], nil
}
// ValidateResponse verifies the OpenID response signature with Steam
func (s *SteamOpenID) ValidateResponse(params url.Values) error {
// Create validation request with same params but mode=check_authentication
validationParams := url.Values{}
for key, values := range params {
if len(values) > 0 {
validationParams.Set(key, values[0])
}
}
validationParams.Set("openid.mode", "check_authentication")
// POST to Steam to verify
req, err := http.NewRequest("POST", steamOpenIDURL, strings.NewReader(validationParams.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := s.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
// Steam returns key-value pairs, one per line
// We're looking for "is_valid:true"
responseStr := string(body)
if !strings.Contains(responseStr, "is_valid:true") {
return fmt.Errorf("Steam validation failed: %s", responseStr)
}
return nil
}
// HandleSteamCallback handles the Steam OpenID callback for OAuthService
func (s *OAuthService) HandleSteamCallback(w http.ResponseWriter, r *http.Request) {
log.Printf("Steam callback received")
// Get state parameter
state := r.URL.Query().Get("state")
if state == "" {
log.Printf("Steam callback: missing state parameter")
http.Error(w, "Missing state parameter", http.StatusBadRequest)
return
}
// Get pending OAuth info
stateData, ok := s.pendingOAuth.Load(state)
if !ok {
log.Printf("Steam callback: state not found: %s", state)
http.Error(w, "Invalid or expired state", http.StatusBadRequest)
return
}
oauthState := stateData.(OAuthState)
if time.Since(oauthState.CreatedAt) >= stateExpiration {
s.pendingOAuth.Delete(state)
s.completeWithError(state, "OAuth session expired")
http.Error(w, "OAuth session expired", http.StatusBadRequest)
return
}
// Create Steam OpenID handler
realm := getServerBaseURL()
callbackURL := realm + "/oauth/steam/callback"
steamAuth := NewSteamOpenID(realm, callbackURL)
// Handle the callback
steamID, err := steamAuth.HandleCallback(r)
if err != nil {
log.Printf("Steam callback failed: %v", err)
s.completeWithError(state, err.Error())
http.Error(w, "Steam login failed: "+err.Error(), http.StatusBadRequest)
return
}
log.Printf("Steam callback: success, steamID=%s", steamID)
// Create user info from Steam ID
// Note: Steam OpenID only provides the Steam ID, no email or username
userInfo := &ProviderUserInfo{
ID: steamID,
Email: "", // Steam doesn't provide email via OpenID
Username: "", // Steam doesn't provide username via OpenID
AvatarURL: "", // Steam doesn't provide avatar via OpenID
}
// Mark as completed
s.pendingOAuth.Delete(state)
s.completedOAuth.Store(state, OAuthResult{
Success: true,
UserInfo: userInfo,
Provider: "steam",
InvitationCode: oauthState.InvitationCode,
})
// Redirect or show close message
if oauthState.ReturnURL != "" {
redirectURL := oauthState.ReturnURL
if strings.Contains(redirectURL, "?") {
redirectURL += "&state=" + state
} else {
redirectURL += "?state=" + state
}
http.Redirect(w, r, redirectURL, http.StatusFound)
return
}
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, `<!DOCTYPE html>
<html>
<head><title>Login Successful</title></head>
<body>
<h1>Login Successful!</h1>
<p>You can close this window and return to the game.</p>
<script>window.close();</script>
</body>
</html>`)
}
// getServerBaseURL returns the server base URL from environment
func getServerBaseURL() string {
if baseURL := os.Getenv("SERVER_BASE_URL"); baseURL != "" {
return baseURL
}
return "http://localhost:8080"
}
@@ -41,6 +41,8 @@ enum OAuthProvider {
OAUTH_PROVIDER_GOOGLE = 2;
OAUTH_PROVIDER_GITHUB = 3;
OAUTH_PROVIDER_APPLE = 4;
OAUTH_PROVIDER_STEAM = 5;
OAUTH_PROVIDER_TWITCH = 6;
}
// Request to initiate OAuth flow
@@ -68,20 +68,6 @@ scala_binary(
# ],
#)
#scala_binary(
# name = "saved_game_utils",
# srcs = ["SavedGameUtils.scala"],
# main_class = "net.eagle0.eagle.SavedGameUtils",
# resources = [],
# visibility = ["//visibility:public"],
# deps = [
# "//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
# "//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
# "//src/main/scala/net/eagle0/eagle/service:persisted_history",
# "//src/main/scala/net/eagle0/eagle/service/persistence:local_file_persister",
# ],
#)
scala_library(
name = "eagle_pkg",
srcs = ["package.scala"],
+3 -3
View File
@@ -9,7 +9,7 @@ import io.sentry.Sentry
import net.eagle0.common.{FunctionalRandom, SeededRandom, SimpleTimedLogger}
import net.eagle0.eagle.api.auth.auth.AuthGrpc
import net.eagle0.eagle.api.eagle.EagleGrpc
import net.eagle0.eagle.auth.{JwtService, JwtServiceImpl, OAuthService, OAuthServiceImpl, UserService, UserServiceImpl}
import net.eagle0.eagle.auth.{JwtService, OAuthService, OAuthServiceImpl, UserService, UserServiceImpl}
import net.eagle0.eagle.service.*
import net.eagle0.eagle.service.persistence.{AsyncS3Persister, LocalFilePersister, SaveDirectory}
@@ -200,8 +200,8 @@ object Main {
): Server = {
val serverBuilder = ServerBuilder.forPort(grpcPort)
// Initialize JWT service
val jwtService: Option[JwtService] = JwtServiceImpl.fromEnvironment()
// Initialize JWT service (validation only - signing is handled by Auth service)
val jwtService: Option[JwtService] = JwtService.fromEnvironment()
// Add Eagle game service
serverBuilder.addService(
@@ -1,266 +0,0 @@
package net.eagle0.eagle
import scala.annotation.tailrec
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.internal.game_state.GameState
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
import net.eagle0.eagle.service.persistence.LocalFilePersister
import net.eagle0.eagle.service.PersistedHistory
import scalapb.{GeneratedMessage, GeneratedMessageCompanion}
import scalapb.descriptors.*
object MessageComparators {
@tailrec
def fieldDescriptors(
comp: GeneratedMessageCompanion[?],
fieldNames: Vector[String],
acc: Vector[FieldDescriptor] = Vector()
): Vector[FieldDescriptor] = fieldNames match {
case name +: tail =>
val field =
comp.scalaDescriptor.fields
.find(_.name == name)
.getOrElse {
println(s"No field $name in ${comp.scalaDescriptor.name}")
sys.exit(1)
}
if tail.isEmpty then acc :+ field
else
fieldDescriptors(
comp.messageCompanionForFieldNumber(field.number),
tail,
acc :+ field
)
case _ => acc
}
def nestedPValue(
gm: GeneratedMessage,
fieldDescriptors: Vector[FieldDescriptor]
): PValue =
if gm == null then PEmpty
else
fieldDescriptors match {
case Vector(fd) => gm.getField(fd)
case fd +: tail =>
nestedPValue(
gm.getFieldByNumber(fd.number).asInstanceOf[GeneratedMessage],
tail
)
case _ => gm.toPMessage
}
def typedComparison[T](
fieldDescriptors: Vector[FieldDescriptor],
comparator: String,
value: String
): (GeneratedMessage => Boolean) = comparator match {
case "=" =>
gm =>
nestedPValue(gm, fieldDescriptors) match {
case PInt(i) => i == value.toInt
case PDouble(d) => d == value.toDouble
case PString(s) => s == value
case PBoolean(b) => b == value.toBoolean
case PEmpty => value.isEmpty
case x =>
throw new NotImplementedError(s"$x pvalue not implemented")
}
case x => throw new NotImplementedError(s"$x comparator not implemented")
}
}
object GameStateFilters {
val comparisonChars: Vector[Char] = Vector('=')
def filter(filter: String): ActionWithResultingState => Boolean = {
val (left, rightWithComparator) = filter.span(!comparisonChars.contains(_))
val (comparator, right) = rightWithComparator.splitAt(1)
val fieldNames = left.split('.').toVector
val (prefix, remaining) = fieldNames.splitAt(1)
prefix.head match {
case "gs" =>
val fieldDescriptors =
MessageComparators.fieldDescriptors(
GameState.messageCompanion,
remaining
)
awrs =>
MessageComparators.typedComparison(
fieldDescriptors,
comparator,
right
)(
awrs.gameState
)
case "ar" =>
val fieldDescriptors =
MessageComparators.fieldDescriptors(
ActionResult.messageCompanion,
remaining
)
awrs =>
MessageComparators.typedComparison(
fieldDescriptors,
comparator,
right
)(
awrs.actionResult
)
}
}
}
object SavedGameUtils {
def filterOne(
results: Vector[ActionWithResultingState],
filter: String
): Vector[ActionWithResultingState] =
results.filter {
case awrs =>
GameStateFilters.filter(filter)(awrs)
}
def filteredResults(
results: Vector[ActionWithResultingState],
filters: Vector[String]
): Vector[ActionWithResultingState] =
filters.foldLeft(results)(filterOne)
case class FieldWithValue(
name: String,
value: PValue
)
def fieldValues(
results: Vector[ActionWithResultingState],
fields: Vector[String]
): Vector[FieldWithValue] =
for {
result <- results
qualifiedFieldName <- fields
} yield {
val (ident, subfieldNames) = qualifiedFieldName.split('.').splitAt(1)
ident.head match {
case "ar" =>
FieldWithValue(
name = qualifiedFieldName,
value = MessageComparators.nestedPValue(
result.actionResult,
MessageComparators.fieldDescriptors(
ActionResult.messageCompanion,
subfieldNames.toVector
)
)
)
case "gs" =>
FieldWithValue(
name = qualifiedFieldName,
value = MessageComparators.nestedPValue(
result.gameState,
MessageComparators.fieldDescriptors(
GameState.messageCompanion,
subfieldNames.toVector
)
)
)
}
}
def main(args: Array[String]): Unit = {
val argList = args.toList
if argList.size < 1 then {
println("Must supply a directory path")
sys.exit(1)
}
val dirPath = argList.head
val allResults = PersistedHistory(123L, LocalFilePersister(dirPath)).get.all
val _ = go(FilteredResultsState(allResults, Vector(), Vector(), allResults))
}
case class FilteredResultsState(
unfilteredResults: Vector[ActionWithResultingState],
filters: Vector[String],
prints: Vector[String],
partialResults: Vector[ActionWithResultingState]
) {
def withFilters(newFilters: Vector[String]): FilteredResultsState =
this.copy(
filters = filters ++ newFilters,
partialResults = filteredResults(partialResults, newFilters)
)
def withPrints(newPrints: Vector[String]): FilteredResultsState =
this.copy(
prints = prints ++ newPrints
)
def withoutFilterIndices(indices: Vector[Int]): FilteredResultsState = {
val newFilters = filters.zipWithIndex.filterNot { case (_, idx) => indices.contains(idx) }
.map(_._1)
this.copy(
filters = newFilters,
partialResults = filteredResults(unfilteredResults, newFilters)
)
}
def withoutPrintIndices(indices: Vector[Int]): FilteredResultsState =
this.copy(
prints = prints.zipWithIndex.filterNot { case (_, idx) => indices.contains(idx) }
.map(_._1)
)
}
def go(
state: FilteredResultsState
): FilteredResultsState = {
println("Your filters:")
state.filters.zipWithIndex.foreach {
case (filt, idx) =>
println(s" ($idx) $filt")
}
println("Your prints:")
state.prints.zipWithIndex.foreach {
case (pf, idx) =>
println(s" ($idx) $pf")
}
println(s"${state.partialResults.size} filtered results")
println("Your command: ")
val args = scala.io.StdIn.readLine().split(' ').toList
val newState = args match {
case Nil => state
case "f" :: filters => state.withFilters(filters.toVector)
case "p" :: prints => state.withPrints(prints.toVector)
case "df" :: idxStrings =>
state.withoutFilterIndices(idxStrings.map(_.toInt).toVector)
case "dp" :: idxStrings =>
state.withoutPrintIndices(idxStrings.map(_.toInt).toVector)
case "q" :: _ => sys.exit(0)
case x :: _ =>
println(s"Unknown command $x, stopping")
state
}
if newState.partialResults.size > 1000 then
println(
s"Not printing now, too many results (${newState.partialResults.size}"
)
else if newState.prints.nonEmpty then
fieldValues(newState.partialResults, newState.prints)
.foreach(v => println(s"${v.name}: ${v.value}"))
else newState.partialResults.foreach(println)
println(s"${newState.partialResults.size} filtered results")
go(newState)
}
}
@@ -1,17 +1,15 @@
package net.eagle0.eagle.auth
import java.security.MessageDigest
import java.security.interfaces.RSAPublicKey
import java.security.spec.X509EncodedKeySpec
import java.security.KeyFactory
import java.time.Instant
import java.util.{Date, UUID}
import java.util.{Base64, Date}
import scala.jdk.CollectionConverters.*
import scala.util.Try
import com.nimbusds.jose.{JWSAlgorithm, JWSHeader}
import com.nimbusds.jose.crypto.{RSASSASigner, RSASSAVerifier}
import com.nimbusds.jose.jwk.gen.RSAKeyGenerator
import com.nimbusds.jose.jwk.RSAKey
import com.nimbusds.jwt.{JWTClaimsSet, SignedJWT}
import com.nimbusds.jose.crypto.RSASSAVerifier
import com.nimbusds.jwt.SignedJWT
/** JWT claims extracted from a validated token */
case class JwtClaims(
@@ -22,84 +20,29 @@ case class JwtClaims(
expiresAt: Instant
)
/** Refresh token with metadata */
case class RefreshTokenInfo(
tokenId: String,
userId: String,
tokenHash: String,
expiresAt: Instant
)
/** Service for creating and validating JWT tokens */
/**
* Service for validating JWT tokens.
*
* This service is validation-only. Token signing is handled by the Auth service (Go).
*/
trait JwtService {
/** Create an access token for a user */
def createAccessToken(userId: String, displayName: String, isAdmin: Boolean): String
/** Create a refresh token (returns token string and info for storage) */
def createRefreshToken(userId: String): (String, RefreshTokenInfo)
/** Validate an access token and extract claims */
def validateAccessToken(token: String): Option[JwtClaims]
/** Hash a refresh token for storage */
def hashRefreshToken(token: String): String
}
class JwtServiceImpl(val rsaKey: RSAKey) extends JwtService {
/**
* JWT validation service using an RSA public key.
*
* This implementation validates tokens signed by the Auth service. It cannot create or sign tokens.
*
* @param publicKey
* RSA public key for signature verification
*/
class JwtValidationService(publicKey: RSAPublicKey) extends JwtService {
private val accessTokenDuration = java.time.Duration.ofDays(7)
private val refreshTokenDuration = java.time.Duration.ofDays(30)
private val issuer = "eagle0"
private val signer = new RSASSASigner(rsaKey)
private val verifier = new RSASSAVerifier(rsaKey.toPublicJWK)
override def createAccessToken(
userId: String,
displayName: String,
isAdmin: Boolean
): String = {
val now = Instant.now()
val expiresAt = now.plus(accessTokenDuration)
val claimsBuilder = new JWTClaimsSet.Builder()
.subject(userId)
.issuer(issuer)
.issueTime(Date.from(now))
.expirationTime(Date.from(expiresAt))
.claim("name", displayName)
// Only include admin claim if true to keep tokens smaller
if isAdmin then {
claimsBuilder.claim("admin", true)
}
val signedJWT = new SignedJWT(
new JWSHeader.Builder(JWSAlgorithm.RS256).keyID(rsaKey.getKeyID).build(),
claimsBuilder.build()
)
signedJWT.sign(signer)
signedJWT.serialize()
}
override def createRefreshToken(userId: String): (String, RefreshTokenInfo) = {
val tokenId = UUID.randomUUID().toString
val tokenSecret = UUID.randomUUID().toString
val token = s"$tokenId.$tokenSecret"
val tokenHash = hashRefreshToken(token)
val expiresAt = Instant.now().plus(refreshTokenDuration)
val info = RefreshTokenInfo(
tokenId = tokenId,
userId = userId,
tokenHash = tokenHash,
expiresAt = expiresAt
)
(token, info)
}
private val issuer = "eagle0"
private val verifier = new RSASSAVerifier(publicKey)
override def validateAccessToken(token: String): Option[JwtClaims] =
Try {
@@ -139,59 +82,69 @@ class JwtServiceImpl(val rsaKey: RSAKey) extends JwtService {
)
)
}.toOption.flatten
override def hashRefreshToken(token: String): String = {
val digest = MessageDigest.getInstance("SHA-256")
val hashBytes = digest.digest(token.getBytes("UTF-8"))
hashBytes.map("%02x".format(_)).mkString
}
}
object JwtServiceImpl {
object JwtService {
/** Generate a new RSA key pair for JWT signing */
def generateKey(): RSAKey =
new RSAKeyGenerator(2048)
.keyID(UUID.randomUUID().toString)
.generate()
/** Load RSA key from JSON string (JWK format) */
def loadKey(json: String): RSAKey =
RSAKey.parse(json)
/** Create a JwtService with a new generated key (for testing) */
def withGeneratedKey(): JwtServiceImpl =
new JwtServiceImpl(generateKey())
/** Create a JwtService from a JWK JSON string */
def fromKeyJson(json: String): JwtServiceImpl =
new JwtServiceImpl(loadKey(json))
private val defaultPublicKeyPath = "/etc/eagle0/keys/public.pem"
/**
* Create a JwtService from environment variables.
* Create a validation-only service from a PEM-format public key file.
*
* Looks for JWT_PRIVATE_KEY environment variable containing RSA key in JWK format. If not found, returns None (auth
* will fall back to Basic Auth only).
* @param path
* Path to the PEM file containing the RSA public key
* @return
* JwtService for token validation
*/
def fromEnvironment(): Option[JwtServiceImpl] =
sys.env.get("JWT_PRIVATE_KEY") match {
case None =>
println("[JwtService] JWT_PRIVATE_KEY environment variable not set")
None
case Some(keyJson) if keyJson.trim.isEmpty =>
println("[JwtService] JWT_PRIVATE_KEY environment variable is empty")
None
case Some(keyJson) =>
Try {
val service = fromKeyJson(keyJson)
val keyId = service.rsaKey.getKeyID
val keyLen = service.rsaKey.size()
println(s"[JwtService] Loaded RSA key: id=$keyId, size=${keyLen}bits")
service
}.recover {
case e: Exception =>
println(s"[JwtService] Failed to parse JWT_PRIVATE_KEY: ${e.getMessage}")
throw e
}.toOption
def fromPublicKeyFile(path: String): JwtService = {
val pem = scala.io.Source.fromFile(path).mkString
val publicKey = loadPublicKeyFromPem(pem)
new JwtValidationService(publicKey)
}
/**
* Create a JwtService from environment or public key file.
*
* Attempts to load JWT validation keys in order:
* 1. JWT_PUBLIC_KEY_FILE environment variable (explicit path)
* 2. /etc/eagle0/keys/public.pem (default location from shared volume with auth service)
*
* @return
* Some(JwtService) if keys are available, None otherwise
*/
def fromEnvironment(): Option[JwtService] = {
val path = sys.env.get("JWT_PUBLIC_KEY_FILE").filter(_.trim.nonEmpty) match {
case Some(p) => p
case None =>
val defaultFile = new java.io.File(defaultPublicKeyPath)
if defaultFile.exists() then defaultPublicKeyPath
else {
println(s"[JwtService] No public key found (checked JWT_PUBLIC_KEY_FILE env and $defaultPublicKeyPath)")
return None
}
}
Try {
val service = fromPublicKeyFile(path)
println(s"[JwtService] Loaded public key from: $path")
service
}.recover {
case e: Exception =>
println(s"[JwtService] Failed to load public key from $path: ${e.getMessage}")
throw e
}.toOption
}
private def loadPublicKeyFromPem(pem: String): RSAPublicKey = {
// Strip PEM headers/footers and whitespace, then base64 decode
val base64 = pem
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "")
val keyBytes = Base64.getDecoder.decode(base64)
val keySpec = new X509EncodedKeySpec(keyBytes)
val keyFactory = KeyFactory.getInstance("RSA")
keyFactory.generatePublic(keySpec).asInstanceOf[RSAPublicKey]
}
}
@@ -8,8 +8,8 @@ scala_library(
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
],
)
@@ -20,13 +20,16 @@ scala_library(
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
exports = [":pregenerated_text_store"],
exports = [
":pregenerated_text_store",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
],
deps = [
":client_text",
":pregenerated_text_store",
":text_generation_result",
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
],
)
@@ -48,6 +51,8 @@ scala_library(
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:generated_text_request_converter",
"//src/main/scala/net/eagle0/eagle/service/persistence:persister",
],
)
@@ -1,6 +1,6 @@
package net.eagle0.eagle.client_text
import net.eagle0.eagle.internal.generated_text_request.GeneratedTextRequest
import net.eagle0.eagle.model.action_result.generated_text_request.GeneratedTextRequestT
import net.eagle0.eagle.ClientTextId
sealed trait ClientText {
@@ -21,7 +21,7 @@ case class IncompleteClientText(
id: ClientTextId,
partialText: String,
requestedAfterHistoryCount: Int,
llmRequest: GeneratedTextRequest,
llmRequest: GeneratedTextRequestT,
requestedAtMillis: Long,
lastUpdateAtMillis: Long
) extends ClientText {
@@ -36,7 +36,7 @@ case class IncompleteClientText(
case class UnrequestedClientText(
id: ClientTextId,
requestedAfterHistoryCount: Int,
llmRequest: GeneratedTextRequest
llmRequest: GeneratedTextRequestT
) extends ClientText {
def complete: Boolean = false
def text: String = ""
@@ -1,7 +1,7 @@
package net.eagle0.eagle.client_text
import net.eagle0.eagle.{ClientTextId, FactionId}
import net.eagle0.eagle.internal.generated_text_request.GeneratedTextRequest
import net.eagle0.eagle.model.action_result.generated_text_request.GeneratedTextRequestT
case class ClientTextStoreWithUpdate(
clientTextStore: ClientTextStore,
@@ -31,7 +31,7 @@ trait ClientTextStore {
def withAddedTextRequest(
id: ClientTextId,
accessibleTo: Vector[FactionId],
llmRequest: GeneratedTextRequest,
llmRequest: GeneratedTextRequestT,
requestedAfterHistoryCount: Int
): ClientTextStore
@@ -10,9 +10,10 @@ import scala.util.Try
import net.eagle0.common.ProtoParser
import net.eagle0.eagle.{ClientTextId, FactionId}
import net.eagle0.eagle.internal.client_text.{IncompleteText, IncompleteTextStore, UnrequestedText}
import net.eagle0.eagle.internal.generated_text_request.GeneratedTextRequest
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.library.EagleInternalException
import net.eagle0.eagle.model.action_result.generated_text_request.GeneratedTextRequestT
import net.eagle0.eagle.model.proto_converters.GeneratedTextRequestConverter
import net.eagle0.eagle.service.persistence.Persister
case class ClientTextStoreImpl(
@@ -31,7 +32,7 @@ case class ClientTextStoreImpl(
def withAddedTextRequest(
id: ClientTextId,
accessibleTo: Vector[FactionId],
llmRequest: GeneratedTextRequest,
llmRequest: GeneratedTextRequestT,
requestedAfterHistoryCount: Int
): ClientTextStore = {
internalRequire(
@@ -240,7 +241,7 @@ object ClientTextStoreImpl {
IncompleteText(
id = ict.id,
partialText = ict.text,
llmRequest = Some(ict.llmRequest),
llmRequest = Some(GeneratedTextRequestConverter.toProto(ict.llmRequest)),
requestedAfterHistoryCount = ict.requestedAfterHistoryCount,
requestedAtMillis = ict.requestedAtMillis,
lastUpdateAtMillis = ict.lastUpdateAtMillis
@@ -250,7 +251,7 @@ object ClientTextStoreImpl {
case (_: String, ur: UnrequestedClientText) =>
UnrequestedText(
id = ur.id,
llmRequest = Some(ur.llmRequest),
llmRequest = Some(GeneratedTextRequestConverter.toProto(ur.llmRequest)),
requestedAfterHistoryCount = ur.requestedAfterHistoryCount
)
}.toVector
@@ -335,7 +336,7 @@ object ClientTextStoreImpl {
IncompleteClientText(
id = it.id,
partialText = it.partialText,
llmRequest = it.llmRequest.get,
llmRequest = GeneratedTextRequestConverter.fromProto(it.llmRequest.get),
requestedAfterHistoryCount = it.requestedAfterHistoryCount,
// Use persisted timestamp if available, otherwise use current time
// (for backwards compatibility with old persisted data)
@@ -350,7 +351,7 @@ object ClientTextStoreImpl {
icts.unrequestedTexts.map { it =>
UnrequestedClientText(
id = it.id,
llmRequest = it.llmRequest.get,
llmRequest = GeneratedTextRequestConverter.fromProto(it.llmRequest.get),
requestedAfterHistoryCount = it.requestedAfterHistoryCount
)
}.toVector
@@ -374,13 +375,17 @@ object ClientTextStoreImpl {
.fromInputStream(inputStream)
.mkString
.split(separatorPattern)
.map { entryText =>
.flatMap { entryText =>
entryText.split("\n") match {
case Array(
id,
accessibleToString
) =>
id -> accessibleToString.split(",").map(_.toInt).toVector
Some(id -> accessibleToString.split(",").map(_.toInt).toVector)
case x =>
println(s"Unexpected entry text: ${x.mkString(", ")}")
None
}
}
.toMap
@@ -389,6 +394,8 @@ object ClientTextStoreImpl {
case _: FileNotFoundException =>
println("No accessibleTo store found")
Map()
case x => throw x
}
def loaded(
@@ -1,49 +1,22 @@
package net.eagle0.eagle.library
import net.eagle0.eagle.common.action_result_notification_details.Notification
import net.eagle0.eagle.common.action_result_type.ActionResultType.*
import net.eagle0.eagle.common.action_result_type.ActionResultType as ActionResultTypeProto
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.view_filters.GameStateViewFilter
import net.eagle0.eagle.library.util.GameStateViewDiffer
import net.eagle0.eagle.model.action_result.types.ActionResultType as ActionResultTypeScala
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.view.game_state.GameStateViewDiffConverter
import net.eagle0.eagle.model.proto_converters.NotificationConverter
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.RoundPhase
import net.eagle0.eagle.model.view.game_state.GameStateViewDiff as ScalaGameStateViewDiff
import net.eagle0.eagle.views.action_result_view.ActionResultView
import net.eagle0.eagle.views.game_state_view.GameStateViewDiff as GameStateViewDiffProto
import net.eagle0.eagle.FactionId
object ActionResultFilter {
private val UNIVERSALLY_VISIBLE_TYPES: Vector[ActionResultTypeProto] = Vector(
GAME_START,
START_BATTLE,
BATTLE_ENDED,
PROVINCE_CONQUERED,
PROVINCE_HELD,
END_PROVINCE_EVENTS_PHASE,
END_UNAFFILIATED_HERO_ACTIONS_PHASE,
END_VASSAL_COMMANDS_PHASE,
END_PLAYER_COMMANDS_PHASE,
END_RESOLUTION_PHASE,
END_AFTERMATH_PHASE,
FACTION_LEADER_REMOVED,
NEW_ROUND_ACTION,
NEW_YEAR_ACTION,
TRUCE_ACCEPTED,
ARMY_SHATTERED,
RIOT_OCCURRED,
WEATHER_TOOK_EFFECT,
EPIDEMIC_TOOK_EFFECT,
SUPPRESS_BEASTS_FAILED,
INVITATION_ACCEPTED,
PRISONERS_EXCHANGED,
RANSOM_ACCEPTED
)
private val UNIVERSALLY_VISIBLE_TYPES_SCALA: Vector[ActionResultTypeScala] = Vector(
private val UNIVERSALLY_VISIBLE_TYPES: Vector[ActionResultTypeScala] = Vector(
ActionResultTypeScala.GameStart,
ActionResultTypeScala.StartBattle,
ActionResultTypeScala.BattleEnded,
@@ -71,15 +44,15 @@ object ActionResultFilter {
private def includeForPlayer(
factionId: Option[FactionId],
result: ActionWithResultingState
result: ActionResultWithResultingState
): Boolean =
factionId.forall { fid =>
val scalaResult = result.scalaActionResult
val scalaResult = result.actionResult
scalaResult.actingFactionId.contains(fid) ||
scalaResult.affectedFactionIds.contains(fid) ||
UNIVERSALLY_VISIBLE_TYPES_SCALA.contains(scalaResult.actionResultType) ||
UNIVERSALLY_VISIBLE_TYPES.contains(scalaResult.actionResultType) ||
scalaResult.provinceId.exists(pid =>
result.scalaGameState
result.resultingState
.provinces(pid)
.rulingFactionId
.contains(fid)
@@ -91,7 +64,7 @@ object ActionResultFilter {
private def observeOne(
startingState: GameState,
result: ActionWithResultingState
result: ActionResultWithResultingState
): Option[ActionResultView] =
transformOne(
result = result,
@@ -101,7 +74,7 @@ object ActionResultFilter {
private def filterOne(
startingState: GameState,
result: ActionWithResultingState,
result: ActionResultWithResultingState,
factionId: FactionId
): Option[ActionResultView] =
transformOne(
@@ -133,7 +106,7 @@ object ActionResultFilter {
private def transformOne(
startingState: GameState,
result: ActionWithResultingState,
result: ActionResultWithResultingState,
factionId: Option[FactionId]
): Option[ActionResultView] = {
val actionResult = result.actionResult
@@ -141,17 +114,17 @@ object ActionResultFilter {
val gsDiff = filteredGameStateDiff(
before = startingState,
after = result.scalaGameState,
after = result.resultingState,
factionId = factionId
)
def shouldInclude(note: Notification): Boolean =
note.targetFactions.isEmpty || factionId.exists(fid => note.targetFactions.exists(_.factionId == fid))
def shouldIncludeNotification(targetFactionIds: Vector[FactionId]): Boolean =
targetFactionIds.isEmpty || factionId.exists(targetFactionIds.contains)
if gsDiff.isDefined &&
result.actionResult.player.isDefined &&
actionResult.actingFactionId.isDefined &&
factionId.exists { askingFid =>
!result.actionResult.player.exists(targetFid =>
!actionResult.actingFactionId.exists(targetFid =>
targetFid == askingFid ||
FactionUtils.hasAlliance(
askingFid,
@@ -166,20 +139,26 @@ object ActionResultFilter {
println(actionResult)
}
Option.when(includeForPlayer(factionId, result) || gsDiff.nonEmpty) {
// Convert Scala types to proto for the view
val filteredNotifications = actionResult.newNotifications
.filter(n => shouldIncludeNotification(n.targetFactionIds))
.map(NotificationConverter.toProto)
.collect { case (proto, true) => proto }
ActionResultView(
`type` = actionResult.`type`,
player = actionResult.player,
province = actionResult.province,
leader = actionResult.leader,
`type` = ActionResultTypeProto.fromValue(actionResult.actionResultType.value),
player = actionResult.actingFactionId,
province = actionResult.provinceId,
leader = actionResult.actingHeroId,
gameStateDiff = gsDiff,
notifications = actionResult.notificationsToDeliver.filter(shouldInclude)
notifications = filteredNotifications
)
}
}
def filterForOptionalPlayer(
startingState: GameState,
results: Vector[ActionWithResultingState],
results: Vector[ActionResultWithResultingState],
factionId: Option[FactionId]
): Vector[ActionResultView] =
factionId.map { pid =>
@@ -189,7 +168,7 @@ object ActionResultFilter {
def filterForPlayer(
startingState: GameState,
results: Vector[ActionWithResultingState],
results: Vector[ActionResultWithResultingState],
factionId: FactionId
): Vector[ActionResultView] =
results
@@ -201,13 +180,13 @@ object ActionResultFilter {
factionId = factionId
)
(res.scalaGameState, acc ++ maybeNewRes)
(res.resultingState, acc ++ maybeNewRes)
}
._2
def filterForObserver(
startingState: GameState,
results: Vector[ActionWithResultingState]
results: Vector[ActionResultWithResultingState]
): Vector[ActionResultView] =
results.flatMap(res => observeOne(result = res, startingState = startingState))
}
@@ -11,6 +11,7 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/service/controller:__pkg__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
@@ -18,10 +19,9 @@ scala_library(
],
deps = [
":game_history",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/views:action_result_view_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
@@ -45,8 +45,6 @@ scala_library(
":engine",
":game_history",
":round_phase_advancer",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/views:action_result_view_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
@@ -70,9 +68,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
@@ -94,19 +90,19 @@ scala_library(
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/views:action_result_view_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/views:game_state_view_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/util:game_state_view_differ",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:game_state_view_filter",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/types",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/game_state:game_state_view_diff_converter",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/view/game_state:game_state_view_diff",
],
)
@@ -151,21 +147,16 @@ scala_library(
"//src/main/protobuf/net/eagle0/shardok/api:action_result_view_scala_proto",
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_scala_proto",
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_scala_proto",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/shardok/api:action_result_view_scala_proto",
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_scala_proto",
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:action_result_proto_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
@@ -3,7 +3,7 @@ package net.eagle0.eagle.library
import scala.collection.immutable.SortedMap
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.state.command.available.OneProvinceAvailableCommands
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
import net.eagle0.eagle.model.state.game_state.GameState
@@ -12,7 +12,7 @@ import net.eagle0.eagle.views.action_result_view.ActionResultView
trait EngineAndResults {
def engine: Engine
def results: Vector[ActionResult]
def results: Vector[ActionResultT]
def saveNow: EngineAndResults
}
@@ -5,7 +5,6 @@ import scala.collection.immutable.SortedMap
import net.eagle0.common.SeededRandom
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.library.actions.applier.{ActionResultApplierImpl, ActionResultWithResultingState}
import net.eagle0.eagle.library.actions.availability.AvailableCommandsFactory
import net.eagle0.eagle.library.actions.impl.action.{
@@ -20,7 +19,6 @@ import net.eagle0.eagle.library.util.validations.RuntimeValidator
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.library.EngineImpl.{appliedResults, withUpdateChecks}
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.state.command.available.OneProvinceAvailableCommands
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
import net.eagle0.eagle.model.state.game_state.GameState
@@ -35,7 +33,7 @@ object EngineImpl {
): EngineImpl =
EngineImpl(
gameId = gameId,
currentState = GameStateConverter.fromProto(history.last.gameState).copy(gameId = gameId),
currentState = history.last.resultingState.copy(gameId = gameId),
heroGenerator = heroGenerator,
history = history
)
@@ -92,18 +90,16 @@ object EngineImpl {
currentState = results.lastOption
.map(_.resultingState)
.getOrElse(engine.currentState),
history = engine.history.withNewResultsScala(results)
history = engine.history.withNewResults(results)
),
results = results.map(awrs =>
net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter.toProto(awrs.actionResult)
)
results = results.map(_.actionResult)
)
)
}
final case class EngineAndResultsImpl(
engine: EngineImpl,
results: Vector[ActionResult]
results: Vector[ActionResultT]
) extends EngineAndResults {
def recursiveTransformScala(
f: EngineImpl => Vector[ActionResultWithResultingState]
@@ -111,7 +107,7 @@ final case class EngineAndResultsImpl(
@tailrec
def go(
eng: EngineImpl,
acc: Vector[ActionResult]
acc: Vector[ActionResultT]
): EngineAndResultsImpl = {
val goResults = f(eng)
@@ -244,7 +240,7 @@ case class EngineImpl(
val truncatedHistory = history.truncateTo(targetActionCount)
copy(
currentState = GameStateConverter.fromProto(truncatedHistory.last.gameState).copy(gameId = gameId),
currentState = truncatedHistory.last.resultingState.copy(gameId = gameId),
history = truncatedHistory
)
}
@@ -2,9 +2,6 @@ package net.eagle0.eagle.library
import net.eagle0.eagle.{FactionId, RoundId, ShardokGameId}
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.proto_converters.ActionResultProtoConverter
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.shardok.api.action_result_view.ActionResultView as ShardokActionResultView
@@ -12,13 +9,13 @@ import net.eagle0.shardok.api.command_descriptor.AvailableCommands as ShardokAva
import net.eagle0.shardok.storage.action_result.ActionResult as ShardokActionResult
case class CappedResults(
results: Vector[ActionWithResultingState],
results: Vector[ActionResultWithResultingState],
startingState: Option[GameState]
)
trait GameHistory {
def all: Vector[ActionWithResultingState]
def since(count: Int): Vector[ActionWithResultingState]
def all: Vector[ActionResultWithResultingState]
def since(count: Int): Vector[ActionResultWithResultingState]
def sinceCapped(count: Int, resultSizeCap: Int): CappedResults =
if this.count - count <= resultSizeCap then CappedResults(since(count), None)
else
@@ -27,31 +24,21 @@ trait GameHistory {
Some(stateAfter(this.count - resultSizeCap))
)
def stateAfter(count: Int): GameState
def sinceDate(date: Date): Vector[ActionWithResultingState]
def sinceDate(date: Date): Vector[ActionResultWithResultingState]
def count: Int
def last: ActionWithResultingState
def last: ActionResultWithResultingState
def recentResultsForRound(
roundId: RoundId,
predicate: ActionWithResultingState => Boolean = _ => true
): Vector[ActionWithResultingState]
predicate: ActionResultWithResultingState => Boolean = _ => true
): Vector[ActionResultWithResultingState]
def saveNow: GameHistory
def withNewResults(newResults: Vector[ActionWithResultingState]): GameHistory
def withNewResults(newResults: Vector[ActionResultWithResultingState]): GameHistory
def truncateTo(targetActionCount: Int): GameHistory
def withNewResultsScala(newResults: Vector[ActionResultWithResultingState]): GameHistory =
withNewResults(newResults.map { awrs =>
ActionWithResultingState(
actionResult = ActionResultProtoConverter.toProto(awrs.actionResult),
gameState = GameStateConverter.toProto(awrs.resultingState),
precomputedScalaState = Some(awrs.resultingState), // Preserve Scala state to avoid re-conversion
precomputedScalaActionResult = Some(awrs.actionResult) // Preserve Scala result to avoid re-conversion
)
})
def shardokCount(shardokGameId: ShardokGameId): Int
def shardokGameState(
@@ -7,6 +7,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/service:__pkg__",
],
deps = [
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
@@ -128,15 +128,12 @@ scala_library(
exports = [
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:game_history",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request/chronicle_event",
"//src/main/scala/net/eagle0/eagle/model/action_result/types",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:notification_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
"//src/main/scala/net/eagle0/eagle/model/state/date",
],
)
@@ -1,25 +1,25 @@
package net.eagle0.eagle.library.actions.impl.action
import net.eagle0.eagle.library.actions.impl.common.ActionWithResultingState
import net.eagle0.eagle.library.actions.applier.ActionResultWithResultingState
import net.eagle0.eagle.library.GameHistory
import net.eagle0.eagle.model.action_result.generated_text_request.chronicle_event.*
import net.eagle0.eagle.model.action_result.types.ActionResultType
import net.eagle0.eagle.model.action_result.NotificationDetails
import net.eagle0.eagle.model.proto_converters.date.DateConverter
import net.eagle0.eagle.model.proto_converters.NotificationConverter
import net.eagle0.eagle.model.state.date.Date
object ChronicleEventGenerator {
private def relevantNotificationDetails(
awrs: ActionWithResultingState
awrs: ActionResultWithResultingState
): Vector[NotificationDetails] =
awrs.actionResult.notificationsToDeliver.distinct.toVector
.map(n => NotificationConverter.fromProto(n.details))
awrs.actionResult.newNotifications
.map(_.details)
.distinct
private def notificationEntries(
awrs: ActionWithResultingState
awrs: ActionResultWithResultingState
): Vector[ChronicleEvent] = {
val date = DateConverter.fromProto(awrs.gameState.currentDate)
// Chronicle events are only generated during active gameplay when currentDate is present
val date = awrs.resultingState.currentDate.get
relevantNotificationDetails(awrs).collect {
case NotificationDetails.AllianceAccepted(
offeringFid,
@@ -317,17 +317,17 @@ object ChronicleEventGenerator {
}
private def basicActionResultEntries(
awrs: ActionWithResultingState
awrs: ActionResultWithResultingState
): Vector[ChronicleEvent] =
ActionResultType.fromValue(awrs.actionResult.`type`.value) match {
awrs.actionResult.actionResultType match {
case ActionResultType.FactionDestroyed =>
val date = DateConverter.fromProto(awrs.gameState.currentDate)
val date = awrs.resultingState.currentDate.get
awrs.actionResult.removedFactionIds.map { factionId =>
FactionDestroyedChronicleEvent(
date = date,
factionId = factionId
)
}.toVector
}
case _ => Vector()
}
@@ -336,7 +336,7 @@ object ChronicleEventGenerator {
gameHistory: GameHistory,
since: Date
): Vector[ChronicleEvent] =
gameHistory.sinceDate(since).flatMap { (awrs: ActionWithResultingState) =>
gameHistory.sinceDate(since).flatMap { (awrs: ActionResultWithResultingState) =>
notificationEntries(awrs) ++ basicActionResultEntries(awrs)
}
}
@@ -191,7 +191,6 @@ scala_library(
srcs = ["GeneratorUtilities.scala"],
visibility = ["//visibility:public"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
@@ -226,7 +225,6 @@ scala_library(
":faction_info_utilities",
":generator_utilities",
":hero_info_utilities",
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
@@ -343,7 +341,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
@@ -490,7 +487,6 @@ scala_library(
":faction_info_utilities",
":hero_description_generator",
":hero_info_utilities",
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/client_text:client_text_store",
"//src/main/scala/net/eagle0/eagle/client_text:text_generation_result",
@@ -8,8 +8,8 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer/status",
],
)
@@ -1,15 +1,8 @@
package net.eagle0.eagle.library.actions.generated_text_request_generators.diplomacy_llm_helpers
import net.eagle0.eagle.{FactionId, GameId, HeroId, RoundId}
import net.eagle0.eagle.common.diplomacy_offer_status.DiplomacyOfferStatus
import net.eagle0.eagle.internal.generated_text_request.{
AllianceOfferResolutionMessage,
BreakAllianceResolutionMessage,
GeneratedTextRequest,
InvitationResolutionMessage,
RansomResolutionMessage,
TruceResolutionMessage
}
import net.eagle0.eagle.model.action_result.generated_text_request.LlmRequestT
import net.eagle0.eagle.model.state.diplomacy_offer.status.Status
object DiplomacyResolutionLlmRequestGenerator {
def truceResolutionRequest(
@@ -18,17 +11,15 @@ object DiplomacyResolutionLlmRequestGenerator {
originatingFactionId: FactionId,
targetFactionId: FactionId,
messengerHeroId: HeroId,
resolution: DiplomacyOfferStatus
): GeneratedTextRequest =
GeneratedTextRequest(
id = s"$currentRoundId truce offer $originatingFactionId $targetFactionId resolution $resolution",
resolution: Status
): LlmRequestT =
LlmRequestT.TruceResolutionMessage(
requestId = s"$currentRoundId truce offer $originatingFactionId $targetFactionId resolution $resolution",
eagleGameId = gameId,
details = TruceResolutionMessage(
offeringFactionId = originatingFactionId,
targetFactionId = targetFactionId,
messengerHeroId = messengerHeroId,
resolutionStatus = resolution
)
offeringFactionId = originatingFactionId,
targetFactionId = targetFactionId,
messengerHeroId = messengerHeroId,
resolution = resolution
)
def allianceResolutionRequest(
@@ -37,17 +28,15 @@ object DiplomacyResolutionLlmRequestGenerator {
originatingFactionId: FactionId,
targetFactionId: FactionId,
messengerHeroId: HeroId,
resolution: DiplomacyOfferStatus
): GeneratedTextRequest =
GeneratedTextRequest(
id = s"$currentRoundId alliance offer $originatingFactionId $targetFactionId resolution $resolution",
resolution: Status
): LlmRequestT =
LlmRequestT.AllianceOfferResolutionMessage(
requestId = s"$currentRoundId alliance offer $originatingFactionId $targetFactionId resolution $resolution",
eagleGameId = gameId,
details = AllianceOfferResolutionMessage(
offeringFactionId = originatingFactionId,
targetFactionId = targetFactionId,
messengerHeroId = messengerHeroId,
resolutionStatus = resolution
)
offeringFactionId = originatingFactionId,
targetFactionId = targetFactionId,
messengerHeroId = messengerHeroId,
resolution = resolution
)
def breakAllianceResolutionRequest(
@@ -56,17 +45,15 @@ object DiplomacyResolutionLlmRequestGenerator {
originatingFactionId: FactionId,
targetFactionId: FactionId,
messengerHeroId: HeroId,
resolution: DiplomacyOfferStatus
): GeneratedTextRequest =
GeneratedTextRequest(
id = s"$currentRoundId break alliance $originatingFactionId $targetFactionId resolution $resolution",
resolution: Status
): LlmRequestT =
LlmRequestT.BreakAllianceResolutionMessage(
requestId = s"$currentRoundId break alliance $originatingFactionId $targetFactionId resolution $resolution",
eagleGameId = gameId,
details = BreakAllianceResolutionMessage(
breakingFactionId = originatingFactionId,
targetFactionId = targetFactionId,
messengerHeroId = messengerHeroId,
resolutionStatus = resolution
)
breakingFactionId = originatingFactionId,
targetFactionId = targetFactionId,
messengerHeroId = messengerHeroId,
resolution = resolution
)
def invitationResolutionRequest(
@@ -75,17 +62,16 @@ object DiplomacyResolutionLlmRequestGenerator {
originatingFactionId: FactionId,
invitedFactionId: FactionId,
messengerHeroId: HeroId,
resolution: DiplomacyOfferStatus
): GeneratedTextRequest =
GeneratedTextRequest(
id = s"$currentRoundId invitation resolution $originatingFactionId $invitedFactionId resolution $resolution",
resolution: Status
): LlmRequestT =
LlmRequestT.InvitationResolutionMessage(
requestId =
s"$currentRoundId invitation resolution $originatingFactionId $invitedFactionId resolution $resolution",
eagleGameId = gameId,
details = InvitationResolutionMessage(
offeringFactionId = originatingFactionId,
targetFactionId = invitedFactionId,
messengerHeroId = messengerHeroId,
resolutionStatus = resolution
)
invitingFactionId = originatingFactionId,
targetFactionId = invitedFactionId,
messengerHeroId = messengerHeroId,
resolution = resolution
)
def ransomResolutionRequest(
@@ -94,16 +80,15 @@ object DiplomacyResolutionLlmRequestGenerator {
originatingFactionId: FactionId,
targetFactionId: FactionId,
ransomedHeroId: HeroId,
resolution: DiplomacyOfferStatus
): GeneratedTextRequest =
GeneratedTextRequest(
id = s"$currentRoundId ransom resolution $originatingFactionId $targetFactionId resolution $resolution",
resolution: Status
): LlmRequestT =
LlmRequestT.RansomResolutionMessage(
requestId = s"$currentRoundId ransom resolution $originatingFactionId $targetFactionId resolution $resolution",
eagleGameId = gameId,
details = RansomResolutionMessage(
offeringFactionId = originatingFactionId,
targetFactionId = targetFactionId,
ransomedHeroId = ransomedHeroId,
resolutionStatus = resolution
)
offeringFactionId = originatingFactionId,
targetFactionId = targetFactionId,
offeringHeroId = ransomedHeroId, // Note: This seems like it should be a separate field
ransomedHeroId = ransomedHeroId,
resolution = resolution
)
}
@@ -11,8 +11,9 @@ scala_library(
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
"//src/main/scala/net/eagle0/common:tsv_utils",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
],
)
@@ -5,7 +5,7 @@ import java.net.URL
import scala.util.Try
import net.eagle0.common.TsvUtils
import net.eagle0.eagle.common.battalion_type.{BattalionType, BattalionTypeId}
import net.eagle0.eagle.model.state.{BattalionType, BattalionTypeId}
object BattalionTypeLoader {
private def loadBattalionTypesFromTsv(
@@ -38,7 +38,7 @@ object BattalionTypeLoader {
private def battalionTypeFromMap(m: Map[String, String]): BattalionType =
BattalionType(
typeId = BattalionTypeId.fromValue(m("TypeId").toInt),
typeId = BattalionTypeId.fromInt(m("TypeId").toInt),
name = m("TypeName"),
capacity = m("Capacity").toInt,
allowsCasting = m("AllowsCasting").toBoolean,
@@ -87,6 +87,7 @@ scala_library(
deps = [
":map_generator",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"@maven//:org_json4s_json4s_ast_3",
"@maven//:org_json4s_json4s_core_3",
"@maven//:org_json4s_json4s_native_3",
@@ -184,10 +185,12 @@ scala_library(
],
exports = ["//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
"//src/main/scala/net/eagle0/common:tsv_utils",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:hero_cap_per_castle",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
],
)
@@ -3,19 +3,21 @@ package net.eagle0.eagle.library.util
import java.net.URL
import net.eagle0.common.TsvUtils
import net.eagle0.eagle.internal.province.{Neighbor, Province}
import net.eagle0.eagle.library.settings.HeroCapPerCastle
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.model.state.province.{Neighbor, ProvinceT}
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
import net.eagle0.eagle.ProvinceId
object MapGenerator {
def loadMap(tsvUrl: URL): Vector[Province] =
def loadMap(tsvUrl: URL): Vector[ProvinceT] =
TsvUtils
.loadMaps(url = tsvUrl)
.get
.map(provinceFromMap)
def provinceFromMap(map: Map[String, String]): Province =
Province(
def provinceFromMap(map: Map[String, String]): ProvinceC =
ProvinceC(
id = map("id").toInt,
name = map("name"),
neighbors = neighborsFromMap(map),
@@ -23,6 +23,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/util/quest_fulfillment:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/util/validations:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/main/scala/net/eagle0/eagle/service:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util/validations:__pkg__",
],
@@ -1,7 +1,8 @@
package net.eagle0.eagle.model.action_result
import net.eagle0.eagle.ClientTextId
import net.eagle0.eagle.{ClientTextId, FactionId}
trait ClientTextVisibilityExtensionT {
def textId: ClientTextId
def recipientFactionIds: Vector[FactionId]
}
@@ -7,8 +7,11 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/util:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
"//src/test/scala/net/eagle0/eagle/service:__pkg__",
"//src/test/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_component_trait",
@@ -4,6 +4,7 @@ scala_library(
name = "generated_text_request",
srcs = ["GeneratedTextRequestT.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/client_text:__pkg__",
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/util:__subpackages__",
@@ -12,9 +13,12 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request/concrete:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/llm_request:__pkg__",
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
"//src/test/scala/net/eagle0/eagle/client_text:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
"//src/test/scala/net/eagle0/eagle/service:__pkg__",
],
deps = [
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
@@ -33,6 +33,7 @@ import net.eagle0.eagle.model.proto_converters.date.DateConverter
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
import net.eagle0.eagle.model.proto_converters.generated_text_request.LlmRequestConverter
import net.eagle0.eagle.model.proto_converters.hero.{GenderConverter, HeroConverter}
import net.eagle0.eagle.model.proto_converters.province.ProvinceConverter
import net.eagle0.eagle.model.proto_converters.shardok_battle.ShardokBattleConverter
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.chronicle_entry.ChronicleEntry
@@ -118,6 +119,7 @@ object ActionResultProtoConverter {
newBattalions = newBattalions.map(BattalionConverter.toProto),
newFactions = newFactions.map(FactionConverter.toProto),
newHeroes = newHeroes.map(HeroConverter.toProto),
newProvinces = newProvinces.map(ProvinceConverter.toProto),
newBattalionTypes = newBattalionTypes.map(BattalionTypeConverter.toProto),
destroyedBattalionIds = removedBattalionIds,
removedHeroes = removedHeroIds,
@@ -9,10 +9,12 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
],
exports = [
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
@@ -60,6 +62,24 @@ scala_library(
],
)
scala_library(
name = "generated_text_request_converter",
srcs = ["GeneratedTextRequestConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/client_text:__pkg__",
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
"//src/main/scala/net/eagle0/eagle/service:__subpackages__",
"//src/test/scala/net/eagle0/eagle/service:__pkg__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero:gender",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/llm_request",
],
)
scala_library(
name = "army_converter",
srcs = ["ArmyConverter.scala"],
@@ -97,6 +117,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/available:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
@@ -117,15 +138,7 @@ scala_library(
scala_library(
name = "battalion_type_id_converter",
srcs = ["BattalionTypeIdConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/battalion:__pkg__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
],
visibility = ["//visibility:public"],
exports = [
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
],
@@ -140,19 +153,7 @@ scala_library(
scala_library(
name = "battalion_type_converter",
srcs = ["BattalionTypeConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/util/province:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/game_state:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
],
visibility = ["//visibility:public"],
exports = [
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
],
@@ -373,6 +374,7 @@ scala_library(
name = "notification_converter",
srcs = ["NotificationConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
@@ -0,0 +1,63 @@
package net.eagle0.eagle.model.proto_converters
import net.eagle0.eagle.{FactionId, GameId, HeroId}
import net.eagle0.eagle.internal.generated_text_request.{
FixedHeroNameMessage,
GeneratedHeroNameMessage,
GeneratedTextRequest
}
import net.eagle0.eagle.model.action_result.generated_text_request.{
FixedHeroName,
GeneratedHeroName,
GeneratedTextRequestT,
LlmRequestT
}
import net.eagle0.eagle.model.proto_converters.generated_text_request.LlmRequestConverter
import net.eagle0.eagle.model.proto_converters.hero.GenderConverter
object GeneratedTextRequestConverter {
def toProto(gtr: GeneratedTextRequestT): GeneratedTextRequest = gtr match {
case llmRequestT: LlmRequestT =>
LlmRequestConverter.toProto(llmRequestT)
case fixedHeroName: FixedHeroName =>
GeneratedTextRequest(
id = fixedHeroName.requestId,
eagleGameId = fixedHeroName.eagleGameId,
recipientFactionIds = fixedHeroName.recipientFactionIds,
alwaysGenerate = fixedHeroName.alwaysGenerate,
details = FixedHeroNameMessage(
heroId = fixedHeroName.heroId,
fixedName = fixedHeroName.fixedName
)
)
case generatedHeroName: GeneratedHeroName =>
GeneratedTextRequest(
id = generatedHeroName.requestId,
eagleGameId = generatedHeroName.eagleGameId,
recipientFactionIds = generatedHeroName.recipientFactionIds,
alwaysGenerate = generatedHeroName.alwaysGenerate,
details = GeneratedHeroNameMessage(
gender = GenderConverter.toProto(generatedHeroName.gender)
)
)
}
def fromProto(proto: GeneratedTextRequest): GeneratedTextRequestT =
proto.details match {
case FixedHeroNameMessage(heroId, fixedName, _ /* unknownFields */ ) =>
FixedHeroName(
requestId = proto.id,
eagleGameId = proto.eagleGameId,
heroId = heroId,
fixedName = fixedName
)
case GeneratedHeroNameMessage(gender, _ /* unknownFields */ ) =>
GeneratedHeroName(
requestId = proto.id,
eagleGameId = proto.eagleGameId,
gender = GenderConverter.fromProto(gender)
)
case _ =>
LlmRequestConverter.fromProto(proto)
}
}
@@ -21,6 +21,9 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/library/util/command_choice_helpers:__pkg__",
"//src/test/scala/net/eagle0/eagle/service:__subpackages__",
],
exports = [
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
@@ -14,6 +14,7 @@ import net.eagle0.eagle.internal.generated_text_request.{
DivineMessage as DivineMessageProto,
ExileVassalMessage as ExileVassalMessageProto,
GeneratedTextRequest as LlmRequestProto,
GeneratedTextRequestDetails,
HandleCapturedHeroPlea as HandleCapturedHeroPleaProto,
HeroBackstoryUpdateRequest as HeroBackstoryUpdateRequestProto,
HeroDepartureMessage as HeroDepartureMessageProto,
@@ -703,4 +704,649 @@ object LlmRequestConverter {
)
}
)
def fromProto(
llmRequestProto: LlmRequestProto
): LlmRequestT = {
val requestId = llmRequestProto.id
val eagleGameId = llmRequestProto.eagleGameId
val recipientFactionIds = llmRequestProto.recipientFactionIds.toVector
val alwaysGenerate = llmRequestProto.alwaysGenerate
llmRequestProto.details match {
case AllianceOfferMessageProto(
offeringFactionId,
targetFactionId,
messengerHeroId,
_ /* unknownFields */
) =>
AllianceOfferMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
offeringFactionId = offeringFactionId,
targetFactionId = targetFactionId,
messengerHeroId = messengerHeroId
)
case AllianceOfferResolutionMessageProto(
offeringFactionId,
targetFactionId,
messengerHeroId,
resolutionStatus,
_ /* unknownFields */
) =>
AllianceOfferResolutionMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
offeringFactionId = offeringFactionId,
targetFactionId = targetFactionId,
messengerHeroId = messengerHeroId,
resolution = StatusConverter.fromProto(resolutionStatus)
)
case BreakAllianceMessageProto(
breakingFactionId,
targetFactionId,
messengerHeroId,
_ /* unknownFields */
) =>
BreakAllianceMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
breakingFactionId = breakingFactionId,
targetFactionId = targetFactionId,
messengerHeroId = messengerHeroId
)
case BreakAllianceResolutionMessageProto(
breakingFactionId,
targetFactionId,
messengerHeroId,
resolutionStatus,
_ /* unknownFields */
) =>
BreakAllianceResolutionMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
breakingFactionId = breakingFactionId,
targetFactionId = targetFactionId,
messengerHeroId = messengerHeroId,
resolution = StatusConverter.fromProto(resolutionStatus)
)
case CapturedHeroExecutedMessageProto(
capturedHeroId,
capturedHeroFactionId,
actingHeroId,
actingFactionId,
provinceId,
_ /* unknownFields */
) =>
CapturedHeroExecutedMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
capturedHeroId = capturedHeroId,
capturedHeroFactionId = capturedHeroFactionId,
actingHeroId = actingHeroId,
actingFactionId = actingFactionId,
provinceId = provinceId
)
case CapturedHeroExiledMessageProto(
capturedHeroId,
capturedHeroFactionId,
actingHeroId,
actingFactionId,
provinceId,
_ /* unknownFields */
) =>
CapturedHeroExiledMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
capturedHeroId = capturedHeroId,
capturedHeroFactionId = capturedHeroFactionId,
actingHeroId = actingHeroId,
actingFactionId = actingFactionId,
provinceId = provinceId
)
case CapturedHeroImprisonedMessageProto(
capturedHeroId,
capturedHeroFactionId,
actingHeroId,
actingFactionId,
provinceId,
_ /* unknownFields */
) =>
CapturedHeroImprisonedMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
capturedHeroId = capturedHeroId,
capturedHeroFactionId = capturedHeroFactionId,
actingHeroId = actingHeroId,
actingFactionId = actingFactionId,
provinceId = provinceId
)
case ChronicleUpdateMessageProto(
currentDate,
previousEntries,
newEntries,
chroniclerStyle,
_ /* unknownFields */
) =>
ChronicleUpdateMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
current_date = DateConverter.fromProto(currentDate),
previous_entries = previousEntries.map { pe =>
ChronicleUpdatePreviousEntry(
date = DateConverter.fromProto(pe.date),
generatedTextId = pe.generatedTextId,
chroniclerStyle =
if pe.chroniclerStyle.isEmpty then None
else Some(pe.chroniclerStyle)
)
}.toVector,
new_entries = newEntries.map(ChronicleEventConverter.fromProto).toVector,
chronicler_style = chroniclerStyle
)
case DivineMessageProto(
divinedHeroId,
actingHeroId,
actingFactionId,
provinceId,
_ /* unknownFields */
) =>
DivineMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
divinedHeroId = divinedHeroId,
actingHeroId = actingHeroId,
actingFactionId = actingFactionId,
provinceId = provinceId
)
case ExileVassalMessageProto(
exiledHeroId,
actingHeroId,
actingFactionId,
provinceId,
requestingHeroIds,
_ /* unknownFields */
) =>
ExileVassalMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
exiledHeroId = exiledHeroId,
actingHeroId = actingHeroId,
actingFactionId = actingFactionId,
provinceId = provinceId,
requestingHeroIds = requestingHeroIds.toVector
)
case HandleCapturedHeroPleaProto(
actingFactionId,
capturedHeroId,
provinceId,
alreadyExecutedHeroIds,
alreadyImprisonedHeroIds,
alreadyExiledHeroIds,
_ /* unknownFields */
) =>
HandleCapturedHeroPlea(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
actingFactionId = actingFactionId,
capturedHeroId = capturedHeroId,
provinceId = provinceId,
alreadyExecutedHeroIds = alreadyExecutedHeroIds.toVector,
alreadyImprisonedHeroIds = alreadyImprisonedHeroIds.toVector,
alreadyExiledHeroIds = alreadyExiledHeroIds.toVector
)
case HeroBackstoryUpdateRequestProto(
heroId,
previousBackstoryVersions,
events,
_ /* unknownFields */
) =>
HeroBackstoryUpdateRequest(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
heroId = heroId,
previousBackstoryVersions = previousBackstoryVersions.map { bv =>
BackstoryVersion(
textId = bv.textId,
date = DateConverter.fromProto(bv.date)
)
}.toVector,
events = events.map(EventForHeroBackstoryConverter.fromProto).toVector
)
case HeroDepartureMessageProto(
departingHeroId,
fromFactionId,
provinceId,
_ /* unknownFields */
) =>
HeroDepartureMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
departingHeroId = departingHeroId,
fromFactionId = fromFactionId,
provinceId = provinceId
)
case HeroInitialBackstoryRequestProto(
heroId,
factionId,
personalityWords,
_ /* unknownFields */
) =>
HeroInitialBackstoryRequest(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
heroId = heroId,
factionId = factionId,
personalityWords = personalityWords.toVector
)
case InvitationMessageProto(
offeringFactionId,
targetFactionId,
messengerHeroId,
_ /* unknownFields */
) =>
InvitationMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
invitingFactionId = offeringFactionId,
targetFactionId = targetFactionId,
messengerHeroId = messengerHeroId
)
case InvitationResolutionMessageProto(
offeringFactionId,
targetFactionId,
messengerHeroId,
resolutionStatus,
_ /* unknownFields */
) =>
InvitationResolutionMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
invitingFactionId = offeringFactionId,
targetFactionId = targetFactionId,
messengerHeroId = messengerHeroId,
resolution = StatusConverter.fromProto(resolutionStatus)
)
case PleaseRecruitMeMessageProto(
heroId,
targetFactionId,
provinceId,
_ /* unknownFields */
) =>
PleaseRecruitMeMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
heroId = heroId,
targetFactionId = targetFactionId,
provinceId = provinceId
)
case QuestFailedMessageProto(
heroId,
provinceId,
factionId,
quest,
_ /* unknownFields */
) =>
QuestFailedMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
heroId = heroId,
provinceId = provinceId,
factionId = factionId,
quest = QuestConverter.fromProto(quest.get)
)
case QuestFulfilledMessageProto(
heroId,
provinceId,
factionId,
quest,
_ /* unknownFields */
) =>
QuestFulfilledMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
heroId = heroId,
provinceId = provinceId,
factionId = factionId,
quest = QuestConverter.fromProto(quest.get)
)
case RansomOfferMessageProto(
offeringFactionId,
targetFactionId,
offeringHeroId,
ransomedHeroId,
goldOffered,
hostageHeroIdsOffered,
prisonerHeroIdsOffered,
_ /* unknownFields */
) =>
RansomOfferMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
offeringFactionId = offeringFactionId,
targetFactionId = targetFactionId,
offeringHeroId = offeringHeroId,
ransomedHeroId = ransomedHeroId,
goldOffered = goldOffered,
hostageHeroIdsOffered = hostageHeroIdsOffered.toVector,
prisonerHeroIdsOffered = prisonerHeroIdsOffered.toVector
)
case RansomResolutionMessageProto(
offeringFactionId,
targetFactionId,
offeringHeroId,
ransomedHeroId,
resolutionStatus,
_ /* unknownFields */
) =>
RansomResolutionMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
offeringFactionId = offeringFactionId,
targetFactionId = targetFactionId,
offeringHeroId = offeringHeroId,
ransomedHeroId = ransomedHeroId,
resolution = StatusConverter.fromProto(resolutionStatus)
)
case RecruitmentRefusedMessageProto(
capturedHeroId,
capturedHeroFactionId,
actingHeroId,
actingFactionId,
provinceId,
alreadyExecutedHeroIds,
alreadyImprisonedHeroIds,
alreadyExiledHeroIds,
_ /* unknownFields */
) =>
RecruitmentRefusedMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
capturedHeroId = capturedHeroId,
capturedHeroFactionId = capturedHeroFactionId,
actingHeroId = actingHeroId,
actingFactionId = actingFactionId,
provinceId = provinceId,
alreadyExecutedHeroIds = alreadyExecutedHeroIds.toVector,
alreadyImprisonedHeroIds = alreadyImprisonedHeroIds.toVector,
alreadyExiledHeroIds = alreadyExiledHeroIds.toVector
)
case SuppressBeastsFailedMessageProto(
heroId,
factionId,
battalionId,
beastTypeId,
beastCount,
provinceId,
_ /* unknownFields */
) =>
SuppressBeastsFailedMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
heroId = heroId,
factionId = factionId,
battalionTypeId = battalionId,
beastTypeId = beastTypeId,
beastCount = beastCount,
provinceId = provinceId
)
case SuppressBeastsSucceededMessageProto(
heroId,
factionId,
battalionId,
beastTypeId,
beastCount,
provinceId,
casualtyCount,
foodGained,
goldGained,
_ /* unknownFields */
) =>
SuppressBeastsSucceededMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
heroId = heroId,
factionId = factionId,
battalionTypeId = battalionId,
beastTypeId = beastTypeId,
beastCount = beastCount,
provinceId = provinceId,
casualtyCount = casualtyCount,
foodGained = foodGained,
goldGained = goldGained
)
case SwearBrotherhoodMessageProto(
factionId,
swornBrotherHeroId,
_ /* unknownFields */
) =>
SwearBrotherhoodMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
factionId = factionId,
swornBrotherHeroId = swornBrotherHeroId
)
case TruceOfferMessageProto(
offeringFactionId,
targetFactionId,
messengerHeroId,
_ /* unknownFields */
) =>
TruceOfferMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
offeringFactionId = offeringFactionId,
targetFactionId = targetFactionId,
messengerHeroId = messengerHeroId
)
case TruceResolutionMessageProto(
offeringFactionId,
targetFactionId,
messengerHeroId,
resolutionStatus,
_ /* unknownFields */
) =>
TruceResolutionMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
offeringFactionId = offeringFactionId,
targetFactionId = targetFactionId,
messengerHeroId = messengerHeroId,
resolution = StatusConverter.fromProto(resolutionStatus)
)
case ProfessionGainedMessageProto(
heroId,
factionId,
newProfession,
_ /* unknownFields */
) =>
ProfessionGainedMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
heroId = heroId,
factionId = factionId,
newProfession = ProfessionConverter.fromProto(newProfession)
)
case PrisonerExecutedMessageProto(
executedHeroId,
lastFactionId,
executingFactionId,
provinceId,
_ /* unknownFields */
) =>
PrisonerExecutedMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
executedHeroId = executedHeroId,
lastFactionId = lastFactionId,
executingFactionId = executingFactionId,
provinceId = provinceId
)
case PrisonerReleasedMessageProto(
releasedHeroId,
lastFactionId,
releasingFactionId,
provinceId,
_ /* unknownFields */
) =>
PrisonerReleasedMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
releasedHeroId = releasedHeroId,
lastFactionId = lastFactionId,
releasingFactionId = releasingFactionId,
provinceId = provinceId
)
case PrisonerExiledMessageProto(
exiledHeroId,
lastFactionId,
exilingFactionId,
provinceId,
_ /* unknownFields */
) =>
PrisonerExiledMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
exiledHeroId = exiledHeroId,
lastFactionId = lastFactionId,
exilingFactionId = exilingFactionId,
provinceId = provinceId
)
case PrisonerReturnedMessageProto(
returnedHeroId,
lastFactionId,
returningFactionId,
toFactionId,
provinceId,
_ /* unknownFields */
) =>
PrisonerReturnedMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
returnedHeroId = returnedHeroId,
lastFactionId = lastFactionId,
returningFactionId = returningFactionId,
toFactionId = toFactionId,
provinceId = provinceId
)
case NewFactionHeadMessageProto(
newHeadHeroId,
factionId,
previousFactionName,
newFactionName,
previousHeadHeroId,
_ /* unknownFields */
) =>
NewFactionHeadMessage(
requestId = requestId,
eagleGameId = eagleGameId,
recipientFactionIds = recipientFactionIds,
alwaysGenerate = alwaysGenerate,
newHeadHeroId = newHeadHeroId,
factionId = factionId,
previousFactionName = previousFactionName,
newFactionName = if newFactionName.isEmpty then None else Some(newFactionName),
previousHeadHeroId = previousHeadHeroId
)
case GeneratedTextRequestDetails.Empty =>
throw new IllegalArgumentException("Empty GeneratedTextRequestDetails")
}
}
}
@@ -3,20 +3,7 @@ load("@rules_scala//scala:scala.bzl", "scala_binary", "scala_library")
scala_library(
name = "province",
srcs = ["ProvinceConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/ai:__pkg__",
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/availability:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/util:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/util:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/util/command_choice_helpers:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
],
visibility = ["//visibility:public"],
exports = [
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/province:orders",
@@ -77,6 +64,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/actions/impl:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/util/view_filters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view:__subpackages__",
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
],
exports = [
@@ -9,6 +9,9 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
"//src/main/scala/net/eagle0/eagle/service:__pkg__",
],
exports = [
"//src/main/protobuf/net/eagle0/eagle/internal:shardok_battle_scala_proto",
],
deps = [
"//src/main/protobuf/net/eagle0/common:victory_condition_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:shardok_battle_scala_proto",
@@ -52,16 +52,7 @@ scala_library(
scala_library(
name = "battalion_type",
srcs = ["BattalionType.scala"],
visibility = [
":__subpackages__",
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/action_result:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/view/game_state:__pkg__",
"//src/test/scala/net/eagle0/eagle/ai:__pkg__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
],
visibility = ["//visibility:public"],
deps = [
":battalion_type_id",
],
@@ -156,6 +147,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/view/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/view/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
"//src/test/scala/net/eagle0/eagle/ai:__pkg__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/util:__subpackages__",
@@ -17,6 +17,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/state/game_state:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/state/hero:__pkg__",
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
"//src/test/scala/net/eagle0/eagle/ai:__pkg__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
@@ -27,4 +27,6 @@ trait BattalionT {
def withSize(newSize: Int): BattalionT =
updateWith(size = newSize)
def withId(id: BattalionId): BattalionT
}
@@ -6,6 +6,7 @@ scala_library(
visibility = [
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/main/scala/net/eagle0/eagle/service/new_game_creation:__pkg__",
"//src/main/scala/net/eagle0/eagle/shardok_interface:__pkg__",
"//src/test/scala/net/eagle0/eagle/ai:__pkg__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",

Some files were not shown because too many files have changed in this diff Show More