jfr-sidecar has `pid: "service:eagle-blue"` to share PID namespace for
JFR profiling. It must be started after eagle-blue exists, not before.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add DeleteUser and DeleteInvitation RPCs to admin.proto
- Add Delete methods to UserService and InvitationService
- Add delete handlers in admin_handlers.go and admin_server.go
- Add delete buttons to users and invitations admin UI templates
- Users can be deleted permanently (with self-deletion prevention)
- Only non-pending invitations (revoked, expired, redeemed) can be deleted
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The warmup binary was being built in build-eagle but each job has its
own checkout, so the binary wasn't available in the deploy job when
we tried to scp it to the droplet.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The mac history editor is no longer needed now that we have the Admin
console for game management.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add blue-green deployment infrastructure for Eagle server
- Add ReloadGames RPC to eagle.proto for reloading game state from disk
- Implement reloadAllGames() and flushToDisk() in GamesManager.scala
- Add reloadGames() handler to EagleServiceImpl.scala
- Update docker-compose.prod.yml with eagle-blue/green services
- Update nginx.conf for switchable upstream
- Create scripts/deploy-blue-green.sh for zero-downtime deployment
- Create Go warmup tool (src/main/go/net/eagle0/warmup) that:
- Uses bidirectional streaming to create test games
- Posts Improve command and verifies ActionResults
- Cleans up test game after warmup
- Create scripts/warmup-eagle.sh wrapper that uses Go tool or falls back to grpcurl
- Update docker_build.yml to build and deploy warmup binary
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add X-Warmup-User header support with localhost restriction
- AuthorizationInterceptor: Accept X-Warmup-User header for warmup authentication
- Only allow X-Warmup-User from localhost connections (security)
- Go warmup tool: Send x-warmup-user metadata header
- Add grpc/metadata dependency to warmup BUILD
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Instead of asking users to copy/paste a PowerShell command, emails now
link to a landing page at /invite/{code} that:
- Shows a branded "Accept Invitation" page
- Offers a "Download & Install" button that downloads a .bat file
- The .bat file downloads the installer and runs it with --code=XXX
- Shows clear instructions for running the .bat file
- Displays error messages for invalid/expired/redeemed codes
- Falls back to showing the invitation code for manual entry
Changes:
- Add invitation_handlers.go with landing page and .bat download routes
- Update main.go to register the new HTTP routes
- Simplify email template to just link to the landing page
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove Debug.Log calls from ArrowVolleyAnimator and MeleeAnimator.
Keep Debug.LogWarning calls that indicate configuration issues.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Changed all animators to use GetCellCenterPosition instead of
GetCellLocalPosition so animations start and end at the actual
hex center rather than offset towards the top.
- MoveAnimator: footprints centered
- MeleeAnimator: weapon animations centered
- CatapultAnimator: projectile source now also centered (target already was)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The converter was using factionId = Some(viewingFactionId) which
applied visibility restrictions to captured heroes. The old proto-based
factory used factionId = None (with comment "can see unaffiliated
heroes") which always showed full hero info.
When handling captured heroes, the player needs to see all hero stats
to make informed decisions about recruiting, imprisoning, etc.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When observing battles, Move animations were playing backwards because
the model is fully updated before animations play. The animation code
was using the unit's current location (post-move) as the source, but
for multi-step moves this caused animations to go from the final
position back to intermediate positions.
Fix: Track source coordinates in ShardokGameModel.MoveSourceCoords
before applying each diff, then use these stored coordinates for
animation source positions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add hostility and originProvinceId fields to Scala ArmyStats that were
present in the proto definition but missing from the Scala model. This
fixes the AttackDecisionCommandChooser which uses hostility to determine
friendly vs enemy armies.
Unlike #5174, this uses required fields instead of defaults, ensuring
all call sites explicitly provide the values rather than relying on
potentially incorrect defaults.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add tutorial trigger hooks to both strategic (EagleGameController) and
tactical (ShardokGameController) layers to enable the tutorial system
to respond to game events.
Strategic layer hooks:
- TutorialManager initialization with auto-start onboarding
- OnModelUpdated for game state changes
- OnProvinceSelected for province selection
- OnCommandIssued for command submission
Tactical layer hooks:
- TutorialManager initialization on battle entry
- OnBattleEntered when entering combat
- OnBattleAction for each action result
- OnUnitSelected for tile selection
- OnTurnEnded for turn completion
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace Docker-based appleboy/scp-action and appleboy/ssh-action with
native scp and ssh commands. This eliminates the Docker container build
overhead that was causing ~3 minute delays during each deploy.
Changes:
- deploy job now runs on self-hosted runner
- Use native scp to copy files to droplet
- Use native ssh with heredoc for deployment script
- Add DO_DROPLET_IP and DO_REGISTRY_TOKEN to env block
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The UpdateAction callback was being set before hexGrid.SetUp() was called.
Since SetUp() is queued via MainQueue, game updates arriving in between
would call ModelUpdated() before cells were initialized.
Fixed by:
1. Moving Model.UpdateAction assignment inside the queued block, after SetUp()
2. Added defensive null checks in HexGrid.SetProfessionImage/SetUnitTypeImage
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a melee animation was cancelled (either by starting a new animation
or calling CancelAnimation), the weapon GameObjects were not destroyed
because they were local variables in the coroutine.
Fixed by storing weapons in class-level fields and adding a CleanupWeapons()
method that is called when animations are cancelled or complete normally.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Scala DiplomacyOfferInfo was missing the eligibleStatuses field,
causing the client to not know what actions are available when
resolving diplomacy offers (alliance, truce, ransom, invitation,
break alliance).
Changes:
- Add eligibleStatuses: Vector[Status] to DiplomacyOfferInfo
- Update all resolve command factories to populate eligibleStatuses
- Update AvailableCommandConverter to apply eligibleStatuses to proto
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
StopAll() could be called before a game was set up (ModelUpdater not
initialized) or called multiple times (second call after ModelUpdater
was set to null). Added null check to prevent the crash.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
DigitalOcean blocks SMTP ports (587, 465) by default. Switch to
Fastmail's JMAP API which uses HTTPS and is not blocked.
Changes:
- Rewrite sendgrid.go to use JMAP API (Email/set + EmailSubmission/set)
- Update docker-compose.prod.yml with FASTMAIL_* env vars
- Update auth_build.yml workflow with new secrets
Required GitHub secrets:
- FASTMAIL_API_TOKEN: API token with email submission scope
- FASTMAIL_FROM_EMAIL: Sender email (optional, uses identity default)
- FASTMAIL_FROM_NAME: Sender name (optional, defaults to "Eagle0 Game")
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The workflow writes SMTP credentials to .env, but docker-compose only
passes explicitly listed environment variables to containers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add SMTP_USERNAME, SMTP_PASSWORD, SMTP_FROM_EMAIL, and SMTP_FROM_NAME
to the deploy job so the auth service can send invitation emails.
The credentials are now:
1. Mapped from GitHub secrets in the env section
2. Passed via SSH in the envs parameter
3. Written to .env file on the production server
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Use Go's net/smtp package with STARTTLS instead of SendGrid API.
This allows using Fastmail (or any SMTP provider) without DNS changes.
Environment variables:
- SMTP_HOST (default: smtp.fastmail.com)
- SMTP_PORT (default: 587)
- SMTP_USERNAME
- SMTP_PASSWORD (app password)
- SMTP_FROM_EMAIL
- SMTP_FROM_NAME
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add invitation code capture to Windows installer
Support --code=XXXX command line argument to pass invitation codes.
The code is saved to invitation.json in the install directory for
the Unity client to read during OAuth.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add invitation code support to Unity client OAuth flow
- Add InvitationCodeManager to read codes from installer file or PlayerPrefs
- Pass invitation code in GetOAuthUrlRequest
- Handle OAUTH_STATUS_INVITATION_REQUIRED response
- Clear invitation code after successful new account creation
- Add OnInvitationRequired event for UI handling
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add PowerShell install option and manual code entry UI
Email template:
- Add Option 1: PowerShell one-liner to download and run with code
- Add Option 2: Manual download with code entry instructions
Unity client:
- Add invitation code entry panel UI references
- Handle OnInvitationRequired event to show code entry
- OnSubmitInvitationCodeClicked saves code and returns to login
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add tutorial system foundation
Create the core architecture for a comprehensive tutorial system:
- TutorialState: PlayerPrefs-based persistence for tutorial progress
- TutorialStep/TutorialSequence: Data structures for tutorial content
- TutorialManager: Singleton coordinating state, triggers, and UI
- TutorialTriggerRegistry: Event-based trigger system for contextual tutorials
- TutorialUIManager: Coordinates modal, overlay, and hint UI components
- TutorialModalPanel: Full-screen modal dialogs for important tutorials
- TutorialOverlayController: Highlighting UI elements with tooltips
- TutorialHintIndicator: Subtle pulsing hints on UI elements
Supports:
- Guided onboarding sequences for new players
- Contextual tutorials triggered on first encounter/attempt
- Mixed UI: modals, overlays, and hint indicators
- Skip/dismiss functionality
- Progress persistence via PlayerPrefs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix missing namespace imports in tutorial system
Add using statements for eagle and Shardok namespaces to resolve
compiler errors referencing EagleGameController, ShardokGameController,
and IGameModel.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Unity meta files for tutorial system
Unity requires .meta files for all assets including scripts and
directories. These are needed for the Unity build to succeed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Rename namespace to Eagle0.Tutorial to avoid conflict
The Eagle namespace is used by generated protobuf code (Eagle.EagleClient).
Using Eagle.Tutorial was shadowing this, causing compilation errors.
Renamed to Eagle0.Tutorial to avoid the conflict.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Make AvailableCommandsFactory return Scala types instead of proto
Convert AvailableCommandsFactory to work entirely with Scala types internally
and return Scala OneProvinceAvailableCommands. Proto conversion now happens at
API boundaries (EngineImpl) rather than inside the factory. This continues the
protoless migration by pushing proto dependencies to the edges of the system.
- Add Scala OneProvinceAvailableCommands case class
- Add OneProvinceAvailableCommandsConverter for proto conversion
- Update AvailableCommandsFactory to return Scala types
- Update callers (EngineImpl, RoundPhaseAdvancer, action classes)
- Remove proto overloads from diplomacy resolution factories
- Update tests to work with new Scala types
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove dead proto code from AvailablePleaseRecruitMeCommandFactory
Delete unused proto overload and ExpandedUnaffiliatedHeroUtils which
was only used by the proto code path.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix missing DEVASTATION case in SelectedCommandConverter
Add missing case for ImprovementTypeProto.DEVASTATION in the
improvementTypeFromProto match expression.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add invitation-based account creation system (Phase 1-2)
Implement invitation system to restrict new account creation to
invited users only. Existing users are grandfathered.
Proto definitions:
- Add Invitation, InvitationStatus, InvitationDatabase to user.proto
- Add invitation management RPCs to admin.proto (CreateInvitation,
ListInvitations, RevokeInvitation, ResendInvitation)
- Add invitation_code field to GetOAuthUrlRequest
- Add OAUTH_STATUS_INVITATION_REQUIRED status
Go auth service:
- Add InvitationService for managing invitations with persistence
- Add EmailService for SendGrid integration (disabled if API key not set)
- Update OAuth flow to pass invitation code through state
- Validate invitation code for new users in CheckOAuthStatus
- Add admin handlers for invitation management
Environment variables:
- SENDGRID_API_KEY: Required for email sending
- SENDGRID_FROM_EMAIL: Sender email (default: noreply@eagle0.net)
- SENDGRID_FROM_NAME: Sender name (default: Eagle0 Game)
- INSTALLER_DOWNLOAD_URL: URL for installer download link
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add invitation management UI to admin panel (Phase 3)
- Add "Invitations" link to navigation
- Create invitations.html and invitations_rows.html templates
- Add invitation management handlers:
- handleInvitationsPage: List all invitations with filtering
- handleInvitationsSearch: Search/filter invitations (htmx)
- handleCreateInvitation: Create and send invitation
- handleResendInvitation: Resend invitation email
- handleRevokeInvitation: Revoke a pending invitation
Features:
- Status filtering (All/Pending/Redeemed/Expired/Revoked)
- Email search
- Create invitation modal with expiration days
- Resend and Revoke actions for pending invitations
- Copy invitation code to clipboard
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add feature flag for invitation code requirement
Add REQUIRE_INVITATION_CODE environment variable to control whether
new users must provide invitation codes. Defaults to false, allowing
the invitation system to be deployed without immediately blocking
new signups.
- Add isInvitationRequired() function that checks env var
- Only validate invitation codes when feature flag is enabled
- Log feature flag status at startup
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Now that all Available*CommandFactory classes use ScalaAvailableCommandsFactory,
remove the adapter infrastructure:
- Delete UnifiedCommandFactory trait
- Delete LegacyFactoryAdapter and ScalaFactoryAdapter
- Delete AvailableCommandsFactoryForType trait
- Update AvailableCommandsFactory to use ScalaAvailableCommandsFactory directly
- Update tests to mock ScalaAvailableCommandsFactory
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Old installers saved eagle0.net to the registry. The new installer
(without auth) was reading that saved URL and failing with 401.
Now the URL is hardcoded to assets.eagle0.net with no registry storage.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate Windows installer to public CDN at assets.eagle0.net
- Change default URL from eagle0.net to assets.eagle0.net
- Make Basic Auth optional (public CDN doesn't require credentials)
- Allow users to proceed without credentials for public CDN
- Retain credential validation for custom authenticated servers
This prepares the installer for the migration from the local Go
asset server to DigitalOcean Spaces CDN with public access.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add public ACL support for S3 uploads
Update Go AWS utilities and build handlers to upload files with
public-read ACL, enabling direct CDN access without presigned URLs.
- Add UploadFilePublic and UploadBytesPublic functions to s3.go
- Update unity3d_windows_build_handler to use public uploads
- Update manifest_manager to use public uploads
- Update installer_build_handler to use public uploads
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove HTTP Basic Auth and credentials UI from installer
With public CDN access at assets.eagle0.net, authentication is no longer
needed. This significantly simplifies the installer:
- Remove LoginDialog.cs entirely
- Simplify CredentialManager to only store server URL
- Remove auth headers and credential handling from EagleUpdater
- Remove login panel, credentials button from MainForm
- Remove credential-related CLI flags from Program.cs
The installer now just downloads from the public CDN without any
authentication prompts or credential storage.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This is the final factory conversion. All Available*CommandFactory
classes now extend ScalaAvailableCommandsFactory instead of the
legacy AvailableCommandsFactoryForType.
Changes:
- Update CapturedHeroOption enum to add Exile and Return (previously
only had Release which mapped to Exile)
- Update AvailableCommandConverter for new CapturedHeroOption cases
- Convert factory to use Scala GameState and return Scala AvailableCommand
- Update AvailableCommandsFactory to call the converted factory with
Scala GameState and convert result to proto
- Rewrite test to construct Scala GameState directly without proto
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Update build_sysroot.yml to upload to eagle0-sysroot bucket
- Update MODULE.bazel sysroot URLs to new bucket location
Note: Before merging, copy existing sysroot files to new bucket:
aws s3 cp s3://eagle0-windows/sysroot/v3/ s3://eagle0-sysroot/v3/ --recursive
aws s3 cp s3://eagle0-windows/sysroot/v4/ s3://eagle0-sysroot/v4/ --recursive
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Make meteor animation more dramatic
- Slower fall duration (0.4s -> 0.8s) with visible rock rotation
- Continuous fiery trail that follows behind the meteor with hot-to-cool
color gradient
- Bigger explosion (endScale 50 -> 80) with initial flash effect
- Rock debris particles that fly outward with gravity arc and spin
- Trail particles wobble perpendicular to fall direction for more dynamic look
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Configure MeteorAnimator sprite references in scene
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert AvailableAttackDecisionCommandFactory to Scala types
- Convert factory to extend ScalaAvailableCommandsFactory
- Use Scala GameState, Army, MovingArmy, HostileArmyGroup
- Use Scala AttackDecisionType, ArmyStats, ExpandedCombatUnit
- Use Scala FactionUtils.provinces instead of LegacyFactionUtils
- Update AvailableCommandsFactory to use ScalaFactoryAdapter
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix test to use Scala GameState directly instead of proto
Convert AvailableAttackDecisionCommandFactoryTest to construct Scala
GameState directly using concrete types (HeroC, ProvinceC, FactionC,
BattalionC, MovingArmy, HostileArmyGroup, FactionRelationship) instead
of proto types.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert AvailableFreeForAllDecisionCommandFactory to Scala types
- Convert factory to extend ScalaAvailableCommandsFactory
- Use Scala GameState, Army, MovingArmy, HostileArmyGroupStatus
- Use Scala AttackDecisionType, ArmyStats, ExpandedCombatUnit
- Use Scala RoundPhase.FreeForAllDecision
- Update AvailableCommandsFactory to use ScalaFactoryAdapter
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix test to use Scala GameState directly instead of proto
Convert AvailableFreeForAllDecisionCommandFactoryTest to construct Scala
GameState directly using concrete types (HeroC, ProvinceC, FactionC,
BattalionC, MovingArmy, HostileArmyGroup) instead of proto types.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert AvailableManagePrisonersCommandFactory to Scala types
- Convert factory to extend ScalaAvailableCommandsFactory
- Use Scala GameState, UnaffiliatedHeroType, PrisonerManagementOption
- Expand PrisonerManagementOption enum with Exile, Move, Return cases
- Update AvailableCommandConverter and SelectedCommandConverter
- Update AvailableCommandsFactory to use ScalaFactoryAdapter
- Fix tests to use new enum cases
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix test to use Scala GameState directly instead of proto
Convert AvailableManagePrisonerCommandsFactoryTest to construct Scala
GameState directly using concrete types (HeroC, ProvinceC, FactionC,
UnaffiliatedHeroC) instead of converting from proto.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert AvailableDefendCommandsFactory from proto types to Scala types:
- Extend ScalaAvailableCommandsFactory trait
- Use Scala GameState, ProvinceT, HeroT, BattalionT types
- Use Scala Profession enum with scalaProfessionOrdering
- Use FactionUtils and BattalionUtils (not Legacy versions)
- Convert SuitableBattalions from util to AvailableCommand type
- Update BUILD.bazel deps for both factory and test
- Convert test to use Scala types (BattalionType, Neighbor, etc.)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Extend ScalaAvailableCommandsFactory trait instead of
AvailableCommandsFactoryForType
- Take Scala GameState as input, return Scala AvailableCommand
- Use ProvinceUtils, HeroUtils, BattalionUtils instead of Legacy versions
- Convert proto CombatUnit to Scala RecommendedCombatUnit
- Convert BattalionSuitability.SuitableBattalions to AvailableCommand.SuitableBattalions
- Update AvailableCommandsFactory to use ScalaFactoryAdapter
- Update tests to use GameStateConverter.fromProto for proto test setup
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The doctl --no-header flag wasn't working reliably, causing the script to
treat column headers ("Name", "Manifest", "Digest") as actual repository
names and digests.
Fixes:
- Filter out "Name" from repository list
- Filter out "Digest" from manifest list
- Validate digests start with "sha256:" before attempting delete
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Extend ScalaAvailableCommandsFactory trait instead of
AvailableCommandsFactoryForType
- Take Scala GameState as input, return Scala AvailableCommand
- Use FactionUtils, HeroUtils, ProvinceUtils instead of Legacy versions
- Group diplomacy options by targetFactionId using
DiplomacyOption(targetFactionId, optionTypes: Vector[DiplomacyOptionType])
- Update AvailableCommandsFactory to use ScalaFactoryAdapter
- Update tests to use GameStateConverter.fromProto for proto test setup
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert AvailableResolveTributeCommandsFactory to Scala types
Updates AvailableResolveTributeCommandsFactory to:
- Extend ScalaAvailableCommandsFactory
- Take Scala GameState and ProvinceT input
- Return Scala ResolveTributeAvailable
- Use Scala HostileArmyGroup and HostileArmyGroupStatus types
- Inline hero/troop counting to avoid proto dependencies
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Convert ResolveTribute test to use Scala types
Update AvailableResolveTributeCommandsFactoryTest to use Scala GameState
and related types instead of proto types, matching the factory conversion.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change factory to extend ScalaAvailableCommandsFactory
- Use Scala GameState, ProvinceT, UnaffiliatedHeroT, RecruitmentInfo types
- Use Scala ProvinceUtils instead of LegacyProvinceUtils
- Return Scala DivineAvailable with ExpandedUnaffiliatedHero
- Simplify test to use makeGameState helper pattern
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert AvailableRecruitHeroesCommandFactory to Scala types
- Change factory to extend ScalaAvailableCommandsFactory
- Use Scala GameState, ProvinceT, UnaffiliatedHeroT, HeroT types
- Use Scala FactionUtils, ProvinceUtils, RecruitmentOdds instead of Legacy versions
- Return Scala RecruitHeroesAvailable with ExpandedUnaffiliatedHero
- Update BUILD.bazel with Scala dependencies
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix test to use Scala types instead of proto types
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change factory to extend ScalaAvailableCommandsFactory
- Use Scala GameState, ProvinceT, UnaffiliatedHeroT, RecruitmentInfo types
- Use Scala ProvinceUtils instead of LegacyProvinceUtils
- Return Scala DeclineQuestAvailable with ExpandedUnaffiliatedHero
- Update test to use Scala types with makeGameState helper
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert the HandleRiotCrackDown factory to use Scala GameState and return
Scala AvailableCommand instead of proto types. Updates:
- Factory extends ScalaAvailableCommandsFactory
- Uses ProvinceUtils.hasImminentRiot instead of LegacyProvinceUtils
- Uses ProvinceT instead of proto Province
- Returns AvailableCommand.HandleRiotCrackDownAvailable
- Updated BUILD.bazel with Scala dependencies
- Updated AvailableCommandsFactory to use ScalaFactoryAdapter
- Converted test to use Scala types with makeGameState() pattern
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Updates AvailableIssueOrdersCommandFactory to:
- Extend ScalaAvailableCommandsFactory
- Take Scala GameState input
- Return Scala IssueOrdersAvailable
- Use FactionUtils.provinceCount instead of LegacyFactionUtils
- Add toCommandOrderType converter between province and command ProvinceOrderType
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Updates AvailableHandleRiotCrackDownCommandFactory to:
- Extend ScalaAvailableCommandsFactory
- Take Scala GameState and ProvinceT input
- Return Scala HandleRiotCrackDownAvailable
- Use ProvinceUtils.hasImminentRiot instead of LegacyProvinceUtils
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Changes factory to use Scala GameState and return Scala AvailableCommand.
Uses ProvinceUtils.effectiveAgriculture and effectiveEconomy for battalion
type availability checks. Returns OrganizeTroopsAvailable with proper
BattalionTypeId.value conversions.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Changes factory to use Scala GameState and return Scala AvailableCommand.
Uses ProvinceUtils.effectiveInfrastructure instead of LegacyProvinceUtils,
BattalionTypeFinder for battalion type lookups, and proper BattalionTypeId
enum values with .value conversion for ArmamentCost integer fields.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Updates AvailableHandleRiotGiveCommandFactory to:
- Extend ScalaAvailableCommandsFactory
- Take Scala GameState input
- Return Scala HandleRiotGiveAvailable
- Use ProvinceUtils.hasImminentRiot instead of LegacyProvinceUtils
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add separate animations for meteor phases
MeteorAnimator now supports distinct animations for each meteor phase:
- MeteorStart: Charging effect with growing glow and spiraling particles
- MeteorTarget: Pulsing target indicator with contracting ring
- MeteorCast: Falling meteor with trail and explosion (existing)
- MeteorCancel: Fizzle effect with dispersing particles
Update ShardokGameController to map each ActionType/CommandType to
the appropriate animation instead of using MeteorCast for all phases.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add meteor phase animation settings to scene
Configure MeteorAnimator with sprites and settings for:
- Charge phase (orange glow)
- Target phase (red indicator)
- Cancel phase (grey fizzle)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify Charge animation and add weapon orientation to Melee
ChargeAnimator:
- Remove defender weapon, show only attacker thrusting
- Accelerating thrust motion (slow start, fast finish)
- Add impact flash on each thrust
- Cleaner, less chaotic animation
MeleeAnimator:
- Add per-weapon rotation offset and flip settings
- Settings for sword, mace, small spear, large spear, dagger, bone
- Each weapon type can be independently oriented
- Follows same pattern as ToolAnimator
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Center Charge animation on hex centers
Use GetCellCenterPosition instead of GetCellLocalPosition to
position the weapon at the center of the hex.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make Melee animation slower with deliberate swings
- Increase clashDuration from 0.12s to 0.3s (slower, more visible)
- Reduce clashCount from 3 to 2 (fewer but deliberate)
- Increase swingArc from 60 to 90 degrees (more pronounced)
- Remove shake during swing motion (only at impact)
- Reduce shakeIntensity from 8 to 3
- Clean, smooth swing interpolation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Change cavalry weapons from spears to swords in Melee
Light cavalry now uses falchion (was small spear)
Heavy cavalry now uses two-handed sword (was large spear)
Both cavalry types now swing instead of thrust.
Renamed sprite and orientation fields:
- smallSpearSprite -> falchionSprite
- largeSpearSprite -> twoHandedSwordSprite
- smallSpearRotationOffset -> falchionRotationOffset
- largeSpearRotationOffset -> twoHandedSwordRotationOffset
- (and corresponding flip settings)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix remaining spear references in MeleeAnimator
Update sprite null check to use new weapon names.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update animator settings in Unity scene
MeleeAnimator:
- Rename spear sprites to falchion/twoHandedSword
- Add per-weapon rotation/flip settings
ChargeAnimator:
- Add flash settings
- Update thrust/pullback settings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert the HandleRiotDoNothing factory to use Scala GameState and return
Scala AvailableCommand instead of proto types. Updates:
- Factory extends ScalaAvailableCommandsFactory
- Uses ProvinceUtils.hasImminentRiot instead of LegacyProvinceUtils
- Returns AvailableCommand.HandleRiotDoNothingAvailable
- Updated BUILD.bazel with Scala dependencies
- Updated AvailableCommandsFactory to use ScalaFactoryAdapter
- Converted test to use Scala types with makeGameState() pattern
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert the Trade factory to use Scala GameState and return
Scala AvailableCommand instead of proto types. Updates:
- Factory extends ScalaAvailableCommandsFactory
- Uses Scala ProvinceT for province access
- Returns AvailableCommand.TradeAvailable
- Updated BUILD.bazel with Scala dependencies
- Updated AvailableCommandsFactory to use ScalaFactoryAdapter
- Converted test to use Scala types with makeGameState() pattern
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update the SwearBrotherhood command factory to use Scala GameState and
return Scala AvailableCommand types instead of proto.
Key changes:
- Factory extends ScalaAvailableCommandsFactory
- Uses Scala types: GameState, FactionT, HeroT
- Returns SwearBrotherhoodAvailable (Scala enum case)
- Rewrote test to use Scala types with makeGameState() pattern
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update the Improve command factory to use Scala GameState and return
Scala AvailableCommand types instead of proto.
Key changes:
- Factory extends ScalaAvailableCommandsFactory
- Uses Scala types: GameState, ProvinceT, HeroT, Profession
- Returns ImproveAvailable (Scala enum case)
- Added Devastation to ImprovementType enum in command/common
- Added conversion function between ImprovementType types
- Updated AvailableCommandConverter for Devastation handling
- Rewrote test to use Scala types with makeGameState() pattern
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert AvailableHeroGiftCommandFactory from proto types to Scala:
- Extends ScalaAvailableCommandsFactory instead of AvailableCommandsFactoryForType
- Uses Scala GameState, ProvinceT, FactionT instead of proto types
- Uses FactionUtils and HeroUtils instead of Legacy versions
- Returns HeroGiftAvailable with EligibleGift instead of proto types
- Update AvailableCommandsFactory to use ScalaFactoryAdapter
- Rewrite test to use Scala types with inside() pattern
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert AvailableExileVassalCommandFactory from proto types to Scala:
- Extends ScalaAvailableCommandsFactory instead of AvailableCommandsFactoryForType
- Uses Scala GameState, ProvinceT, FactionT instead of proto types
- Uses FactionUtils and ProvinceUtils instead of Legacy versions
- Returns ExileVassalAvailable instead of ExileVassalAvailableCommand proto
- Update AvailableCommandsFactory to use ScalaFactoryAdapter
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Extend ScalaAvailableCommandsFactory instead of AvailableCommandsFactoryForType
- Use Scala GameState, ProvinceT types
- Use Scala ControlWeatherAvailable, TargetProvinceOptions, ControlWeatherType
- Use ProvinceUtils.hasBlizzard/hasDrought instead of LegacyProvinceUtils
- Use Profession.Mage instead of profession.isMage
- Update test to use HeroC, ProvinceC, Neighbor, makeGameState pattern
- Use inside() pattern for type matching in tests
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Extend ScalaAvailableCommandsFactory instead of AvailableCommandsFactoryForType
- Use Scala GameState, ProvinceT, HeroT, UnaffiliatedHeroT types
- Use Scala AvailableCommand.ApprehendOutlawAvailable and ResidentOutlaw
- Update test to use HeroC, ProvinceC, UnaffiliatedHeroC, makeGameState pattern
- Use inside() pattern for type matching in tests
- Update BUILD.bazel deps to use Scala state types
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update the factory to extend ScalaAvailableCommandsFactory and use
Scala GameState and ProvinceUtils instead of proto types.
Return Scala TravelAvailable instead of proto.
Update test to use HeroC, ProvinceC, and Scala GameState.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update the factory to extend ScalaAvailableCommandsFactory and use
Scala GameState, HeroT, and Profession instead of proto types.
Return Scala TrainAvailable instead of proto.
Update test to use HeroC, ProvinceC, BattalionC, and Scala GameState.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change AvailableReconCommandFactory to extend ScalaAvailableCommandsFactory
- Use Scala GameState, ProvinceT, HeroT, Profession, IncomingRecon
- Returns Scala AvailableCommand.ReconAvailable instead of proto
- Wrap with ScalaFactoryAdapter in AvailableCommandsFactory
- Update test to use HeroC, ProvinceC, FactionC, and Scala GameState
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update the factory to extend ScalaAvailableCommandsFactory and use
Scala GameState instead of proto. Return Scala SendSuppliesAvailable
instead of proto.
Update test to use HeroC, ProvinceC, and Scala GameState.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change AvailableAlmsCommandFactory to extend ScalaAvailableCommandsFactory
- Use Scala GameState, ProvinceT, HeroT, Profession instead of proto types
- Returns Scala AvailableCommand.AlmsAvailable instead of proto
- Wrap with ScalaFactoryAdapter in AvailableCommandsFactory
- Update test to use HeroC, ProvinceC, FactionC, and Scala GameState
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update the factory to extend ScalaAvailableCommandsFactory and use
Scala GameState instead of proto. Return Scala ReturnAvailable
instead of proto.
Update test to use HeroC, ProvinceC, and Scala GameState.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change AvailableTravelCommandsFactory to extend ScalaAvailableCommandsFactory
- Use Scala GameState, ProvinceT, and return AvailableCommand.TravelAvailable
- Wrap with ScalaFactoryAdapter in AvailableCommandsFactory
- Update test to use HeroC, ProvinceC, and Scala GameState
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Adjust animation sprite settings in scene
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Reduce animation to land in hex center
Use GetCellCenterPosition for the target position so the catapult
projectile lands in the center of the hex, consistent with RaiseDead.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix hammer orientation in Repair animation
Add baseRotation offset (default 180 degrees) to flip the hammer
so the head strikes the target instead of the handle.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Flip hammer horizontally to show striking face
Add flipHorizontal option (default true) to flip the hammer so the
striking face is forward instead of the claw.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Separate rotation/flip settings for hammer vs alternate tool
- hammerBaseRotation, hammerFlipHorizontal for repair
- alternateBaseRotation, alternateFlipHorizontal for bridge building
- Allows axe to be oriented differently from hammer
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Center arrow volley on hex centers
Use GetCellCenterPosition for source and target so arrows start
and end centered on the hex cells, with spread applied around
the center points.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make Extinguish water effect larger and more chaotic
- Increase droplet count (12 → 25) and steam count (5 → 8)
- Increase fall height (60 → 100) and spread (25 → 40)
- Add variable droplet sizes (5-15 scale range)
- Add staggered launch spread for chaotic timing
- Add horizontal chaos movement during fall (sine wave pattern)
- Longer fall duration for more dramatic effect
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Scout vision cone to extend outward from eye
Position the cone offset from source by half its length so the
base of the triangle stays at the eye while the tip extends
outward toward the target during the sweep.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Redesign Scout animation with traveling glow effect
Replace sweep-based cone animation with straight extension:
- Eye appears at source hex with scale-up animation
- Cone extends directly toward target (no sweep rotation)
- Glow effect travels with cone tip, growing from small to 7-hex coverage
- Glow size calculated from hex radius for consistent area coverage
- Hold phase with pulsing glow at target before fade out
Add triangle sprite for vision cone effect.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add cone rotation offset for Scout animation
Triangle sprite with apex pointing up needs -90° offset to point
toward target. Default coneRotationOffset=-90 handles this case.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Scout cone scaling axis after rotation
After +90° rotation, the sprite's Y axis is the length direction.
Swap scale values so Y controls length and X controls width.
This keeps the cone base anchored at the eye while extending toward target.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Takes Scala GameState instead of proto
- Returns Scala AvailableCommand.RestAvailable instead of proto
- Wrapped with ScalaFactoryAdapter in AvailableCommandsFactory
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
First factory to use the new ScalaAvailableCommandsFactory trait:
- Takes Scala GameState instead of proto
- Uses Scala ProvinceT and HeroUtils instead of proto/LegacyHeroUtils
- Returns Scala AvailableCommand.FeastAvailable instead of proto
Wrapped with ScalaFactoryAdapter in AvailableCommandsFactory, demonstrating
the incremental migration pattern where new Scala factories get zero
conversion overhead while legacy factories remain unchanged.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Introduces adapter pattern to allow incremental migration of Available*CommandFactory
classes from proto to Scala types:
- ScalaAvailableCommandsFactory: trait for new factories using Scala GameState
- UnifiedCommandFactory: unified interface taking both Scala and Proto GameState
- LegacyFactoryAdapter: wraps existing proto-based factories
- ScalaFactoryAdapter: wraps new Scala factories, converts output to proto
AvailableCommandsFactory now takes Scala GameState as input and converts to proto
internally, passing both states to factories. All existing factories wrapped with
LegacyFactoryAdapter. New Scala factories can be added using ScalaFactoryAdapter
with zero conversion overhead.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Implement footprint trail animation for unit movement
Replace single-sprite slide animation with footprint/hoofprint trail:
- Boot prints for infantry (alternating left/right with flip)
- Horseshoe prints for cavalry
- Prints appear sequentially along path
- FIFO fade out (first prints fade first)
- Configurable print count, timing, colors, and scale
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Test both boot and horseshoe animations in TestMove
Shows infantry boot prints first, waits 1.5s, then shows cavalry
horseshoe prints so both can be seen in sequence.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add missing System.Collections using for IEnumerator
* Improve footprint animation: smaller prints, more prints, lateral offset for hooves
- Reduce print scale from 3.0 to 1.5
- Increase prints per hex from 3 to 5
- Faster print interval (0.06s vs 0.08s)
- Add lateral offset to horse prints (front/rear hoof distinction)
- Make lateral offset configurable
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Increase lateral offset from 3 to 6 for more visible zigzag
* Add footprint sprites and wire up MoveAnimator in scene
Add sprites:
- leather_boot.png, armored_boot.png (infantry prints)
- light_horse.png, heavy_horse.png (cavalry prints)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update MoveAnimator settings in scene
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Creates SelectedCommandConverter.fromProto() to convert proto
SelectedCommand messages to their Scala equivalents. This is the
inverse of AvailableCommandConverter which converts Scala to proto.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add AvailableCommandConverter for Scala-to-Proto conversion
Implements toProto conversion for AvailableCommand types, enabling
conversion from the Scala domain types to proto format. Key changes:
- Add AvailableCommandConverter with toProto method taking GameState context
- Fix SelectedCommand.scala enum types to match proto structure:
- AttackDecisionType: Advance/Withdraw/DemandTribute/SafePassage
- ControlWeatherType: StartBlizzard/EndBlizzard/StartDrought/EndDrought
- ImprovementType: Agriculture/Economy/Infrastructure
- ProvinceOrderType: Develop/Mobilize/Expand/Entrust
- Handle ScalaPB oneof patterns (use case classes directly, not SealedValue)
- Look up full data from GameState for simplified Scala types
- Add visibility for legacy_battalion_view_filter and hero_view_filter
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add tests for AvailableCommandConverter
Comprehensive unit tests covering:
- Simple commands (AlmsAvailable, FeastAvailable, RestAvailable, etc.)
- Commands with enum conversions (ControlWeatherType, ImprovementType, ProvinceOrderType)
- Commands that expand multiple proto options (DiplomacyAvailable)
- Commands that require GameState lookups (ApprehendOutlawAvailable, DefendAvailable)
- Error cases for unsupported conversions (DemandTribute, SafePassage, Ransom)
- Complex nested structures (MarchAvailable, OrganizeTroopsAvailable)
Also adds test visibility to command/available BUILD.bazel.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor tests to use inside() pattern instead of asInstanceOf
Replace shouldBe a[] and asInstanceOf with ScalaTest's inside()
pattern for better error messages and idiomatic type matching.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add type annotations to pattern match destructuring
Adds explicit type annotations to all destructured case class parameters
in pattern matches for improved readability and type safety.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add data to AttackDecisionType and DiplomacyOptionType for full conversion
Changes AttackDecisionType and DiplomacyOptionType from simple enums to
sealed traits with case classes that carry the data needed for proper
proto conversion:
- AttackDecisionType.DemandTribute now takes gold and food amounts
- AttackDecisionType.SafePassage now takes destination province ID
- DiplomacyOptionType.Ransom now takes RansomOfferDetails
Also adds supporting case classes for ransom data:
- RansomOfferDetails
- PrisonerToBeRansomed
- PrisonerOfferedInExchange
- HostageOfferedInExchange
This removes the UnsupportedOperationException cases in the converter.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update converter to import from common package
- Changed imports from selected to common package for shared types
- Updated BUILD.bazel deps from selected to common
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Moves shared enums and supporting types from SelectedCommand to a new
common package, breaking the dependency between AvailableCommand and
SelectedCommand.
Types moved to common:
- AttackDecisionType (with DemandTribute(gold, food) and SafePassage(provinceId))
- ControlWeatherType
- CapturedHeroOption
- DiplomacyOptionType (with Ransom(details))
- ImprovementType
- ProvinceOrderType
- PrisonerManagementOption
- RansomOfferDetails and related case classes
Uses Scala 3 enum syntax with parameterized cases where data is needed.
Enum values now match the proto definitions.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Users who haven't set a display name should not be able to interact
with the Eagle game server. This adds validation in AuthorizationInterceptor
to reject JWT-authenticated requests where displayName is null or empty.
Returns FAILED_PRECONDITION with message:
"Display name not set. Please set a display name before playing."
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
If a user completes OAuth but closes the app before setting their
display name, subsequent session restores or stored account connections
would bypass the display name panel. Now we check for empty display
names after validating the session and show the display name panel
if needed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Creates Scala 3 types for command deproto work:
- SelectedCommand enum with parameterized cases for all selected command variants
- AvailableCommand enum with parameterized cases for all available command variants
- Supporting case classes in AvailableCommand companion object to avoid namespace confusion
- Supporting enums shared between both (AttackDecisionType, ControlWeatherType, etc.)
defined in SelectedCommand and imported by AvailableCommand
These types mirror the proto structure but use native Scala types.
Converters will be added in a follow-up PR.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add environment dropdown to lobby for seamless switching
Replace static lobbyEnvironmentText with lobbyEnvironmentDropdown.
Users can now switch between prod/qa environments while in the lobby
without logging out - the connection is automatically re-established.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix lobby environment dropdown initialization
- Move dropdown setup to SetupLobbyUI() so it's ready at start
- Clear default Unity options before adding environment options
- Set initial value from PlayerPrefs
- UpdateLobbyStatusDisplays now only updates the selection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up lobby environment dropdown in Unity scene
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove environment dropdown from connection panel
Use PlayerPrefs (set by lobby dropdown) for environment selection.
The lobby dropdown now handles all environment switching, so the
connection panel dropdown is no longer needed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove environment dropdown reference from ConnectionHandler
Wire up the lobby environment dropdown and remove the old connection
panel dropdown reference in Unity.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add global uncaught exception handler in Main.scala to catch and log
exceptions from any thread, plus report to Sentry
- Add logging and Sentry reporting to LlmResolver.scala recover block
so LLM processing failures are visible in logs
- Fix exception swallowing in GamesManager.scala game loading - was using
Try().toOption which silently dropped exceptions. Now logs and reports
to Sentry before returning None
This ensures exceptions during startup, game loading, and LLM processing
are properly logged to stdout and sent to Sentry for alerting.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix type mismatch in UpgradeBattalionQuest causing NoSuchElementException
The UpgradeBattalionQuest pattern match was extracting battalionTypeId as
the proto enum type (net.eagle0.eagle.common.battalion_type.BattalionTypeId)
but comparing it against gameState.battalionTypes which uses the Scala
sealed class type (net.eagle0.eagle.model.state.BattalionTypeId).
This type mismatch caused the .find() to always return None, leading to
NoSuchElementException on .get when generating LLM prompts for quest
completion/failure.
Fix: Convert proto BattalionTypeId to Scala type using BattalionTypeIdConverter
before comparing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Convert BattalionTypeId to Scala 3 enum with CanEqual
Modernize BattalionTypeId to use Scala 3 enum syntax and add a CanEqual
instance. This enables type-safe equality checking when files opt in with
`import scala.language.strictEquality`, which would catch proto/model type
mismatches at compile time.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Multi-account OAuth token persistence
- TokenStorage now stores multiple accounts keyed by provider:userId
- Tokens are preserved on logout for quick re-login
- ConnectionHandler displays stored account buttons
- Clicking a stored account button connects (refreshing token if needed)
- Removed legacy/classic auth toggle - OAuth only
- Legacy single-account data is automatically migrated
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add provider icon support to stored account buttons
- Added discordProviderIcon and googleProviderIcon sprite references
- Button prefab should have an Image child for the provider icon
- Icon is set based on account.Provider when creating buttons
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix provider icon to look for child named ProviderIcon
GetComponentInChildren<Image>() was finding the Button's own Image.
Now uses transform.Find("ProviderIcon") to find the specific child.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Unity assets for stored account buttons
- Add Discord Blurple symbol sprite for provider icons
- Add StoredAccountButton prefab with ProviderIcon child
- Wire up stored accounts container and prefab in Gameplay scene
- Update OAuth button sprite import settings
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove Basic Auth (username in header) support from the Eagle server.
Users must now authenticate via OAuth (Discord or Google) to play.
Changes:
- Remove parseBasicAuth method from AuthorizationInterceptor
- Remove Basic Auth fallback in interceptCall (now goes straight to unauthenticated)
- Remove contextWithUserName helper from AuthorizationUtils
- Update documentation and comments to reflect JWT-only auth
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Document completed DiplomacyOfferStatus enum migration (PR #5093)
- Add Enum Type Migrations section tracking proto enum conversions
- Add Next Candidates section with priority items for future work
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Hetzner SSH: remove invalid protocol param, add explicit port
The appleboy/ssh-action doesn't support the 'protocol' parameter.
Adding explicit port: 22 helps with IPv6 address parsing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use self-hosted runner for Hetzner deploy (IPv6 connectivity)
* Use native SSH instead of container action for macOS runner
* Fix: Stop containers by port/name filter before starting new one
* Set SHARDOK_RESOURCES_PATH and SHARDOK_MAPS_PATH env vars for Docker
* Set SHARDOK_EAGLE_INTERFACE_ADDRESS=0.0.0.0:40042 to listen on all interfaces
* Mount TLS certs and config file into Shardok container
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert EligibleDiplomacyStatuses to use Scala Status types internally,
with conversion to proto at the call sites where needed for building
AvailableCommand proto messages.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add animations for all command types to hide latency
Adds 12 new animator classes to provide visual feedback for all
command types, reducing perceived latency when issuing commands:
- ChargeAnimator: Cavalry charge with lance sprites
- ToolAnimator: Hammer strikes for Repair and BuildBridge
- FearAnimator: Dark wave effect from source to target
- ControlAnimator: Mind control beam with spiraling particles
- FireEffectAnimator: Rising flames for StartFire and FireDamage
- ExtinguishAnimator: Water spray with steam for fire extinguishing
- FreezeAnimator: Ice crystal formation for FreezeWater
- WaterEffectAnimator: Splash and ripples for BraveWater/WaterDamage
- ScoutAnimator: Scanning eye with vision cone sweep
- DismissAnimator: Dissolve particles drifting away
- FleeAnimator: Motion blur trail and dust clouds
- DuelAnimator: Crossed swords clashing with sparks
All animators auto-discover HexGrid and use GetCellCenterPosition
for proper hex centering. Integrated into ShardokGameController
with switch cases in PlayAnimationAndSound and PlayAnimation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update ChargeAnimator to support all battalion types
- Add separate sprite fields for each battalion type:
- swordSprite for light infantry
- maceSprite for heavy infantry
- smallSpearSprite for light cavalry
- largeLanceSprite for heavy cavalry
- daggerSprite for longbowmen
- boneSprite for undead
- Rename internal types from Lance* to Weapon* for clarity
- Keep charge-specific animation behavior (more dramatic motion)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update LightningAnimator with multiple bolts and hex centering
- Use GetCellCenterPosition for proper hex center alignment
- Add boltCount field to control number of lightning bolts
- All bolts start from same source hex center
- Each bolt ends at a random point within the target hex
- Add targetSpread field to control endpoint spread within target hex
- Reduce default thicknessMultiplier to 1f for thinner bolts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add test methods for all new animators to AnimationTestController
Adds test buttons for Charge, Repair, BuildBridge, Fear, Control,
Fire, Extinguish, Freeze, WaterSplash, Scout, Dismiss, Flee, and Duel
animations. Updates TestAllSequence to include all new animations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix test method calls to match animator signatures
- AnimateFire instead of AnimateStartFire
- AnimateScout takes source and target cell indices
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up all animator components in Unity scene
Connects all new animator references in the Gameplay scene so
test buttons can trigger animations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Unity scene adjustments
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add CD step to deploy Shardok ARM64 to Hetzner
After building and pushing the ARM64 image, automatically deploy it to
the Hetzner server by SSHing in, pulling the new image, and restarting
the container.
Requires secrets: HETZNER_IP, HETZNER_SSH_KEY
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use tcp6 protocol for IPv6 Hetzner connection
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The SetUserAdmin endpoint was using ParseForm() which doesn't handle
multipart/form-data (what JavaScript FormData sends). This caused the
is_admin field to be empty, defaulting to false even when checked.
Applied the same fix as SetDisplayName - try ParseMultipartForm first,
fall back to ParseForm for compatibility.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This command type was only used by Eagle (which uses oneof, not this enum)
and had dead code in AIHeuristicWeighting.cpp.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add test for applyResolvedBattle in ActionResultApplierImpl
This adds test coverage for the resolvedBattle functionality that was
fixed in #5075. The test verifies that when an ActionResult contains a
resolvedBattle field, the corresponding battle is removed from the
game state's outstandingBattles vector.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove dead applyAccumulatedDetails and add notification tests
- Remove unused applyAccumulatedDetails from GameStateMiscExtensions
(replaced by applyNewNotifications which correctly filters by .deferred)
- Add tests for notification filtering behavior:
- Deferred notifications are added to deferredNotifications
- Non-deferred notifications are NOT added to deferredNotifications
- Mixed notifications are correctly filtered
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Holy Wave animation for InspiredTroops action
Implements an expanding white glow animation that spreads from the
acting unit's hex outward. When the wave reaches hexes containing
undead units, it triggers a violent damage effect with flashing
and burst animations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix hex distance calculation to use Row/Column coordinates
The Coords struct uses Row and Column properties, not Q and R.
Added HexDistance helper that converts offset coordinates to cube
coordinates for accurate hex distance calculation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make HexGrid auto-discovered in animators, fix HolyWave bugs
- Changed all 8 animators to auto-find HexGrid at runtime instead of
requiring manual inspector hookup
- Fixed MissingReferenceException in HolyWaveAnimator by having damage
effects manage their own cleanup lifecycle
- Added glowVerticalOffset parameter to adjust holy wave positioning
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use true hex center and scale glow based on hex size
- Added GetCellCenterPosition() to HexGrid for true hex center
(GetCellLocalPosition returns terrain image position which is offset)
- Added GetHexInnerRadius() to HexGrid for hex size reference
- HolyWaveAnimator now uses true hex center for positioning
- Glow scale now computed from hex radius (glowEndRadii=2.5 means
the glow extends 2.5 hex radii from center, into neighbors)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix naming convention for hexGrid field, add gradient circle sprite
Renamed _hexGrid to __hexGrid per linter naming rules.
Added Gradient Circle sprite for holy wave animation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Center Meteor explosion and RaiseDead glow on hex
Use GetCellCenterPosition instead of GetCellLocalPosition for
proper hex centering.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update Unity scene with HolyWaveAnimator configuration
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix: Remove non-existent DuelChallenged and DuelDeclined ActionTypes
Only ActionType.DuelAccepted exists in the proto definition.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The admin console edit form is sending empty values even when the user
enters data. This adds debugging to identify the root cause:
- Add type="button" to modal buttons to prevent default submit behavior
- Add console.log in JavaScript to show what values are being read
- Add server-side logging to show Content-Type and parsed form values
This is temporary debugging to diagnose why displayName="" and
isAdmin=false are being received when the user enters values.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The DUEL_CHALLENGED action type was defined but never used in any code.
The ChallengeDuelCommand implementation skips directly to emitting
DUEL_ACCEPTED or DUEL_DECLINED without an intermediate challenge step.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This replaces the complex code generation system for ActionResultType
(Go generators + bazel rules creating individual files per enum value)
with a simple Scala 3 enum.
Key changes:
- Delete Go generators and bazel action_result_type_rule.bzl
- Replace per-file generated ResultTypes with single ActionResultType enum
- Add ActionResultType.fromValue() for O(1) lookup by int value
- Add two Scala-only types: HeroStatGained and ProfessionGained
- Remove "ResultType" suffix from all enum values throughout codebase
- Resolve name collisions with qualified imports
- Add parity test ensuring proto and Scala enums stay in sync
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Several functions in users.go were ignoring errors from save(), which
could cause user edits to appear to succeed but not persist to disk.
If save() failed (disk permissions, disk full, etc.), the changes would
be lost on service restart.
Fixed functions:
- SetDisplayName: now returns error if save fails
- SetDisplayNameAdmin: now returns error if save fails (2 places)
- FindOrCreateUser: now logs warning if save fails (can't change signature)
This fixes admin console "Edit" not persisting display name or admin
status changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add HERO_STAT_GAINED (151) and PROFESSION_GAINED (152) to
action_result_type.proto to maintain parity with the Scala enum
- Remove update-action-result-types pre-commit hook and script
(no longer needed with simplified Scala enum approach)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add plan doc for two-stage sound system and new animations
Documents the implementation plan for:
- Two-stage sound system for conditional actions (attempt + result sounds)
- Five new animators: Move, Lightning, Catapult, RaiseDead, Meteor
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Implement two-stage sounds and new combat animations
Two-stage sound system:
- Add attempt sounds for conditional actions (StartFire, Repair, etc.)
- Play attempt sound immediately when command issued
- Play result sound (success/failure) when server responds
- Handle both own commands and other players' actions
New animators:
- MoveAnimator: Smooth hex movement with arc
- LightningAnimator: Jagged electric arc with flicker
- CatapultAnimator: Arcing rock with impact explosion (Reduce)
- RaiseDeadAnimator: Ground glow with rising figures
- MeteorAnimator: Falling meteor with trail and explosion
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up animator sprites in Unity scene
- Add Animation Sprites folder with basic shapes
- Assign sprites to animator components in Gameplay scene
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add AnimationTestController for testing animations
Debug controller with public methods for each animation type:
- TestArrowVolley, TestMelee, TestMove, TestLightning
- TestCatapult, TestRaiseDead, TestMeteor
- TestAll (runs all in sequence)
Wire methods to UI buttons to test animations without game setup.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify AnimationTestController to use ShardokGameController reference
Instead of duplicating animator references, pull them from
ShardokGameController at Start(). Auto-finds controller if not assigned.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up AnimationTestController in Unity scene
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix arc animations and sound playback for non-animated actions
Arc fixes:
- Change arc offset from Z to Y axis so arcs are visible in top-down view
- Affects ArrowVolleyAnimator, CatapultAnimator, and MoveAnimator
Sound fix:
- Add missing sound playback for current player's non-animated actions
- Previously only actions WITH animations played sounds in PerformAction
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Allow animations to run simultaneously without cancellation
Remove animation cancellation logic so multiple animations can play
at once. Each animation manages its own lifecycle and cleans up
when complete, preventing orphaned objects.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update TestMelee to cycle through all battalion types
Runs 3 sequential melee animations covering all 6 battalion types:
- LightInfantry vs HeavyInfantry
- LightCavalry vs HeavyCavalry
- Longbowmen vs Undead
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove HolyWave from two-stage sound system
HolyWave always succeeds - it's deterministic. HolyWaveDamage is a
separate action that fires when undead are damaged, but the wave
itself cannot fail.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove FailedInspireTroops reference after proto update
FailedInspireTroops was removed from the ActionType enum.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Link additional attempt sounds in Unity scene
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Uncomment HTTPS server block for admin.eagle0.net
- HTTP now redirects to HTTPS
Requires SSL certs to be in place before merge.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The FAILED_INSPIRE_TROOPS action type was defined in action_type.proto
but never used in production code. The HolyWaveCommand always succeeds
when inspiring troops - there is no failure path.
The only reference was an unused import in HolyWaveCommand_test.cpp.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add nginx server blocks for admin.eagle0.net and admin.prod.eagle0.net
- Enable IPv6 listeners on all nginx server blocks
- Remove direct port exposure for admin (now accessed via nginx)
- Admin console will be available at https://admin.eagle0.net
Requires SSL certificate setup after DNS propagation:
certbot certonly --webroot -w /var/www/certbot \
-d admin.eagle0.net -d admin.prod.eagle0.net
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add nginx server blocks for admin.eagle0.net and admin.prod.eagle0.net
- Enable IPv6 listeners on all nginx server blocks
- Remove direct port exposure for admin (now accessed via nginx)
- Admin console will be available at https://admin.eagle0.net
Requires SSL certificate setup after DNS propagation:
certbot certonly --webroot -w /var/www/certbot \
-d admin.eagle0.net -d admin.prod.eagle0.net
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Log configuration at startup: eagle-addr, auth-addr, auth-tls
- Improve admin check failure logging to include auth server details
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Two issues prevented CreateGame from succeeding:
1. UNKNOWN_PHASE error: GameStateProto was created without currentPhase,
defaulting to UNKNOWN_PHASE (0) which caused ProtoConversionException
2. None.get in BattalionSuitability: newBattalionTypes was in the proto
but commented out in the Scala model. When creating games, battalion
types were set in ActionResult but lost during proto-to-Scala conversion,
so they never got applied to GameState
Fixed by:
- Setting currentPhase = NEW_ROUND in PersistedHistory initial states
- Adding newBattalionTypes field to ActionResultT, ActionResultC
- Adding conversion in ActionResultProtoConverter
- Adding applyNewBattalionTypes extension method
- Calling extension in ActionResultApplierImpl
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Log whether JWT_PRIVATE_KEY environment variable is set/empty
- Log the key ID and size when successfully loaded
- Log parse errors instead of silently swallowing them
- Helps diagnose OAuth token validation issues across environments
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add AnimationType enum to unify CommandType and ActionType for animations
- Change Model.PerformTargetedCommand to return CommandType? (null = failure)
- Add PlayAnimationAndSound helper that handles both animation and sound
- Add conversion functions: AnimationTypeForCommand, AnimationTypeForAction
- Update PerformAction to play sound immediately with animation
- Update ModelUpdated to use helper for other players' actions
- Skip duplicate sound for current player (already played in PerformAction)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Unity client now always connects to prod.eagle0.net:40033 for OAuth,
regardless of which Eagle server is selected for gameplay. This allows
QA testing with prod authentication.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Each battalion type now has its own weapon and animation style:
- Light Infantry: Swords (swung)
- Heavy Infantry: Maces/Hammers (swung, larger, more violent)
- Light Cavalry: Small spears (thrust)
- Heavy Cavalry: Large lances (thrust, largest, most violent)
- Longbowmen: Daggers (thrust, smaller, less violent)
- Undead: Bones (thrust, violent)
Weapon configs include scale multiplier and violence multiplier for
differentiated visual feedback per unit type.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Uses SpriteRenderer for world-space rendering (like particle effects)
instead of UI Image, which has issues with the rotated Grid Canvas.
This approach follows the proven pattern used by SetCellModifierEffect.
Key changes:
- ArrowVolleyAnimator: Creates arrows as SpriteRenderer GameObjects
parented to gridCanvas, positioned with transform.localPosition
- HexGrid: Added GetCellLocalPosition() to expose cell positions
- ShardokGameController: Triggers animation in PerformAction() for
player's archery commands (latency hiding) and in ModelUpdated()
for other players' archery actions
The Grid Canvas uses Screen Space - Camera with 90° X rotation
(lies flat like a tabletop). UI Image components have rendering
issues in this configuration, but SpriteRenderers (3D objects)
render correctly - matching how particle effects already work.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Previously, uploading a game would extract files to disk before checking
if the game already existed, potentially losing data if the import failed.
Changes:
- Add CheckGameExists RPC to check if game exists in memory or on disk
- Implement checkGameExists in GamesManager and EagleServiceImpl
- Update admin server upload handler to check before extracting files
- When conflict detected, show options: "Use New ID" or "Replace Existing"
- "Use New ID" generates a random new game ID for the upload
- "Replace Existing" removes existing game from memory before overwriting
- Add JavaScript in games.html to handle conflict resolution flow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add DeleteGame RPC to proto with request/response messages
- Implement deleteGame in GamesManager to remove game from memory
and optionally delete save files from disk
- Implement deleteGame override in EagleServiceImpl
- Add handleGameDelete handler in Go admin server
- Add delete button and confirmation modal to game detail page
- Modal includes checkbox to also delete save files from disk
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
A battalion could be added twice if:
1. Its size was 0, AND
2. Its unit was Captured or Outlawed
This caused "key not found" errors when the applier tried to remove
the same battalion twice.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The QA admin server connecting to prod auth was getting 404 because
nginx only routed the Auth service, not the Admin service which
handles ListUsers RPC for admin privilege checks.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When connecting to a remote auth server (e.g., prod.eagle0.net:40033),
TLS is required. This adds an --auth-tls flag that switches from
insecure credentials to TLS with system root CAs.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When --jfr-sidecar-addr is set to empty string (""), JFR functionality
is disabled instead of failing with connection errors. This is useful
for QA environments that don't have the JFR sidecar container.
- Add jfrDisabled() helper function
- Return "JFR sidecar not configured" for /jfr/status (as JSON)
- Return 503 Service Unavailable for /jfr/start, /jfr/stop, /jfr/download
- Update startup log to indicate JFR status
Usage: --jfr-sidecar-addr ""
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
After PR #5056, History classes now use the Scala ActionResultApplierImpl
instead of the proto-based applier. This removes the now-unused proto applier:
- Delete ActionResultProtoApplier.scala and ActionResultProtoApplierImpl.scala
- Delete ActionResultProtoApplierImplTest.scala
- Remove unused proto applier dependencies from BUILD files
- Update comments in MarchCommand.scala and SendSuppliesCommand.scala to
reference ActionResultApplierImpl instead
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add fromProto method to ActionResultProtoConverter to convert proto
ActionResult to Scala ActionResultT
- Add fromProto method to ChangedProvinceConverter to convert proto
ChangedProvince to Scala ChangedProvinceT
- Update InMemoryHistory and PersistedHistory to use ActionResultApplierImpl
(Scala) instead of ActionResultProtoApplierImpl
- Store precomputed Scala state in ActionWithResultingState to avoid
redundant proto-to-Scala conversions
This eliminates the need to maintain two separate applier implementations
that must stay in sync. The proto applier is no longer used by production
code and can be deleted in a follow-up.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Delete dead Action trait and TRandomSequentialResultsAction
- Delete Action.scala - trait with execute method, nothing implements it
- Delete TRandomSequentialResultsAction.scala - nothing extends it
- Remove :action dependency from all BUILD files
- Remove dead WaitingAction implicit class from test package.scala
- Remove unused WaitingAction imports from test files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove unused actionResultProtoApplier parameter from EngineImpl
- Remove unused actionResultProtoApplier constructor parameter (declared but never used)
- Remove unused ActionResultProtoApplier/Impl imports from EngineImpl
- Remove unused RuntimeValidator import from EngineImpl
- Remove action_result_proto_applier_impl dependency from engine_impl and round_phase_advancer
- Remove unused runtime_validator dependency from engine_impl
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Delete Action.scala - trait with execute method, nothing implements it
- Delete TRandomSequentialResultsAction.scala - nothing extends it
- Remove :action dependency from all BUILD files
- Remove dead WaitingAction implicit class from test package.scala
- Remove unused WaitingAction imports from test files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Hetzner on-demand compute implementation is complete:
- ARM64 builds working in CI
- Shardok running on Hetzner CAX41 in Helsinki
- Production Eagle connected via TLS + token auth
- IPv6/NAT64 networking configured
Delete the completed planning doc and add a new doc covering
future latency optimization strategies:
1. Client-side animation masking (recommended first step)
2. Split Shardok architecture (future if needed)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Move Custom Battle to lobby, use OAuth authentication
- Remove _createConnection() from _internalCustomBattle() since connection
already exists when called from lobby
- Hide gameSelectionPanel instead of connectionPanel when entering custom battle
- Add customBattleButton field for lobby UI button
- Add cancelCustomBattleButton and CancelCustomBattle() to return to lobby
- Wire up button click handlers in SetupLobbyUI()
The Custom Battle button should now be placed in the gameSelectionPanel (lobby)
in Unity and assigned to the customBattleButton field. A cancel button in the
customBattlePanel should be assigned to cancelCustomBattleButton.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up Custom Battle and Cancel buttons in Unity scene
- Assign customBattleButton in lobby panel
- Assign cancelCustomBattleButton in custom battle panel
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix CancelCustomBattle to properly reset canvas states
Hide shardokCanvas and ensure connectionCanvas is visible when
returning to lobby from custom battle.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Back button onClick handler in custom battle panel
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update DEPROTO_PLAN.md: 100% action files now protoless
- ResolveBattleAction migrated to Scala types (PR #5048)
- All 52 action files are now fully protoless
- CommandChoiceHelpers fully migrated to Scala types
- Update progress summary and validation checklist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove dead execute method from ProtolessRandomSequentialResultsAction
The execute(startingState: GameStateProto, applier: ActionResultProtoApplier) method
was never called - all actions using this base class now call .results() directly
and apply results via RandomStateSequencer or ActionResultApplier.
This removes the dead method and its unused dependencies (SeededRandom,
ActionResultProtoApplier, VigorXPApplier, ActionResultProtoConverter).
Note: The proto GameState export is retained because downstream actions use
ProvinceViewFilter which has overloaded methods taking both proto and Scala
GameState - overload resolution requires both types to be visible.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- ResolveBattleAction migrated to Scala types (PR #5048)
- All 52 action files are now fully protoless
- CommandChoiceHelpers fully migrated to Scala types
- Update progress summary and validation checklist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The deploy user doesn't have passwordless sudo. Change from trying
to configure Docker (which fails) to just checking and warning.
Docker IPv6 is a one-time server setup done manually.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change ResolveBattleAction to take Scala GameState and use ActionResultApplier
- Add resolvedBattle field to ActionResultT, ActionResultC, and proto converter
- Update EngineImpl.resolveBattle to use Scala-based applier
- Update test to convert proto GameState to Scala and handle Scala results
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Hetzner Shardok server is IPv6-only. Docker containers need
IPv6 support to reach it.
Changes:
- docker-compose.prod.yml: Add IPv6-enabled network
- docker_build.yml: Configure Docker daemon for IPv6 on deploy
(with ip6tables for NAT)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The auth service loads users.pb into memory at startup. When authcli
modifies the file on disk (e.g., to grant admin), the service still has
the old in-memory copy. On next login, it would overwrite the file with
stale data, losing the admin grant.
Fix: Check file modification time before each FindOrCreateUser call.
If the file was modified externally, reload it before proceeding.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Replace proto notification detail imports with Scala NotificationDetails
- Use NotificationConverter.fromProto to convert proto -> Scala
- Remove dependency on action_result_notification_details_scala_proto
- Add dependencies on notification_trait and notification_converter
The internal pattern matching now uses Scala sealed trait NotificationDetails
instead of proto case classes, reducing proto coupling in the action layer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Allows switching between local Shardok (default: shardok:40042) and
remote Hetzner Shardok (shardok.prod.eagle0.net:40042) via GitHub
Actions secrets.
To use Hetzner Shardok in production:
1. Add secret SHARDOK_ADDRESS=shardok.prod.eagle0.net:40042
2. Add secret SHARDOK_AUTH_TOKEN=<your-token>
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix admin console OAuth flow and route protection
Changes:
- Gate all admin routes with requireAuth (games, settings, JFR, APIs)
Only login, logout, health, and static files are public now
- Add return_url to OAuth flow so auth service redirects back to admin
console after callback, instead of showing "close this window"
- handleLoginComplete now immediately completes login on redirect
- Update authcli help text with production usage examples
The OAuth flow now works properly for web clients:
1. User clicks Sign in -> admin console redirects to OAuth provider
2. OAuth provider calls auth service callback
3. Auth service redirects back to admin console's /login/complete
4. Admin console sets JWT cookie and redirects to /games
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix oauth_test.go for GetAuthURL signature change
Update test calls to pass empty string for the new returnURL parameter.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Document all media assets in Unity project for licensing review before
potentially opening public access to game downloads. Identifies:
- Asset Store purchases (properly licensed)
- CC-licensed music with attribution
- Items needing verification (Shardok sounds, StrategyGameIcons, etc.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Replace sealed trait + case class pattern with Scala 3 enum
- Update all import sites to use `ProvinceEvent.{BeastsEvent, ...}` pattern
- More idiomatic Scala 3 with cleaner syntax
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add admin user management to admin console
Adds functionality to view and manage user identity→display name links:
Backend (auth service):
- New admin.proto with AdminService gRPC (ListUsers, SetUserDisplayName, SetUserAdmin)
- AdminHandler validates JWT is_admin claim for authorization
- UserService methods for listing users and admin operations
Frontend (admin console):
- OAuth login flow with Discord/Google (JWT stored in HTTP-only cookie)
- Auth middleware requiring is_admin claim for /users routes
- Users page with search, display name editing, and admin toggle
- HTMX-powered table with infinite scroll pagination
Deployment:
- admin container now connects to auth service for user management
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add authcli tool for user management
CLI tool for managing auth service users directly via the users.pb file.
Useful for bootstrapping the first admin user or emergency access.
Commands:
- list: List all users with their admin status
- find <email>: Find user by email (partial match)
- set-admin <id> true|false: Set admin status by user ID
- grant-admin <email>: Grant admin to user by exact email match
Usage:
authcli --data-dir=/app/data list
authcli --data-dir=/app/data grant-admin your@email.com🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add authcli to auth server Docker image
- Include authcli_linux_amd64 in auth_binary_layer
- Update auth_build.yml paths to trigger on authcli and admin proto changes
Usage on VM:
docker exec auth-server /app/authcli_linux_amd64 --data-dir=/app/data list
docker exec auth-server /app/authcli_linux_amd64 --data-dir=/app/data grant-admin user@email.com🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Scala overloads to ProvinceEventUtils
- Add Scala overloads for isBlizzardEvent, isBeastsEvent, isEpidemicEvent,
isFestivalEvent, isDroughtEvent, isFloodEvent, isImminentRiotEvent
- Add Scala overloads for beastsCount and beastInfo
- Update BUILD.bazel with required Scala model dependencies
- Add province event dependency to LegacyProvinceUtils for overload resolution
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Replace isInstanceOf with pattern matching in ProvinceEventUtils
- Use match expressions instead of isInstanceOf for type checks
- More idiomatic Scala style
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless overload that takes ScalaGameState and CombatUnitC
- Uses BattalionViewFilter (protoless) and BattalionViewConverter for output
- Exports CombatUnitC and ScalaGameState types for downstream callers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- EligibleDiplomacyStatuses: Add Scala overload for maybeImprisonStatus
that takes provinces Iterable instead of proto GameState
- AvailableResolveTruceOfferCommandFactory: Add Scala overload that
filters Scala TruceOffer instances and converts to proto output
- AvailableResolveAllianceOfferCommandFactory: Add Scala overload for
AllianceOffer filtering
- AvailableResolveBreakAllianceCommandFactory: Add Scala overload for
BreakAlliance filtering
- AvailableResolveInvitationCommandFactory: Add Scala overload with
protoless invitation validation using FactionUtils
- AvailableResolveRansomOfferCommandFactory: Add Scala overload with
protoless RansomValidity and conflict checking
All Scala overloads take ScalaGameState and output proto commands,
allowing callers with Scala model types to avoid GameStateConverter.toProto().
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
TokenStorage was calling PlayerPrefs.GetString() which can only be
called from Unity's main thread. When PersistentClientConnection
attempts to reconnect from a background thread, JwtAuthInterceptor
tries to get the access token, causing a UnityException.
This caused an infinite loop: DeadlineExceeded → reconnect attempt →
PlayerPrefs exception → reconnect fails → idle timeout → retry...
Changes:
- TokenStorage: Add in-memory cache for thread-safe token access
- OAuthManager: Initialize cache on main thread in Awake()
- PersistentClientConnection: Schedule reconnect when stream ends
normally (was missing, causing silent connection death)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Enable QA Eagle to connect to production auth service over TLS.
- Use TLS for external connections (non-localhost hosts)
- Keep plaintext for local container-to-container connections
(localhost, auth, 127.x.x.x)
This allows running QA Eagle with --auth-service-url prod.eagle0.net:40033
to share user accounts with production.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add io.sentry:sentry dependency
- Initialize Sentry from SENTRY_DSN environment variable
- Report uncaught exceptions via ExceptionInterceptor
- Add SENTRY_DSN to docker-compose and CI workflow
When SENTRY_DSN is configured, uncaught exceptions in gRPC handlers
will be reported to Sentry for email/Slack alerts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add filteredProvinceView(ProvinceT, ScalaGameState, FactionId) overload
- Use protoless FactionUtils.hasAlliance, Visibility.hasFullVisibility, and ProvinceUtils.incomingOthers
- Add helper methods: fullProvinceInfoScala, maybeIncomingAttackersScala, unaffiliatedHeroInfoScala
- Handle reconned provinces directly from Scala FactionT.reconnedProvinces
GameStateViewFilter improvements:
- Eliminate GameStateConverter.toProto() call in Scala overload
- Use new ProvinceViewFilter Scala overload for faction filtering
- Convert battalionTypes and chronicleEntries to proto only at output boundary
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Phase 2 of moving user management to Go auth service:
- Eagle now forwards setDisplayName, getCurrentUser, logout to Go auth
- Removed InternalUserServiceImpl and internal gRPC server (port 40034)
- UserService is now optional (only created when not using external auth)
- Updated docker-compose to remove port 40034 exposure
This makes Eagle stateless for user data when configured with
--auth-service-url, enabling QA to point to prod auth service.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate CheckForFulfilledQuestsAction to protoless BattalionTypeFinder
- Change battalionTypes parameter from proto Vector[BattalionType] to Scala Vector[BattalionType]
- Update callers (EngineImpl, EndVassalCommandsPhaseAction) to pass Scala types directly
- Eliminate wasteful BattalionTypeConverter.toProto() conversions
- Update test to use Scala BattalionType
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Scala overload to ExpandedUnaffiliatedHeroUtils, eliminate proto conversions
- Add ExpandedUnaffiliatedHeroUtils.expandedUnaffiliatedHero(ScalaGameState, UnaffiliatedHeroT)
- Add UnaffiliatedHeroConverter.unaffiliatedHeroTypeToProto() helper for efficient enum conversion
- Use reverse map instead of inefficient .find() in UnaffiliatedHeroConverter.toProto()
- Update AvailablePleaseRecruitMeCommandFactory to use Scala overload directly
- Eliminate wasteful GameStateConverter.toProto() and UnaffiliatedHeroConverter.toProto() calls
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add exports to expanded_unaffiliated_hero_utils for transitive type visibility
Callers that import ExpandedUnaffiliatedHeroUtils now see both overloads in
method signatures, which exposes ScalaGameState and UnaffiliatedHeroT types.
Add these as exports so callers can compile without needing explicit deps.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Move user management to Go auth service (Phase 1)
Auth service now manages users locally instead of calling Eagle:
- Add users.go with UserService for protobuf-based user persistence
- Implement SetDisplayName, GetCurrentUser, Logout handlers
- Extract JWT from gRPC metadata (authorization header)
- Add ValidateAccessToken function to jwt.go
- Add user_go_proto target for user.proto
- Add AUTH_DATA_DIR env var and /app/data volume mount
Auth service no longer depends on Eagle's InternalUserService.
Eagle's user management code remains for now (Phase 2 cleanup).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add unit tests and migration path for user service
- Add comprehensive unit tests for UserService (users_test.go)
- Add automatic migration from Eagle's users.pb location
- Add atomic write pattern for crash safety (write .tmp, then rename)
- Add AUTH_LEGACY_DATA_DIR config for migration path
- Mount Eagle's saves volume read-only for migration access
Migration strategy:
1. Auth service checks /app/data/users.pb first
2. If not found, reads from /app/saves/auth/users.pb (Eagle's location)
3. Saves migrated data to new location
4. Subsequent reads/writes use new location only
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add sortKey to FactionUtils to match LegacyFactionUtils API
- Add sortKey method and sortIgnoredChars constant to protoless FactionUtils
- Update LegacyFactionUtils to use direct proto field access (avoid converter
failures on incomplete test data)
- Both implementations now have matching APIs for the deproto migration
- Update DEPROTO_PLAN.md with progress
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Document Legacy* utility migration status in DEPROTO_PLAN.md
All Legacy* utilities now have protoless equivalents with matching APIs:
**Parallel Implementations (proto mirrors protoless):**
- FactionUtils / LegacyFactionUtils - 24+ boundary callers
- HeroUtils / LegacyHeroUtils - 10 boundary callers
- ProvinceUtils / LegacyProvinceUtils - 20 boundary callers
**Awaiting migration of callers:**
- BattalionUtils / LegacyBattalionUtils - 4 callers
- BattalionViewFilter / LegacyBattalionViewFilter - 3 callers
- BattalionTypeFinder / LegacyBattalionTypeFinder - 2 callers
Legacy versions are appropriately used by boundary code (availability
factories, view filters, action appliers) that works with proto GameState.
The AI layer already uses the protoless versions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When saving to paths like "auth/users.pb", the parent directory
may not exist. FileOutputStream doesn't create parent directories,
so the save would silently fail. Now we ensure parent directories
exist before writing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update Hetzner docs with actual deployment details
- Use shardok.prod.eagle0.net as the domain name
- Step 4: Specify GitHub Actions secrets location
- Step 5: Recommend Hillsboro, OR (hil) + IPv6 floating IP
- Update all code examples and cloud-init scripts
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire SHARDOK_AUTH_TOKEN through Eagle's Shardok connection
Implements Step 7 of Hetzner setup guide:
- Main.scala reads SHARDOK_AUTH_TOKEN env var and creates ShardokSecurityConfig
- TLS is auto-enabled when Shardok address contains ".eagle0.net"
- Security config passed to both newGamesManager and newCustomBattleManager
- docker-compose.prod.yml passes SHARDOK_AUTH_TOKEN to Eagle container
- docker_build.yml passes secret during deployment
This is backward compatible: local Docker Shardok (shardok:40042) continues
to work without TLS/auth since the address doesn't contain ".eagle0.net".
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update ShardokInstanceManager for DigitalOcean registry and TLS
- Update cloud-init script to use DigitalOcean Container Registry instead of ghcr.io
- Add Let's Encrypt/Certbot setup for TLS certificates
- Configure automatic certificate renewal via cron
- Pass TLS cert paths and auth token file to Shardok container
- Default to shardok.prod.eagle0.net domain
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix cloud-init to use eagle0.conf instead of environment variables
The Shardok C++ server reads configuration from /usr/local/share/eagle0/eagle0.conf,
not environment variables. Updated cloud-init script to:
- Create eagle0.conf with TLS and auth paths
- Mount the config directory into the container
- Remove unused -e environment variable flags
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify spin-up strategy: trigger on human turns with 10-min idle timeout
Instead of trying to predict battles, we now:
- Spin up Shardok when any human player takes a turn
- Shut down after 10 minutes of no human turns
This is simpler and more reliable. Battles happen regularly during active
play, so Shardok will be ready when needed. The 10-minute timeout is long
enough to cover thinking time but short enough to minimize idle costs.
Updated:
- SHARDOK_ON_DEMAND_COMPUTE.md with new strategy and state machine
- ShardokInstanceManager: renamed onPlayerActivity -> onHumanTurn,
changed default idle timeout from 60 to 10 minutes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Document Hetzner testing progress and ARM64 blocker
Testing status:
- Hetzner CAX41 ARM64 server created in Helsinki
- Floating IPv6 configured with netplan persistence
- DNS and Let's Encrypt certificates working
- NAT64 configured for IPv6→IPv4 registry access
BLOCKER: ARM64 container crashes with runfiles error:
"cannot find runfiles (argv0="/app/shardok-server")"
Needs BUILD.bazel investigation for ARM64 image packaging.
Also documented known issues:
- IPv6-only servers need NAT64 DNS (nat64.net)
- Floating IP requires manual netplan config
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix ARM64 container crash: add missing environment variables
The ARM64 Shardok container was crashing with "cannot find runfiles"
because SHARDOK_RESOURCES_PATH and SHARDOK_MAPS_PATH weren't set.
Without these, the binary tries to use Bazel runfiles which don't
exist in the container.
Added the required environment variables to the docker run command
in the cloud-init script, matching docker-compose.prod.yml.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When the only ruling faction hero departed from a province, clearRulingFaction
would create duplicate UnaffiliatedHeroC entries - one from the departure logic
and another from clearRulingFaction iterating over all rulingFactionHeroIds.
Fix: Filter out heroIds that already have entries in newUnaffiliatedHeroes
before creating new ones.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix upload section styling in admin panel
- Add explicit positioning to prevent overlap with other elements
- Style the details/summary for clearer expand/collapse indicators
- Add border and background when expanded for visual clarity
- Prevent button from shrinking in flex layout
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix upload section contrast and button sizing
- Use card background color for summary with proper text contrast
- Change Upload button to btn-small class for appropriate sizing
- Add proper border and styling for expanded form area
- Remove inline styles in favor of CSS classes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Upload button to not be full-width
- Add width: auto !important to override Pico CSS defaults
- Add flex-grow: 0 to prevent stretching in flex container
- Add position: static to prevent any fixed/absolute positioning
- Add z-index: auto to upload section to prevent stacking issues
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This was never successfully deployed. OAuth callbacks are handled
directly by the Go auth service via nginx routing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove dead code from LegacyHeroUtils
- Remove unused `seniorityOrder` method (protoless version in HeroUtils is used)
- Remove unused `sortOrdering` method (protoless version in HeroUtils is used)
- Remove unused `discordance` method (protoless version in HeroUtils is used)
- Remove unused `faction` method (protoless version in HeroUtils is used)
- Remove unused `Faction` import and proto dependency
- Remove corresponding tests for deleted methods
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove dead code from LegacyFactionUtils
- Remove unused `trust` method (protoless version in FactionUtils is used)
- Remove unused `factionHead` method
- Remove unused `ownedNeighbors` method (protoless version in FactionUtils is used)
- Remove unused `hostileNeighbors` methods (protoless version in FactionUtils is used)
- Remove unused `neutralNeighbors` method (protoless version in FactionUtils is used)
- Remove unused `truceExpirationDate` method
- Remove unused `alliedFactions` method (protoless version in FactionUtils is used)
- Remove unused `provincesWithHostileNeighbors` method
- Remove unused `ProvinceWithHostileNeighbors` case class
- Remove unused `Hero` import and `hero_scala_proto` dependency
- Remove corresponding tests for deleted methods
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update DEPROTO_PLAN.md with Phase 5 progress
- Add section for thin wrapper refactorings (LegacyRansomValidity, LegacyRecruitmentOdds)
- Note LegacyHeroUtils dependency on LegacyFactionUtils
- Note LegacyBattalionUtils has intentionally different power multipliers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove dead code from LegacyBattalionUtils
- Remove unused `power` method (superseded by BattalionPower.power)
- Remove unused `estimatedPower` method (superseded by BattalionPower.estimatedPower)
- Remove unused `powerMultiplier` map (only used by removed methods)
- Remove unused `BattalionView` import and proto dependency
All callers already use BattalionPower for power calculations.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The code checked _lobbySubscriber != null before enqueueing a lambda,
but the lambda captured the field reference and executed later on the
main thread when _lobbySubscriber could have become null (e.g., during
Dispose).
Fix by capturing the subscriber in a local variable before the null
check. This ensures the lambda uses the subscriber that was active
when the message arrived, even if _lobbySubscriber changes later.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Runs daily at 3am UTC to delete container images older than 5 days from
DigitalOcean Container Registry. Protected tags (latest, arm64-latest)
are never deleted.
Also runs garbage collection after cleanup to reclaim storage.
Includes manual trigger with dry-run option for testing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Phase 5 of deproto cleanup - remove Legacy* utilities that have no production callers:
- LegacyProvinceDistances: no callers besides its own test
- LegacyBattalionSuitability: no callers besides its own test
- LegacyFoodConsumptionUtils: no callers besides its own test
- LegacyHandleRiotUtils: no callers besides its own test
Updated DEPROTO_PLAN.md to track Phase 5 progress.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Eliminate GameState toProto() conversion in LLM pipeline
Phase 1 of deproto optimization: Make LlmRequestWithGameState use Scala
GameState instead of proto GameState, eliminating wasteful round-trip
conversion in UnrequestedTextHandler (the heaviest proto conversion in profiles).
Changes:
- LlmResolver: Change LlmRequestWithGameState.gameState to Scala GameState
- UnrequestedTextHandler: Remove GameStateConverter.toProto() calls, pass
gameHistory.stateAfter() directly
- Update all prompt generators (~38 files) to use Scala model types:
- FactionT instead of proto Faction
- HeroT instead of proto Hero
- ProvinceT instead of proto Province
- Scala enums (Gender.Male, Profession.Mage) instead of proto enums
- DivineMessagePromptGenerator: Rewrite quest pattern matching to use
Scala QuestC types instead of proto quest types
- ChronicleUpdatePromptGenerator: Inline province lookup, remove
LegacyFactionUtils dependency
- BattalionDescriptions: Add Scala BattalionT and BattalionTypeId overloads
- Update test files to use concrete Scala types (HeroC, FactionC, ProvinceC)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make AttackCommandChooser fully protoless
- Add `estimatedPower(BattalionViewC)` to `BattalionPower` for handling recon
data with optional training/armament (defaults to 50.0 when unknown)
- Update `AttackCommandChooser.chosenAttackCommand` to use Scala `BattalionViewC`
instead of proto `BattalionView`
- Update `MidGameAIClient` to pass battalions directly without proto conversion
- Remove `LegacyBattalionUtils` and proto `battalion_view_scala_proto` dependencies
- Update DEPROTO_PLAN.md: Phase 1 complete, CommandChoiceHelpers fully protoless
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Scala overloads to BattalionNameFilter and BattleFilter
- Add Scala GameState overload to BattalionNameFilter using FactionUtils.provinces
- Add Scala overload to BattleFilter using FactionUtils.hostilityStatus
- Update GameStateViewFilter Scala overload to use the new Scala sub-filters
- Add shardok_battle visibility for view_filters package
- Update DEPROTO_PLAN.md to reflect completed work
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add Scala overloads to Visibility.scala for hasFullVisibility using
Vector[FactionT] instead of proto GameState
- Add Scala overload to HeroViewFilter.filteredHeroView taking HeroT
and ScalaGameState with proper type conversions
- Add Scala overload to FactionViewFilter.filteredFactionView taking
FactionT and ScalaGameState
- Update GameStateViewFilter Scala overload to use Scala sub-filters
where available (HeroViewFilter, FactionViewFilter)
- Update BUILD.bazel files for visibility and exports
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
DigitalOcean free tier limits to 5 repositories. Use the existing
shardok-server repository with arm64-prefixed tags instead of a
separate shardok-server-arm64 repository.
- shardok-server:latest (x86)
- shardok-server:arm64-latest (ARM64)
Also adds HETZNER_SETUP.md with infrastructure setup instructions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The user database was being written to ~/eagle0/eagle/save/ but Docker
only mounted ./saves to /app/saves. This caused user data (including
displayName) to be lost on every container restart.
Add EAGLE_SAVE_DIR and EAGLE_ARCHIVE_DIR environment variables to
SaveDirectory, defaulting to the existing paths for local development.
Docker compose now sets these to /app/saves and /app/archived which
are properly mounted to persistent volumes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When the admin panel requests game history with limit=0, it only needs
the total count for pagination. Previously this would load all entries
and serialize each one to JSON, causing timeouts on games with many
actions.
Now when limit <= 0, we return just the count with empty entries,
making the count request fast regardless of history size.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The download handler was using ParseInt which fails for game IDs that
have the high bit set (appear as negative when formatted as signed).
The gameID is already parsed and validated in handleGameRoutes using
ParseUint, so just pass it through instead of re-parsing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add `filteredGameState(gs: ScalaGameState, factionId: Option[FactionId])`
overload that converts internally for now (sub-filters still need proto)
- Update HumanPlayerClientConnectionState to pass Scala GameState directly,
removing 3 wasteful toProto conversions at call sites
- Add visibility for view_filters to access GameStateConverter
- Export Scala GameState from game_state_view_filter target
- Update DEPROTO_PLAN.md to reflect Phase 4 progress
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Use DigitalOcean registry for ARM64 images instead of GitHub Container
Registry for consistency with all other images (Eagle, Shardok x86,
admin, auth, jfr-sidecar).
- Update ci/BUILD.bazel: shardok_server_push_arm64 → registry.digitalocean.com
- Update shardok_arm64_build.yml: use DO_REGISTRY_TOKEN auth
- Update docs to reflect the registry change
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a user sets their display name, the current JWT still has the old
(blank) displayName. This caused games to be created with blank usernames.
Fix: SetDisplayName now returns a new access token with the updated
displayName claim. The client stores this new token so subsequent requests
(like joining games) use the correct displayName.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add TLS and token authentication support to Shardok server
Enables secure communication between Eagle and remote Shardok instances:
- TLS support: Read SSL cert/key from config paths, create SslServerCredentials
- Token auth: Validate "Authorization: Bearer <token>" header on all RPCs
- Config: Added authTokenPath to ServerConfiguration
- TokenValidator class reads token from file and validates requests
- Graceful fallback: If TLS not configured, uses insecure credentials
Configuration options (via eagle0.conf or environment):
- sslCertPath: Path to TLS certificate (e.g., Let's Encrypt fullchain.pem)
- sslPrivateKeyPath: Path to TLS private key
- authTokenPath: Path to file containing the auth token
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update implementation plan to mark completed tasks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add TLS and token authentication support to Eagle's Shardok client
Add ShardokSecurityConfig to configure TLS and auth token for connecting
to remote Shardok instances. When useTls is enabled, uses system trust
store for Let's Encrypt certificates. When authToken is set, adds Bearer
token to all gRPC requests via BearerTokenInterceptor.
This enables Eagle to connect securely to Shardok instances running on
Hetzner Cloud with Let's Encrypt TLS and token-based authentication.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Manages the lifecycle of Shardok instances on Hetzner Cloud:
- Activity-based spin-up when players connect
- Automatic shutdown after idle timeout (default 60 minutes)
- State machine tracking: Stopped -> Starting -> Ready -> InUse
- Cloud-init script generation for Docker container setup
- Health checking during startup
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Phase 2 of OAuth extraction: Unity client now routes OAuth RPCs
(GetOAuthUrl, CheckOAuthStatus, RefreshToken) directly to the Go
auth service on port 40033, while user RPCs (SetDisplayName,
GetCurrentUser, Logout) continue to go to Eagle.
Changes:
- AuthClient: Use separate gRPC channels for auth service and Eagle
- OAuthManager: Accept both authServiceUrl and eagleUrl parameters
- ConnectionHandler: Pass both URLs when configuring OAuthManager
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Previously, getGameHistory loaded all action results with history.all
then paginated in-memory. For games with many actions, this caused
timeouts.
Now uses history.since(startIndex).take(limit) which efficiently loads
only the save files containing the requested range. The since() method
in PersistedHistory already calculates which partial game files to read.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Eliminate GameState toProto() conversion in LLM pipeline
Phase 1 of deproto optimization: Make LlmRequestWithGameState use Scala
GameState instead of proto GameState, eliminating wasteful round-trip
conversion in UnrequestedTextHandler (the heaviest proto conversion in profiles).
Changes:
- LlmResolver: Change LlmRequestWithGameState.gameState to Scala GameState
- UnrequestedTextHandler: Remove GameStateConverter.toProto() calls, pass
gameHistory.stateAfter() directly
- Update all prompt generators (~38 files) to use Scala model types:
- FactionT instead of proto Faction
- HeroT instead of proto Hero
- ProvinceT instead of proto Province
- Scala enums (Gender.Male, Profession.Mage) instead of proto enums
- DivineMessagePromptGenerator: Rewrite quest pattern matching to use
Scala QuestC types instead of proto quest types
- ChronicleUpdatePromptGenerator: Inline province lookup, remove
LegacyFactionUtils dependency
- BattalionDescriptions: Add Scala BattalionT and BattalionTypeId overloads
- Update test files to use concrete Scala types (HeroC, FactionC, ProvinceC)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make AttackCommandChooser fully protoless
- Add `estimatedPower(BattalionViewC)` to `BattalionPower` for handling recon
data with optional training/armament (defaults to 50.0 when unknown)
- Update `AttackCommandChooser.chosenAttackCommand` to use Scala `BattalionViewC`
instead of proto `BattalionView`
- Update `MidGameAIClient` to pass battalions directly without proto conversion
- Remove `LegacyBattalionUtils` and proto `battalion_view_scala_proto` dependencies
- Update DEPROTO_PLAN.md: Phase 1 complete, CommandChoiceHelpers fully protoless
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Admin server and Eagle run in separate containers in production, so the
admin server cannot directly access Eagle's save directory. Added a streaming
gRPC endpoint DownloadGameSave that zips and streams the save directory,
with the admin server proxying the download through this endpoint.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix JWT claims to match Eagle's expected format
Go auth service was using custom claim names (userId, displayName, isAdmin)
but Eagle's JwtServiceImpl expects standard claims:
- sub (Subject) for userId
- name for displayName
- admin for isAdmin
This fixes "User not found" error when client calls SetDisplayName after
OAuth login, because Eagle couldn't extract the userId from the JWT.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix jwt_test.go to use updated claim field names
Update tests to use Subject instead of UserID and Name instead of
DisplayName, matching the changes to EagleClaims and RefreshClaims.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add ImportGame RPC to eagle.proto for registering uploaded games
- Implement importGame in GamesManager to load saves from disk
- Add download handler: zips game save folder and serves as download
- Add upload handler: accepts zip, extracts, calls gRPC to register
- Add "Download Save" button to game detail page
- Add upload form (collapsible) to games list page
Uploaded games start with all AI players; admin can assign humans
using existing player management features.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Production code:
- Remove LegacyCommandChooser trait and all legacy methods from CommandChooser.scala
- Remove all proto overloads from CommandChoiceHelpers.scala (~1100 lines removed)
- Remove proto imports and converter dependencies
- Rename chosenFulfillEasyQuestsCommandProtoless to chosenFulfillEasyQuestsCommand
- Update MidGameAIClient to use renamed method
- Remove unused CommandChooserImplicits import from AIClient
Test code:
- Update CommandChoiceHelpersTest to use Scala model types
- Update FulfillQuestsCommandSelectorTest to use Scala model types
- Update PerformVassalCommandsPhaseActionTest to use Scala model types
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The /oauth/callback endpoint should proxy to auth:8080 (Go auth service)
not eagle:8080. This fixes 502 errors during OAuth flow.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Implements a Scala client for the Hetzner Cloud API to manage
on-demand Shardok instances for tactical combat. Features:
- Create/delete servers with cloud-init user data
- Get server status and list servers by label
- Power on/off and graceful shutdown operations
- Async execution with Futures
- Proper error handling for API errors
Also updates the plan doc to track ARM64 build completion.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add JWT_PRIVATE_KEY to auth_build.yml deploy job so the auth service
can bootstrap its RSA keys from the JWK secret. This ensures the
JWT key is properly deployed without manual server intervention.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The nimbus-jose-jwt library may output standard base64 (with +/)
instead of base64url (with -_). Try multiple decode strategies:
1. base64url without padding (JWK spec)
2. base64url with padding
3. standard base64 without padding
4. standard base64 with padding
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add ARM64 Linux build infrastructure for Shardok
Infrastructure to support cross-compiling Shardok for ARM64 Linux,
enabling deployment to Hetzner ARM instances (CAX41) for on-demand
compute.
Changes:
- Add linux_arm64 platform definition
- Add LLVM toolchain for ARM64 cross-compilation
- Add ARM64 sysroot placeholder (needs workflow run to populate)
- Add ARM64 busybox for container health checks
- Add Ubuntu 24.04 ARM64 base image
- Add Shardok ARM64 container image targets
- Update sysroot build workflow to support ARM64
Next steps:
1. Run "Build Linux Sysroot" workflow with architecture=arm64
2. Update MODULE.bazel with generated sysroot SHA
3. Build and push ARM64 container
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update ARM64 sysroot SHA from workflow build
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix ARM64 container build issues
- Use linux/arm64/v8 platform string (rules_oci requires variant suffix)
- Remove busybox_layer_arm64 due to busybox.net SSL certificate issues
- Health checks can be added later using an alternative busybox source
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add GitHub Actions workflow for ARM64 Shardok build
Builds and pushes ARM64 container image to GitHub Container Registry
for deployment on Hetzner ARM instances (CAX41).
Triggered on:
- Push to main (when Shardok-related files change)
- Manual workflow_dispatch
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove direct port 40033 binding from auth service since nginx
now proxies this port (PR #4987). Both can't bind to the same
host port.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add bootstrapKeysFromJWK() to convert JWK to PEM files on first run
- Uses only Go stdlib (no third-party JWK libraries)
- Pass JWT_PRIVATE_KEY env var to auth service in docker-compose
- PEM files persist in jwt-keys volume after first bootstrap
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Remove proto GameState overloads from 8 AI files, keeping only Scala model versions
- Update all AI tests to use Scala model types (GameState, FactionC, ProvinceC, HeroC)
- Clean up BUILD.bazel deps to remove proto dependencies
- Net reduction: -489 lines of code
Files converted:
- AIClientUtils.scala
- EarlyGameAIClient.scala
- FactionLeaderProvinceRanker.scala
- FixLeaderAloneCommandSelector.scala
- InvitationCommandSelector.scala
- MoveLeaderToBetterProvinceCommandChooser.scala
- ResolveDiplomacyCommandSelector.scala
- All corresponding test files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Documents the architecture for running Shardok on Hetzner ARM instances
with on-demand spin-up based on player activity:
- Hetzner CAX41 (16 ARM cores) in Ashburn at ~$0.04/hr
- Activity-based spin-up: start when players connect, stop after 1hr idle
- TLS + token authentication (Let's Encrypt for certs, auto-renewed)
- Estimated cost: $2-7/month vs $20-40/month for equivalent always-on
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Deproto MidGameAIClient: flip main entry point to protoless
Completes the deproto migration of MidGameAIClient by:
- Adding protoless chosenMidGameStrategicCommand
- Flipping the main entry point so proto delegates to protoless
- Adding protoless maybeMoveToRecruitCommand to CommandChoiceHelpers
- Adding protoless chosenFulfillEasyQuestsCommandProtoless
- Adding FactionUtils.neutralNeighbors helper
- Fixing test GameState constructors to include currentPhase
The proto versions now delegate to protoless versions via
GameStateConverter.fromProto(), eliminating proto usage in the
main command selection logic.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove proto deps from MidGameAIClient and make test protoless
- Remove all protobuf dependencies from MidGameAIClient BUILD.bazel
- Delete legacy proto-based methods from MidGameAIClient.scala (now fully protoless)
- Convert MidGameAIClientTest to construct Scala GameState directly
- Add helper methods for creating test fixtures (makeGameState, makeFaction, etc.)
- Add visibility for battalion/concrete and state packages for test access
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Uses dynamic DNS resolution to avoid startup dependency on auth container.
Previous version failed because static upstreams resolve at nginx startup.
Changes:
- nginx.conf: Add server block on port 40033 with variable-based grpc_pass
- nginx.conf: Add /health endpoint on port 40033
- docker-compose: Expose 40033 on nginx container
Key fix: Using `set $auth_backend "auth:40033"; grpc_pass grpc://$auth_backend;`
instead of static upstream, so DNS resolution happens at request time
(cached by resolver for 10s) rather than at nginx startup.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds nginx server block to listen on port 40033 and route gRPC
traffic to the Go auth service. This enables Phase 2 clients
to connect directly to the auth service.
Changes:
- nginx.conf: Add auth_grpc upstream and server block on port 40033
- docker-compose: Expose 40033 on nginx, add auth dependency
This is safe to deploy before Phase 2 clients - old clients still
use the Eagle proxy through port 443.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Auth service now has independent deployment lifecycle:
- Only triggers on authservice/** or auth proto changes
- Deploys only the auth container (no Eagle restart)
- Preserves in-memory OAuth state during Eagle deploys
docker_build.yml changes:
- Remove build-auth job
- Preserve existing AUTH_IMAGE in .env
- Only force-recreate eagle, shardok, admin, jfr-sidecar
- Auth container starts but isn't force-recreated
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The deployment was failing because AUTH_IMAGE wasn't being built or
passed to the deploy script. This adds:
- build-auth job to build and push the Go auth service image
- AUTH_IMAGE to deploy job dependencies and env vars
- Auth image pulling in deploy script
- GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET env vars
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Extract OAuth to Go service (Phase 1)
Move OAuth authentication handling from Eagle Scala server into a separate Go
service running in its own container. This simplifies Eagle, enables independent
deployment, and sets up for future JWT validation extraction.
Architecture:
- Go auth service handles OAuth flows, JWT creation, state management
- Eagle proxies Auth gRPC calls to Go service when configured
- Go service calls Eagle's InternalUserService for user persistence
- Shared JWT keys via volume mount
New files:
- src/main/go/net/eagle0/authservice/ - Go auth service
- auth_internal.proto - Internal gRPC for Go→Eagle communication
- InternalUserServiceImpl.scala - User service wrapper for internal gRPC
- ExternalAuthClient.scala - Client for forwarding to Go service
Backward compatible: Eagle runs in standalone mode without --auth-service-url
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add unit tests for Go auth service
- jwt_test.go: Tests for JWT token creation, validation, and refresh
- oauth_test.go: Tests for OAuth state management and status checking
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless validateNoFactionLeaderAlone using FactionUtils
- Add protoless provincesWithFactionLeader using FactionUtils and ProvinceUtils
- Add protoless foodSurplus using ProvinceUtils.monthlyFoodSurplus
- Add protoless maybeChosenRaiseSupportCommand (removes fromProto calls)
- Add protoless chosenAttackCommandWithReconInfo using Scala BattalionView
- Proto versions now delegate to protoless versions via fromProto conversion
- Update BUILD.bazel visibility for BattalionViewConverter and BattalionView
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless maybeChosenRelaySuppliesCommand using FoodConsumptionUtils
and ProvinceUtils instead of Legacy versions
- Add protoless selectedReconCommand and maybeChosenReconCommand using
Scala Date extensions and FactionRelationshipC
- Update chosenMidGameCommand to use protoless versions via fromProto conversion
- Add required imports and BUILD.bazel dependencies
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update OAUTH_NEXT_STEPS.md with current status and remaining work
- Mark completed items (headshots, logout, lobby display, etc.)
- Add new issues discovered (intermittent expired errors, token expiry bug)
- Reorganize implementation plan into Phase 2 (remaining) and Phase 3 (nice-to-haves)
- Update status of all known issues
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up lobby UI elements in Unity scene
- Connect logout button
- Connect lobbyEnvironmentText and lobbyUserText fields
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add lobby environment and user display fields to ConnectionHandler
- Add lobbyEnvironmentText and lobbyUserText fields
- Add UpdateLobbyStatusDisplays() to populate them when entering lobby
- Shows OAuth DisplayName or classic login username
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless helper methods in CommandChoiceHelpers:
- isInterestingUHC: uses Scala UnaffiliatedHeroT and RecruitmentInfo
- hasInterestingUHC: uses Scala GameStateC
- maybeChosenTravelToRecruitCommand: protoless overload
- Update MidGameAIClient to use protoless version via fromProto conversion
- Import RecruitmentInfo and UnaffiliatedHeroT for protoless checks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add diagnostic logging to OAuth flow for debugging expired errors
Adds detailed logging to trace OAuth state through the flow:
- getAuthUrl: logs state creation and map sizes
- checkStatus: logs non-pending results with map state
- handleCallback: logs entry, success, and error cases
This will help diagnose why clients sometimes get 'expired' errors
even when the OAuth callback succeeds on the server.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix expiresAt to return access token expiry instead of refresh token expiry
The CheckOAuthStatusResponse.expiresAt field was returning the refresh
token expiry (30 days) but clients interpret this as the access token
expiry (7 days). This fix calculates the correct access token expiry.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless overload that takes native GameState
- Rename proto GameState import to GameStateProto for clarity
- Update AIClient to use protoless version directly
- Remove unused GameStateConverter import from AIClient
- Update BUILD.bazel dependencies
This continues the deproto migration by moving the toProto() conversion
from AIClient into MidGameAIClient, making the public API protoless.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add a logoutButton field that can be wired in Unity, along with
OnLogoutClicked handler that:
- Clears OAuth tokens via OAuthManager.LogoutAsync()
- Disposes gRPC connection and HTTP client
- Returns to connection screen
- Re-shows appropriate auth panel
This allows OAuth users who are auto-logged in to return to the
connection screen to use a different account or auth method.
Note: The button still needs to be added in Unity and wired to
the logoutButton field.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change chooseFrom to accept Scala GameState instead of proto
- Use protoless CommandChooser with protoless helper methods
- Update chooseMidGameCommandFrom to accept Scala GameState
- Move toProto() conversion to MidGameAIClient call only (mid-game path)
- Use protoless EarlyGameAIClient methods (early game path)
This eliminates the unconditional toProto() conversion in chooseCommand,
now only converting when needed for MidGameAIClient which still uses proto.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless version of resolveDiplomacySelectedCommand using
CommandChooser instead of LegacyCommandChooser
- Add protoless versions of all private helper methods that use
Scala GameState and extract factions/provinces from it
- Rename proto GameState import to GameStateProto to disambiguate
- For methods that don't use gameState (ransom, break alliance),
delegate to Core implementations to avoid code duplication
This enables callers to use Scala GameState directly without
conversion, preparing for elimination of toProto() call in
AIClient.chooseFrom.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add null check for cells array in SetUnitInfoLabels, matching
the pattern used in other methods like ClearCellLabels.
This fixes a race condition where model updates arrive before
the hex grid is fully initialized on the first battle.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Switch from server-mediated headshot fetching (eagle0.net with auth)
to direct CDN access (eagle0-headshots.sfo3.cdn.digitaloceanspaces.com).
- No auth required (bucket is public)
- No server-side changes needed
- Works identically for QA, prod, and local testing
- Removes coupling between headshot fetching and game server
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless overloads for handleCapturedHeroesSelectedCommand,
resolvePleaseRecruitMeSelectedCommand, and freeForAllDecisionSelectedCommand
- These methods don't actually use gameState, so the proto versions now
delegate to core implementations that don't take gameState
- Simplify freeForAllDecisionSelectedCommand to not use CommandChooser
since the inner methods don't use gameState
This prepares the groundwork for eliminating toProto() calls in AIClient.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless overloads for helper methods:
- maybeChosenFeastCommand, maybeChosenGiftCommand
- maybeSpreadIntoOwnedProvince, maybeSpreadIntoEmptyProvince
- maybeChosenGetUnderHeroCapCommand
- maybeChosenAttackToSaveLeaderCommand
- maybeRansomLeaderCommand, chosenRescueLeaderCommand
- Update protoless overloads to use native implementations:
- chosenLoyaltyManagementCommand now uses CommandChooser directly
- chosenRescueLeaderIfAllPrisonersCommand uses leaderIds and
UnaffiliatedHeroType.Prisoner instead of proto equivalents
- Add quest dependency to BUILD.bazel (required for UnaffiliatedHeroT)
This eliminates all GameStateConverter.toProto() calls from
CommandChoiceHelpers.scala protoless overloads.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix JWT auth to set legacy userName for backwards compatibility
When using JWT authentication, contextWithJwtClaims was not setting
the legacy userNameCtxKey, causing AuthorizationUtils.userName to
return null. This broke game management code that relies on userName
for mapping users to factions.
Fix by also setting userNameCtxKey to displayName when using JWT auth,
ensuring backwards compatibility with existing game management code.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add OAuth implementation next steps and design doc
Comprehensive design document covering:
- Known issues (identity fragility, headshots, logout, uniqueness)
- Proposed user identity model with userId as stable key
- Multi-provider account linking strategy
- Avatar/headshot strategy
- Phased implementation plan
- Technical debt and open questions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update OAuth design doc with headshot investigation
- Clarify that headshots are AI-generated character portraits, not user avatars
- Document headshot architecture: client → eagle0.net (home Mac) → S3 signed URL
- Explain why OAuth breaks headshots: eagle0.net nginx only validates Basic Auth
- Clarify PR #4964 is required now (fixes admin server NPE crash)
- Phase 2 migration to userId-based identity deferred
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless chosenBuyFoodCommand using protoless foodAmountToBuy
- Add protoless maybeChosenDivineCommand using HeroUtils.power
- Add protoless maybeChosenArmTroopsCommand using ProvinceUtils
- Add protoless maybeChosenRecruitCommand and chosenReturnCommand (impl helpers)
- Convert chosenCommandWhileTraveling to native protoless implementation
using CommandChooser with protoless choosers
This eliminates wasteful GameStateConverter.toProto() conversions when
EarlyGameAIClient calls chosenCommandWhileTraveling with Scala GameState.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless isEarlyGame(GameState, FactionId) using FactionUtils and ProvinceUtils
- Add protoless chooseEarlyGameCommand using CommandChooser with protoless choosers
- Update BUILD.bazel with required deps (FactionUtils, ProvinceUtils, GameState)
- Add exports for GameState to support downstream callers
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The userId was being read inside the Future block, which runs on a
thread from ExecutionContext.global. However, gRPC Context.current
returns the context for the current thread, not the gRPC handler
thread where the JWT claims were attached.
This caused setDisplayName and getCurrentUser to always get null/empty
userId, resulting in "User not found" errors after successful OAuth.
Fix by capturing userId before creating the Future, on the gRPC thread
where Context.current has the proper claims attached. This matches
the pattern used in EagleServiceImpl.lockAndDoWithUserName.
Also update AuthorizationInterceptor publicEndpoints to use
CheckOAuthStatus instead of the old ExchangeCode method name.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless overloads for heroCount and troopCount methods
- Protoless versions take Map[BattalionId, BattalionT] instead of GameState
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Client changes for server-mediated OAuth:
- AuthClient: Use PollForOAuthCompletionAsync instead of deep links
- OAuthManager: Remove deep link handling, use polling instead
The client now:
1. Calls GetOAuthUrl to get browser URL and state token
2. Opens browser for user to authorize
3. Polls CheckOAuthStatus every 2s until success/failure/timeout
Works in Unity Editor and all platforms without URL scheme registration.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless extraHeroCount and desiredHeroCount to AIClientUtils
- Add protoless truceOfferAcceptanceChance to ResolveDiplomacyCommandSelector
- Add protoless allianceOfferAcceptanceChance to ResolveDiplomacyCommandSelector
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Previously image tags were only passed via ssh-action envs, which meant
manual docker-compose restarts would fall back to :latest. Now the SHA-tagged
image names are persisted in .env.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless overloads to FixLeaderAloneCommandSelector
- Add protoless overloads to InvitationCommandSelector
- Add protoless overloads to MoveLeaderToBetterProvinceCommandChooser
- Add protoless overloads to FactionLeaderProvinceRanker
- Add hostileNeighbors helper to FactionUtils for protoless use
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add location block for /net.eagle0.eagle.api.auth.Auth gRPC service
- Add location block for /oauth/callback HTTP endpoint
These routes are required for OAuth login to work.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert protoless overloads to use Vector[CommandChooser] directly instead
of delegating to legacy implementations with proto conversion.
Changes:
- chosenExpandCommand: now uses Scala GameState directly
- chosenEntrustCommand: now uses Scala GameState directly
- Added protoless overload for chosenMobilizeIfAdjacentEnemyCommand
This completes the protoless conversion for all main command chooser
entry points (chosenUniversalCommand, chosenDevelopCommand,
chosenMobilizeCommand, chosenExpandCommand, chosenEntrustCommand).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert protoless overloads to use Vector[CommandChooser] directly instead
of delegating to legacy implementations with proto conversion.
Changes:
- chosenDevelopCommand: now uses Scala GameState directly
- chosenMobilizeCommand: now uses Scala GameState directly
- Added protoless overloads for helper methods:
- excessBeyondMaximumSupplies
- chosenShipExcessSuppliesCommand
- chosenImproveInfrastructureIfBelowTrainingCommand
- maybeChosenTravelToArmTroopsCommand
- chosenTrainCommand
- suppliesToSend
- preferredSuppliesDestination
- chosenSendSuppliesCommandWithSuppliesAndDestination
- chosenSendSuppliesCommandWithSupplies
- maybeChosenSendSuppliesCommand
This eliminates Scala→proto→Scala conversion roundtrips for callers
that already have a Scala GameState.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Convert protoless chosenUniversalCommand overload to use Vector[CommandChooser]
directly instead of delegating to legacy implementation with proto conversion
- Add protoless overloads for nested chooser methods:
- chosenCommandWhileTraveling
- chosenLoyaltyManagementCommand
- chosenRescueLeaderIfAllPrisonersCommand
- Fix ambiguous method reference in EarlyGameAIClient by using explicit lambda
This eliminates the Scala→proto→Scala conversion roundtrip for callers
that already have a Scala GameState.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds DISCORD_CLIENT_ID and DISCORD_CLIENT_SECRET to deploy config,
enabling Discord OAuth login in production.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Enables OAuth JWT authentication in production by:
- Adding JWT_PRIVATE_KEY secret to deploy workflow env
- Passing it through SSH to the droplet .env file
- Configuring docker-compose to pass it to the Eagle container
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Previously, changes to these files wouldn't trigger a deploy since they
weren't in the paths filter. The deploy step already copies these files
to the droplet, it just wasn't being triggered.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add protoless overloads for CommandChoiceHelpers entry points
Add Scala GameState (GameStateC) accepting overloads for these methods:
- handleRiotGiveSelectedCommand
- handleRiotCrackDownSelectedCommand
- handleRiotDoNothingSelectedCommand
- handleRiotSelectedCommand
- resolveTributeSelectedCommand
- defendSelectedCommand
- defendingUnits
- chosenRestCommand
The proto-accepting versions now delegate to the Scala versions,
allowing callers to switch to passing Scala GameState directly
and eliminate unnecessary proto conversions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Eliminate proto GameState conversions in action files
Update EndHandleRiotsPhaseAction and PerformVassalDefenseDecisionsAction
to call the new protoless overloads in CommandChoiceHelpers directly,
eliminating unnecessary GameStateConverter.toProto calls.
This removes the following wasteful round-trip conversions:
- EndHandleRiotsPhaseAction: handleRiotSelectedCommand call
- PerformVassalDefenseDecisionsAction: resolveTributeSelectedCommand
and defendSelectedCommand calls
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Server changes for server-mediated OAuth:
- OAuthService: Store pending/completed sessions by state, handle callbacks
- AuthServiceImpl: Implement checkOAuthStatus gRPC method
- OAuthHttpHandler: New HTTP server for /oauth/callback endpoint
- Main: Start OAuth HTTP handler on configurable port (default 8080)
New command line options:
- --server-base-url: Base URL for OAuth callbacks (default: https://prod.eagle0.net)
- --oauth-http-port: Port for OAuth HTTP server (default: 8080)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Convert CommandChooser trait to use Scala GameState (protoless)
- Add LegacyCommandChooser trait for callers still using proto GameState
- Add legacy implicit conversions (LegacyDeterministicCommandChooser, LegacyRandomCommandChooser)
- Convert AttackDecisionCommandChooser to fully protoless
- Add Scala overload for IncomingArmyUtils.armyPower
- Update all callers to use LegacyCommandChooser where proto GameState is used:
- CommandChoiceHelpers, MidGameAIClient, AIClient, EarlyGameAIClient, ResolveDiplomacyCommandSelector
This establishes a migration path where new code uses CommandChooser with Scala
GameState while legacy code uses LegacyCommandChooser with proto GameState.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace client-side deep link OAuth with server-mediated polling:
- Add CheckOAuthStatus RPC and OAuthStatus enum
- Remove ExchangeCode RPC (server handles callback internally)
- Remove redirect_uri from GetOAuthUrlRequest (server uses its own)
Flow: Client opens browser → user authorizes → server receives callback →
client polls CheckOAuthStatus until success/failure.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Set 1GB hard limit so Shardok can't OOM the entire droplet.
If exceeded, Docker kills just that container and it restarts
automatically (restart: unless-stopped).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Cloudflare Worker for OAuth callback relay
Discord doesn't support custom URL schemes (eagle0://) as redirect URIs.
This worker receives the OAuth callback and redirects to the deep link.
- Worker deployed at eagle0-oauth-relay.eagle0-auth-relay.workers.dev
- GitHub Actions workflow for auto-deploy on push to main
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add GenerateJwtKey utility for generating JWT signing keys
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This generates the Auth gRPC client stubs needed by the Unity OAuth
client to call GetOAuthUrl, ExchangeCode, RefreshToken, etc.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The 4GB droplet was running OOM because the JVM was configured to use
all 4GB, leaving nothing for the OS, Shardok, nginx, or Docker.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
ResourceFetcher was triggering multiple parallel HTTP requests for the same
headshot when LoadIntoRawImage or Prefetch were called before a prior fetch
completed. This occurred because there was no tracking of in-flight requests.
Add _inFlightPaths HashSet to track paths currently being fetched. FetchRemote
now checks this set before starting a fetch and returns early if the path is
already being fetched. The existing fetch will process the _imageLoadQueue
when it completes. Prefetch also checks _inFlightPaths to avoid redundant work.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When receiving battle updates from Shardok, we were calling
copyWithAtomicSave which re-serializes all Eagle recentHistory,
even though no Eagle results changed. Shardok results are already
saved in withNewShardokResults.
This was showing as ~11% of CPU time in saveNow during battles.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Each call to saveNow re-serializes all results in the current batch.
With batch size 200, this means 1+2+3+...+200 = 20,100 serializations
per 200 results. Reducing to 25 gives 8 batches × 325 = 2,600
serializations, an 8x improvement.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The hero generation pipeline (commit 836c975a9) output columns in a different
order than the file header. When the data was reformatted, two column pairs
were swapped:
1. wisdom ↔ charisma (columns 11 & 12)
2. vigor ↔ bravery (columns 14 & 18)
This fix swaps these columns back for all heroes added after line 1746.
Verified by checking backstories match stats (e.g., "brilliant theoretical
work" now correctly corresponds to high wisdom).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a battle has only AI players (no humans), use the faster time budget
limit (allAiBattleTimeBudgetMaximum = 0.5s) instead of the normal limit
(lookaheadTimeBudgetMaximumSeconds = 3s). This makes all-AI battles run
faster since there's no human waiting.
The isAllAiBattle flag is computed once at battle start in MakeAIClients()
and passed to each ShardokAIClient, which uses it when calculating time
budgets.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add GetGameStateAtAction RPC to fetch full game state as JSON at any action index
- Add "State" button to history table rows for on-demand state viewing
- Display game state in expandable panel with close button
- Fix table layout with white-space: nowrap for button cells
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Without this flag, JFR can only get accurate stack traces at safepoints,
causing inlined methods to be invisible in profiles. This makes profiling
show time attributed to gRPC infrastructure instead of actual application
code.
The flag adds metadata during JIT compilation but has no runtime overhead.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add action_json, faction_name, and game_date fields to GameHistoryEntry proto
- Serialize ActionResult to JSON in EagleServiceImpl.getGameHistory()
- Display faction name and date columns in history table
- Show full JSON inline when clicking action rows (no separate AJAX call)
- Add build timestamp and git commit to admin header on all pages
- Create workspace_status.sh for Bazel build stamping
- Enable --stamp in .bazelrc
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a battle ended, the client would never see the final few actions
that caused the game to end (e.g., the killing blow). This was because
WaitForUpdatesAndPush() would return immediately after calling
OnGameOver() without first sending the pending updates.
The fix moves the update-sending code before the gameOver check,
ensuring all action results are streamed to clients before the
game over notification is sent.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Unity OAuth client infrastructure
Client-side OAuth implementation for Unity:
- AuthClient: gRPC client for Auth service (GetOAuthUrl, ExchangeCode, RefreshToken, SetDisplayName)
- JwtAuthInterceptor: Adds JWT bearer token to gRPC requests
- OAuthManager: MonoBehaviour managing OAuth flow with system browser
- TokenStorage: Secure storage for access/refresh tokens using PlayerPrefs
- ConnectionHandler: OAuth panel with Google/Discord buttons, toggle to classic auth
- EagleConnection: Support for JWT-authenticated connections
The Auth gRPC service is already defined in auth.proto. Server will return
"unimplemented" until server-side OAuth is deployed, but legacy auth works
as fallback via the toggle button.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Default to Classic auth until server-side OAuth is ready
Toggle button now starts with "Use OAuth Sign-in" and shows the
legacy auth panel by default. Click to switch to OAuth view.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add game rewind feature to admin server (Phase 3)
Implement the ability to rewind a game to a previous action count via the
admin web UI. This is useful for debugging, testing "what if" scenarios,
and recovering from bugs.
Changes:
- Add RewindGame RPC to eagle.proto with request/response messages
- Add truncateTo() method to GameHistory trait and implementations
- Add rewindTo() method to Engine trait and EngineImpl
- Add rewindTo() method to GameController (disconnects clients, resets AI)
- Add rewindGame() to GamesManager with validation
- Implement rewindGame RPC in EagleServiceImpl
- Add /games/{id}/rewind POST handler to admin server
- Add rewind button to each history row with confirmation dialog
- Add success/error feedback UI with alert styling
- Add tests for InMemoryHistory.truncateTo, EngineImpl.rewindTo, and
GameController.rewindTo
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Resync connected clients on rewind instead of disconnecting them
- Add resyncAfterRewind() to HumanPlayerClientConnectionState that sends
a starting_state with the rewound GameStateView and new action count
- Update GameController.rewindTo() to keep clients and send resync messages
- Rename proto field disconnected_clients -> resynced_clients
- Update admin server messages to reflect new behavior
- Unity client's HandleStartingState() handles the resync automatically
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When receiving a StartingState from the server, clear all client state
that could be stale after a game rewind or reset:
- ShardokGameModels, _shardokResultCounts, _shardokNeedsResync
- CommandToken, LastPostedToken (reset to -1)
- AvailableCommandsByProvince
- _commandSubmittedTime
This fixes issues where rewinding the game caused token validation
failures or displayed stale battle/command state.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Document GameState round-trip elimination (PRs #4913, #4914, #4915)
- Add detailed next steps for CommandChoiceHelpers migration
- Outline phases for CommandChooser trait and MidGameAIClient
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Show environment indicator (prod/qa) in connection status area
Adds a text field that displays the connected environment name with
color coding: green for prod, yellow for qa. The indicator appears
after connecting and clears when the connection is disposed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Move environment indicator to Eagle game view ConnectionStatusUI
Instead of showing the environment on the Connection panel, show it
in the Eagle game view next to the connection status indicator.
- Add environmentIndicatorText field to ConnectionStatusUI
- Pass environment name from ConnectionHandler to EagleGameController
- Display "prod." in green or "qa." in yellow
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up environment indicator text in Unity
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The UpgradeBattalionQuest case was comparing a proto BattalionTypeId
(from net.eagle0.eagle.common.battalion_type.BattalionTypeId) with
a model BattalionTypeId (from net.eagle0.eagle.model.state.BattalionTypeId).
These are different types that never match, causing .find() to return None
and .get to throw NoSuchElementException.
This caused a production crash preventing users from reconnecting to
games with UpgradeBattalionQuest quests.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add /status, /start, /stop endpoints to JFR sidecar for dynamic recording control
- Remove -XX:StartFlightRecording from Eagle - JFR is now started on demand
- Add JFR control handlers to admin server to proxy sidecar requests
- Update admin UI with status indicator and Start/Stop/Download buttons
- Download button only shown when recording is active
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
JVM attach (used by jcmd) communicates via socket files in /tmp.
With shared PID namespace but separate filesystems, the sidecar
couldn't find Eagle's .java_pid1 socket file.
Add a shared named volume for /tmp between both containers.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
PR #4917 added the jfr-sidecar image configuration but the CI workflow
wasn't building or pushing it, causing deploy to fail with "image not found".
- Add build-jfr-sidecar job to build and push the sidecar image
- Add JFR_SIDECAR_IMAGE to deploy job dependencies and env vars
- Add crane pull/load steps for jfr-sidecar in deploy script
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add a JFR sidecar container that shares PID namespace with Eagle to
enable JFR dumps without giving admin container Docker socket access.
Components:
- jfr_server: Minimal Go HTTP server that runs jcmd and serves JFR files
- Sidecar uses shared PID namespace (pid: "service:eagle") to see Eagle JVM
- Admin console proxies /jfr/download to sidecar
- UI button in nav bar triggers download
Security:
- Sidecar can only see Eagle's processes (no Docker socket)
- No host filesystem access
- Minimal attack surface
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Eliminate GameState toProto() conversion in LLM pipeline
Phase 1 of deproto optimization: Make LlmRequestWithGameState use Scala
GameState instead of proto GameState, eliminating wasteful round-trip
conversion in UnrequestedTextHandler (the heaviest proto conversion in profiles).
Changes:
- LlmResolver: Change LlmRequestWithGameState.gameState to Scala GameState
- UnrequestedTextHandler: Remove GameStateConverter.toProto() calls, pass
gameHistory.stateAfter() directly
- Update all prompt generators (~38 files) to use Scala model types:
- FactionT instead of proto Faction
- HeroT instead of proto Hero
- ProvinceT instead of proto Province
- Scala enums (Gender.Male, Profession.Mage) instead of proto enums
- DivineMessagePromptGenerator: Rewrite quest pattern matching to use
Scala QuestC types instead of proto quest types
- ChronicleUpdatePromptGenerator: Inline province lookup, remove
LegacyFactionUtils dependency
- BattalionDescriptions: Add Scala BattalionT and BattalionTypeId overloads
- Update test files to use concrete Scala types (HeroC, FactionC, ProvinceC)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix UnrequestedTextHandlerTest to use Scala GameState
Update test to use Scala GameState instead of proto GameState for
LlmRequestWithGameState, matching the updated API. Also remove
deprecated Scala 3 trailing underscore syntax for function references.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Preserve Scala GameState in PostResults to avoid round-trip conversion
Add optional scalaGameState field to PostResults that preserves the Scala
GameState when it's already available. This eliminates the pattern where
we convert Scala→proto (for PostResults) and then proto→Scala (in
synchronizedHandlePostResults).
Changes:
- PostResults: Add scalaGameState field
- GameController: Pass scalaGameState when creating PostResults
- GamesManager: Use scalaGameState in synchronizedHandlePostResults if available
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify PostResults to use only Scala GameState
- Change PostResults.gameState from proto to Option[GameState] (Scala model)
- Remove redundant scalaGameState field since gameState is now the Scala type
- Update all PostResults creation sites to pass Some(scalaState) or None
- CustomBattleManager returns None since it has no Eagle game state
- Remove unused GameStateConverter import from GameController
This eliminates the need to maintain both proto and Scala GameState in
PostResults. The proto was only used as a fallback when scalaGameState
was None, but all callers now provide the Scala state directly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add confirmation panel for drop game button
- Add DropGameConfirmationPanel component with confirm/cancel buttons
- Show confirmation before dropping a game
- Falls back to direct drop if no panel is configured
Note: Requires creating and wiring up the confirmation panel prefab in Unity.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up drop game confirmation panel in Unity
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add lazy val scalaGameState to ActionWithResultingState that either uses
a precomputed Scala state or lazily converts from proto.
Key optimizations:
- When results come from Scala-based actions via withNewResultsScala(),
preserve the Scala GameState to avoid unnecessary proto->Scala conversion
- When loaded from disk/S3 (proto only), convert lazily on first access
- The lazy val ensures conversion happens at most once per instance
Files changed:
- ActionWithResultingState: Add precomputedScalaState parameter and lazy val
- GameHistory.withNewResultsScala: Pass Scala state to avoid re-conversion
- PersistedHistory: Use scalaGameState in stateAfter()
- InMemoryHistory: Use scalaGameState in stateAfter()
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Make S3 saves async to avoid blocking the main thread
S3/DO Spaces saves take 50-200ms per request, which was blocking the
main thread during game state persistence. This change wraps S3Persister
in an AsyncS3Persister that queues saves for background execution.
- Add AsyncS3Persister that uses a single-threaded executor per game
- LocalGamePersisterCreation now wraps S3Persister in AsyncS3Persister
- Register shutdown hook to flush pending S3 writes on graceful shutdown
- If shutdown is in progress, saves happen synchronously to ensure durability
This improves server responsiveness during game saves while maintaining
data safety through the shutdown flush mechanism.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use coalescing pattern to always save latest version
Instead of queuing saves, use a ConcurrentHashMap keyed by filename.
New saves for the same key overwrite pending ones, ensuring we always
write the most recent data and preventing backlog buildup.
A scheduled task processes pending writes every 500ms. This batches
multiple saves together and ensures the latest version is always written.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add proto messages and RPCs: ConvertAiToHuman, ConvertHumanToAi, ReassignFaction
- Add GameController methods: promoteAiToHuman, demoteHumanToAi, reassignFaction
- Add GamesManager methods with validation and persistence
- Add EagleServiceImpl RPC handlers
- Add Go admin server HTTP handlers under /games/{id}/player-management/
- Add UI with players table, action buttons, and modals for username input
Features:
- Assign User: Convert AI faction to human control with username input
- Drop to AI: Convert human faction back to AI (triggers AI commands if turn)
- Reassign: Change which username controls a human faction
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Previously, copyWithLlmSave was called on every streaming LLM token
(~50-100ms intervals), causing excessive disk I/O. Each save serialized
all incomplete texts to protobuf and wrote multiple files.
Now we only save when the stream completes. Worst case on crash: lose
one in-progress stream which can be regenerated.
This reduces I/O from receiveStreamingLlmResponses by ~95%.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
OperationCanceledException is the base class of TaskCanceledException.
YetAnotherHttpHandler can throw OperationCanceledException directly,
which was falling through to the generic Exception handler and being
logged as "unexpected error". Now we catch OperationCanceledException
which handles both types.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Switch Eagle server base image from JRE to JDK for JFR support
The previous eclipse-temurin image was a JRE which doesn't include jcmd.
This prevented dumping JFR recordings from a running server. Switch to
the 17-jdk tag which includes the full JDK with jcmd.
To dump JFR recording from running server:
docker exec eagle-server jcmd 1 JFR.dump filename=/app/jfr/profile.jfr
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update MODULE.bazel.lock for JDK image
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a toggle becomes unavailable (e.g., Devastation when there's no
devastation), explicitly set isOn=false so it doesn't appear selected.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add DropGame API for users to leave games
- Add DropGameRequest/Response proto messages to streaming API
- Add archived directory path in SaveDirectory for game preservation
- Add delete method to Persister trait and implementations
- LocalFilePersister: delete local files
- S3Persister: no-op (retain as backup)
- CompoundPersister: delete from first (local) only
- Add dropUser() and remainingHumanCount to GameController
- Reassigns dropped faction to AI with immediate takeover
- Add archiveGame() and dropGame() to GamesManager
- Running games: archive if last human, otherwise AI takeover
- Waiting games: remove user or entire game if empty
- Add DropGame handler to EagleServiceImpl streaming
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add deleteAll() to Persister and tests for dropGame
- Add deleteAll() method to Persister trait and implementations
- LocalFilePersister: deletes all files and the directory
- S3Persister: no-op (retain as backup)
- CompoundPersister: delegates to first persister
- Refactor archiveGame to use deleteAll() instead of java.io.File
- Add unit tests for dropGame functionality:
- Removes entire waiting game when only player drops
- Removes only user when other players remain
- Returns USER_NOT_IN_GAME when user not in waiting game
- Returns GAME_NOT_FOUND when game doesn't exist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add drop game support to lobby UI
- Add dropButton and DropCallback to AvailableGameItem and RunningGameItem
- Add DropClicked method to both item types
- Add DropGame method to ConnectionHandler that sends DropGameRequest
- Wire up DropCallback when creating game list items
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up drop buttons in lobby game prefabs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update lobby game prefabs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add DropGame API for users to leave games
- Add DropGameRequest/Response proto messages to streaming API
- Add archived directory path in SaveDirectory for game preservation
- Add delete method to Persister trait and implementations
- LocalFilePersister: delete local files
- S3Persister: no-op (retain as backup)
- CompoundPersister: delete from first (local) only
- Add dropUser() and remainingHumanCount to GameController
- Reassigns dropped faction to AI with immediate takeover
- Add archiveGame() and dropGame() to GamesManager
- Running games: archive if last human, otherwise AI takeover
- Waiting games: remove user or entire game if empty
- Add DropGame handler to EagleServiceImpl streaming
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add deleteAll() to Persister and tests for dropGame
- Add deleteAll() method to Persister trait and implementations
- LocalFilePersister: deletes all files and the directory
- S3Persister: no-op (retain as backup)
- CompoundPersister: delegates to first persister
- Refactor archiveGame to use deleteAll() instead of java.io.File
- Add unit tests for dropGame functionality:
- Removes entire waiting game when only player drops
- Removes only user when other players remain
- Returns USER_NOT_IN_GAME when user not in waiting game
- Returns GAME_NOT_FOUND when game doesn't exist
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Replace Improve command dropdown with toggle buttons
- Replace TMP_Dropdown with 4 toggle buttons (Economy, Agriculture,
Infrastructure, Devastation) showing type name and current value
- Each toggle shows the improvement type and its current/effective value
- Maintains existing default selection logic (Devastation priority, then
lowest stat)
Note: Unity prefab changes are needed to wire up the new toggle buttons.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove unused variable in ImproveCommandSelector
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Show unavailable improvement types as grayed-out instead of hidden
Uses CanvasGroup alpha to dim unavailable toggles and sets
interactable=false so they cannot be selected.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up ImproveCommandSelector toggle buttons in Unity
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless overloads to AIClientUtils for takenHeroIdsForMarchTowardFocus,
mostPowerfulHeroes, and desiredCountForMarchTowardFocus
- Convert MarchTowardProvinceCommandChooser to use native GameState,
ProvinceDistances, and BattalionUtils instead of Legacy* versions
- Update callers (MidGameAIClient, MoveLeaderToBetterProvinceCommandChooser)
to convert proto GameState before calling
- Fix tests by adding currentPhase to proto GameState fixtures
- Update DEPROTO_PLAN.md
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a battle ended, the client would never see the final few actions
that caused the game to end (e.g., the killing blow). This was because
WaitForUpdatesAndPush() would return immediately after calling
OnGameOver() without first sending the pending updates.
The fix moves the update-sending code before the gameOver check,
ensuring all action results are streamed to clients before the
game over notification is sent.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Remove dead minimallyFatiguedHeroesProto from HeroSelector (no callers)
- Convert ProvinceGoldSurplusCalculator to fully protoless
- Callers now use ProvinceConverter.fromProto() to get protoless province
- Removed proto GameState dependency entirely
- Update callers in CommandChoiceHelpers, MarchTowardProvinceCommandChooser,
and MidGameAIClient to use protoless versions
- Update DEPROTO_PLAN.md to reflect progress
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add chronicler_style field to ChronicleEntry proto and Scala case class
- Store which chronicler style was used for each entry
- Update ChronicleUpdatePromptGenerator to label previous entries with their chronicler
- Prompt now distinguishes between same/different chronicler:
- Same chronicler: "Continue in that same style and voice"
- Different chronicler: "Write in YOUR OWN STYLE, don't imitate previous entries"
- Select chronicler style deterministically in NewRoundAction based on game ID and date
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Only generate NewFactionHead notification when the faction head actually
changes (i.e., when the head is killed), not when any non-head leader
is killed (e.g., a sworn brother being executed).
The bug was that maybeFactionLeaderRemovedResult was creating notifications
for every faction where any leader died, regardless of whether the faction
head changed. Now it correctly checks if originalFaction.factionHeadId !=
revisedFaction.factionHeadId before generating notifications.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds Java Flight Recorder with continuous low-overhead profiling (~1%):
- Keeps 1 hour of data in a 100MB circular buffer
- Auto-dumps on JVM exit
- Stack depth of 256 for detailed traces
To capture a profile:
docker exec eagle-server jcmd 1 JFR.dump filename=/app/jfr/profile.jfr
docker cp eagle-server:/app/jfr/profile.jfr .
Analyze with JDK Mission Control (jmc) or IntelliJ's JFR viewer.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The ShardokGameModel was being removed from ShardokGameModels BEFORE
UpdateAction.Invoke() was called. This meant the UI callback received
a model that no longer contained the ended battle's final results.
Flow before:
1. Final Shardok results arrive with GameStatus = Victory/Defeat
2. Model updated with final results
3. Model REMOVED from ShardokGameModels
4. UpdateAction.Invoke() - UI doesn't see the model
5. Final results never displayed
Flow after:
1. Final Shardok results arrive with GameStatus = Victory/Defeat
2. Model updated with final results
3. Model STAYS in ShardokGameModels
4. UpdateAction.Invoke() - UI sees model with final results
5. Model removed AFTER callback
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The deploy and manifest update steps had the branch check commented out,
causing builds from PRs and feature branches to be published to production.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add settings management to admin server (Phase 2)
- Add GetSettings gRPC endpoint to eagle.proto
- Enhance SettingsLoader generator to include getAllSettings method
- Implement getSettings in EagleServiceImpl
- Add settings page with live search and inline editing
- Uses existing AddSettings endpoint for updates
- Modified settings are highlighted in the UI
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make settings table rows more compact
- Reduce cell padding and font sizes
- Make input and button elements more compact
- Override Pico CSS defaults for better density
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make settings input fields even more compact
- Use !important to override Pico CSS defaults
- Set fixed height of 22px for input and button
- Reduce padding to 2px
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add GetActionDetail gRPC endpoint to fetch individual action data
- Display action history in reverse chronological order (most recent first)
- Make action rows clickable to expand and show JSON representation
- Add action_detail.html template for htmx-powered action expansion
- Update CSS for clickable rows and action detail styling
- Mark Phase 1 as complete in enhancement plan
New routes:
- GET /games/{id}/action/{index} - htmx partial for action detail
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When `filteredResults` was empty (e.g., AI turns filtered out as not
visible to the human player), the `update` method returned early
without sending a message to the client. However, it still advanced
the server's tracking of the client's count (`unfilteredKnownHistoryCount`).
This caused sync mismatches: the server thought the client was up-to-date
(so it wouldn't send the "missing" results on subsequent updates), but
the client never received the new count.
The fix: always call `afterSendingResults`, even when `filteredResults`
is empty. This ensures the client receives the count update (plus
`availableCommands` and `serverGameStatus`) even when there are no
visible results.
This was particularly problematic after battles ended, when AI turns
might be filtered out, causing the "last turns of the battle" to never
appear on the client.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Phase 1 of admin server enhancements:
- Add Go templates with embed for layout, games list, game detail
- Add Pico CSS from CDN for styling, htmx for interactivity
- Implement htmx infinite scroll for action history
- Keep JSON API endpoints for backward compatibility
New routes:
- GET / - redirect to /games
- GET /games - HTML games list page
- GET /games/{id} - HTML game detail page
- GET /games/{id}/history - htmx partial for history rows
Also adds docs/ADMIN_SERVER_ENHANCEMENTS.md with full enhancement plan
including settings management and game rewind features.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The textTemplate in PrisonerExecutedDetailsNotificationGenerator was
missing the \n\n separator between lead text and LLM-generated text,
causing them to run together. This matches the pattern used in other
notification generators like CapturedHeroExecutedDetailsNotificationGenerator.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Two bugs were causing commands to be lost when connection dropped
while posting:
1. PostRequest removed commands from pending queue even when
DoWithStreamingCall returned false (connection dead). Now only
removes on successful send.
2. TryPendingCommands dropped pending commands when CurrentEagleToken
was null (which happens when HandleAvailableCommands skips due to
LastPostedToken match). Now retries the command anyway.
Together these fixes ensure that if you post a command while the
connection is dead or dying, the command will be retried after
reconnect.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
WriteAsync can block indefinitely if a connection is dead but not yet
detected (e.g., network issues before TCP keepalive kicks in). This can
exhaust the .NET thread pool and cause the client to freeze.
Changes:
- Add 10-second timeout to DoWithStreamingCall using Task.WhenAny
- Add 10-second timeout to SendUpdateStreamRequestAsync
- Fix double PostRequest bug in TryPendingCommands where commands were
posted twice (once in switch case, once after)
- Add ConfigureAwait(false) to network operations to avoid deadlocks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Switch ResourceFetcher from HttpClientHandler to YetAnotherHttpHandler with
HTTP/2 enabled. This provides:
- Connection multiplexing (all headshots to same host share one connection)
- Keep-alive pings to detect and recover from dead connections
- Same reliability settings as the gRPC connection
This should help headshots load more reliably on high-latency or lossy networks.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Several exception handling issues could cause the gRPC connection to die
silently with no recovery:
1. HandleStreamingCall() is async void but only caught RpcException and
ObjectDisposedException. Any other exception type (e.g., from Logger,
protobuf, null refs) would escape and leave the connection dead.
Added catch-all that logs and schedules reconnect.
2. Connect() catch block logged errors but never called ScheduleReconnect().
If Connect() failed after creating the streaming call, the connection
would die with no recovery attempt. Now properly cleans up and reconnects.
3. Timer callbacks (idle check, heartbeat) had no exception handling.
Exceptions in timer callbacks can stop the timer from firing again.
Now wrapped in try-catch.
4. Task.Run(() => Connect()) fire-and-forget calls silently swallowed
exceptions. Created RunConnectAsync() helper that logs exceptions.
5. Task.Run(() => SendHeartbeat()) also silently swallowed exceptions.
Now properly awaits and catches exceptions inside the Task.Run.
These issues could explain "connection freezes" where the client stops
receiving updates but doesn't recover - an unhandled exception kills
the streaming thread or prevents reconnection.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Logger was blocking ThreadPool threads when StreamWriter.Flush()
was slow (due to antivirus, cloud sync like OneDrive, or disk I/O).
This caused timer callbacks to stop firing, which led to heartbeat
failures and connection freezes.
The fix uses a ConcurrentQueue and dedicated background thread:
- LogLine() just enqueues the formatted message and returns immediately
- A dedicated writer thread processes the queue and handles file I/O
- File I/O blocking only affects the writer thread, not callers
- Timestamps are captured immediately when LogLine() is called
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add new overload that takes native GameState and uhsWithQuests directly
- Proto version now delegates to protoless version
- Export unaffiliated_hero_with_quest and game_state for callers
- Extract choosers list to a val for reuse
This is Part B of splitting PR #4876 - add a protoless public API while maintaining backward compatibility with CommandChooser framework.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change UnaffiliatedHeroWithQuest to use heroId: HeroId and quest: QuestT instead of uh: UnaffiliatedHero and quest: Quest proto
- Update all 9 quest command choosers to pattern match on native quest types directly (no more .quest.details unwrapping)
- Update FulfillQuestsCommandSelector to use QuestConverter.fromProto
- Update all 6 test files to use native quest types
- Add quest/concrete visibility and deps where needed
This is Part A of splitting PR #4876 - deproto the quest command selectors' internal data structures while keeping the public interface unchanged.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The shardokGameIdsToCheck list was constructed by concatenating
outstandingBattles and knownShardokResultCounts without deduplication.
When a battle appears in both lists, the same ShardokGameResultResponse
was generated and sent twice.
This caused every Shardok result to appear twice in client logs:
#197 UPDATE ShardokResult shardok=...49cbefb8 countAfter=7 results=6
#198 UPDATE ShardokResult shardok=...49cbefb8 countAfter=7 results=6
Fix: Add .distinct to remove duplicate shardokGameIds.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change attackDecisionSelectedCommand to take native GameState
- Convert to proto internally using GameStateConverter.toProto
- Keep private methods using proto GameStateProto since they receive
proto from CommandChooser lambdas
- Update callers (AIClient, CommandChoiceHelpers) to convert proto → native
- Update tests to use GameStateConverter.fromProto and add currentPhase
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove legacy 5-second polling wait from GetUpdates
The wait in GetUpdates was used for long-polling to make AI turns feel
responsive. With streaming, WaitForUpdatesAndPush already waits for
updates via updateCondition.wait(), making this redundant.
GetGameStatus is deprecated in favor of SubscribeToGame streaming.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove GetGameStatus RPC (now using streaming)
GetGameStatus polling is no longer needed since Eagle now uses
SubscribeToGame streaming. This also removes the 5-second wait in
GetUpdates that was only needed to support long-polling.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Exclude pre-existing font files from LFS tracking
These font files were committed as regular blobs before LFS tracking
was set up for *.ttf files. Adding explicit exclusions prevents the
"files that should have been pointers" warning.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix out-of-bounds access in GetGameStateAtStartOfAction
Add upper bounds check to GetGameStateAtStartOfAction to match
GetGameHistory behavior. When requesting a state at or beyond the
current action count, return the current game state instead of
accessing an invalid array index.
This fixes signal 11 crashes in FilterNewResults called from
SubscribeToGame streaming flow.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Revert "Fix out-of-bounds access in GetGameStateAtStartOfAction"
This reverts commit c725b7a4ed87028bdc4860b4e7753497dbb3f040.
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Log countAfter (NewResultViewCount) for each ShardokGameResponse to
detect missing Shardok battle results. Similar to the ActionResultResponse
logging, this will show gaps in the sequence if messages are lost.
This helps diagnose why clients sometimes miss the last moves of battles -
observed as shardok sync mismatch (e.g., client=54, server=65).
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Complete deproto conversion of CommandChoiceHelpers
- Replace all Legacy utility usages with protoless versions:
- LegacyFactionUtils.isFactionLeader → FactionUtils.isFactionLeader
- LegacyProvinceUtils.containsFactionLeader → FactionUtils.factionLeaderLocation
- LegacyProvinceUtils.effectiveInfrastructure → ProvinceUtils.effectiveInfrastructure
- LegacyBattalionTypeFinder.battalionType → BattalionTypeFinder.battalionType
- LegacyHeroUtils.power → HeroUtils.power
- Remove unused proto helper methods: suppressBeastsValueProto, beastPowerProto,
closestLeaderProto, lowLoyaltyHeroesProto, provinceFoodSurplusProto, destinationCloserToProto
- Use targeted object conversion for foodAmountToBuy (convert individual objects
rather than full GameState)
- Update DateConverter and RoundPhaseConverter to handle missing/default values
for test compatibility (month=0 → January, UNKNOWN_PHASE → PlayerCommands)
- Remove Legacy dependencies from BUILD.bazel
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Revert converter defaults, fix tests to set required fields
- Revert DateConverter and RoundPhaseConverter to throw on invalid data
- Update tests to set currentDate and currentPhase in GameState fixtures
- Tests should explicitly set required data rather than relying on defaults
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add incrementing counter (#1, #2, #3...) to each LogFlow call to diagnose
the duplicate logging issue observed in testing. Interpretation:
- Same seq# appears twice: two threads processing same message
- seq# increments but lines doubled: StreamWriter/Logger issue
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Log UnfilteredResultCountAfter for each ActionResultResponse to help
diagnose sync mismatches. If we see gaps in the sequence (e.g., 2512
then 2519 with no 2513-2518), we know messages never arrived from the
server. If we see all counts but the client's internal counter doesn't
match, something dropped them internally.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add destinationCloserTo(FactionId, Map[ProvinceId, ProvinceT], ProvinceId, Vector[ProvinceId], ProvinceId)
using FactionUtils.ownedNeighbors
- Rename proto version to destinationCloserToProto
- Add imports for ProvinceT and FactionUtils
- Update preferredSuppliesDestination to use destinationCloserToProto
- Update BUILD.bazel with protoless dependencies
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless beastPower(ProvinceT) using ProvinceUtils.beastCount/beastInfo
- Add proto version beastPowerProto(Province) and update chosenSuppressBeastsCommand
to use it instead of inline calculation
- Also adds protoless suppressBeastsValue(HeroT) alongside proto version
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add closestLeader(FactionId, FactionT, Map[ProvinceId, ProvinceT], ProvinceId)
using ProvinceUtils.locationOf and ProvinceDistances.distanceThroughFriendliesOption
- Rename proto version to closestLeaderProto
- Add imports for ProvinceT and ProvinceUtils
- Update preferredSuppliesDestination to use closestLeaderProto
- Update BUILD.bazel with protoless dependencies
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add provinceFoodSurplus(ProvinceT, GameStateC) using protoless utilities
- Rename proto version to provinceFoodSurplusProto
- Add imports for GameStateC, ProvinceT, and ProvinceUtils
- Update BUILD.bazel with protoless dependencies
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add lowLoyaltyHeroes(Vector[HeroT], Vector[FactionT]) using HeroUtils.effectiveLoyalty
- Add suppressBeastsValue(HeroT) using protoless Profession enum
- Rename proto versions to lowLoyaltyHeroesProto and suppressBeastsValueProto
- Update callers to use the renamed proto versions
- Add FactionT and NoProfession imports
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add protoless utility methods to FactionUtils
- Add isFactionHead(HeroId, Iterable[FactionT]) - checks if hero is any faction head
- Add leadersBesidesHead(FactionT) - gets leader IDs excluding faction head
- Add hasProvinces(FactionId, Iterable[ProvinceT]) - checks if faction owns provinces
- Add provinceCount(FactionId, Iterable[ProvinceT]) - counts faction's provinces
These mirror the proto versions in LegacyFactionUtils and enable future migration.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add tests for isFactionHead, leadersBesidesHead, hasProvinces, provinceCount
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add protoless utility methods to HeroUtils
- Add seniorityOrder(FactionT, HeroId) - returns leader index or Int.MaxValue
- Add archeryCapable(HeroT) - checks if hero meets agility threshold
- Add startFireCapable(HeroT) - checks profession and stats for fire ability
These mirror the proto versions in LegacyHeroUtils and enable future migration
of CommandChoiceHelpers methods.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add tests for seniorityOrder, archeryCapable, and startFireCapable
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds LogFlow() helper for precise HH:mm:ss.fff timestamps throughout
the connection flow to diagnose reconnection issues:
PersistentClientConnection.cs:
- Connect(): state transitions, subscription start
- StreamOneGameAsync(): subscribe request/ack with counts
- HandleGameUpdate(): update types with token, command count, status
- HandleStreamingCall(): start/end with exit reason
- HandleHeartbeatResponse(): heartbeat response, sync mismatch detection
- SendHeartbeat(): heartbeat send/skip with reason
- ScheduleReconnect(): reconnect scheduling with backoff
EagleGameModel.cs:
- ReceiveGameUpdate(): token comparison and skip decision logging
This is logging-only - no behavioral changes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add protoless closestProvinceThroughFriendliesOption to ProvinceDistances
- Find closest province from candidates through friendly territory
- Mirrors LegacyProvinceDistances.closestProvinceThroughFriendliesOption
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add tests for closestProvinceThroughFriendliesOption
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add protoless adjacentHostiles and absoluteFoodSurplus to ProvinceUtils
- adjacentHostiles: Get provinces ruled by hostile factions
- absoluteFoodSurplus: Compute absolute food surplus for a province
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add tests for adjacentHostiles
Note: absoluteFoodSurplus tests skipped due to complex GameState requirements
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add protoless locationOf and containsFactionLeader to ProvinceUtils
- locationOf: Find the province containing a hero
- containsFactionLeader: Check if a province contains the faction head
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add tests for locationOf and containsFactionLeader
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The timers stopped logging entirely during the 28-second gap. To determine
if the timers are firing but the Logger is blocked vs timers not firing:
1. Add Console.WriteLine in timer Elapsed handlers BEFORE the callback
- These bypass our Logger and write directly to stdout
- Will show if timers are firing even if Logger is blocked
2. Make Logger thread-safe with locks
- LogLine now uses lock(_lock) to prevent concurrent access issues
- GetLogger now uses lock(_loggersLock) for thread-safe singleton access
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add logging to diagnose why heartbeat/idle timers stop firing on Windows:
- Log when heartbeat timer fires (before any checks)
- Log when heartbeat is skipped and why (cancellation, not connected)
- Log when heartbeat send fails (stream unavailable)
- Log idle check when idle > 5s with cancellation status
This will help diagnose the 90-second gap where no heartbeat or idle
logs appear before PROTOCOL_ERROR on Windows client.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When HandleHeartbeatResponse detects a sync mismatch and triggers a
reconnect, it wasn't stopping the heartbeat and idle check timers.
This could cause them to fire during reconnection, potentially
interfering with the new connection.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Handle the case where a gRPC stream is completed (client disconnected) but
the LLM update consumer thread still tries to send updates. This causes
IllegalStateException: "Stream is already completed, no further calls allowed".
Added catch for IllegalStateException in both humanClientsAfterUpdatingLlmStream
and humanClientsAfterPostingResults to gracefully handle disconnected clients.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The heartbeat handler was reading game state and sending responses without
holding the GamesManager lock. Since game updates ARE sent while holding
this lock, there was a race condition where:
1. AI processing holds GamesManager lock
2. Updates start being sent to clients via the stream
3. Heartbeat request arrives, acquires EagleServiceImpl lock (different lock)
4. Heartbeat reads game count and sends response (can interleave with updates)
5. Client receives heartbeat before some in-flight updates
6. Client detects sync mismatch despite updates being in transit
This particularly affected slower/remote connections (e.g., Windows clients)
because the timing window for the race was longer.
Fix: Wrap heartbeat handling in gamesManager.synchronized to ensure the
response is sent only after any in-progress game updates have been queued.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Upgrade YetAnotherHttpHandler from 1.5.3 to 1.11.4 to get:
- ConnectTimeout support (added in 1.8.0)
- Fix deadlock during cancellation (1.11.4)
- Memory management improvements (1.11.1)
- Backpressure control (1.10.0)
Configure explicit timeouts to detect and recover from dead connections
faster, especially on Windows where firewalls may silently drop HTTP/2
keep-alive pings:
- Http2KeepAliveTimeout (5s): Close connection if ping not acknowledged
- Http2KeepAliveWhileIdle: Continue pinging during idle periods
- ConnectTimeout (10s): Don't wait forever for initial connection
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Dispose() method held lock(this) while calling Thread.Join() on the
streaming thread. If the streaming thread was trying to acquire the same
lock, this caused a deadlock - making "Return to Lobby" hang indefinitely.
Fix: Capture thread reference inside lock, then join OUTSIDE the lock.
Also adds diagnostic logging for sync mismatch debugging:
- Heartbeat now logs client's count per game
- Result count updates are logged when they change
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless shouldRest(Iterable[HeroT]) using HeroUtils.fatigue
- Rename proto version to shouldRestProto(Iterable[Hero])
- Update callers (PerformVassalCommandsPhaseAction) to use shouldRestProto
- Add protoless tests using HeroC
- Add hero_utils dependency to BUILD.bazel
This follows the established pattern for incremental deproto migration.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove SwornBrotherChooser.bestChoiceProto and convert test to protoless
- Delete bestChoiceProto method from SwornBrotherChooser
- Convert SwornBrotherChooserTest to use HeroC (protoless) instead of proto Hero
- Remove proto and LegacyHeroUtils dependencies from sworn_brother_chooser target
- Update DEPROTO_PLAN.md to mark SwornBrotherChooser as fully protoless
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Convert ImproveCommandSelectorTest to protoless and update DEPROTO_PLAN.md
- Replace proto Hero/Province/GameState with native HeroC/ProvinceC/GameState
- Remove GameStateConverter.fromProto calls - test now uses native types directly
- Remove proto dependencies from BUILD.bazel
- Update DEPROTO_PLAN.md with comprehensive status of all command selectors:
- Document 11 fully protoless command selectors
- Document 10 protoless quest command selectors
- Document 2 files with dual proto/protoless versions
- Document 4 files still blocked on proto GameState
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The --platforms flag doesn't affect oci_image/pkg_tar dependencies -
they're exec rules that run on the host. This caused the Go binary
to be built for darwin/arm64 instead of linux/amd64.
Fix by creating an explicit admin_server_linux_amd64 target with
goos = "linux" and goarch = "amd64" set in the BUILD file.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add oci_image, oci_load, oci_push targets for admin_server in ci/BUILD.bazel
- Add Alpine Linux 3.21 base image to MODULE.bazel (lightweight for Go binary)
- Add admin service to docker-compose.prod.yml with health check
- Add build-admin job to GitHub Actions workflow
- Update deploy job to pull and deploy admin image alongside eagle/shardok
The admin server connects to Eagle via internal Docker network (eagle:40032)
and exposes HTTP on port 8080 for the admin console.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Go admin server to Docker deployment
- Add oci_image, oci_load, oci_push targets for admin_server in ci/BUILD.bazel
- Add Alpine Linux 3.21 base image to MODULE.bazel (lightweight for Go binary)
- Add admin service to docker-compose.prod.yml with health check
- Add build-admin job to GitHub Actions workflow
- Update deploy job to pull and deploy admin image alongside eagle/shardok
The admin server connects to Eagle via internal Docker network (eagle:40032)
and exposes HTTP on port 8080 for the admin console.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Go cross-compilation by enabling pure Go build
Add `pure = "on"` to admin_server go_binary to disable CGO.
This avoids linker errors when cross-compiling from macOS ARM64
to Linux x86_64 with the LLVM toolchain.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use rules_go platform for Go cross-compilation
Use @io_bazel_rules_go//go/toolchain:linux_amd64 instead of
//:linux_x86_64 for the admin server build. The rules_go platform
uses Go's native cross-compiler and doesn't involve the LLVM C
toolchain, avoiding linker errors.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Profiling showed that GenerateDistances accounts for only 0.04% of
total runtime while the disk cache consumed 168 GB of storage. The
modifier hash explosion (ice melt, fire, etc.) caused unbounded cache
growth without providing meaningful performance benefit.
The in-memory caching layers (persistent, thread-local, and shared)
remain and provide effective caching for the hot paths.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Pass all required env vars from GitHub secrets
- Fix .env file permissions (rm before write, chmod 600)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Split ProvinceDistances into protoless version + LegacyProvinceDistances
- Convert SeekMoreLeadersCommandChooser to protoless (MidGameAIClient converts proto→Scala)
- Add protoless overloads to FactionUtils.provinces and CommandChoiceHelpers
- Create protoless SeekMoreLeadersCommandChooserTest using Scala model types
- Add DEPROTO_PLAN.md documenting migration status and decisions
- Update visibility for model state concrete types to support AI tests
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Enable S3/DO Spaces persistence via environment variables
Add support for persisting game saves to DigitalOcean Spaces (S3-compatible)
via environment variables, preventing game state loss during deployments.
Configuration:
- EAGLE_ENABLE_S3=true to enable S3 persistence
- DO_SPACES_ACCESS_KEY and DO_SPACES_SECRET_KEY for credentials
- Falls back to ~/.s3cfg if env vars not set
Changes:
- S3Credentials: Load credentials from env vars or ~/.s3cfg
- LocalGamePersisterCreation: Read EAGLE_ENABLE_S3 from env
- ServerSetupHelpers: Enable S3 persister based on env var
- S3Utils: Fix transferManager to use credentials
- docker-compose.prod.yml: Add S3 environment variables
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Centralize S3 config and add endpoint env var
- Move all S3 config to S3Credentials (single source of truth)
- Add DO_SPACES_ENDPOINT env var (defaults to sfo3.digitaloceanspaces.com)
- Remove duplicate config reading from LocalGamePersisterCreation and ServerSetupHelpers
- Pass S3 secrets from GitHub Actions to droplet via .env file
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Eagle image was being built without --platforms, defaulting to the
self-hosted runner's platform (darwin/arm64). This caused digest mismatch
errors when trying to pull the image on the linux/amd64 production droplet.
Also fix the bazel-bin symlink issue: when building with --platforms,
the output goes to a platform-specific directory. Save the resolved path
immediately after build (before other bazel commands change the symlink)
and use that path for pushing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Delete package.scala containing the unused ActionResultProtoUpdater type alias
- Remove action_pkg target from BUILD.bazel
- Remove unnecessary action_pkg dependencies from quest_creation BUILD targets
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Instead of computing SHA tags locally in the deploy step or falling back
to :latest (which has caching issues), pass the exact image tags used
during push as job outputs. This ensures deploy always uses the exact
images that were just built and pushed.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Remove xpForStatBump method from ActionResultProtoApplier, ActionResultApplier,
and ActionResultTApplier traits and their implementations
- Update tests to use GameStateExtensions.xpForStatBump directly instead of
calling through the applier
- Remove duplicate xpForStatBump tests from ActionResultProtoApplierImplTest
- Clean up unused imports from ActionResultProtoApplierImpl
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
docker compose pull has issues with DO registry caching.
Use direct docker pull which may handle errors better,
and fall back to :latest tag if SHA tag fails.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The DigitalOcean registry seems to have caching issues where the local
Docker daemon has a cached manifest that doesn't match the new content.
- Remove specific images before pulling to clear cached manifests
- Add --quiet flag to pull
- Increase retry delay to 10s
- Clear buildkit cache too
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Push was using git rev-parse --short (variable length)
Deploy was using cut -c1-9
Now both use exactly 8 characters consistently.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change push strategy:
1. Push with unique SHA tag first
2. Copy that tag to :latest
This ensures both :latest and :SHA have the same digest and
avoids issues with crane's "existing manifest" deduplication.
Also simplified the image path finding to just use readlink.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The bazel-bin symlink doesn't work correctly for cross-compiled targets.
It always points to the exec platform output, not the target platform.
Using `bazel cquery --output=files` with the platform flags gets the
actual output path for the cross-compiled image.
Also add validation that the path contains 'linux' to catch this issue
early if it happens again.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The :latest tag has a corrupted manifest in the DO registry causing
persistent digest mismatch errors. Using the SHA-tagged image
(which is pushed alongside :latest) should work around this.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The previous fix used set -e which caused immediate exit before
the || fallback could run. Now uses explicit loop with success flag.
Also adds 5s delay between retries to allow registry to settle.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The deploy step was silently failing because:
1. script_stop was not set, so SSH action continued on errors
2. docker compose pull was failing with digest mismatch errors
3. containers weren't recreated because Docker thought image was unchanged
Fixes:
- Add script_stop: true to fail on any command error
- Add set -ex for verbose error handling
- Add retry logic for pull with cache clear
- Use --force-recreate to ensure containers use new images
- Add verification step to show container image tags
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add server-side OAuth authentication infrastructure that supports both
Discord and Google as identity providers, while maintaining backwards
compatibility with existing Basic Auth.
## New Components
- **Auth Proto Definitions**: gRPC service with endpoints for OAuth flow
(GetOAuthUrl, ExchangeCode, SetDisplayName, RefreshToken, GetCurrentUser)
- **User Proto**: Storage schema with admin flag for impersonation support
- **JwtService**: RS256 token creation/validation with 7-day access tokens
- **OAuthService**: OAuth code exchange with Discord and Google APIs
- **UserService**: User CRUD with file-based persistence via Persister
- **AuthServiceImpl**: gRPC service wiring OAuth flow together
## Key Features
- Dual auth: Basic Auth continues working; JWT activates when configured
- Admin impersonation: Set X-Impersonate-User header to debug as another user
- Display name validation: 3-20 chars, alphanumeric + underscore, unique
- Graceful degradation: Server starts without OAuth if keys not configured
## Environment Variables
```bash
# JWT (required for OAuth)
JWT_PRIVATE_KEY='{"kty":"RSA",...}' # RSA key in JWK format
# Discord OAuth
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
# Google OAuth
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
```
## Next Steps for Full Integration
1. Generate RSA key for JWT signing
2. Configure Discord/Google OAuth apps with redirect URI eagle0://auth/callback
3. Implement Unity client OAuth flow with system browser + deep links
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The previous fix (#4825) set lastCommandTypeForActingProvince on the
ActionResult after the ActionResultApplier had already computed the
GameState, so the province's lastCommand was never updated.
This fix adds withTCommandAndLastCommand to RandomStateSequencer, which
wraps the first result with lastCommandTypeForActingProvince BEFORE
passing it to the applier. This mimics how the old Command.resultWithLastCommand
worked and maintains the invariant that only ActionResultApplier creates
GameStates.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add a protoless version of invitationAcceptanceChance to
ResolveDiplomacyCommandSelector that uses FactionUtils.prestige
instead of LegacyFactionUtils.prestige.
Update InvitationCommandSelector to use the protoless version,
since it already converts to native GameState internally.
Also:
- Add ai package to visibility of FactionT and ProvinceT
- Add FactionUtils dependency to resolve_diplomacy_command_selector
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The transition to TCommand-based commands lost the code that sets
lastCommandTypeForActingProvince on action results. The old Command.execute
used to wrap results with this field, but the new protoless flow didn't.
Fix by updating EngineImpl.doCommand to set lastCommandTypeForActingProvince
on the first result from the sequencer, matching the original behavior.
Also:
- Add withLastCommandTypeForActingProvince helper to ActionResultT
- Export selected_command_scala_proto from action_result_trait
- Remove unused dep from hero_backstory_update_action_generator
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Documents the design for replacing HTTP Basic Auth with OAuth 2.0:
- System browser + deep link flow for secure authentication
- JWT tokens (RS256) for session management
- User-chosen display names
- 5-phase implementation covering proto definitions, server auth services,
Unity client OAuth flow, platform configuration, and testing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add protoless `bestChoice(HeroT)` to SwornBrotherChooser, rename proto version to `bestChoiceProto`
- Update SeekMoreLeadersCommandChooser to use `bestChoiceProto`
- Update SwornBrotherChooserTest to use `bestChoiceProto`
- Update InvitationCommandSelector to use Scala `provinceGoldSurplus(ProvinceT)` since it already has nativeGameState
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert RansomOfferHelpers to use Scala GameState
- Change RansomOfferHelpers from proto GameState to Scala GameState
- Update imports from proto to Scala types (FactionT.OutgoingOfferRound, GameState)
- Change FactionUtils.trust to use Scala factions vector instead of proto GameState
- Change FactionUtils.isFactionLeader to use Scala factions vector
- Simplify OutgoingOfferRound pattern match (no unknownFieldSet in Scala version)
- Update BUILD.bazel: remove proto game_state dependency, add Scala model deps
- Add GameStateConverter.fromProto call in CommandChoiceHelpers.maybeRansomLeaderCommand
- Update RansomOfferHelpersTest to use Scala types (FactionC, FactionRelationship)
- Add currentPhase to test GameStates in CommandChoiceHelpersTest for conversion
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update DEPROTO_PLAN.md: mark RansomOfferHelpers as protoless
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Shows connection state in the Connection Panel:
- Connecting... (yellow)
- Connected (green)
- Server unavailable with countdown (red)
- Reconnecting with countdown (orange)
User can click Connect button to retry when connection fails.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The gameState parameter was passed but never used in the function body.
This also removes the GameStateConverter import and dependency from
SeekMoreLeadersCommandChooser since callers were converting proto to Scala
just to pass an unused parameter.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix crane path discovery for cross-compiled Shardok image
Save the cross-compiled image path before building the push target,
since building push target without --platforms would overwrite it.
Also use find to locate crane binary dynamically.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add diagnostic steps to debug cross-compilation
- Build binary separately first with platform flag
- Verify binary is ELF format (Linux) not Mach-O (macOS)
- Verify binary in pkg_tar layer is also ELF
- This will help identify where cross-compilation breaks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add --extra_toolchains flag to force Linux cross-compilation
The --platforms flag wasn't working because the Linux toolchain is
registered with dev_dependency=True in MODULE.bazel. Adding
--extra_toolchains=@llvm_toolchain_linux//:all forces Bazel to
use the cross-compilation toolchain.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Check binary directly from bazel-bin instead of searching bazel-out
The bazel-bin symlink points to the correct output directory, so we
should check that directly instead of searching through bazel-out
which may contain stale build artifacts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add platform flags to push target build to preserve cross-compilation
The push target build was running without --platforms and --extra_toolchains,
which caused Bazel to rebuild the image without cross-compilation. This
overwrote the cross-compiled binary with a macOS binary before pushing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use Darwin crane from Eagle push target for Shardok push
When using --platforms for cross-compilation, Bazel downloads the
target-platform crane (Linux) which can't run on the macOS host.
Instead, use the Eagle push target to get a Darwin crane binary,
since Eagle doesn't need cross-compilation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix crane detection to handle symlinks
Crane in Bazel runfiles is a symlink, not a regular file.
Changed find to not filter by type, and use -e instead of -f
for existence check.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix nginx DNS caching for container IP changes
- Add Docker DNS resolver (127.0.0.11) with 10s TTL to nginx.conf
- Restart nginx after eagle/shardok in deploy workflow
- This ensures nginx picks up new container IPs after redeploy
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The gameState parameter was passed but never used in the function body.
Removing it eliminates the proto GameState dependency from this target.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The OnPlayModeStateChanged callback was defined but never registered to
EditorApplication.playModeStateChanged. This meant when exiting play mode
in the editor, the cancellation token was never cancelled, so background
threads (especially the gRPC streaming thread) kept running, causing
domain reload to hang.
Added OnEnable/OnDisable to properly register/unregister the callback.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix crane path discovery for cross-compiled Shardok image
Save the cross-compiled image path before building the push target,
since building push target without --platforms would overwrite it.
Also use find to locate crane binary dynamically.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add diagnostic steps to debug cross-compilation
- Build binary separately first with platform flag
- Verify binary is ELF format (Linux) not Mach-O (macOS)
- Verify binary in pkg_tar layer is also ELF
- This will help identify where cross-compilation breaks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add --extra_toolchains flag to force Linux cross-compilation
The --platforms flag wasn't working because the Linux toolchain is
registered with dev_dependency=True in MODULE.bazel. Adding
--extra_toolchains=@llvm_toolchain_linux//:all forces Bazel to
use the cross-compilation toolchain.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Check binary directly from bazel-bin instead of searching bazel-out
The bazel-bin symlink points to the correct output directory, so we
should check that directly instead of searching through bazel-out
which may contain stale build artifacts.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add platform flags to push target build to preserve cross-compilation
The push target build was running without --platforms and --extra_toolchains,
which caused Bazel to rebuild the image without cross-compilation. This
overwrote the cross-compiled binary with a macOS binary before pushing.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use Darwin crane from Eagle push target for Shardok push
When using --platforms for cross-compilation, Bazel downloads the
target-platform crane (Linux) which can't run on the macOS host.
Instead, use the Eagle push target to get a Darwin crane binary,
since Eagle doesn't need cross-compilation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix crane detection to handle symlinks
Crane in Bazel runfiles is a symlink, not a regular file.
Changed find to not filter by type, and use -e instead of -f
for existence check.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Create new battalion_type_finder package under library/util
- BattalionTypeFinder: protoless version using Scala BattalionType
- LegacyBattalionTypeFinder: proto version for legacy callers
- Update callers to use the appropriate finder:
- OrganizeCommandSelector uses new BattalionTypeFinder
- CommandChoiceHelpers, RuntimeValidator, CheckForFulfilledQuestsAction
use LegacyBattalionTypeFinder
- Remove unused battalion_type_finder dep from TrainCommand
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- OrganizeCommandSelector now accepts Scala GameState instead of proto
- Added protoless monthlyFoodSurplus method to ProvinceUtils
- BattalionTypeFinder.battalionType now has protoless overload; proto
versions renamed to battalionTypeProto
- Updated callers (CommandChoiceHelpers, MidGameAIClient,
CheckForFulfilledQuestsAction, RuntimeValidator) to use the
appropriate method variant
- Updated OrganizeCommandSelectorTest to use Scala types
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Convert ImproveCommandSelector from proto to Scala types (GameState, ProvinceT, HeroT)
- Add protoless overload to HeroSelector.minimallyFatiguedHeroes, keep proto version as minimallyFatiguedHeroesProto
- Update all callers in CommandChoiceHelpers and MidGameAIClient to wrap with GameStateConverter.fromProto()
- Update ImproveCommandSelectorTest to use GameStateConverter.fromProto()
- Add exports to improve_command_selector for GameState visibility
- Update DEPROTO_PLAN.md to reflect progress
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Convert ExpandCommandSelector from proto to Scala types (GameState, ProvinceT, FactionT)
- Add isAdjacentEnemy method to ProvinceUtils for protoless usage
- Add protoless overloads to ProvinceGoldSurplusCalculator
- Update ExpandCommandSelectorTest to use GameStateConverter.fromProto()
- Update DEPROTO_PLAN.md to reflect progress
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Shardok cross-compilation in Docker workflow
The push step was rebuilding without --platforms flag, overwriting
the cross-compiled Linux binary with a macOS ARM64 binary.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update nginx config to use prod.eagle0.net
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert quest command choosers to use native GameState
- Changed QuestCommandChooser trait to accept native GameState
- Updated FulfillQuestsCommandSelector to convert proto once at top level
- Updated all quest command choosers to use native GameState:
- AllianceQuestCommandChooser
- AlmsAcrossRealmQuestCommandChooser
- AlmsToProvinceQuestCommandChooser
- DismissSpecificVassalCommandChooser
- GiveToHeroesAcrossRealmQuestCommandChooser
- GiveToHeroesInProvinceQuestCommandChooser
- ImproveQuestCommandChooser
- TruceCountQuestCommandChooser
- TruceWithFactionQuestCommandChooser
- Updated helper command selectors to use native types:
- TrustForDiplomacy
- TruceOfferCommandSelector
- AllianceOfferCommandSelector
- ExileVassalCommandSelector
- HeroGiftCommandSelector
- Added trust() method to FactionUtils for native faction access
- Updated all test files to use native types (HeroC, FactionC, ProvinceC)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix callers of TrustForDiplomacy and HeroGiftCommandSelector
Add GameStateConverter.fromProto() calls in:
- InvitationCommandSelector.maybeInviteOtherFaction
- SeekMoreLeadersCommandChooser.maybeSeekMoreLeadersCommand
These callers pass proto GameState but the underlying methods now
expect native GameState.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix tests: add currentPhase to proto GameState in test files
Tests were failing because GameStateConverter.fromProto() can't
convert UNKNOWN_PHASE (the proto default). Added PLAYER_COMMANDS
to test GameState instances in:
- InvitationCommandSelectorTest
- SeekMoreLeadersCommandChooserTest
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add round_phase_scala_proto dependency to test BUILD files
The tests import PLAYER_COMMANDS from round_phase proto but the
BUILD files were missing the dependency.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Trigger CI
* Fix OutgoingOfferRound field order in FactionConverter
The positional pattern matching in both toProto and fromProto for
OutgoingOfferRound had the fields in the wrong order (roundId, toFactionId)
when the correct order is (toFactionId, roundId). This caused values to
be swapped when converting between proto and native types, breaking
the "recently invited" check in TrustForDiplomacy.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Change connection UI from editable URL field to dropdown with options:
- prod. (prod.eagle0.net)
- qa. (qa.eagle0.net)
- (none) (eagle0.net)
Selection is stored in PlayerPrefs. Unity scene update required to wire
up the new environmentDropdown field.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The previous change defaulted to 0.0.0.0:40042 which broke local
development - Shardok would bind to the network IP instead of localhost,
causing Eagle to fail to connect.
- Default to localhost:40042 for local development
- Docker sets SHARDOK_EAGLE_INTERFACE_ADDRESS=0.0.0.0:40042 to listen
on all interfaces for container networking
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Docker health checks use `nc -z` to verify services are listening,
but the minimal base images (eclipse-temurin, ubuntu:24.04) don't
include netcat. This adds a statically-linked busybox binary to both
container images, providing nc and other basic utilities.
- Add http_file rule to download busybox 1.35.0 x86_64
- Create busybox_layer with symlink from nc -> busybox
- Include busybox_layer in both eagle and shardok images
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Create SuitableBattalionsConverter to convert from Scala to proto types
- Update AvailableDefendCommandsFactory to use BattalionSuitability + converter
- Update AvailableMarchCommandFactory to use BattalionSuitability + converter
- Add visibility for proto_converters to access battalion_suitability
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Move BattalionSuitability to battalion_suitability/ package as LegacyBattalionSuitability
- Create new protoless BattalionSuitability with Scala types:
- SuitabilityLevel enum (Optimal, Suboptimal, Restrictive)
- BattalionIdWithSuitability case class
- SuitableBattalions case class
- Convert CombatUnitSelector to use protoless version
- Keep AvailableDefendCommandsFactory and AvailableMarchCommandFactory on Legacy
(they return proto types to the API)
- Add missing exports for Gender and BattalionType in BUILD files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Mount ./auth directory containing htpasswd file for nginx basic
authentication. This enables user authentication at the nginx
reverse proxy level.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Updated callers to convert proto GameState to native Scala types before
calling AlmsCommandSelector:
- CommandChoiceHelpers: uses GameStateConverter.fromProto
- AlmsToProvinceQuestCommandChooser: uses GameStateConverter.fromProto
- AlmsAcrossRealmQuestCommandChooser: uses both GameStateConverter and
ProvinceConverter
Removed proto compatibility overloads and unused imports from
AlmsCommandSelector since all callers now use native types directly.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Check OPENAI_API_KEY and ANTHROPIC_API_KEY environment variables first,
fall back to api_keys.txt file if not set. This allows the Docker
container to receive API keys via environment without needing a file.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix crane binary discovery in CI workflow
- Add set -ex for better error visibility
- Try finding crane in bazel-bin/external first
- Fall back to bazel cquery if not found
- Add debug output for digest and crane path
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix crane path - use runfiles directory
Crane binary is in the runfiles directory after bazel run:
bazel-bin/ci/push_*.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use crane push directly instead of bazel run
The registry converts OCI to Docker format immediately, changing the
digest. We can't reference the image by its original OCI digest after
push.
Solution: Use crane push directly to a tag (not by digest). Bazel is
still used to build the image and get crane in runfiles.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert AttackCommandChooser and dependencies to use Scala types
- AttackCommandChooser: Use Scala GameState, HeroT, ProvinceT instead of proto
- CombatUnitSelector: Use Scala HeroT, BattalionT, BattalionType
- BattalionSuitability: Use Scala HeroT, BattalionT, BattalionType
- MarchSuppliesHelpers: Use Scala BattalionT (fixes pre-existing broken dep)
- CommandChoiceHelpers: Add GameStateConverter.fromProto() at call boundary
- AttackCommandChooserTest: Rewrite to use Scala types (GameState, HeroC, etc.)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix type conversion for callers of CombatUnitSelector and BattalionSuitability
After deproto-ing CombatUnitSelector to use Scala types, all callers need
to convert proto types to Scala at call sites:
- AvailableDefendCommandsFactory: Add converter imports and calls
- AvailableMarchCommandFactory: Add converter imports and calls
- CommandChoiceHelpers: Add converter imports and calls in defendingUnits
BUILD file updates:
- Add converter deps to availability factories
- Add battalion/battalion_type state deps for return types
- Update visibility on battalion_type_converter
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Convert BattalionSuitabilityTest to use Scala types
BattalionSuitability now expects Scala types (HeroT, BattalionT, BattalionType),
so its test needs to:
- Use HeroC and BattalionC instead of proto Hero and Battalion
- Use Scala Profession enum
- Convert BattalionTypesTestData (proto) via BattalionTypeConverter.fromProto
Keep BattalionTypesTestData returning proto types since other tests
(like AttackDecisionCommandChooserTest) pass it to proto GameState.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix MidGameAIClient to convert proto GameState when calling AttackCommandChooser
AttackCommandChooser now expects Scala GameState, so MidGameAIClient needs
to convert via GameStateConverter.fromProto() at the call sites.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Convert AlmsCommandSelector from proto types (GameState, Hero, Province) to Scala model types (GameState, HeroT, ProvinceT)
- Add backward-compatible proto type overloads that convert to Scala types internally
- Add Scala type overloads to FoodConsumptionUtils for foodConsumptionMonthsToHold
- Add fatigue() method to HeroUtils for Scala types
- Convert AlmsCommandSelectorTest to use Scala model types (ProvinceC, HeroC, BattalionC)
- Update BUILD.bazel dependencies, exports, and visibility for proper type resolution
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
DigitalOcean registry converts OCI manifests to Docker format, causing
digest mismatch when Bazel's oci_push tries to tag by digest.
Solution:
- Remove remote_tags from oci_push in BUILD.bazel (push by digest only)
- Use Bazel for the push, then crane copy/tag for tagging (handles format conversion)
This keeps Bazel as the primary build/push tool while working around
the registry's format conversion.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update AlmsCommandSelector to use new FoodConsumptionUtils
- Replace LegacyFoodConsumptionUtils with FoodConsumptionUtils
- Add private foodConsumptionMonthsToHold helper that converts proto
types to Scala types using DateConverter and RoundPhaseConverter
- Update visibility of DateConverter and RoundPhaseConverter to allow
access from command_choice_helpers
- Update tests to set currentPhase = PLAYER_COMMANDS (required by
RoundPhaseConverter)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix quest command chooser tests to set currentPhase
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add parameter names to foodConsumptionMonthsToHold call
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Docker deployment connectivity issues
- Shardok: Listen on 0.0.0.0:40042 instead of localhost for container networking
- Shardok: Add env var fallback for resource paths (SHARDOK_RESOURCES_PATH, SHARDOK_MAPS_PATH)
- Eagle: Use plaintext gRPC for internal container-to-container communication
- docker-compose: Pass CLI args directly instead of env vars, default to gpt-5.1
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Document Docker networking and resource configuration
Added section explaining:
- Why Shardok binds to 0.0.0.0 instead of localhost for container networking
- Why Eagle uses .usePlaintext() for internal gRPC
- How env var fallbacks replace Bazel runfiles in Docker
- Future considerations for multi-host deployment
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Rename FoodConsumptionUtils to LegacyFoodConsumptionUtils, create new Scala-types version
- Renamed proto-based FoodConsumptionUtils to LegacyFoodConsumptionUtils
- Created new FoodConsumptionUtils that uses native Scala model types (Date, ProvinceT, GameState, RoundPhase) instead of proto types
- Updated all callers (AlmsCommandSelector, CommandChoiceHelpers, ExpandCommandSelector, MidGameAIClient) to use LegacyFoodConsumptionUtils
- Renamed test to LegacyFoodConsumptionUtilsTest
- Updated BUILD.bazel files with new targets
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Move FoodConsumptionUtils to dedicated food_consumption package
- Move FoodConsumptionUtils.scala and LegacyFoodConsumptionUtils.scala
from command_choice_helpers to new food_consumption directory
- Add FoodConsumptionUtilsTest using pure Scala types instead of protos
- Update all dependent BUILD.bazel files with new import paths
- Update importing Scala files to use new package location
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Rename food_consumption_utils target to food_consumption
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix missing dependency and visibility for food_consumption package
- Add legacy_food_consumption_utils dependency to command_choice_helpers
- Fix visibility to use __pkg__ instead of __subpackages__
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Next steps for productionization
* Run deploy job on self-hosted runner for secure SSH
* Temporarily disable production environment to debug runner
* Use ubuntu-latest for deploy job
* Add remote_tags to oci_push for latest tag
The push script runs on the host (macOS) and needs native tools like jq.
Using --platforms=//:linux_x86_64 caused it to try running Linux binaries.
The image is already built for Linux; the push just uploads it.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds NewFactionHeadDetailsNotificationGenerator to display a notification
when a faction's leader changes. Shows both the new and previous leader
headshots, with appropriate text for player vs other factions.
Features:
- 10 randomized notification titles (New Leadership, Succession, etc.)
- Shows province where new leader is located (if player's faction)
- Includes previous leader name in text when available
- References LLM-generated text for full narrative
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The NewFactionHeadMessage LLM request was added in #4785 but without a
corresponding notification. This adds the notification type so clients
can display the event.
Changes:
- Add NewFactionHeadDetails proto message with new_head_hero_id,
faction_id, and previous_head_hero_id fields
- Add NewFactionHead case class to NotificationDetails
- Add toProto/fromProto converters in NotificationConverter
- Create notification alongside LLM request in CheckForFactionChangesAction
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix sysroot .so plugins and registry auth
- Exclude GCC plugin .so files from sysroot (Bazel can't handle them)
- Only copy GCC headers and static libs needed for cross-compilation
- Add tarball structure verification to build script
- Fix DO registry auth by setting DOCKER_CONFIG env var for Bazel
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add triple symlinks to sysroot for clang compatibility
Clang uses --target=x86_64-unknown-linux-gnu but Ubuntu's GCC uses
x86_64-linux-gnu (without "unknown"). This caused clang to fail to detect
the GCC installation and couldn't find C++ standard library headers.
Add symlinks in the sysroot:
- /usr/lib/gcc/x86_64-unknown-linux-gnu -> x86_64-linux-gnu
- /lib/x86_64-unknown-linux-gnu -> x86_64-linux-gnu
- /include/x86_64-unknown-linux-gnu -> x86_64-linux-gnu
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix sysroot tarball structure for toolchains_llvm
The tarball was wrapping contents in sysroot/ which caused double nesting
when extracted by the sysroot() repo rule. Changed to tar from inside the
sysroot directory so usr/, lib/, etc. are at the root of the archive.
Before: sysroot/usr/...
After: ./usr/... (or usr/...)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add libgcc_s.so symlink to sysroot
The linker looks for libgcc_s.so but Ubuntu only provides libgcc_s.so.1.
Added a symlink to make -lgcc_s work.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix sysroot for cross-compilation
1. Add ld-linux-x86-64.so.2 symlink in lib/ since libc.so linker script
references /lib64/ld-linux-x86-64.so.2 as an absolute path
2. Change linkopt to host_linkopt in .bazelrc to avoid passing macOS-specific
linker flags (-no_warn_duplicate_libraries) to Linux cross-compilation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update sysroot to v3.4 with all required symlinks
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add missing includes for cross-compilation portability
These files relied on platform-specific transitive includes that work on
macOS but not on Linux. Added explicit includes for:
- <cstdint> for uint8_t, uint64_t
- <stdexcept> for std::out_of_range
- <cinttypes> for PRIu64 portable format specifier
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change return type from Vector[EventForChronicle] (proto) to Vector[ChronicleEvent] (Scala)
- Remove intermediate EventForChronicleDetails layer - create Scala types directly
- Update all 23 event type mappings to use Scala *ChronicleEvent types
- Add DateConverter.fromProto() to convert proto dates to Scala dates
- Update NewRoundAction to use Scala types directly (remove ChronicleEventConverter)
- Update DEPROTO_PLAN.md: now 47/52 (90%) action files are fully protoless
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add LLM notification when new faction head is declared
When a hero becomes the new head of a faction, generate an LLM message
where they declare their new leadership to the world. If the faction
is also being renamed (because the new head has a ledFactionName),
the declaration includes explanation of the new faction name.
Changes:
- Add NewFactionHeadMessage proto and Scala case to LlmRequestT
- Add NewFactionHeadPromptGenerator for generating the LLM prompt
- Update CheckForFactionChangesAction to create LLM requests
- Update LlmRequestConverter to handle the new message type
- Update LlmResolver to use the new prompt generator
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Include previous faction head's execution in LLM notification
The notification now mentions that the previous faction head was executed
and includes their name, providing context for the new leader's declaration.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Include previous faction head's description in LLM prompt
Add the executed leader's full description (including backstory) to the
prompt, allowing the LLM to potentially reference their history when
generating the new leader's declaration message.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a faction head changes (e.g., due to leader death), if the new head
hero has a ledFactionName set, the faction is automatically renamed to
that name. This enables "great person" heroes to rename factions they lead.
Changes:
- Add new_name field to ChangedFaction proto and ChangedFactionC
- Add new_name field to FactionViewDiff for client updates
- Update ChangedFactionConverter for new field
- Update GameStateFactionExtensions applier to apply name changes
- Update GameStateViewDiffer to diff faction names
- Update CheckForFactionChangesAction to set newName from hero's ledFactionName
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The v2 sysroot includes GCC installation directories that clang needs
to find libstdc++ headers for cross-compilation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Skip AWS CLI install if already present
- Use eagle0-windows bucket (same as other workflows, credentials have access)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add a new field to the Hero proto and Scala models to store the name of the
faction that a hero would lead, if they became a faction leader. This is
populated from the "faction_name" column in the heroes TSV for "great person"
heroes, and is empty (or None in Scala) for other heroes.
This will be used in the future to rename factions when a great person
becomes the leader of a different faction.
Changes:
- Add led_faction_name (string) to Hero proto
- Add ledFactionName: Option[String] to HeroT trait and HeroC case class
- Add ledFactionName to LoadedHero intermediate type
- Update HeroConverter, LoadedHeroConversion, and FixedHeroes to handle the new field
- Update FixedHeroesTest to include the new field
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Use existing ACCESS_KEY_ID and SECRET_KEY secrets instead of
non-existent DO_SPACES_KEY and DO_SPACES_SECRET.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-23 08:12:19 -08:00
1039 changed files with 82554 additions and 86771 deletions
Eliminated wasteful Scala→proto→Scala conversions in the hot path:
1.**LLM Pipeline** (#4913): `LlmRequestWithGameState` now uses Scala `GameState` instead of proto. All ~38 prompt generators updated to use Scala model types (`FactionT`, `HeroT`, `ProvinceT`).
2.**ActionWithResultingState Caching** (#4914): Added `precomputedScalaState: Option[GameState]` to cache Scala state when available, avoiding `fromProto()` conversion in `stateAfter()`.
3.**PostResults Simplification** (#4915): Changed `PostResults.gameState` from proto to `Option[GameState]` (Scala), eliminating `toProto()` calls when creating PostResults.
## Migration Pattern
The codebase follows a **Legacy* pattern** for separating proto-dependent and protoless code:
- **Protoless utilities**: `FactionUtils`, `HeroUtils`, `ProvinceUtils`, `ProvinceDistances`, etc.
- **Proto-dependent utilities**: `LegacyFactionUtils`, `LegacyHeroUtils`, `LegacyProvinceUtils`, `LegacyProvinceDistances`, etc.
When migrating a file:
1. Create a `Legacy*` version containing the proto-dependent methods
2. Keep the original file name for protoless methods
3. Update callers to use the appropriate version based on their context
## Migration Status
### Fully Protoless (no proto imports)
**Utilities:**
- [x]`FactionUtils` - has protoless `ownedNeighbors` method
- [x]`ProvinceDistances` - split into protoless + `LegacyProvinceDistances`
- [x]`FactionUtils` / `LegacyFactionUtils` - both have matching APIs; LegacyFactionUtils used by boundary code (24+ callers)
- [x]`HeroUtils` / `LegacyHeroUtils` - both have matching APIs; LegacyHeroUtils used by boundary code (10 callers)
- [x]`ProvinceUtils` / `LegacyProvinceUtils` - both have matching APIs; LegacyProvinceUtils used by boundary code (20 callers: availability factories, view filters)
**Parallel Implementations (awaiting migration of callers):**
- [x]`BattalionUtils` / `LegacyBattalionUtils` - both have matching core methods; LegacyBattalionUtils used by boundary code (4 callers)
- [x]`BattalionViewFilter` / `LegacyBattalionViewFilter` - protoless version exists; Legacy used by view filters, action appliers (3 callers)
- [x]`BattalionTypeFinder` / `LegacyBattalionTypeFinder` - protoless version exists; Legacy used by validators (1 caller: RuntimeValidator)
### Recent Caller Migration
**CheckForFulfilledQuestsAction** - migrated to use protoless `BattalionTypeFinder`:
- Changed `battalionTypes` parameter from proto `Vector[BattalionType]` to Scala `Vector[BattalionType]`
- Updated callers (EngineImpl, EndVassalCommandsPhaseAction) to pass Scala types directly
This document outlines enhancements to the Go admin server (`src/main/go/net/eagle0/admin_server/`) to provide a proper web UI for game administration.
### Current State
The admin server provides a full web UI with htmx interactivity:
-`GET /` - Redirect to games list
-`GET /games` - Game list page (HTML)
-`GET /games/{id}` - Game detail with action history
-`GET /games/{id}/history` - History rows (htmx partial, infinite scroll)
4.**Terrain Hexes** - 85 hex tiles of unknown source
- **TODO:** Confirm if this is a Unity Asset Store purchase (owner believes it is)
5.**StrategyGameIcons** - 138 icons of unknown source
- **TODO:** Investigate origin - check Unity Asset Store purchase history
6.**AUDIUS music tracks** - Verify Dima Koltsov tracks are licensed for commercial use
7.**Discord logo** (`Eagle/Discord-Logo-Blurple.png`) - Likely fine for "Login with Discord" button per Discord brand guidelines, but verify usage complies with their terms
### Already Safe:
- All Asset Store purchases (license tied to your account)
- CC-licensed music (attribution in Music Credits.txt)
- CC0 SimpleFileBrowser icons
- Google Fonts / Liberation fonts
- NuGet packages
---
## Recommendation
Before removing HTTP basic auth:
1.~~Delete or replace the 4 suspicious JPG/JPEG files in `Assets/Eagle/` and `Assets/Images/`~~**DONE** (2025-01-04)
2. Replace clip art images (`bridge.png`, `startFire.png`) with properly licensed alternatives (e.g., from [game-icons.net](https://game-icons.net) CC BY 3.0)
3. Verify source of `Assets/Shardok/soundEffects/` MP3s
4. Verify source of `Assets/Terrain Hexes/` and `Assets/StrategyGameIcons/`
5. If any are from early development with unclear licensing, replace them
The bulk of your assets (95%+) are properly licensed Asset Store purchases or CC content.
Move OAuth authentication handling from the Eagle Scala server into a separate Go service. This simplifies Eagle (gRPC-only, no HTTP), reduces complexity, and sets up for potentially moving JWT validation outside Eagle too.
## Architecture Decision: Sidecar Service (Not DO Functions)
**Recommendation: Go sidecar service on the same droplet, in a separate container**
**Why not DO Functions:**
- OAuth requires **stateful sessions** (pendingOAuth/completedOAuth maps with 10-min TTL)
- Client polling pattern (every 2 seconds) would incur high function invocation costs
- Cold start latency problematic for auth flows
- State would require external store (Redis), adding complexity
**Why sidecar (separate container):**
- Simple process on same droplet, minimal network latency
- In-memory state management (like current Scala impl)
- Easy to monitor/debug alongside Eagle
- Can share filesystem for key files (RSA keys) via volume mounts
Replace HTTP Basic Auth with OAuth 2.0 (Discord + Google) for Eagle0. Users authenticate via system browser, receive JWT tokens, and choose their own display names.
The OAuth implementation is functional but has several gaps that need addressing before it's production-ready. This document outlines the known issues, proposes a comprehensive user identity model, and provides a prioritized implementation plan.
## Current State (Updated January 2026)
### What Works ✅
- Discord OAuth flow (server-mediated polling)
- Google OAuth flow
- JWT token generation and validation
- User creation and display name setting
- Auto-login with stored tokens
- Basic game creation and play with OAuth users
- Headshot fetching via public CDN (no auth required)
- Logout button in lobby (preserves tokens for quick reconnect)
- Environment (prod/qa) and user display in lobby
- Game identity with userName = displayName (PR #4964 merged)
### Known Issues
#### 1. Game Identity Model Fragility (Deferred)
**Status**: Accepted for now. PR #4964 merged with `userName = displayName`.
**Current behavior**:
- Games store `userNameToFactionId: Map[String, Int]`
- For JWT users, this maps displayName → factionId
- displayName is technically mutable (users could change it)
- No migration path when displayName changes
**Why this is acceptable**:
1. We don't currently have a "change display name" feature
2. The alternative (using userId) requires more extensive changes
3. Can migrate to userId-based identity later if needed
#### 2. In-Game Headshot Fetching ✅ FIXED
**Solution**: Made the `eagle0-headshots` S3 bucket public and enabled CDN.
- Client now fetches directly from `https://eagle0-headshots.sfo3.cdn.digitaloceanspaces.com/`
- No authentication required
- Works for both OAuth and Basic Auth users
- Simpler architecture, no dependency on home Mac server
#### 3. Logout from Lobby ✅ FIXED
**Solution**: Added logout button to lobby UI (PR #4967).
- Button disconnects from server and returns to connection screen
- Intentionally does NOT clear OAuth tokens
- Allows quick reconnect with same account without full OAuth flow
#### 4. Display Name Uniqueness Not Enforced (Medium) - OPEN
**Problem**: User was able to set displayName "nolen" when that name was already taken.
- **Pro**: Human-readable in logs, game saves, debugging
- **Con**: Breaks if displayName changes
- **Migration**: None needed now, complex later
#### Option B: userName = userId (Recommended)
- **Pro**: Stable identity, displayName changes are safe
- **Con**: UUIDs in logs are ugly, need display name lookup for UI
- **Migration**: Cleaner long-term, but breaking change for any existing OAuth games
#### Option C: Hybrid with Migration Support
- **userName** = userId for new games
- **Legacy lookup** for old games by displayName
- **Display layer** resolves userId → displayName for UI
**Recommendation**: Option B with a display name resolution layer. The ugliness in logs is acceptable for the stability it provides. Implement a `UserService.resolveDisplayName(identifier: String): String` that returns displayName for UUIDs or the identifier itself for legacy usernames.
### Account Linking Strategy
#### Automatic Linking (Future)
When a user logs in with a new OAuth provider:
1. Check if the provider email matches an existing user's email
2. If match found, prompt: "An account exists with this email. Link accounts?"
3. If confirmed, add new OAuthIdentity to existing user
4. If declined, create separate account (different email required)
#### Manual Linking (MVP)
1. User logs in with primary account
2. User goes to Settings → Linked Accounts
3. User clicks "Link Discord" or "Link Google"
4. OAuth flow adds new identity to current user
### Avatar/Headshot Strategy
#### Phase 1: OAuth Avatars (MVP)
- Store `avatarUrl` from OAuth provider during login
- Server proxies avatar requests to avoid CORS issues
- Cache avatars locally with TTL
#### Phase 2: Avatar Caching
- Download avatar to local storage on login
- Serve from local storage for reliability
- Refresh periodically or on login
#### Phase 3: Custom Avatars (Future)
- Allow users to upload custom avatar
- Store in S3/DO Spaces
- Custom avatar overrides OAuth avatar
---
## Implementation Plan
### Phase 1: Stabilization ✅ COMPLETE
#### 1.1 Fix Display Name Uniqueness Bug - OPEN
- [ ] Investigate why "nolen" was allowed when it existed
- [ ] Add logging to `setDisplayName` to trace the issue
- [ ] Ensure `displayNameIndex` is correctly maintained
- [ ] Add unit tests for uniqueness enforcement
#### 1.2 Add Logout Button to Lobby ✅ DONE
- [x] Add "Logout" button to lobby UI
- [x] Disconnect from server
- [x] Navigate to connection screen
- [x] Preserve OAuth tokens for quick reconnect (intentional change from original plan)
- [ ] Connect `lobbyEnvironmentText` to TextMeshProUGUI in scene
- [ ] Connect `lobbyUserText` to TextMeshProUGUI in scene
#### 2.4 Implement Token Refresh During Gameplay
- [ ] Implement `RefreshToken` RPC on server (currently throws UNIMPLEMENTED)
- [ ] Store refresh tokens server-side for validation
- [ ] Add proactive refresh in client before token expires
- [ ] Handle refresh during reconnection attempts
### Phase 3: Nice-to-Haves (Future)
#### 3.1 Proactive Token Refresh
- [ ] Monitor token expiry in client
- [ ] Refresh automatically when < 5 minutes remaining
- [ ] Update TokenStorage with new access token
#### 3.2 Better Error Messages
- [ ] Distinguish between network errors and auth errors
- [ ] Show user-friendly messages for OAuth failures
- [ ] Add retry suggestions
#### 3.3 Session Persistence Across Server Restarts
- [ ] Move pendingOAuth from in-memory TrieMap to Redis/database
- [ ] Move completedOAuth to Redis with TTL
- [ ] Server can restart without breaking in-flight OAuth flows
#### 3.4 Migrate to userId-based Game Identity (Deferred)
- [ ] Change `AuthorizationUtils.userName` to return `userId` for JWT users
- [ ] Add `UserService.resolveDisplayName(id: String): String` for UI display
- [ ] Update game UI to resolve userIds to displayNames
- [ ] Existing Basic Auth games continue to work (userName is literal)
#### 3.5 Display Name Change Support (Requires 3.4)
- [ ] Add `ChangeDisplayName` RPC
- [ ] Validate new name is unique
- [ ] Update user record
- [ ] No game migration needed (games use userId)
### Phase 3: Multi-Provider Support (Future)
#### 3.1 Account Linking UI
- [ ] Add Settings page with "Linked Accounts" section
- [ ] Show currently linked providers
- [ ] "Link Another Account" button triggers OAuth flow
- [ ]`LinkOAuthProvider` RPC adds identity to current user
#### 3.2 Login Provider Selection
- [ ] If user has multiple providers, any can be used to login
- [ ] All resolve to same userId
- [ ] Session shows which provider was used
#### 3.3 Account Merging (Complex)
- [ ] Handle case where user created separate accounts
- [ ] Merge game history, stats, etc.
- [ ] Delete duplicate user record
- [ ] This is complex - may defer or not implement
### Phase 4: Enhanced Avatars (Future)
#### 4.1 Avatar Caching
- [ ] Download avatars to S3/DO Spaces on login
- [ ] Serve from our CDN
- [ ] Refresh on login if changed
#### 4.2 Custom Avatar Upload
- [ ] Upload endpoint with size/format validation
- [ ] Store in S3/DO Spaces
- [ ] Custom avatar overrides OAuth avatar
---
## Technical Debt to Address
1.**Context Propagation in Futures**: PR #4960 fixed `setDisplayName` and `getCurrentUser`, but audit all `Future` blocks that access `AuthorizationUtils`
2.**Dual Auth Support**: The system supports both Basic Auth and JWT. Consider:
- Should Basic Auth be deprecated for production?
- Should it remain for local development only?
- How do Basic Auth users interact with OAuth users in the same game?
3.**Token Refresh**: `RefreshToken` RPC throws UNIMPLEMENTED. Need to:
- Implement refresh token storage and validation
- Handle token refresh in client
- Consider refresh token rotation for security
4.**Session Management**: No server-side session tracking. Consider:
- Track active sessions per user
- Allow "logout all devices"
- Detect concurrent logins
---
## Open Questions
1.**What happens when a Basic Auth user and OAuth user have the same name?**
- Currently possible - Basic Auth doesn't check UserService
- Could cause confusion in games
- Solution: Require OAuth for multiplayer? Or namespace Basic Auth names?
2.**Should displayName changes be allowed?**
- With userId-based identity, it's safe
- But could cause confusion ("who is this new player?")
- Consider: rate limit changes, show "formerly known as" temporarily
3.**How to handle OAuth provider account deletion?**
- User deletes their Discord account
- Their Eagle0 account still exists
- They can't login unless they linked another provider
- Solution: Encourage linking multiple providers, or add email/password fallback
4.**Admin impersonation with OAuth**
- Currently works via X-Impersonate-User header
- Should this use userId or displayName?
- Probably userId for stability
---
## Appendix: File Locations
### Server (Scala)
-`src/main/scala/net/eagle0/eagle/auth/UserService.scala` - User CRUD
In Docker, each container has its own network namespace. When a server binds to `localhost` (127.0.0.1), it only accepts connections from within the same container. Other containers on the Docker bridge network cannot connect, even if they can resolve the hostname.
**Shardok must bind to `0.0.0.0:40042`** to accept connections from Eagle running in a separate container. This is configured in `ServerConfiguration.cpp`:
```cpp
// Default to 0.0.0.0 for container networking (accepts connections from any interface)
This still works on the local Mac because `0.0.0.0` means "all interfaces", which includes the loopback interface that localhost uses.
### gRPC Plaintext for Internal Communication
By default, gRPC's `ManagedChannelBuilder` attempts TLS connections. Since Shardok runs in plaintext mode, Eagle must use `.usePlaintext()` for internal container-to-container communication:
.usePlaintext()// Required for internal Docker communication
```
TLS termination happens at nginx for external clients. Internal traffic between Eagle and Shardok stays within the Docker network and doesn't need encryption.
**Future consideration:** If Shardok moves to a separate droplet, traffic would traverse the network. Options:
1. Enable TLS between Eagle and Shardok (configure Shardok with certificates)
2. Use DigitalOcean VPC (private network, still unencrypted but isolated)
3. Use WireGuard/VPN tunnel between hosts
### Bazel Runfiles in Docker
Bazel's runfiles system locates resources relative to the executable using a manifest or directory structure that Bazel sets up at runtime. This doesn't exist in Docker containers.
**Solution:** Environment variable fallbacks in `FilesystemUtils.cpp`:
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.