- Update DisplayNames.cs with renamed and new quest types
- Update UnaffiliatedHeroRowController.cs to handle both quest types
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Rename existing ReconSpecificProvincesQuest (count-based) to ReconProvincesQuest
- Add new ReconSpecificProvincesQuest that tracks specific province IDs to recon
- Update proto definitions, Quest.scala, QuestConverter.scala
- Update LLM prompt generators for both quest types
- Update CheckForFulfilledQuestsAction comment
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add sendSuppliesQuests to QuestCreationUtils
- Creates quests to send 1000-3000 food to other faction provinces
- Only available if faction has at least 2 provinces
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add startEpidemicQuests function to QuestCreationUtils to generate
StartEpidemicQuest for provinces that don't already have an epidemic.
Update DivineCommandTest expectations for new random sequence.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add display names and quest descriptions for:
- StartEpidemicQuest: "Start Epidemic" - Start an epidemic in {province}
- SpendOnFeastsQuest: "Host Feasts" - Host feasts costing X gold
- SendSuppliesQuest: "Send Supplies" - Send X food to {province}
- RestProvinceQuest: "Rest Province" - Keep {province} at rest for X months
- ReconSpecificProvincesQuest: "Recon Provinces" - Recon X provinces
- RepairDevastationQuest: "Repair Devastation" - Repair X devastation
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add bug report system enhancements
- Add "Report Bug" button on exception popup with pre-populated info
- Add prompt to include recent exceptions when opening bug report manually
- Add screenshot capture button in bug report panel with Discord upload
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify exception handling: auto-include instead of prompting
Remove exception prompt UI and automatically include recent exceptions
in the bug report description when opening from settings menu.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up bug report UI components in Gameplay scene
- ErrorHandler: reportBugButton, bugReportPanel reference
- BugReportPanelController: screenshot button, status text, preview
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix screenshot preview layout: hide container not just RawImage
Add screenshotPreviewContainer reference to hide/show the entire
container with LayoutElement, so layout group collapses the space
when no screenshot is present.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire screenshotPreviewContainer in Gameplay scene
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add new quest type definitions
Add proto messages, Scala case classes, and converters for:
- StartEpidemicQuest: Start an epidemic in a target province
- SpendOnFeastsQuest: Spend gold on feasts (component-based)
- SendSuppliesQuest: Send food supplies to a province (component-based)
- RestProvinceQuest: Keep a province in rest orders (component-based)
- ReconSpecificProvincesQuest: Recon a target number of provinces
- RepairDevastationQuest: Repair devastation across provinces
Also adds failure conditions for StartEpidemicQuest, SendSuppliesQuest,
and RestProvinceQuest. Other quests have no specific failure conditions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add StartEpidemicQuest to CheckForFulfilledQuestsAction
Fix pattern match exhaustivity warning by adding case for
StartEpidemicQuest (action quest - fulfilled in ControlWeatherCommand).
Also add clarifying comment for other new ComponentQuest types.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add LLM prompt cases for new quest types
Add pattern match cases for StartEpidemicQuest, SpendOnFeastsQuest,
SendSuppliesQuest, RestProvinceQuest, ReconSpecificProvincesQuest,
and RepairDevastationQuest to both DivineMessagePromptGenerator
and QuestEndedGeneratorUtilities.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Bazel 8 changed module extension repository naming from `~~` and `~` to
`++` and `+`. Update workflows to find crane dynamically instead of
using hardcoded paths that break with Bazel version changes.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Upgrade rules_java to 9.3.0 (enables JDK 25 toolchain support)
This adds an explicit dependency on rules_java 9.3.0, which includes
support for JDK 25 remote toolchains. The build continues to use
Bazel's embedded JDK 24 by default, but JDK 25 toolchains are now
available for future use.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Mark MCTS basic test as flaky
The shardok_mcts_ai_basic_test uses Monte Carlo Tree Search which
has inherent non-determinism. Tests like PrefersArcheryOverEndTurn
and DoesNotPreferStartFireWhenNotBeneficial assert on AI decisions
that may vary between runs depending on random exploration paths.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add client UI support for ApprehendOutlawQuest (PR 2/3)
- DisplayNames.cs: Added "Apprehend Outlaw" display name
- UnaffiliatedHeroRowController.cs: Added quest description with
dynamic hero name lookup for the target outlaw
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Enable ApprehendOutlawQuest generation (PR 3/3)
Add quest generation for ApprehendOutlawQuest. The quest is generated
when there are outlaws in the faction's territory:
- Finds all outlaws across all faction-controlled provinces
- Creates one quest candidate per outlaw
- Quest requires apprehending that specific outlaw
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- DisplayNames.cs: Added "Apprehend Outlaw" display name
- UnaffiliatedHeroRowController.cs: Added quest description with
dynamic hero name lookup for the target outlaw
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Move SparklePlugin to separate Bazel workspace to resolve rules_swift conflict
The problem: grpc 1.76.0.bcr.1 requires rules_swift 3.x, but rules_apple 4.x
requires rules_swift 2.x. These have different compatibility levels, causing
bzlmod resolution to fail when both are in the same workspace.
The solution: Move SparklePlugin (the only thing using rules_apple) to a
separate Bazel workspace at `sparkle_workspace/`. This workspace has its own
MODULE.bazel with only rules_apple and Sparkle dependencies, completely
isolated from the grpc/flatbuffers dependency tree.
Changes:
- Create sparkle_workspace/ with isolated MODULE.bazel
- Move SparklePlugin source files to sparkle_workspace/
- Update build_sparkle_plugin.sh to build from subworkspace
- Remove rules_apple from main MODULE.bazel
- Add single_version_override for rules_swift 3.1.2 (for grpc/flatbuffers)
All 315 tests pass.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add WORKSPACE.bazel to sparkle_workspace to prevent parent workspace detection
Without this file, Bazel may walk up the directory tree and find the parent
workspace's MODULE.bazel, causing dependency conflicts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This target was a development utility that's no longer used.
Removing it helps reduce rules_apple dependencies.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add ApprehendOutlawQuest type (PR 1/3)
Add a new quest type where unaffiliated heroes require the faction to
apprehend a specific outlaw currently in their territory. The quest:
- Proto: Added ApprehendOutlawQuest message with outlaw_hero_id field
- Scala: Added case class with quest fulfillment logic
- Failure: Quest fails if the outlaw is no longer in faction territory
- Fulfillment: Completed when ApprehendOutlawCommand targets the outlaw
- Updated ApprehendOutlawCommand to check for quest fulfillment and
return multiple results (main result + quest completion if matched)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix ApprehendOutlawQuest failure condition
Only fail if the hero is no longer an outlaw anywhere. Moving to a
different province is fine - quest remains valid. Quest fails only
when someone else apprehends them.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Enable WinBattlesQuest generation (PR 3/3)
Adds WinBattlesQuest to the available quest generators. The quest
requires winning 2-4 battles (randomly determined).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix DivineCommandTest for new quest type random sequence
Adding WinBattlesQuest changed the random sequence during quest creation,
causing hero 12 to get a quest with a province target. Updated test
expectation to match the new (correct) notification output.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add WinBattlesQuest type (PR 1/3)
Adds a new quest type where a hero asks the player to win X battles
(where X is 2-4). This is a ComponentQuest that tracks progress.
- Proto: Add WinBattlesQuest message and field
- Quest.scala: Add WinBattlesQuest case class extending ComponentQuest
- QuestConverter: Handle WinBattlesQuest toProto/fromProto
- ResolveBattleAction: Increment quest counter when winning battles
- CheckForFulfilledQuestsAction: Quest is fulfilled via ComponentQuest
handling when componentsFulfilled >= componentCount
This quest never fails.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add WinBattlesQuest cases to prompt generators
The exhaustive pattern matching on Quest types requires adding cases
to all prompt generators when a new quest type is added.
Also updates CLAUDE.md to require running full test suite before pushing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- BorderSecurityQuest: Creates one quest per border province (provinces
with neighbors controlled by other factions)
- WinBattleOutnumberedQuest: Simple quest with no parameters
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds quest generation for BetrayAllyQuest, which is available when
the faction has at least one ally.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Instead of a generic "betray any ally" quest, BetrayAllyQuest now
targets a specific faction with `targetFactionId`. This makes the
quest more meaningful and allows players to plan strategically.
Changes:
- Proto: Add target_faction_id field to BetrayAllyQuest message
- Quest.scala: Change from case object to case class with targetFactionId
- QuestConverter: Handle new targetFactionId field
- CheckForFailedQuestsAction: Update failure logic to check if specific
target faction no longer exists or is no longer an ally
- BreakAllianceResolutionHelpers: Check if broken alliance matches
quest's target faction for fulfillment
- DivineMessagePromptGenerator & QuestEndedGeneratorUtilities: Update
quest descriptions to mention specific ally name
- UnaffiliatedHeroRowController (C#): Display specific ally name in UI
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix reconnecting status stuck after sleep/wake
Race condition: when device wakes from sleep, CheckForIdleTimeout and
HandleStreamingCall both try to initiate reconnection, leading to
conflicting state updates and duplicate retry timers.
Fix: Cancel thread token in CheckForIdleTimeout before disposing the
streaming call, and check the token in HandleStreamingCall exception
handlers to skip ScheduleReconnect if another path is handling it.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix threadToken scope to be accessible in catch blocks
Move threadToken declaration outside try block so exception handlers
can check if the token was cancelled.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Updates .bazelrc to use Java 21 for compilation.
Requires PR #5865 (URL deprecation fix) to be merged first, otherwise
the build will have deprecation warnings from the old URL constructor.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds quest generation for SwearBrotherhoodWithHeroQuest, which targets
vassal heroes in the province who are not already faction leaders.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds C# client display support for:
- SwearBrotherhoodWithHeroQuest: "Swear brotherhood with {heroName}"
- BetrayAllyQuest: "Break an alliance with an ally"
- BorderSecurityQuest: "Control all provinces bordering {provinceName}"
- WinBattleOutnumberedQuest: "Win a battle while outnumbered"
Changes:
- DisplayNames.cs: Add quest type strings
- UnaffiliatedHeroRowController.cs: Add quest descriptions in SetQuestText and ShortQuestString
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds server-side support for:
- SwearBrotherhoodWithHeroQuest: fulfilled when player swears brotherhood with the target hero
- BetrayAllyQuest: fulfilled when player breaks an alliance with an ally
- BorderSecurityQuest: fulfilled when all neighboring provinces of the target province are controlled
- WinBattleOutnumberedQuest: fulfilled when winning a battle while having fewer troops
Each quest has proto definitions, Scala case classes, converter logic, fulfillment/failure
checks, and LLM prompt generators. PR 2 will add client UI and PR 3 will add quest generation.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds two utility functions for checking hero faction membership:
- heroIsInFaction(heroId, factionId, heroes): Efficiently checks if a
specific hero belongs to a faction by checking hero.factionId
- heroIdsInFaction(factionId, heroes): Returns Set of all hero IDs
belonging to a faction
These functions check the hero's factionId field, which is the
authoritative source for faction membership. Heroes retain their
factionId when in a province, in a moving army, or imprisoned. They
lose it when exiled, become outlaws, or depart.
This is more reliable than checking province.rulingFactionHeroIds
which misses heroes in moving armies.
Includes comprehensive tests for various scenarios:
- Heroes in provinces
- Heroes in moving armies (retain factionId)
- Imprisoned heroes (retain factionId)
- Exiled heroes (factionId cleared)
- Outlaws (factionId cleared)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Upgrade Eagle server and JFR sidecar from Java 21 to Java 25.
Java 25 is the latest LTS release (September 2025). Scala 3.7.2
fully supports JDK 25.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Initial backstory: 50 → 100 words
- Growth per update: 20 → 50 words
- Soft cap: 150 → 300 words
This allows battalion histories to build up more narrative over time
while still keeping them from growing unbounded.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Upgrade Eagle server and JFR sidecar from Java 17 to Java 21.
Benefits:
- Better garbage collection (Generational ZGC available)
- General JVM performance improvements
- Foundation for future virtual threads adoption
- Supported until 2031
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Use JsonConvert.ToString() from Newtonsoft.Json for proper JSON string
escaping instead of manual character replacement. This handles all
control characters and edge cases correctly.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Eagle and admin server use different S3 buckets:
- Eagle: eagle0 bucket (game data) - uses DO_SPACES_* secrets
- Admin: eagle0-assets bucket (What's New) - uses ACCESS_KEY_ID/SECRET_KEY secrets
The previous change (#5847) broke Eagle by giving it credentials that only
have access to eagle0-assets. This fix:
- Reverts Eagle to use DO_SPACES_* secrets (eagle0 bucket access)
- Adds ADMIN_S3_* env vars for admin server (eagle0-assets bucket access)
- Admin container receives ADMIN_S3_* values as DO_SPACES_* env vars
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Eagle server only needs to run Java, not compile it. Using the JRE
base image instead of JDK reduces the image size from ~273MB to ~120MB.
The JFR sidecar still uses JDK because it needs jcmd for JFR dumps.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
TrySetCanceled() can trigger synchronous continuations that re-enter
the lock (C# locks are reentrant) and call Remove() on the dictionary
while the foreach is still iterating, causing InvalidOperationException.
Fix by snapshotting values and clearing the dictionary before cancelling
the TaskCompletionSources.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The docker_build workflow was using DO_SPACES_ACCESS_KEY/SECRET_KEY
secrets which don't have write access. Use ACCESS_KEY_ID/SECRET_KEY
instead, which are the same secrets that work for CI builds.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add quest creation logic to QuestCreationUtils.scala:
- Only available if faction's provinces have at least 3 prisoners
- Returns ReleaseAllPrisonersQuest when condition is met
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add QuestTypeString case in DisplayNames.cs
- Add ShortQuestString case in UnaffiliatedHeroRowController.cs
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add ReleaseAllPrisonersQuest that requires the faction to release all
prisoners held in their provinces. The quest fails if any prisoner is
executed, moved, or traded away in ransom.
- Add proto definition and Scala case object
- Add proto converter for serialization
- Add fulfillment check (succeeds when 0 prisoners/moving prisoners)
- Add failure handling in ManagePrisonersCommand (Execute, Move)
- Add failure handling in RansomResolutionHelpers (ransom acceptance)
- Add LLM prompts for divine message and quest ended narratives
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The controller was on the What's New Panel itself, which starts disabled.
This meant Awake() wouldn't run until the panel was first enabled, but
the WhatsNewManager tries to call Show() before that happens - resulting
in the modal blocker appearing without the panel.
Moving the controller to the always-active Settings object ensures it
initializes properly at startup.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Enable battalion diversity quest generation (PR 3/3)
Adds quest creation logic for BattalionDiversityQuest to QuestCreationUtils.
Quest availability:
- Only if province currently has 1-2 battalion types
- This encourages players to diversify their army composition
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Clarify PR dependency structure in quest docs
PR 2 (client) and PR 3 (generation) both depend on PR 1 (types),
but are independent of each other. They can be developed in parallel
and merged in either order after PR 1.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The existing cleanup only removed dangling images. Now also removes
images older than 24h to prevent disk space exhaustion while keeping
recent images available for rollback.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The S3 API requires credentials with bucket access, but the file is
publicly accessible at assets.eagle0.net. Use HTTP GET for reading
(no credentials needed) and keep S3 API only for writing.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add long-press support for right-click on touch devices
On iPad and other touch devices, right-click is not available. This adds
long-press detection to GeneralClickDetector (used by the strategic map)
to trigger right-click behavior after holding for 0.5 seconds.
- Tracks pointer down time and position
- Triggers right-click after 0.5s if finger hasn't moved >10 pixels
- Suppresses normal left-click if long-press was triggered
- Cancels long-press detection if finger moves too far
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Only enable long-press for touch input, not mouse
Mouse users can right-click, so long-press should only trigger for
touch devices. Check pointerId >= 0 to distinguish touch (0+) from
mouse (-1, -2, -3 for left, right, middle buttons).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Restore support for ACCESS_KEY_ID/SECRET_KEY env vars (used by CI)
while also supporting DO_SPACES_* env vars (used by docker-compose).
Priority order:
1. ~/.s3cfg file
2. DO_SPACES_* env vars
3. ACCESS_KEY_ID/SECRET_KEY env vars
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds BattalionDiversityQuest - a quest where unaffiliated heroes want
to see three different types of battalion at near-full strength (80%
capacity) in the current province.
This PR adds:
- Proto message definition
- Scala case object
- Proto converter
- Fulfillment check (near UpgradeBattalionQuest)
- LLM prompt generators
Does NOT generate the quest yet - that will be in PR 3 after client
handling is added in PR 2.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Replace duel_challenged sound with licensed asset
Replace duel_challenged.mp3 (unknown license) with Weapon Draw Metal 1.wav
from the purchased Medieval Combat Sounds Unity Asset Store pack.
The dramatic sword unsheathing sound fits well for a duel challenge.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up Weapon Draw Metal 5 as duel_challenged sound
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add S3 credentials to admin server container
The admin server needs DO_SPACES_* environment variables to access
S3/Spaces for What's New data storage. Without these, the What's New
Management page fails with "static credentials are empty".
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix S3 credentials: use DO_SPACES_* env vars
The aws package was looking for ACCESS_KEY_ID/SECRET_KEY env vars,
but docker-compose passes DO_SPACES_ACCESS_KEY/DO_SPACES_SECRET_KEY.
Updated ReadAwsConfig() to use the correct env var names and also
respect DO_SPACES_ENDPOINT.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add stale PR refs cleanup to iOS CI workflows
Self-hosted runners persist .git between runs. When a PR is updated,
old local refs may point to commits that were never fetched, causing
"Could not scan for Git LFS files" errors.
Applied to:
- ios_addressables_build.yml
- ios_testflight.yml
(Same fix already present in mac_build.yml and unity_build.yml)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fetch LFS after checkout to avoid stale ref issues
Move LFS fetching out of actions/checkout and into a separate step.
This ensures our stale PR refs cleanup runs before any LFS operations,
avoiding "Could not scan for Git LFS files" errors.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix LFS fetch order in mac_build and unity_build workflows
Apply the same fix as ios_* workflows: set lfs: false in checkout
and fetch LFS files manually afterward to avoid stale ref issues.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The admin server needs DO_SPACES_* environment variables to access
S3/Spaces for What's New data storage. Without these, the What's New
Management page fails with "static credentials are empty".
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add quest creation logic for DevelopProvincesQuest and MobilizeProvincesQuest
to QuestCreationUtils.
Quest parameters:
- Availability: Only if faction has >= 3 provinces
- Target province count: Random 3-5, capped at current province count
- Target months: Random 3-6
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add ADDING_NEW_QUESTS.md documentation explaining the three-PR
strategy and listing all files that need modification for new quests
- Add DevelopProvincesQuest and MobilizeProvincesQuest cases to
DisplayNames.QuestTypeString()
- Add quest description strings to UnaffiliatedHeroRowController
showing progress (e.g., "3/5 months")
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add click-to-dismiss on modal blockers as safety net
If a modal panel somehow becomes invisible while its blocker remains
active, users can now tap the dimmed area to dismiss it. This prevents
getting stuck with an unresponsive UI.
Applied to all three controllers that share the modal blocker:
- WhatsNewPanelController
- BugReportPanelController
- SettingsPanelController
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Give each modal panel its own dedicated modal blocker
Previously Settings, Bug Report, and What's New panels shared a single
modal blocker, which could cause state conflicts if one panel's blocker
state affected another.
Now each panel has its own dedicated blocker:
- Settings Panel Modal Blocker
- Bug Report Modal Blocker
- What's New Modal Blocker
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The fetchWhatsNewData function was silently swallowing ALL errors and
returning empty data, not just "file not found" errors. This masked
real issues like permission errors.
Now:
- Only returns empty data for actual "NoSuchKey" (file not found) errors
- Logs and returns other errors so they appear on the page
- Logs successful fetches with entry count for debugging
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The htmx form was receiving both an HX-Redirect header and HTML body content.
This caused htmx to swap the content before processing the redirect, making
the button appear to do nothing.
Fix: Return only the HX-Redirect header without body content on success.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Province Order Quests (Develop/Mobilize)
Add two new quest types where unaffiliated heroes want the faction to
maintain a certain number of provinces in specific order states
(Develop or Mobilize) for a cumulative number of months.
Quest mechanics:
- Availability: Only if faction has >= 3 provinces
- Target province count: Random 3-5, capped at current province count
- Target months: Random 3-6 (componentCount)
- Progress: Each round, if faction has >= X provinces with the required
order type, componentsFulfilled increments by 1
- Completion: When componentsFulfilled >= componentCount
- Failure: If faction falls below X total provinces
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove ProvinceOrderQuestProgress action result type
Fold the province order quest progress changes into the EndVassalCommandsPhase
action result instead of creating a separate action result type.
Also improve LLM prompt descriptions to explain what Develop and Mobilize
orders mean (Develop = improve agriculture/economy/infrastructure,
Mobilize = organize/train/arm troops for war).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove quest generation for province order quests
Keep the proto definitions, Scala types, converters, and handling code
(fulfillment, failure, progress tracking, LLM prompts) but remove the
actual quest creation logic from QuestCreationUtils.
This allows the client to be updated to understand these quest types
before the server starts generating them.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change costLabel to only show the numeric cost value
- Add separate costWarningLabel for "(only X available)" warning
- Warning label only shown when insufficient gold available
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update hero and battalion backstory prompt generators to explicitly
request flowing narrative prose rather than dated lists. The LLM
should weave events into a cohesive story that reads as a biography
or unit history, not a timeline.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add underProvinceNameText/underProvinceOwnerText to MapController
- Update SetUpCenterText to populate both regular and under text fields
- Add clickedProvincePanel/underClickedProvincePanel refs to EagleGameController
- Switch panel visibility based on screen width in ArrangeLayout
- Organize EagleGameController Inspector fields with Header attributes
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Notify connected clients when a new version is available after CI deploy.
Flow:
1. CI deploys new build and waits 60s for CDN cache
2. CI calls admin server HTTP endpoint with shared secret
3. Admin server calls Eagle via gRPC
4. Eagle broadcasts to all connected lobby users
Components:
- Proto: ClientUpdateAvailable message, NotifyClientUpdate RPC
- Eagle: notifyClientUpdate() broadcasts to lobby users
- Admin server: /notify-update HTTP endpoint with secret auth
- CI: Notify steps in mac_build.yml and unity_build.yml
- Docker: NOTIFY_SECRET env var passed to admin container
Client-side handling will be added in a follow-up PR.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add battalion backstory to LLM prompt generators
Add descriptionWithBackstory methods to BattalionDescriptions that include
the battalion's backstory text alongside the basic description (name, type,
size). This gives LLM prompts richer context about the units involved.
Updated prompt generators:
- SuppressBeastsPromptGenerator (Failed and Succeeded): Include battalion
backstory when describing beast hunting attempts
- HeroBackstoryUpdatePromptGenerator: Include battalion backstory in
FoughtInBattle, SuppressedBeasts, and SuppressedRiot events
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix battalion backstory usage in prompts
Use basic description inline in sentences and include backstory as
separate context block, matching how hero backstories are handled.
- In SuppressBeastsPromptGenerator: use basic description in
"$heroName took [battalion]" and add fullDescription as context
- In HeroBackstoryUpdatePromptGenerator: use basic description only
in event text (hero's story doesn't need battalion backstory detail)
- Renamed methods in BattalionDescriptions for clarity:
- backstoryText: just the backstory
- fullDescription: "Name is a battalion... $backstory"
- optionalFullDescription: for optional battalions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix duplicate code blocks from rebase
The rebase introduced duplicate code blocks in FoughtInBattle,
SuppressedBeasts, and SuppressedRiot cases that caused syntax errors.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove unused BattalionView methods from BattalionDescriptions
Since EventForHeroBackstory now uses BattalionId instead of BattalionView,
we no longer need the BattalionView overloads in BattalionDescriptions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The FoughtInBattle, SuppressedBeasts, and SuppressedRiot events now store
Option[BattalionId] instead of Option[BattalionView]. This allows looking
up the battalion from GameState at prompt generation time, providing access
to the battalion's current backstory text rather than a snapshot.
Changes:
- Proto: Changed battalion field from BattalionView to optional int32
- EventForHeroBackstoryT.scala: Changed battalion type to Option[BattalionId]
- EventForHeroBackstoryConverter: Updated toProto/fromProto conversion
- ResolveBattleAction, SuppressBeastsCommand, HandleRiotCrackDownCommand:
Now pass battalion.map(_.id) when creating events
- HeroBackstoryUpdatePromptGenerator: Looks up battalion from GameState
using gameState.battalions.getOrElse(id, gameState.destroyedBattalions(id))
- Updated tests and removed unused BattalionViewFilter deps
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update Notification Panel layout
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Additional Notification Panel layout tweaks
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update button text styling
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix empty.tail crash in BattalionBackstoryUpdatePromptGenerator
When a battalion had no previous backstory versions, calling .last on
an empty vector caused a NoSuchElementException. Use lastOption with
a match to safely handle the empty case by returning an empty string.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Generate initial backstory for newly raised battalions
When a player raises a new battalion via OrganizeTroopsCommand, now
generates a BattalionInitialBackstoryRequest so the battalion gets
its first backstory. Previously only battalions created at game start
(in NewGameCreation) received initial backstories.
This ensures all battalions have backstories from their creation,
complementing the empty.tail fix which handles the transitional case.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Update Suppress Beasts command panel layout in Unity
- Only show "(only X available)" when insufficient gold, with newline
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add references for battalion name (GeneratedTextUpdater) and type icon
(RawImage) to the battalion popup panel header. Populate them when
hovering over a battalion row.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When creating backstory events for changed battalions, the code assumed
every battalion in changedBatt would also be in changedBattalions. This
is not true when a battalion is modified indirectly - for example, when
troops are transferred OUT of it to another battalion.
The fix uses find() and handles the None case with default values (0 for
hired, dismissed, and transferred in counts).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add git corruption detection to Unity CI workflows
Self-hosted runners can accumulate corrupted git state over time.
Add a pre-checkout step that runs git fsck and nukes the repo if
corruption is detected, allowing a fresh clone.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix CI git corruption: check target ref objects instead of full fsck
Self-hosted runners can have stale refs (e.g., refs/remotes/pull/*/merge)
pointing to commits whose objects weren't fetched when the PR was updated.
Running git fsck is too broad and slow. Instead, check if the specific
target ref's objects are complete before checkout.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix CI: prune stale PR refs instead of checking all objects
Self-hosted runners persist .git between runs. When a PR is updated,
actions/checkout doesn't prune old local refs like refs/remotes/pull/*/merge.
These stale refs may point to commits whose objects were never fetched,
causing "missing object" errors during checkout.
Fix by explicitly removing PR refs before checkout. This is faster and
more targeted than validating object completeness.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add four new battalion backstory event types:
- SuppressedRiot: Records when a battalion helps suppress a riot, including
the riot size, casualties, and whether the suppression succeeded
- Trained: Records training sessions by a hero, with training improvement
- Armed: Records equipment upgrades with armament improvement and cost
- SurvivedStarvation: Records when battalions suffer casualties due to
food shortage, with loss proportion and whether on march or in garrison
The LLM prompt generator includes guidance to not over-emphasize routine
training and arming events in favor of more dramatic events like battles
and riots.
Also includes per-battalion cost tracking in ArmTroopsCommand to properly
attribute gold spending for each battalion's backstory event.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add What's New feature to Unity client
- Add WhatsNewManager singleton for fetching and tracking changelog
- Add WhatsNewPanelController for modal display with category styling
- Integrate CheckAndShow() call in ConnectionHandler after OAuth login
The manager fetches entries from assets.eagle0.net/whats-new.json,
tracks last seen date in PlayerPrefs, and shows new entries on login.
UI prefab setup required in Unity Editor.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up What's New UI in Unity
- Move WhatsNewEntryUI to separate file for Unity component discovery
- Add What's New Entry prefab
- Wire up WhatsNewManager and WhatsNewPanelController in Gameplay scene
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add battalion backstory infrastructure
Adds support for battalions to have backstory events similar to heroes:
- New proto message EventForBattalionBackstory with event types:
- OrganizedTroops: tracks size changes, hiring, dismissals, transfers
- SuppressedBeasts: records beast suppression operations
- ApprehendedOutlaws: records outlaw apprehension (for future use)
- FoughtInBattle: records battle participation and casualties
- New Scala enum EventForBattalionBackstoryT matching the proto
- Updated BattalionT trait and BattalionC with:
- backstoryVersions: Vector[BackstoryVersion] for LLM-generated text
- backstoryEvents: Vector[EventForBattalionBackstoryT] for events
- Helper methods: withBackstoryVersions, withBackstoryEvents, addBackstoryEvent
- New converters:
- BackstoryVersionConverter (extracted for reuse)
- EventForBattalionBackstoryConverter
- Updated BattalionConverter to handle backstory fields
- Wired up backstory events in:
- SuppressBeastsCommand: adds SuppressedBeasts event to battalion
- ResolveBattleAction: adds FoughtInBattle event to battalions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add LLM-driven battalion backstory updates
This commit adds the system to actually use battalion backstory events
to trigger LLM-generated backstory updates, similar to the hero backstory
update system.
New components:
- BattalionBackstoryUpdateAction: finds battalions with events, generates
LLM requests, adds new backstory versions, and clears events
- BattalionBackstoryUpdateActionGenerator: creates action from game state
- BattalionBackstoryUpdatePromptGenerator: generates prompts describing
events for LLM to update backstory
- BattalionBackstoryUpdateRequest proto message and converter
The system is wired into:
- EndPlayerCommandsPhaseAction
- EndVassalCommandsPhaseAction
- EndBattleAftermathPhaseAction
- EngineImpl (command execution)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Track previousSize/newSize for battalion events, add heroId to OrganizedTroops
Changes:
- SuppressedBeasts event: replaced 'casualties' with 'previousSize' and 'newSize'
- ApprehendedOutlaws event: added 'previousSize' and 'newSize' fields
- OrganizedTroops event: added 'heroId' for the province ruler who ordered the reorganization
- OrganizeTroopsCommand now creates battalion backstory events when troops are organized
- Updated prompt generator to include hero names and size information
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a game is deleted (via admin console or last player dropping),
cancel any pending Shardok battles to:
- Stop processing battles for non-existent games
- Prevent reconnection attempts for deleted games
- Clean up resources
The implementation removes matching entries from pendingBattles map,
which prevents scheduleReconnect from re-establishing streams and
handleStreamingResponse from processing further updates.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add CRUD handlers for managing what's-new entries stored in S3
- Add whats_new.html template with add/edit/delete forms
- Add navigation link to layout.html
- Add player-friendly summary generation to generate_changelog.sh
- Add seed JSON file for initial data
The admin console at /whats-new allows managing changelog entries
that will be displayed to players in the Unity client. The script
integration generates summaries and opens the admin console with
pre-populated content after sending weekly changelogs.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add battalion backstory popup on hover
Add support for displaying battalion backstories when hovering over
battalions in the Battalions panel, similar to hero backstories:
- Added battalionPopupPanel and battalionPopupPanelBackstory fields
- Added BattalionLongHoverRowChanged method to handle hover events
- Uses GeneratedTextUpdater for auto-scrolling text display
To wire up in Unity:
1. Create a popup panel similar to hero popup
2. Add GeneratedTextUpdater component for backstory text
3. Assign references in HeroesAndBattalionsPanelController
4. Wire battalionsTable.LongHoverRowChangedHandler to BattalionLongHoverRowChanged
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Eagle0 > Build Protos menu item for editor proto rebuilds
Provides a convenient way to rebuild protocol buffer files from within
Unity Editor (Cmd+Shift+P on Mac). This helps when proto files change
and the generated C# code needs to be updated.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up battalion backstory popup panel in Unity
- Add TableRowHoverDetector to Battalion Row Prefab for hover events
- Configure battalion popup panel in Gameplay scene
- Wire LongHoverRowChangedHandler to BattalionLongHoverRowChanged
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Position popup column spacer via EagleGameController layout system
Set the popup column spacer width in ArrangeLayout() alongside the
heroes and battalions panel width, ensuring they stay synchronized
when resolution changes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Propagate battalion backstories to clients by adding backstory_text_id
field to BattalionView proto and Scala view classes.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1. Command Interaction Guidance (onboarding step 7):
- Explains clicking provinces for acting vs right-click for target
- Describes using left side panels for heroes/battalions selection
- Reminds that Commit finalizes the command
2. Hero Stat Gain Tutorial:
- Triggered first time a player's hero gains a stat point
- Uses HeroStatGained action result type for reliable detection
- Explains experience, stat growth, and professions
Also updated info button tutorial to mention the '?' keyboard shortcut.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Completed:
- Bug report form in Unity client
- Feedback channel (Discord server)
- Support plan (Discord + bug reporting + email)
- Art/music licenses and open source disclosures (attributions panel)
Added end game ideas:
- Win condition: all other factions defeated
- Mid-game progression: King recognition events
Moved "What's new" changelog to nice-to-have.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add battalion initial backstory generation
Adds LLM-generated backstories for battalions created at game start.
Changes:
- Add backstoryVersions field to Battalion proto and BattalionT/BattalionC
- Add BattalionInitialBackstoryRequest to GeneratedTextRequestT
- Create BattalionInitialBackstoryPromptGenerator for LLM prompts
- Wire up in LlmResolver and NewGameCreation
- Update BattalionConverter to handle backstory versions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Rename hero_backstory_version to backstory_version
Since BackstoryVersion is used for both heroes and battalions, rename:
- hero_backstory_version.proto -> backstory_version.proto
- hero_backstory_version_proto -> backstory_version_proto
- hero_backstory_version_scala_proto -> backstory_version_scala_proto
Also rename leading_hero_id to province_ruler_hero_id in
BattalionInitialBackstoryRequest to clarify this is the hero who
rules the province, not a battalion commander.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix bug report panel not showing on first click
Change Start() to Awake() so initialization happens at load time
rather than when the GameObject first becomes active. This prevents
Start() from disabling the panel after Show() has enabled it.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Enable BugReportPanelController GameObject at scene start
The controller needs to be active so Awake() runs at load time,
allowing Show() to work on first click.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add bug report form to Unity client
Adds a Report Bug button to the Settings panel that opens a bug report
form. Reports include:
- User description
- Game state (game ID, faction, date, active battles)
- System info (OS, CPU, GPU, resolution)
- Recent connection logs
Reports are sent via webhook (supports Discord and Slack).
Configure webhook URL with BugReportPanelController.SetWebhookUrl().
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Configure Discord webhook URL for bug reports
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add escape key handling for bug report and attributions panels
- Bug report panel closes on Escape
- Attributions panel closes on Escape
- Settings panel only toggles if other panels aren't open
- Wire up bug report panel UI in Gameplay scene
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The warmup binary was being copied from a hardcoded bazel-bin path that
doesn't work reliably with cross-compilation and remote caching. When the
binary was cached but not materialized locally, the cp command would fail.
Fix by:
- Create a warmup_tar pkg_tar target that packages the warmup binary
- Use tar extraction instead of cp, which forces Bazel to materialize
the output file before the command runs
- Rename the binary from warmup_linux_amd64 to warmup in the tar
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update Recruit command panel layout
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update Diplomacy toggles to button-style like Improve
Change radio-style toggles to highlighted button toggles using
ToggleGroup and CanvasGroup alpha for disabled state.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Make HeroGenerator immutable with private constructor
- Change `var remainingPregeneratedHeroes` to `val`
- Return tuple `(HeroGenerationResponse, HeroGenerator)` from getHero()
- Add `forTesting()` factory method for test usage
- Filter excluded names during construction instead of lazily
- Make BattalionNameGenerator immutable
- Change `var excludingNames` to `val` (was never mutated anyway)
- Update callers to properly thread the generator through recursion/fold
- PerformUnaffiliatedHeroesAction: thread generator through `go` recursion
- NewGameCreation: thread generator through fold, extract at end
- EngineImplTest: use forTesting() instead of mock
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
casualtyCount was incorrectly set to total damage (including hero vigor
loss) instead of just battalion troop losses. Now:
- No battalion: 0 casualties
- Battalion survived: actual troop losses
- Battalion destroyed: entire battalion size
This ensures casualtyCount semantically represents battalion casualties,
not hero damage which is tracked separately.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a hero suppresses beasts alone (no battalion), casualtyCount can
still be > 0 because casualties are applied to hero vigor. The code
incorrectly tried to access the battalion name for casualty text even
when no battalion existed.
Now only show battalion casualty text when a battalion actually exists.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add detailed help content for command tutorials
Update help text for many commands with more useful information:
- Rest: Vigor mechanics and Constitution bonus
- Train: Strength/Charisma stats, Champion bonus
- Hero Gift: Loyalty thresholds and January check
- Feast: Benefits and cost scaling
- Trade: Exchange rates and Economy bonus
- Arm Troops: Infrastructure requirements
- Organize Troops: Battalion types and requirements
- Recruit Heroes: Ready to Join mechanics
- Travel: List of available town activities
- Send Supplies: Destination selection tips
- Divine: What it reveals and cost considerations
- Recon: Intelligence gathered and risks
- Swear Kinship: Requirements and benefits
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Auto-switch help panel when selecting a different command
If the help panel is open and the user selects a different command,
the help panel now automatically switches to show help for the new
command instead of staying on the old one.
Also fix Rest tutorial: Constitution is max Vigor, not recovery rate.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add combat warning to March help
Mention that marching may trigger a battle if enemy occupies the
destination or is also marching there.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Feast help: only costs gold, not food
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Recon help: scouts can't be captured
Remove incorrect warning about capture risk.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Recon help: remove incorrect agility info
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove non-local returns and -Wconf suppression
Refactor code to avoid non-local returns (using `return` inside closures)
which are deprecated in Scala 3. Changes:
- OAuthService.scala: Replace early returns with if/else patterns in
exchangeCodeForToken, fetchUserInfo, generateAppleClientSecret,
exchangeAppleCode, and parseAppleIdToken methods
- PerformProvinceEventsAction.scala: Replace for-loop with early return
with foldLeft pattern in beastType method
Remove the `msg=Non local returns:silent` suppression from the toolchain.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix non-local returns in MapValidationTest
Refactor checkMonthlyWeather to use exists() instead of foreach with
early returns. Also fixes typo in error message (was "> 0" should be "> 100").
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add minimum width (480px) to tutorial panel
Prevents help text from being too tall and narrow.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Change tutorial panel minimum width to 720px
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Replace automatic command tutorials with info button system
Instead of showing one-time tutorial popups when command panels open,
users can now click an info button in the command panel header to view
help anytime. This makes help more accessible and less intrusive.
Changes:
- Remove automatic tutorial trigger from CommandSelector
- Add info button to CommandPanelController (created programmatically)
- Add ShowCommandHelp() to TutorialManager for repeatable help display
- Add info button intro step to onboarding sequence
- Update March, Defend, Improve help with detailed instructions
- Remove onboarding prerequisite from command tutorials
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove programmatic info button creation
Keep just the public field reference so it can be set up in Unity Editor.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix command tutorial ID lookup
Convert CommandType enum (e.g., "ImproveCommand") to tutorial ID format
(e.g., "command_improve") by removing "Command" suffix and lowercasing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add toggle behavior to info button and improve help content
- Clicking info button while help is showing now closes it
- Add ActiveSequenceId property to TutorialManager
- Update Improve help to mention agility/strength bonuses and engineer bonus
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix tutorial panel positioning to stay above command panel
- Simplify positioning logic for Top anchor to reliably place panel just above target
- Make panel at least 600px wide (or target width) for better readability
- Remove complex size-dependent clamping that caused inconsistent positioning
- Use simpler pivot-based positioning that doesn't depend on panel size calculations
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix command tutorial ID conversion for multi-word commands
Convert PascalCase command names to snake_case to match tutorial IDs:
- "OrganizeTroopsCommand" -> "command_organize_troops"
- "ArmTroopsCommand" -> "command_arm_troops"
- etc.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Reduce tutorial panel minimum width to 540px
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Paladin bonus tip to Give Alms help
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add imprisonment warning to Diplomacy help
Warn that untrusted factions may imprison your ambassador.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix manage_prisoner tutorial ID to match command type
Command is ManagePrisonerCommand (singular), not ManagePrisonersCommand.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add '?' keyboard shortcut to toggle command help
Press Shift+/ (?) to open/close help for the current command.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove forced width matching for tutorial panel
Let the panel use its natural ContentSizeFitter width instead of
forcing it to match the command panel width.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add info button to command panel in Unity scene
Wire up info button UI in Gameplay.unity to show command help.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Scala 3 deprecates `_` for wildcard type arguments in favor of `?`.
This fixes the one occurrence in CommandChoiceHelpers.scala and removes
the -Wconf suppression for this warning.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Enable -Werror and fix compiler warnings (partial)
This commit enables -Werror in the Scala toolchain and fixes warnings
in the main server code and several test files. Main changes:
Server code fixes:
- Remove unused imports (HeroId, ActionResult, unused, Try)
- Remove unused parameters (playerCount cascade in NewGameCreation,
persister in synchronizedCreateGame, userServiceForGames in buildServer)
- Fix discarded value warnings with explicit : Unit type ascriptions
- Fix pattern match warning by removing unnecessary type annotation
Test code fixes:
- Remove unused imports across multiple test files
- Remove unused default parameter values that are always explicitly passed
- Fix ScalaMock expectation setup warnings with : Unit
- Fix MapValidationTest to properly combine boolean checks
- Fix loneElement usage to use proper assertions
The -Werror flag is now enabled along with -Wconf suppressions for:
- Initialization warnings (safe patterns Scala 3 warns about)
- ScalaTest assertion return values
- External/generated sources
Many test files still have warnings that need to be fixed in follow-up
commits before all 315 tests will build with -Werror.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove unused heroes variable in ReturningHeroesTest
Fix CI failure caused by unused private member warning.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Scala compiler warnings in test files for -Werror compatibility
- Remove unused imports across multiple test files
- Remove unused private members and default parameters
- Add `: Unit` type ascription to ScalaMock expectations to silence discarded value warnings
- Fix deprecated `<function> _` syntax by removing trailing ` _`
- Replace deprecated `= _` with `scala.compiletime.uninitialized`
- Add `val _ =` to explicitly discard unused return values
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix remaining Scala compiler warnings in main sources for -Werror
- Remove unused imports: JString in OpenAIResponsesServiceImpl, FactionId in
DiplomacyOptionConverter and RansomOfferDetailsConverter,
OpenAIChatCompletionsServiceImpl in HeroLibraryGenerator
- Replace deprecated `= _` with `= uninitialized` in SseSubscriber and
ShardokInstanceManager
- Remove unused private member MessageType in SseSubscriber
- Remove unused private member openAICaller in HeroLibraryGenerator
- Add missing pattern match cases: UNKNOWN_UNIT in UnitStatusConverter,
Gender.Unknown in HeroLibraryGenerator
- Fix discarded value warnings with `val _ =` in ShardokInstanceManager
- Fix unused pattern variables in NameListChecker
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Previously the tutorial created a 1-faction game (solo with no AI).
Now it creates a 7-faction game (1 human + 6 AI), matching the
default when creating a game from the lobby.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
For the closed alpha, we're using a simple notice instead of a full
privacy policy. The notice explains:
- This is a private alpha test
- We collect OAuth email and gameplay data
- Users can delete their account via accounts.eagle0.net
- Contact email for questions
Added to:
- Invitation landing page (/invite/{code})
- Invitation email (HTML and plaintext)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove "The" from faction names where it sounds better without:
- Jade Caravan
- Maniacal Monks
- Bulwark Brotherhood
- Builders of a Better World
- Iron Fist Confederacy
- Mad Doombringers
- Shadow Army
- Verdant Fellowship
Keep "The" where it adds gravitas or feels essential:
- The Ashen Circle
- The Fracture Covenant
- The Hollow Throne
- The King's Loyalists
- The Syncopated Sanctum
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Set ITSAppUsesNonExemptEncryption=false in Info.plist during iOS build.
This indicates the app only uses standard OS-provided encryption (HTTPS)
and avoids the manual compliance questionnaire in App Store Connect.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Remove unused imports (FactionT, HostileArmyGroupProto, GameId)
- Remove unused parameters (jwtService in AuthServiceImpl, gameId in MarchCommand, provinces in ProvinceDistances)
- Add exhaustive match handling for ImprovementTypeProto UNKNOWN case
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up heroesColumn and battalionsColumn references in March Command Selector
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update command selector layout when aspect ratio changes
Previously, hero/battalion column visibility was only set when a command
selector was first opened. Now layout changes are propagated to the
active selector via OnLayoutModeChanged(), so columns hide/show
dynamically when the window is resized.
- Add OnLayoutModeChanged() virtual method to CommandSelector
- Override in MarchCommandSelector and DefendCommandSelector
- Add UpdateSelectorLayoutMode() to CommandPanelController
- Call from EagleGameController when layout mode changes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Increase narrow screen threshold from 1.35 to 1.45
The previous threshold was too low to catch some narrow aspect ratios
that should trigger narrow layout mode.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This is a stepping stone toward enabling -Werror for exhaustiveness checking
on sealed traits. Changes include:
- Enable -Wall in Scala toolchain configuration
- Add -Wconf suppressions for generated code and external dependencies
- Fix discarded value warnings by explicitly discarding with `val _ =`
- Fix non-local return warnings in JwtService using nested if/else
- Fix deprecated `= _` syntax using `= uninitialized` in OAuthHttpHandler
- Fix implicit parameter syntax: `(ec)` -> `(using ec)` in GrpcRetrier
- Remove unused imports across multiple files
- Add @unused annotation for intentionally unused parameters
- Fix wrong imports in SettingUpdater (was using wrong proto package)
- Fix BUILD.bazel exports to make GrpcRetrier available transitively
- BUG FIX: StringConstructionParser was silently ignoring parse errors
when closing character wasn't found (added missing return)
Note: -Werror is not yet enabled (TODO in tools/BUILD.bazel) because
there are still unused import warnings in some files. Once those are
fixed, -Werror can be uncommented to get compile-time exhaustiveness
checking.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Eagle's GameAdminServiceImpl now expects userId directly in requests,
removing its dependency on UserService for displayName lookups.
Changes:
- Rename new_username -> new_user_id in game_admin.proto
- Rename previous_username -> previous_user_id in ReassignFactionResponse
- Rename Result enum values: INVALID_USERNAME -> INVALID_USER_ID,
USERNAME_ALREADY_IN_USE -> USER_ALREADY_IN_USE
- Add resolveToUserId() to admin_server.go that accepts either userId
or displayName and resolves to userId using the Admin API
- Remove resolveToUserId and UserService dependency from GameAdminServiceImpl
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Shardok never used the user_name field - the C++ code completely ignores
it and the Unity client creates its own player names ("You" and "AI").
Changes:
- Mark user_name as reserved in player_info.proto
- Change ShardokInterfaceProxy to use humanFactions: Set[FactionId]
instead of playerToUserMap: Map[FactionId, String]
- Simplify GamesManager.resolveBattle to pass just the set of human factions
- Remove UserName assignments from CustomBattleHandler.cs
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The resolveDisplayName function calls adminClient.GetUser, which
requires admin authentication. Previously, fetchGames was passing
a plain context without the admin JWT token, causing GetUser to fail
and fall back to showing the UUID instead of the display name.
This fix changes fetchGames to accept the HTTP request and use
createAdminContext(r) which extracts the admin JWT token from the
request and adds it to the gRPC metadata.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The "Eagle0 is built with..." paragraph now appears after the
Development Team section, giving proper prominence to the team.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Eagle now sends userId in the userName field for running games.
The admin server looks up the displayName via GetUser and falls
back to showing the userId if displayName is not set.
This is cleaner separation of concerns - Eagle handles game logic,
admin server handles user display.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add widescreen/narrow layout support for Attack Decision panel
- Add separate control containers for widescreen and narrow layouts
- Add duplicate references for toggles, images, sliders, and labels
- Use Active* properties to return the appropriate controls based on layout
- Enable/disable layout containers in SetUpUI based on IsNarrowLayout
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up Attack Decision panel layout references
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Revert AttackDecisionCommandSelector changes, keep layout updates
Reverted the widescreen/narrow code changes - simpler approach works.
Keep Unity layout improvements.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Additional layout updates
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Since we're starting fresh with no legacy displayName-based games,
remove the migration code that ran at startup. All new games will
use userId (UUID) from the start.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Use UUID for user-to-game mapping instead of displayName
Previously, Eagle mapped users to games/factions using displayName. This
meant users who changed their display name would lose connection to their
games.
This change migrates to using userId (UUID) for the mapping:
- Add new proto fields user_id_to_pid and last_played_by_user_id
- Update GameController to use userIdToFactionId
- Add migration logic in GamesManager to convert legacy games on load
- Update EagleServiceImpl to use AuthorizationUtils.userId
- Update GameAdminServiceImpl to resolve display names from userIds
- Write to both old and new fields during transition period
Existing games are automatically migrated when first loaded after deploy.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add UserId opaque type, startup migration, and backup logic
Major changes:
- Create UserId opaque type for compile-time safety between userId and displayName
- Add explicit startup migration in GamesManager.begin() that converts all games
from displayName-based mapping to userId-based mapping
- Create backup of games.e0es before migration (games.e0es.backup.<timestamp>)
- Remove mixed-state handling logic ("if it looks like UUID") - all games now
use userId format after migration
- Update GameController, EagleServiceImpl, GameAdminServiceImpl, AuthorizationUtils
to use UserId type throughout
- Add comprehensive migration tests covering:
- Normal migration of displayName to userId
- Skipping already-migrated games
- Handling users not found (faction becomes AI)
- Backup file creation verification
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use UserId type in UserService for full type safety
- Updated UserService.findByUserId to take UserId instead of String
- Updated UserService.setDisplayName to take UserId instead of String
- Updated UserServiceImpl and NoOpUserService implementations
- Updated all call sites to pass UserId directly instead of using .value
- This completes the type safety for userId throughout the codebase,
preventing accidental mixing of userId and displayName strings
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Style credits panel with colors and better layout
- Gold (#FFD700) section headers
- White names/titles, gray license/source details
- Centered and larger team section at top
- Better spacing between sections
- Helper method for consistent section headers
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update credits panel background color
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Game Design: Dan Crosby & Dan Kiracofe
- Programming: Dan Crosby
- Map Design: Dan Kiracofe
Updated both client and web attributions JSON files.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add AttributionsController to load and format attributions from JSON
- Add scrollable attributions panel to Settings UI
- Display credits for music, sound effects, icons, fonts, and software
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Implements a user-facing account management portal at accounts.eagle0.net
that allows non-admin users to view their account information and delete
their account.
The same admin server backend handles both admin.eagle0.net (admin-only)
and accounts.eagle0.net (user self-service), distinguished by Host header.
Changes:
- nginx: Add server blocks for accounts.eagle0.net
- admin_server: Add host detection (isAccountsHost/isAdminHost)
- admin_server: Add requireUser auth wrapper (auth without admin check)
- admin_server: Add /my-account, /my-account/delete, /goodbye routes
- admin_server: Modify login flow to not require admin for accounts portal
- templates: Add my_account_layout.html, my_account.html, goodbye.html
- templates: Update login.html to show different text per portal
Post-deploy manual steps required:
1. DNS: Add A record for accounts.eagle0.net -> droplet IP
2. SSL: Run certbot on droplet for accounts.eagle0.net certificate
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Layout improvements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Hero row and gameplay layout updates
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Additional Trade command selector layout
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Incremental update to get bug fixes and improvements in the LLVM/Clang
toolchain.
Note: LLVM 21.x has libc++ ABI incompatibilities that cause linker errors,
so staying on 20.x for now.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix iOS build by checking for iOS module before skipping install
The script was checking if Unity was installed but not whether the iOS
module was present. This caused iOS builds to fail when Unity was
installed without iOS support.
Now checks for platform-specific module directories before declaring
Unity ready.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use install-modules when editor exists but modules missing
Unity Hub CLI has separate commands:
- install: installs editor with modules (fails if editor exists)
- install-modules: adds modules to existing installation
Now detects which command to use based on whether the editor
directory exists.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Output Unity log on build failure for easier debugging
When Unity builds fail, the actual error is hidden in a log file that
requires downloading an artifact. Now all Unity build scripts output
the last 200 lines of the log directly to the console on failure.
Updated scripts:
- build_unity_ios.sh
- build_windows.sh
- build_mac.sh
- build_ios_addressables.sh
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Treat 'module already installed' as success
Unity Hub CLI returns non-zero exit code when all requested modules
are already installed, with message "No modules found to install".
Now check output for "already installed" messages and treat as success.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Environment selection UI (dropdowns and indicator) should only be
visible when running in the Unity Editor. In builds, always use prod.
Changes:
- Connection panel environment dropdown: hidden in builds
- Lobby environment dropdown: hidden in builds
- Environment indicator text (top left of Eagle gameplay): hidden in builds
- Force environment to prod (index 0) when not in editor
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Major version upgrade with new features including:
- Feature flag evaluations on scope(s)
- MDC properties attached to structured logs as attributes
- Various bug fixes and improvements
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Upgrade to the latest AWS SDK for Java v2, which includes many bug fixes,
performance improvements, and new features.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add eagle0.net as server name alias
Allows accessing the site via eagle0.net in addition to prod.eagle0.net.
Note: After deploying, run certbot to add eagle0.net to the SSL certificate:
certbot --nginx -d prod.eagle0.net -d eagle0.net
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add admin container image verification in deploy workflow
The admin container was running a stale image after the entrypoint was
changed from /app/admin_server to /app/admin_server_linux_amd64. This
happened because:
1. crane pull + docker load doesn't update the :latest tag locally
2. docker-compose fallback to :latest used the old cached image
3. --force-recreate recreated the container but with the old image
Fixes:
- Tag pulled admin image as :latest after docker load (ensures fallback
uses correct image)
- Add image digest verification after deploy (fails early if wrong
image is running)
- Add admin startup verification in deploy-blue-green.sh (catches
container crashes with helpful logs)
This follows the same pattern used in auth_build.yml for auth service
verification.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Preserve the vertical anchor values from the prefab instead of
overwriting them to span 0-1, which was causing vertical stretching.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Allows accessing the site via eagle0.net in addition to prod.eagle0.net.
Note: After deploying, run certbot to add eagle0.net to the SSL certificate:
certbot --nginx -d prod.eagle0.net -d eagle0.net
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Proxy the credits/attributions page from the auth service
so it's accessible at prod.eagle0.net/credits.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When connected but no server status has been received yet (e.g., during
server warmup after deployment), show "Waiting for server..." with a
yellow indicator instead of just "Connected" with green. This gives
users better feedback that something is still happening.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The DiplomacyOfferInfo was storing messengerHeroId but the lookup in
ResolveDiplomacyCommandSelector was matching against prisonerHeroId,
causing a "RansomOffer not found" exception.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Paladins are still prioritized first. Within profession groups,
heroes are now sorted by fatigue (constitution - vigor) ascending,
so less tired heroes are recommended first.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
DroughtEffect:
- Increase texture tiling from 1x1 to 10x10 to reduce pixelation
FloodEffect:
- Increase texture tiling from 10x10 to 40x40 for finer detail
- Increase distortion strength from 0.02 to 0.1 for more visible waves
- Increase distortion speed from 2 to 5 for livelier animation
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The CancelAllAnimations() call in ModelUpdated() was interrupting normal
animations (like archery) because ModelUpdated() is called frequently
during gameplay. The ChargeAnimator fix alone (tracking _weapon in a
class field) is sufficient to prevent floating weapons.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Use effective development values (base - devastation) when showing
current total development, matching how the quest completion is
actually evaluated.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
SuppressRiotByForceQuest and FightBeastsAloneQuest were incorrectly
checking all provinces for quest fulfillment. This meant if an ally
performed the action, unaffiliated heroes in your provinces would
have their quests fulfilled.
Now filters to only check provinces ruled by the acting faction.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add _weapon field to ChargeAnimator to track created weapon GameObjects
- Add CleanupWeapon() method to ChargeAnimator for proper cleanup
- Update CancelAnimation() in ChargeAnimator to destroy lingering weapons
- Add CancelAllAnimations() method to ShardokGameController
- Call CancelAllAnimations() at start of ModelUpdated() to clean up any
lingering animation effects when the model state changes
Previously, if a charge animation was interrupted (e.g., by rapid clicking
or state changes), the weapon GameObject would remain floating on the map
because it was stored in a local variable rather than a class field.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Rename _EffectTex to _MainTex in the ProvinceWeather shader and update
all weather effect materials to use the new property name. Unity's
RawImage component expects _MainTex as the default texture property.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds the following missing cases:
- RescueImprisonedLeaderQuest
- ExpandToProvincesQuest
- SuppressRiotByForceQuest
- FightBeastsAloneQuest
This ensures all Quest subtypes are handled in pastTenseQuestDescription.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Hook up ProvinceWeatherController references in the Gameplay scene to
enable animated weather overlays (blizzard, flood, drought) on the
strategic map.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Display the current total development value alongside the target in the
quest description, making it easier for players to track their progress.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The TCommand.RandomSequential case was missing from the match expression,
causing a MatchError when SuppressBeastsCommand (which returns RandomSequential)
was executed during vassal commands phase.
Also made randomResults public in ProtolessRandomSequentialResultsAction
so it can be called from the match.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds /credits endpoint to the auth service that displays all attribution
information from attributions.json in a readable HTML format.
The page includes:
- Creative Commons music (31 tracks with artist and license)
- Sound effects from Freesound (with source links)
- Fonts (with license info)
- Open source software dependencies (Java, Go, C++, C#)
The attributions.json file is now included in the auth server Docker
image so it can be served at runtime.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Structured JSON file containing attribution data for:
- Creative Commons music (31 tracks)
- CC sound effects from Freesound (7 files)
- Fonts (Open Sans, Alata, Josefin Sans, etc.)
- Open source software dependencies (Java, Go, C++, C#)
- License reference definitions
File is placed in both server resources and Unity Resources
folder for use in web and in-app attribution displays.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update SMALL_EAGLE_TODO with completed items
Mark as done:
- Fix the Mac installer
- Generatedtext healing
- Audit assets for anything we don't have rights to and replace it
- Replace heroes that are based on real 20th or 21st century people or IP
- Running low on food tutorial
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Mark warlord profession tutorial as complete
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Shows an arrow indicating the original food/gold balance, making it
easy to return the slider to its starting position (no change).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* HeroesAndBattalions layout improvements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Additional layout improvements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Restores the ransom offer scoring logic that was removed during deproto work.
**Acceptance side (ResolveDiplomacyCommandSelector):**
- AI now scores ransom offers using: prisoners + hostages + gold - base threshold
- Offers with positive score are accepted, others rejected
**Offering side (CommandChoiceHelpers.maybeRansomLeaderCommand):**
- Uses RansomOfferHelpers.chosenOffer to create minimal acceptable offers
- Checks trust threshold between factions before offering
- Enforces minimum time between repeat offers to same faction
- Filters out faction leaders from hostage offers
- Calculates minimal offer (prisoners + gold first, adds hostages only if needed)
**New file: RansomOfferHelpers.scala**
- ransomOfferScore(): Calculate offer value using settings
- chosenResolution(): Accept/reject based on score
- chosenOffer(): Create a reasonable offer from available options
Settings used:
- RansomOfferScorePerPrisoner, RansomOfferScorePerHostage, RansomOfferScorePerGold
- RansomOfferBaseNegative (threshold)
- MinimumTrustToOfferRansom, MinimumMonthsBeforeRepeatRansomOffer
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add client support for SuppressRiotByForceQuest and FightBeastsAloneQuest
- DisplayNames.cs: Add display names for new quest types
- UnaffiliatedHeroRowController.cs: Add quest text formatting
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Clarify FightBeastsAloneQuest text
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add widescreen-aware HeroesAndBattalions width and layout improvements
- Add popupCanvasLeftSpacer and popupCanvasRightSpacer LayoutElement refs
- In widescreen: HeroesAndBattalions width = left + right spacer
- In non-widescreen: HeroesAndBattalions width = left spacer only
- PleaseRecruitMe layout improvements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add client support for SuppressRiotByForceQuest and FightBeastsAloneQuest
- DisplayNames.cs: Add display names for new quest types
- UnaffiliatedHeroRowController.cs: Add quest text formatting
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Clarify FightBeastsAloneQuest text
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add FightBeastsAloneQuest - fulfilled when hero fights beasts without battalion
- Add proto message FightBeastsAloneQuest (field 27)
- Add Quest case class and converter
- Add soothsayer text for divine message prompt
- Add to CheckForFulfilledQuestsAction (returns false - action-based quest)
- Add to QuestCreationUtils for provinces with BeastsEvent
- Modify SuppressBeastsCommand to check quest fulfillment when optionalBattalion.isEmpty
- Add TCommand.RandomSequential variant for commands with both random rolls and multiple results
- Update RandomStateSequencer to handle RandomSequential
The quest is fulfilled simply by making the attempt to fight beasts alone,
regardless of whether the hero survives or not.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make FightBeastsAloneQuest province-agnostic
Fighting beasts alone in any province now fulfills the quest, rather than
requiring a specific province. Also replace isInstanceOf with pattern matching.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add missing override modifier to withProtolessRandomSequentialResultsAction
The method in RandomStateSequencerImpl overrides the trait definition
but was missing the override keyword, causing a compilation error.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The quest should be offered regardless of whether a riot is currently
imminent - it's a general "when riots happen, crack down on them" quest
requirement that applies to future riots.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When the AI makes ransom offers, it was including faction leaders as
potential hostages. This change:
1. Filters out any hostages that are leaders of the offering faction
2. Skips making the offer entirely if after filtering there's nothing
left to offer (no prisoners, no non-leader hostages, no gold)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Previously, when a second instance was launched without a deep link,
it would allow itself to run as a duplicate. This caused issues in
the invitation flow where the user could end up with two copies.
Now the second instance always exits, regardless of whether it has
a deep link. If you want to run a new instance, close the existing one.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add wideSpacer GameObject reference to EagleGameController
- Enable spacer only when aspect ratio > 2.0 (widescreen)
- Additional layout adjustments in Gameplay.unity
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add SuppressRiotByForceQuest
Adds a new quest type that is fulfilled when a player successfully
suppresses a riot using the crack down command (hero survives).
Changes:
- Add SuppressRiotByForceQuest to proto and Quest.scala
- Add TCommand.RandomSequential variant for commands that return
multiple results with random rolls
- Refactor HandleRiotCrackDownCommand to use the new variant and
check for quest fulfillment on success
- Add quest creation for provinces with ImminentRiotEvent
- Update tests for new command signature
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make SuppressRiotByForceQuest province-agnostic
Suppressing a riot in any province now fulfills the quest, rather than
requiring a specific province.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Replace isInstanceOf with pattern matching in QuestCreationUtils
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- New user without invitation (when required): Show clear error page
explaining that an invitation is needed, no deep link
- Existing user: Auto-redirect to app via JavaScript (no button click)
- New user with invitation: Auto-redirect to app for registration flow
The invitation flow (separate handler) keeps the button with deep link
as users may need to download the app first.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Upgrade Unity from 6000.3.0f1 to 6000.3.6f1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add missing .meta files for notification generators
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix OAuth flow for new users - don't create user prematurely
The generateDeepLinkURL function was calling FindOrCreateUser immediately
in the OAuth callback, which:
1. Created users before invitation code validation
2. Created users before display name was set
This caused new users without invitations to get through the OAuth flow
successfully but then fail to log in because they had no display name.
Fix:
- Server: Check if user exists first. For existing users, generate
session transfer code. For new users, return state parameter so
client can poll CheckOAuthStatus which properly handles invitation
validation and new user registration.
- Client: Add handling for eagle0://auth/result/{state} deep link
that polls CheckOAuthStatus for new user registration flow.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add CheckOAuthStatusAsync method to AuthClient
The HandleOAuthResultAsync method was calling a non-existent method.
Add a public single-check method that wraps the gRPC call.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Automate Unity version sync and CI installation
1. UnityVersionSync.cs: Editor script that automatically updates
ci/unity_version.sh when the project is opened in a newer Unity version.
This runs on project load via [InitializeOnLoad].
2. ensure_unity_installed.sh: CI script that checks if the required Unity
version is installed and attempts to install it via Unity Hub CLI if not.
Added to all Unity build workflows.
3. Updated workflow paths to trigger on ci/unity_version.sh changes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add platform-specific Unity modules and concurrent install safety
- Pass platform argument (mac/windows/ios) to ensure_unity_installed.sh
- Add lock file mechanism to prevent concurrent Unity installations
- Install only needed modules per platform (mac-il2cpp, windows-mono, ios)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Read Unity version from ProjectVersion.txt instead of manual file
- Remove ci/unity_version.sh (manually maintained)
- Remove Assets/Editor/UnityVersionSync.cs (auto-sync script)
- Update ensure_unity_installed.sh to parse ProjectVersion.txt directly
- Unity automatically maintains ProjectVersion.txt, no manual step needed
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update all build scripts to read Unity version from ProjectVersion.txt
- build_windows.sh
- build_unity_ios.sh
- build_ios_addressables.sh
- build_mac.sh
- ensure_unity_installed.sh (fix comment)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a hero gives food or gold to avert a potential riot, a backstory
event is now recorded with whether the attempt succeeded or failed.
This complements the existing SuppressedRiot event for crackdowns.
- Add GaveToAvertRiotBackstoryEvent to proto with province_id, food_given,
gold_given, and succeeded fields
- Add GaveToAvertRiot case to EventForHeroBackstory enum
- Update HandleRiotGiveCommand to accept currentDate and create the
backstory event for the ruling hero
- Add handler in HeroBackstoryUpdatePromptGenerator for LLM text generation
- Add tests verifying backstory events are created for both success and
failure cases
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Quit app when opening browser for OAuth
Instead of keeping the app running and polling for OAuth completion,
quit the app after opening the browser. The server's "Return to Eagle"
deep link will relaunch a fresh instance with the auth session code,
which HandleSessionTransferAsync processes.
This eliminates the dual-instance problem where both the original app
(polling/waiting) and the deep-linked instance were running, leaving
an orphaned app window.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Generate session transfer code in OAuth callbacks
When a desktop client completes OAuth, the callback now:
1. Creates/finds the user immediately (instead of during polling)
2. Generates a session transfer code
3. Returns deep link with the code: eagle0://auth/session/{code}
This allows the Unity app to quit during OAuth and relaunch cleanly
when the user clicks "Return to Eagle" - the new instance exchanges
the session transfer code for tokens via HandleSessionTransferAsync.
Updated all three OAuth callbacks (generic, Apple, Steam) to use
the new generateDeepLinkURL helper.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add weekly Bazel cache cleanup workflow
Removes Bazel output bases not accessed in 7 days to prevent disk
space accumulation on self-hosted runners. Keeps 'cache' and 'install'
directories. Remote cache means minimal perf impact from clearing
local output bases.
Runs weekly on Sunday at 00:00 UTC, can also be triggered manually.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use bazel clean instead of find-based cleanup
The previous approach looked for output bases not accessed in 7 days,
but since the runner is used regularly, those directories always have
recent access times and would never be cleaned up.
Using `bazel clean` weekly clears the local output base, freeing disk
space. The next build will be slightly slower but will pull from remote
cache.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Use AllowSetForegroundWindow to let game take foreground
Instead of complex window closing timing, use the Windows API
AllowSetForegroundWindow() to grant the game process permission
to bring itself to foreground. This is the proper way to allow
a child process to take focus.
Changes:
- Add foreground_windows.go with AllowSetForegroundWindow wrapper
- Installer calls AllowSetForegroundWindow(pid) after launching game
- Pass --foreground flag to game
- Re-add --foreground handling in SingleInstanceEnforcer
- Game calls WindowFocusManager.BringToForeground() after 500ms delay
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add foreground_stub.go for non-Windows builds
The AllowSetForegroundWindow function is Windows-only but the
installer needs to compile on all platforms for testing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove unused unsafe import
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add foreground_windows.go to WebView genrule srcs
The genrule for the WebView build manually lists source files
and was missing the new foreground_windows.go file.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add UX support for ExpandToProvincesQuest and TotalDevelopmentQuest
- ExpandToProvincesQuest: Shows "Control X provinces (currently Y)"
- TotalDevelopmentQuest: Shows "Raise total development to X in {province}"
All quest types are now handled in the client.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix ExpandToProvincesQuest to count developed provinces only
A province is considered "developed" when support >= 40 (MinSupportForTaxes).
Changed text from "Control X provinces" to "Have X developed provinces".
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
New quest type that challenges players to raise the total development
(effective agriculture + economy + infrastructure) of the quest province
significantly higher than its current level. Uses 2x the standard
improvement quest range.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The installer must close its window before launching the game so the
game can naturally become the foreground window. But ui.Close()
terminates the event loop, causing main() to exit.
Solution: Use a channel to make main() wait for the game to launch
after the window closes:
1. Goroutine: ui.Close() - closes window
2. Main thread: ui.Run() returns, waits on gameLaunched channel
3. Goroutine: sleeps 200ms (window now fully closed)
4. Goroutine: cmd.Start() - launches game (no foreground competition)
5. Goroutine: close(gameLaunched)
6. Main thread: receives signal, exits
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add ExpandToProvincesQuest for faction expansion goals
New quest type that challenges factions to expand to X provinces (where X is
current count + 2 to 5). Success requires having X provinces with support at
or above the tax minimum threshold. No failure condition.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Cap ExpandToProvincesQuest target to total map size
Ensures the quest never requires more provinces than exist on the map.
If the faction already controls enough provinces that the minimum
expansion (2 provinces) would exceed the map size, don't offer the quest.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
All four workflows that use EAGLE0_BUILD_DIR were creating build
directories at /tmp/eagle0-{run_id} but never cleaning them up,
causing disk space accumulation on self-hosted runners.
Added `rm -rf "$EAGLE0_BUILD_DIR"` cleanup step with `if: always()`
to ensure cleanup runs even if the build fails.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* move out of concrete/
* Rename QuestC to Quest
The "C" suffix was for "Concrete" to distinguish from the old QuestT trait.
Now that QuestT is removed, simplify to just Quest.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove redundant explicit imports
The wildcard import already covers all quest types.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Instead of trying to force the game to foreground with SetForegroundWindow
(which doesn't work reliably from a background process), close the installer
window before launching the game. This allows the game to naturally become
the foreground window since there's no competing window.
Also removes the --foreground flag handling that was added but didn't work.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Refactor nested flatMap/match to use idiomatic Option chaining with
.flatMap(_.quest).map(...).getOrElse(...) pattern.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The installer now passes --foreground flag when launching the game.
SingleInstanceEnforcer detects this flag and calls WindowFocusManager.BringToForeground()
after a 500ms delay to ensure the window is fully created.
This fixes the issue where the game window would appear behind the installer
after a fresh install or update.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace the non-sealed QuestT trait with the sealed QuestC trait throughout
the codebase. This enables compile-time exhaustiveness checking on quest
type pattern matches, ensuring all quest types are handled.
Changes:
- Delete QuestT.scala and its BUILD target
- Update all imports from quest.QuestT to quest.concrete.QuestC
- Update method signatures and type annotations
- Add quest/concrete to BUILD exports for transitive visibility
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add UX support for RescueImprisonedLeaderQuest
Display the new quest type in the free heroes list with the imprisoned
hero's name and the imprisoning faction. Uses dynamic hero name loading
like other hero-specific quests.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use hero name instead of 'imprisoned leader' in quest text
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use actual hero name in rescue quest fallback text
Look up the imprisoned hero's name from the model instead of showing
generic "a hero" text.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The quest should only be fulfilled when the hero is back as a faction
leader, not just when they're no longer imprisoned. Previously, the
quest would be incorrectly marked as fulfilled if the hero was executed.
- Fulfillment: Hero must be back in the faction's leaderIds
- Failure: Hero is no longer imprisoned but also not back as a leader
(e.g., executed or released as traveler)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When viewing a prisoner in the Manage Prisoners command selector, the UI
now shows any known quests from free heroes that relate to this prisoner.
For example, if a free hero has a quest to execute or release a specific
prisoner, that information is now displayed in the message text area.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
A new quest type that free heroes may offer when the divining faction
has at least one faction leader imprisoned by another faction.
The quest is to rescue that leader, either by ransom or by force
(conquering the province where they are held).
- Quest is fulfilled when the leader is no longer imprisoned by the
specified faction
- Quest fails if the imprisoning faction is eliminated
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove --keychain flag from codesign commands
The --keychain flag was causing codesign to look for the private key in
the build keychain, but the matching cert+key is in login.keychain.
Since we now use the unambiguous SHA-1 hash, codesign will find the
correct certificate and key pair in whichever keychain contains them.
This fixes the intermittent errSecInternalComponent failures.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use build keychain specifically for CI codesigning
In CI, the workflow imports the signing certificate into a temporary
build keychain. Previously, the script searched ALL keychains and
picked the first certificate found, which could be an old certificate
from login.keychain instead of the freshly imported one.
Now when KEYCHAIN_NAME is set (CI environment), the script looks for
certificates only in that specific keychain. For local dev (no
KEYCHAIN_NAME), it still searches all keychains.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Warn when sending faction leader on Suppress Beasts command
Add warning when the player is about to send their faction leader on a
beast suppression mission. Losing the faction leader ends the game.
Three warning cases:
- No battalion: "Your hero may be killed"
- Faction leader with battalion: "You're risking your faction leader!"
- Faction leader without battalion: Combined warning for both risks
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix: Use TryGetValue instead of GetValueOrDefault
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update warning text to use sworn brother/sister title
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add starvation notification visible to all factions
When a province runs out of food and battalions suffer casualties, a notification
is now generated with a darkly humorous LLM message from the province's ruling
hero. The notification is visible to all factions (targetFactionIds = empty).
Changes:
- Add StarvationDetails proto message for notification details
- Add StarvationNotificationMessage proto for LLM text generation
- Add Scala case classes for both notification and LLM request types
- Create StarvationNotificationPromptGenerator with gallows humor prompt
- Wire up in LlmResolver and PerformFoodConsumptionPhaseAction
- Add C# notification generator for Unity client display
- Update proto converters for serialization/deserialization
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add LLM-generated text to outlaw apprehended notification
The captured outlaw now delivers a reflection on their situation.
Also adds apprehendingHeroId to the notification details.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Revert "Add LLM-generated text to outlaw apprehended notification"
This reverts commit 60295e09ba74fd0b755bf4808353be05fa8554f5.
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1. Don't upload IPA artifact after TestFlight upload - it's redundant
and takes 150MB per build. Only keep artifact when skipping upload
(for debugging), with 1-day retention.
2. Fix "broken pipe" error in artifact storage check by writing to temp
file instead of piping through sort | head (which causes SIGPIPE).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add hero backstory events for epidemic, weather, and starvation
- Add StartedEpidemic backstory event when a hero starts an epidemic
- Add ChangedWeather backstory event when a hero changes weather
- Add SurvivedStarvation backstory event for heroes in starving provinces
Changes:
- EventForHeroBackstoryT: Add 3 new enum cases
- Proto: Add corresponding proto messages
- EventForHeroBackstoryConverter: Add toProto/fromProto conversions
- HeroBackstoryUpdatePromptGenerator: Add LLM prompt text for events
- StartEpidemicCommand: Generate backstory event when executed
- ControlWeatherCommand: Generate backstory event when executed
- PerformFoodConsumptionPhaseAction: Generate backstory events for heroes
in provinces experiencing starvation
- CommandFactory: Pass required date/faction params to commands
- Tests: Update to include new command parameters
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Improve backstory events: add acting province, enum weather type
- Add actingProvinceId to StartedEpidemic and ChangedWeather events
to record the province the hero acted from (not just the target)
- Change weatherChangeType from String to WeatherChangeType enum
for type safety in both Scala and proto
- Remove percentage from starvation prompt text (keep severity only)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Mac codesign by isolating build keychain during signing
The --keychain flag alone doesn't prevent codesign from finding matching
identities in other keychains on the search list. Fix by temporarily
setting ONLY the build keychain as the search list during signing, then
restoring the original list on exit.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Include System.keychain for Apple root certificates
The previous fix broke certificate chain verification because we removed
the login keychain but also lost access to Apple's root certificates.
Include System.keychain (which has Apple roots) but not login.keychain
(which has the duplicate signing identity).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use certificate SHA-1 hash instead of name to avoid ambiguity
Instead of manipulating the keychain search list (which breaks certificate
chain verification), extract the SHA-1 hash of the specific certificate
from the build keychain and use that as the signing identity. SHA-1 hashes
are unambiguous and codesign will use the exact certificate specified.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix certificate hash extraction - get first valid identity
The previous grep for SIGNING_IDENTITY failed because GitHub Actions
masks the value. Instead, get the first valid codesigning identity from
the keychain (there should only be one since we just imported it).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use full keychain path for find-identity
security find-identity requires the full path to the keychain file,
not just the keychain name. Resolve the full path from list-keychains
output before querying for identities.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add debug output to certificate import and remove set-keychain-settings
Add debug output to see what identities are available after import.
Also remove the set-keychain-settings line which may be causing issues
(the -u flag locks keychain on sleep).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Search all keychains for identity hash
The build keychain import isn't creating a recognizable codesigning
identity, but the certificate exists in login.keychain. Search all
keychains and use the hash of the first valid identity - hashes are
unique and unambiguous regardless of which keychain contains them.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add --keychain flag to all codesign commands to explicitly specify which
keychain to use. This fixes the "ambiguous" error when the same signing
identity exists in multiple keychains (build keychain and login keychain).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Mac build was failing with errSecInternalComponent because the newly
created keychain wasn't added to the search list. Without this step,
codesign cannot find the certificate.
Changes:
- Add keychain to search list with security list-keychains
- Add -T /usr/bin/security to import command for completeness
- Set 1-hour keychain timeout for signing large app bundles
This matches the keychain setup used in the iOS TestFlight workflow.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix fullscreen toggle not working on Windows
Screen.fullScreen alone doesn't reliably toggle fullscreen on Windows.
Use Screen.SetResolution with explicit FullScreenMode instead.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix fullscreen toggle control wiring in Gameplay scene
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Heroes who suppress riots via the crack down command now get a backstory
event recording the encounter. The event captures the province, riot size,
casualties, battalion used (if any), and whether suppression succeeded.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add province weather effect overlays for strategic map
Implements animated weather effect overlays (blizzard, flood, drought) on
the Eagle strategic map. Effects are province-specific, dynamically created
as child overlays when weather events are active.
- Add ProvinceWeatherShader with scrolling animation and distortion support
- Add ProvinceWeatherController that dynamically creates overlays per province
- Add placeholder textures for snow, water caustics, and cracked earth
- Add effect materials with configured animation parameters
- Integrate weather updates into MapController's model setter
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update weather effect materials with proper settings
- BlizzardEffect: Set scroll direction to move snow downward
- Assign texture tiling for appropriate particle size
- Wire up weatherController reference in Gameplay scene
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When HandleAvailableCommands receives commands with a token that matches
a pending command, skip loading the commands entirely. The token is still
set (so TryPendingCommands can match and post the pending command), but
the commands are not loaded since they're stale - we already acted on them.
This is simpler than the flag-based approach:
- No SuppressNextUIUpdate flag
- Direct check in HandleAvailableCommands via callback
- If pending command fails, error handling refreshes state
Changes:
- Add HasPendingCommandWithToken callback to IClientConnectionSubscriber
- Add HasPendingEagleCommandWithToken helper to PersistentClientConnection
- Wire up callback in Subscribe method
- In HandleAvailableCommands, skip loading commands if token matches pending
- Preserve LastPostedToken across reconnects (don't clear in HandleStartingState)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a province has less than 3 months of food remaining after
consumption, generate a notification where the ruling hero warns
about the food shortage in their personality/voice.
- Add LowFoodWarningDetails proto message for notification details
- Add LowFoodWarningMessage proto message for LLM request
- Create LowFoodWarningPromptGenerator for hero-voiced warnings
- Wire up notification generation in PerformFoodConsumptionPhaseAction
- Add C# notification generator for Unity client display
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix infinite reconnect loop when server rejects command with ERROR
When the server returned a command ERROR (e.g., invalid province selection),
the client would dispose the connection but NOT clear the pending command.
On reconnect, it would retry the same invalid command, get ERROR again,
and loop forever with increasing backoff.
The fix: handle ERROR the same way as BAD_TOKEN - remove the pending command
(it was processed/rejected, don't retry) and refresh the game subscription
to get current valid state. Don't disconnect since the connection is fine.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Display command error to user via ErrorHandler
When server rejects a command with ERROR, invoke OnCommandError callback
which displays the error message via Debug.LogError (caught by ErrorHandler).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Change default lightning interval from (1, 4) to (2, 8) seconds,
making flashes occur approximately half as often.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- List available keychains
- List available signing identities
- Show export options plist
- Remove -allowProvisioningUpdates (not needed for manual signing)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The notification generator was accessing victor.Value unconditionally,
causing a NullReferenceException when the game ended without a victor.
This prevented the "Game Over" notification from being displayed.
Now safely handles the no-victor case and uses TryGetValue for the hero
lookup.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The previous logic only checked if a faction leader was ruling a
province. If all leaders were traveling in armies (or in any other
non-ruling state), the game would incorrectly end.
Simpler fix: a faction can play if ANY leader is NOT imprisoned.
This covers all valid states (ruling, traveling, in province, etc.)
without enumerating them.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
For DeadlineExceeded (5 min gRPC timeout) and StreamEndedNormally,
reconnect immediately without backoff delay. These are expected events,
not failures - there's no reason to wait 2+ seconds before reconnecting.
This eliminates the "Retrying in..." status for normal stream refreshes.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Try using full certificate name "Apple Distribution: Daniel Crosby (UWJ88DX8WQ)"
instead of just "Apple Distribution" to see if that resolves the export signing issue.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Documents analysis of why client notification generators currently
require per-action-result game state diffs, and proposes a solution
to enable diff batching by moving display data to server-side.
Deferred for now as current performance is acceptable.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Archive step now skips signing (CODE_SIGN_IDENTITY="-")
- This avoids "UnityFramework does not support provisioning profiles" error
- Export step handles all signing with proper certificate and profile
- Added signingCertificate to export options
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change UNIVERSALLY_VISIBLE_TYPES from Vector to Set for O(1) contains
- Replace O(n²) Vector concatenation with O(n) ListBuffer in filterForPlayer
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Fixes "xcode-select: error: tool 'xcodebuild' requires Xcode" when
the build machine's active developer directory is set to Command
Line Tools instead of Xcode.app.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The unrequestedTextHandler was being called BEFORE the game was added to
gameControllerInfos. This caused isGameValid(eagleGameId) to return false
for the game being loaded, causing all its LLM requests to be skipped
as "game deleted".
The fix adds the game to gameControllerInfos BEFORE calling the text
handlers, then updates it again after text handling completes.
Also add eagleGameId (in decimal and hex) to LLM request logging.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Logs each unrequested text being processed with its type, ID, and
requestedAfterHistoryCount. Also logs the result of each request
(submitted, bypassed, deferred, or waiting on dependency).
This helps diagnose why certain LLM requests are repeatedly triggering
disk loads for old game states.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
TryRestoreSessionAsync() fires OnLoginSuccess on success, which triggers
ConnectWithOAuth() via the OnOAuthLoginSuccess handler. The calling code
was also calling ConnectWithOAuth() directly when restore returned true,
causing _createConnection() to be called twice in quick succession.
This caused the first connection to be disposed while still connecting,
leading to ObjectDisposedException and slow reconnection recovery.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Skip validation when replaying persisted action results
Create separate appliers:
- validatingApplier: for new results (e.g., new game creation)
- replayApplier: for replaying persisted results from storage
Persisted results were already validated when first applied, so
re-validating them during replay is unnecessary overhead.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add logging to track when stateAfter loads from disk
Logs the requested count, persisted count, and how far back in history
the request is going. This helps diagnose performance issues with LLM
prompt generation that requires loading historical game states.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The previous implementation used Vector with `:+` append in a foldLeft,
which is O(n) per append, giving O(n²) total time for n results.
Changed both formAwrs and formAwrsFromScala to use:
- ListBuffer for O(1) amortized append
- foldLeft with tuple to track (buffer, currentState)
This should significantly improve performance when replaying persisted
game history.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When multiple runners on the same machine try to sign simultaneously,
they were conflicting on the shared keychain names (build.keychain,
ios-build.keychain). This caused errSecInternalComponent errors.
Changes:
- mac_build.yml: Use build-${run_id}.keychain
- ios_testflight.yml: Use ios-build-${run_id}.keychain
- codesign_mac_app.sh: Support KEYCHAIN_NAME env var with fallback
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a command POST fails due to disconnect, the client would get stuck
after reconnecting:
1. PostCommand() sets LastPostedToken before POST succeeds
2. POST fails due to DeadlineExceeded, command kept in pending queue
3. After reconnect, server sends update with same token
4. HandleAvailableCommands() skips update because token == LastPostedToken
5. Client stuck with no commands despite server saying YourTurn
Fix: Instead of skipping when token matches LastPostedToken and we have
no commands, accept the commands. The server will return BAD_TOKEN if
we try to re-post a successfully processed command. But if our POST
failed, we need these commands to proceed.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add Map Editor lighting data and reflection probe assets
- Add missing VassalRisesDetailsNotificationGenerator.cs.meta
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add EAGLE0_BUILD_DIR environment variable to workflows, defaulting to
/tmp/eagle0-${github.run_id}. This allows multiple runners with the
unity-mac label to run builds in parallel without path conflicts.
Changes:
- Workflows set EAGLE0_BUILD_DIR based on run ID
- Build scripts use EAGLE0_BUILD_DIR with fallback to /tmp/eagle0
- Library cache remains shared (beneficial for build speed)
To add a second parallel runner:
1. Set up a new runner with the same 'unity-mac' label
2. GitHub Actions will automatically distribute jobs to available runners
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The cleanup job needs this permission to delete intermediate
xcode-project artifacts. Without it, the gh api DELETE call
fails with HTTP 403.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Update bundle identifier to net.eagle0.eagle (matches provisioning profile)
- Set appleDeveloperTeamID in Unity project settings
- Add CODE_SIGN_IDENTITY="Apple Distribution" to use distribution cert
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Enable PleaseRecruitMe from prisoners
Prisoners can now trigger PleaseRecruitMe, allowing them to request
to join the faction that holds them. The LLM prompt includes context
about their prisoner status, which faction captured them, and when
they were captured.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix BUILD deps and use correct API for prisoner prompt
- Add missing BUILD dependencies for event_for_hero_backstory_trait and
unaffiliated_hero
- Use TextGenerationSuccess instead of non-existent TextGenerationResult.pure
- Use GeneratorUtilities.dateString for date formatting
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Reuse the same credentials already used for Mac notarization instead
of requiring a separate App Store Connect API key.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- New workflow_dispatch-only workflow for iOS TestFlight builds
- build_unity_ios.sh: Builds Unity iOS player (generates Xcode project)
- archive_ios.sh: Archives and exports IPA using xcodebuild
- upload_testflight.sh: Uploads IPA to TestFlight via App Store Connect API
Required secrets:
- IOS_CERTIFICATE: Apple Distribution certificate (.p12, base64)
- IOS_CERTIFICATE_PWD: Certificate password
- IOS_PROVISIONING_PROFILE: App Store provisioning profile (base64)
- APP_STORE_CONNECT_API_KEY: JSON with key_id, issuer_id, and key
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Additional layout improvements
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up ambient volume slider
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Improve rain particle randomization and wind scaling
- Add per-particle speed variation (0.7x to 1.3x) to break up "sheet" effect
- Scale wind offset proportionally to rain speed so angle stays consistent
- Add rainWindMultiplier setting to Inspector for easy tuning
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace the Health link that navigated to a separate page with an
inline status indicator that auto-refreshes every 10 seconds using
htmx. This provides at-a-glance health visibility without requiring
navigation.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Consider alliances when vassals decide to mobilize
Change chosenMobilizeIfAdjacentEnemyCommand to use FactionUtils.hostileNeighbors
instead of ProvinceUtils.adjacentHostiles. The latter treats any province owned
by a different faction as hostile, causing vassals to mobilize (and withhold
supplies) even when bordered only by allied provinces.
Now vassals will correctly send supplies to their faction head when their only
neighbors are allies or have truces, rather than unnecessarily mobilizing.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Delete misleading ProvinceUtils.adjacentHostiles method
This method was named "adjacentHostiles" but actually returned provinces
owned by any different faction, ignoring alliance/truce relationships.
It had only one production usage which was just fixed to use the correct
FactionUtils.hostileNeighbors method instead.
Deleting to avoid future confusion.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add tests for FactionUtils.hostileNeighbors
Test coverage for:
- Returns hostile (no relationship) neighbors
- Excludes allied provinces
- Excludes truce provinces
- Excludes unruled provinces
- Excludes own faction's provinces
- Returns empty for province with no neighbors
- Correctly filters mixed neighbor types
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace the last unlicensed sound effect with Negative Effect 04.wav
from the purchased Magic Spells Sound Effects LITE pack.
This completes the asset audit - all required items are now resolved.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Wrap ensureGameLoaded in try-catch to prevent one corrupted/broken game
from blocking all game loading for a user. On failure:
- Log loudly to stderr with CRITICAL prefix and stack trace
- Send exception to Sentry for monitoring
- Return false so other games can continue loading
This ensures players can still access their other games even if one
game's save data is corrupted or fails to deserialize.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add fullscreen toggle, resolution dropdown, and F11 shortcut
- Add fullscreen toggle to settings panel
- Add resolution dropdown (same as connection canvas)
- F11 key toggles fullscreen from anywhere
- Resolution change preserves current fullscreen/windowed mode
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up fullscreen toggle and resolution dropdown in Unity
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add ambient volume setting for weather sounds
- Add ambientSlider to SettingsPanelController with static AmbientVolume property
- Weather effects (rain, thunderstorm, blizzard) respect ambient volume
- Volume updates in real-time when slider is adjusted during playback
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add distant thunder sound effect
Source: Freesound #351526 by LittleRainySeasons (CC BY 4.0)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add more thunder sound effects
- thunder_loud.mp3 - mokasza (CC BY 4.0, Freesound #810746)
- thunder_crack.wav - OneSoundToRuleThemAll (CC BY 4.0, Freesound #238796)
- thunder_clap.wav - FreqMan (CC BY 4.0, Freesound #32544)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up thunder sounds and ambient volume slider in Unity
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When all human factions are eliminated or have no provinces ruled by
faction leaders, the game now ends automatically instead of continuing
to run AI commands indefinitely.
Changes:
- Add endGame() method to Engine trait and EngineImpl to create a
GameEnded action result
- Add allHumansDefeated() check in GameController.performAiCommands
- Check both before the AI loop (handles game load case) and after
each batch of AI commands (handles AI destroying human factions)
- Handle edge case where game state has no factions yet (initialization)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add resolution change detection to hex grid
Detects when the mapArea size changes (e.g., resolution change) and
rebuilds the grid with new metrics. Preserves terrain textures while
recreating all UI elements with correct sizes for the new resolution.
- Track mapArea size and detect changes in Update()
- RebuildGrid() destroys and recreates cells with new HexMetrics
- OnGridRebuilt event notifies ShardokGameController to refresh labels
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update Unity scene and remove unused turnHistoryButtonText
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds notification generator and dispatcher registration for the VassalRises
notification added in #5642. Displays appropriate messages when a vassal
rises to lead a faction after all leaders are killed or imprisoned.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add sound effect hooks to WeatherEffectAnimator
Audio clips for weather sounds (assign in Inspector):
- rainLoopSound: gentle pitter-patter for normal rain
- thunderstormLoopSound: more intense rain for thunderstorm
- thunderSound: one-shot thunder clap (plays on lightning flash)
- blizzardLoopSound: intense wind sound
Volume controls:
- loopVolume: volume for looping sounds (default 0.5)
- thunderVolume: volume for thunder (default 0.8)
Sound behavior:
- Rain: plays rainLoopSound continuously
- Thunderstorm: plays thunderstormLoopSound + thunderSound on each flash
- Snow: no sound
- Blizzard: plays blizzardLoopSound continuously
- Sun/Clouds: no sound
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Support multiple thunder sounds with random selection
- Change thunderSound field to thunderSounds array
- Each lightning flash picks a random sound from the array
- Allows assigning Lightning Spelll 01-08 for variety
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add rain loop sound effect from Freesound
- rain_loop.ogg by aesqe (CC BY 4.0)
- Source: https://freesound.org/people/aesqe/sounds/37618/
- Rain falling on clay roof tiles, loopable
- Added attribution to docs/ASSET_AUDIT.md
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Document CC BY 4.0 sound effects for weather and runaway
Added to ASSET_AUDIT.md:
- rain_loop.ogg by aesqe (Freesound #37618) - for rain effect
- blizzard_wind_loop.wav by nsstudios (Freesound #651540) - for blizzard
- runaway.mp3 by Yap_Audio_Production (Freesound #218997) - replaces unlicensed file
Note: Files must be downloaded manually from Freesound (requires login).
Only 1 unlicensed sound effect remaining: failure_horn.mp3
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove stray download file
* Add rain and blizzard wind sound effects
- rain_loop.ogg by aesqe (CC BY 4.0, Freesound #37618)
- blizzard_wind_loop.wav by nsstudios (CC BY 4.0, Freesound #651540)
Location: Assets/Shardok/Sounds/
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Replace runaway.mp3 with licensed version
MedievalArmyRunningLoop by Yap_Audio_Production (CC BY 4.0)
Source: https://freesound.org/people/Yap_Audio_Production/sounds/218997/
This resolves the licensing issue for runaway.mp3.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up weather sound effects in scene
- Assign rain_loop.ogg to rainLoopSound
- Assign blizzard_wind_loop.wav to blizzardLoopSound
- Configure thunder sounds array
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add vassal rises to leader when faction has no available leaders
When all faction leaders are killed or imprisoned, the strongest vassal
(by HeroUtils.power) is now promoted to faction leader instead of
destroying the faction. This prevents "zombie factions" that have
provinces and armies but can't act due to imprisoned leaders.
Changes:
- Add VassalRises notification and VassalRisesMessage LLM request types
- Add proto definitions for new types in notification and LLM protos
- Add proto converters for VassalRises types
- Create VassalRisesPromptGenerator for LLM narrative generation
- Modify CheckForFactionChangesAction to detect imprisoned leaders and
promote vassals when no available leaders remain
- Generate appropriate notifications and LLM messages for vassal rises
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Enhance VassalRises prompt with previous leader details
Add previousLeaderIds to VassalRisesMessage to track fallen/imprisoned
leaders. The prompt generator now includes detailed descriptions for
each previous leader including:
- Their name and full backstory via HeroDescriptionGenerator
- Status: KILLED IN ACTION or IMPRISONED in [province] by [faction]
This gives the LLM more context to generate a message that properly
acknowledges and references the fallen/captured leaders.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add weather visual effects to Shardok battles
Adds full-screen weather effects based on the current weather conditions:
- SUN: Subtle brightness overlay
- CLOUDS: Slight dimming overlay
- RAIN: Falling rain particles with wind influence
- THUNDERSTORM: Heavy rain + periodic lightning flashes
- SNOW: Falling snowflakes with drift animation
- BLIZZARD: Heavy snow with strong horizontal wind
The WeatherEffectAnimator creates particles on a dedicated canvas
overlay and updates them continuously. Wind speed from the weather
data influences particle movement.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add procedural sprite generation and pre-made sprite assets
- Add PNG sprites for rain drops and snowflakes in Assets/Shardok/Sprites/
- Add procedural sprite generation as fallback if sprites not assigned
- Update tooltip to indicate sprites are optional
- Clean up generated textures in OnDestroy to prevent memory leaks
The component now works out of the box without requiring any assets
to be manually assigned. Sprites will be generated procedurally if
the inspector fields are left empty.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove procedural sprite generation, fix canvas render mode
- Remove procedural sprite generation code (use pre-made PNGs instead)
- Force canvas to Screen Space - Overlay mode so particles fall
correctly relative to the screen instead of the 3D world
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Weather Canvas and wire up WeatherEffectAnimator in scene
- Add Weather Canvas with Screen Overlay for weather effects
- Wire up rainDropSprite and snowFlakeSprite references
- Connect weatherEffectAnimator to ShardokGameController
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix particle positioning to use screen coordinates
- Anchor particles to bottom-left (0,0) so anchoredPosition is in
screen pixel coordinates
- Use Screen.width/height directly instead of canvas rect size
- Remove unused _canvasRect field
This ensures particles appear correctly on screen regardless of
canvas RectTransform configuration.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix particle positioning using canvas rect size
- Use canvas RectTransform rect.size instead of Screen dimensions
- This properly accounts for CanvasScaler settings
- Anchor particles to center (0.5, 0.5) and position relative to center
- Wrap-around logic uses half-width/height from center
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Double particle count and size for all weather effects
- Rain: 100→200 particles, 2x12→4x24 size
- Thunderstorm: 200→400 particles
- Snow: 80→160 particles, 3-8→6-16 size
- Blizzard: 250→500 particles
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove PregeneratedText proto and unused service handling
Now that clients no longer request pregenerated texts (they're sent via
normal ClientTextStore streaming), remove the unused proto messages:
- PregeneratedTextRequest/Response from eagle.proto
- pregenerated_text.proto file entirely
- Empty case handler in EagleServiceImpl.scala
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove pregenerated_text.proto reference from protos.csproj
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace all icons in Assets/free_icons/ (unknown license) with
equivalents from purchased Asset Store packs and delete the folder:
- blizzard.png → 16_blizzard_nobg.png (4000_Fantasy_Icons)
- rain.png → 12_Magic_rain_nobg.png (4000_Fantasy_Icons)
- thunderstorm.png → 27_Storm_nobg.png (4000_Fantasy_Icons)
- wind.png → 23_Light_blow_nobg.png (4000_Fantasy_Icons)
- thermometer.png → startFire.png (existing licensed asset)
These icons were only used in Map Editor (dev tool).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace the free_icons/snow.png (unknown license) with
16_blizzard_nobg.png from 4000_Fantasy_Icons (purchased Asset Store
pack) in all locations:
- Gameplay.unity (Province Info blizzard indicator)
- Dominion Wide.prefab
- Dominion Two Rows.prefab
- Map Editor.unity
Delete the now-unused snow.png file.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Pregenerated texts are now populated into each game's ClientTextStore
and sent via normal text streaming (see #5630). This removes the
separate PregeneratedTextRequest/Response mechanism.
- Remove PregeneratedText import from EagleServiceImpl
- Replace PregeneratedTextRequest handler with no-op for backward compatibility
- Remove getPregeneratedClientText method from GamesManager
- Remove unused pregenerated_text_scala_proto dependency from BUILD
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update asset audit to reflect current state
- 2 sound effects remaining to replace (failure_horn.mp3, runaway.mp3)
- List the 8 weather icons in free_icons folder
- Reorganize action items into Must Replace / Low Priority / Resolved
- Update recommendation section
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Delete unused weather icons (cloud.png, sun.png)
These icons were not referenced anywhere in the codebase.
Update asset audit to reflect 6 remaining weather icons.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove client-side pregenerated text mechanism
Pregenerated texts (hero names, backstories) are now sent to clients
via normal ClientTextStore streaming (thanks to #5630), so the separate
pregenerated text request/response mechanism is no longer needed.
- Remove ClientPregeneratedText.cs class and its Unity component
- Remove PregeneratedTextRequest sending from ConnectionHandler
- Remove PregeneratedTextResponse handling from PersistentClientConnection
- Remove ILobbySubscriber.ReceivePregeneratedTextUpdate method
- Update ClientTextProvider.GetTextEntry to not check pregenerated cache
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add resolved hero names to AvailableLeader for lobby display
The previous commit removed client-side pregenerated text storage, but
lobby UI (hero dropdowns for Create/Join game) still needed hero names.
This fix has the server resolve names and send them in AvailableLeader.name,
so the client can display them immediately without text lookups.
- Add name field to AvailableLeader proto message
- Server resolves names from ClientTextStore (running games) or
PregeneratedClientTextStore (new game options)
- HeroDropdownController uses PreResolvedNames when available
- RunningGameItem/WaitingGameItem use leader.Name with fallback
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The river crossing warning is only relevant when assaulting enemy
territory. When marching to a province owned by the same faction,
there's no assault so the river crossing isn't a concern.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When creating or loading a game, copy the relevant pregenerated texts
(hero names, backstories, initial chronicle) from the global store into
the game's ClientTextStore as complete texts.
This ensures:
- Games are self-contained and immune to TSV file changes
- Texts are saved with the game and sent to clients normally
- Old games get the pregenerated texts they need on load
This is step 1 of removing the separate pregenerated text mechanism.
The global store is still used as a lookup source, but texts are now
stored in the regular completeTexts map for each game.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Layout tweaks to game list prefabs and Gameplay scene
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix provider icon bug and handle invalid stored accounts
Bug fixes:
1. Provider icon bug: Session transfer was storing "session_transfer" as
the provider instead of using the actual provider (discord, twitch,
etc.) from the response. Now uses response.User.Provider.
2. User not found: When stored account validation fails with "user not
found" (NotFound gRPC status), the invalid account is now removed from
storage instead of just clearing the current selection.
3. NullReferenceException: When _createConnection() fails (no valid token),
_internalConnectEagle() was continuing to use _persistentClientConnection
which was null. Now returns early if connection creation failed.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When suggesting a display name from the OAuth provider's username,
sanitize it to meet our validation requirements:
- Replace spaces with underscores
- Remove invalid characters (only allow alphanumeric + underscore)
- Collapse multiple underscores into one
- Trim leading/trailing underscores
- Truncate to 20 characters max
- Return empty if result is less than 3 characters
This prevents users from seeing a pre-filled invalid name that they
would need to manually fix.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
After a user completes the invitation flow (OAuth sign-in, display name
selection), send them a welcome email with:
- Download instructions for Windows and Mac
- Download links for both platforms
- Deep link button to complete sign-in automatically
- Info about which provider/account they used
Also extends session transfer code expiration from 5 minutes to 1 hour
to give users time to install the game before the deep link expires.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When minimizing the game for OAuth browser flow, we were storing
fullscreen mode but not resolution. When restoring, we only set
Screen.fullScreen=true, which uses whatever windowed resolution
Unity had stored (often 1280x720).
Now we:
- Store both fullscreen mode AND resolution (width/height) before exiting
- Use Screen.SetResolution() to restore both together
This affects both WindowFocusManager (OAuth flow) and ConnectionHandler
(new user sign-in flow).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Remove confirmation dialog for deleting non-pending invitations (already
redeemed/expired/revoked, no need to confirm)
- Reload page after delete actions instead of htmx partial updates (the
search endpoint returns rows with different columns than the accounts page)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update the sign-in tutorial text to:
- Mention the "Complete Sign-In with Eagle0" button on the download page
- Clarify they should use the same account they created
Also exit fullscreen when showing the auth panel to new users (no stored
accounts) so they can see the browser with the sign-in link, then restore
fullscreen after successful login.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: HandleStartingState unconditionally overwrites ChronicleEntries.
During startup, multiple StartingState messages can arrive - a stale one
(e.g., from stateAfter(1) early in the game before chronicles existed)
arriving after a current state would wipe out existing entries.
Fix:
1. Don't overwrite existing chronicle entries with an empty list, since
chronicles are historical records that accumulate (never disappear)
2. Close the chronicle canvas if entries are cleared while visible
(defense-in-depth + correct UX behavior)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
On Windows, when a second instance is launched via deep link, the URL
needs to be passed to the existing instance. Previously we just
activated the window and exited, losing the URL.
Now uses named pipes (the standard .NET IPC mechanism):
- First instance creates a NamedPipeServerStream and listens
- Second instance connects, sends the URL, then exits
- First instance receives the URL and processes it via OnDeepLinkReceived
This is the same pattern used by Electron's requestSingleInstanceLock().
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add unique IDs to filter inputs to prevent hx-include conflicts
- Fix invitation actions to only refresh invitations table, not users
- Use ID-based selectors instead of name-based for HTMX includes
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When the app is launched via a deep link (e.g., from the invitation
download page), the deep link can arrive in Awake() before
ConnectionHandler calls SetServerUrls(). This caused the session
transfer to silently fail because _authClient was null.
Fix by queuing the session transfer code and processing it once
SetServerUrls() is called and the auth client is available.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Merges the separate Users and Invitations pages into a unified
two-column layout under a single "Accounts" navigation tab.
- New /accounts route with side-by-side Users and Invitations panels
- Each panel has independent search/filter functionality
- Responsive layout stacks columns on smaller screens
- Keeps existing /users and /invitations routes for HTMX partial updates
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Use gRPC for session transfer instead of HTTP
The session transfer was using an HTTP endpoint on port 8080 which isn't
exposed to clients. Convert to use gRPC on port 40033 like all other auth
methods.
- Add ExchangeSessionTransferCode RPC to auth.proto
- Update AuthClient.cs to call gRPC instead of HTTP
- Add gRPC handler in Go auth service
- Remove HttpClient and SessionTransferResponse class from C#
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add exchangeSessionTransferCode to Scala auth service
The Scala Eagle server also implements the Auth gRPC service and needs
the new method. Add a stub that proxies to the external Go auth service
when available, or returns UNIMPLEMENTED when not configured.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add single-instance enforcement for Windows deep links
On Windows, URL schemes just launch the executable - they don't redirect
to an existing instance like macOS does. This causes deep links to open
a new game window instead of activating the existing one.
Added SingleInstanceEnforcer which:
- Uses a named mutex to detect if another instance is running
- If launched via deep link (eagle0://) and another instance exists:
- Finds the existing window via FindWindow
- Brings it to foreground via SetForegroundWindow
- Exits the duplicate instance
- Normal launches (without deep link) still allow multiple instances
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix single-instance detection: use process name instead of window title
- Use Process.GetProcessesByName instead of FindWindow - more reliable
- Use Environment.Exit(0) instead of Application.Quit() - works early in lifecycle
- Increase sleep to 500ms to ensure window activation completes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
After completing OAuth on the web invitation flow, users can now click a
"Complete Sign-In in Eagle0" button to automatically sign in to the game
without going through OAuth again.
Implementation:
- New SessionTransferService generates one-time codes after OAuth
- Codes are 256-bit random, expire in 5 minutes, single-use
- Download page shows deep link button: eagle0://auth/session/{code}
- Unity client handles deep link, exchanges code for tokens via HTTP
- Login screen shows hint for users coming from invitations
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Two issues fixed:
1. WebView Close() was calling Destroy() directly from a goroutine,
but this didn't properly terminate the event loop on the main thread.
Now uses Dispatch(Terminate()) to properly exit the event loop.
2. handleInstallerReplacement was using fmt.Println instead of the UI,
so the WebView showed "Initializing..." while the actual logic was
invisible. Now uses ui.UpdateStatus/ShowError/etc for visibility.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds LoadIntoImage alongside LoadIntoRawImage for loading runtime
textures into Unity Image components (which support Preserve Aspect
in layout groups). Factored out common functionality into helpers:
- CreateTexture: creates Texture2D from bytes or placeholder
- CreateSprite: creates Sprite from Texture2D
- CleanupRawImageTexture/CleanupImageSprite: cleanup helpers
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Changed Debug.LogError to Debug.LogWarning for handled OAuth errors.
The SimpleErrorHandler subscribes to Unity log messages and shows
popups for errors, but OAuth timeouts are expected conditions that
are already displayed in the status text.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The auto-redirect with window.close() was closing the page before
users could accept the browser's permission dialog to open the app.
Changed to a button-based approach where the user clicks to return
to the app, giving them control over when the deep link triggers.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Overlay Mesh transform for Shardok map
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Province Info Panel being disabled on dominion toggle
Changed DominionViewToggleClicked to hide only the panel content
instead of disabling the entire controller GameObject.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add iOS Addressables content state and image meta file
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update Unity-generated files (csproj, fonts, addressables settings)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Only log timing warnings when operations take more than 500ms,
reducing noise from normal operation while still catching slow
operations worth investigating.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Register eagle0:// URL scheme on Windows for OAuth deep links
The Windows installer now registers the eagle0:// URL scheme in the
registry (HKEY_CURRENT_USER\Software\Classes\eagle0) pointing to the
game executable. This allows OAuth callbacks to redirect back to the
app automatically.
Also updated OAuthManager to check command line arguments for deep
links since Windows passes URL scheme launches as command line args.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add golang.org/x/sys dependency for Windows registry access
The Windows installer uses golang.org/x/sys/windows/registry to register
the eagle0:// URL scheme. This adds the required dependency to go.mod,
go.sum, and MODULE.bazel.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
After OAuth authentication completes, the browser now redirects to
eagle0://auth/complete to bring the app back to foreground. This
provides a better UX than asking users to manually close the browser.
- Unity client registers for deep link events and focuses window
- DeepLinkPostProcessor adds URL scheme to Info.plist on macOS/iOS
- All OAuth handlers (Google/Discord, Apple, Steam) redirect via deep link
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change HexMetrics to accept RectTransform instead of Canvas
- Rename mapCanvas to mapArea in HexGrid (RectTransform type)
- Constrain map area to exclude sidebar for proper sizing at
different aspect ratios
This allows the map to be properly sized based on the available
space, accounting for the sidebar at various screen resolutions.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add three-tier layout mode (Wide/Medium/Narrow) for command panels
- Add LayoutMode enum: Wide (>2.0), Medium (>=1.35), Narrow (<1.35)
- EagleGameController exposes CurrentLayoutMode property
- CommandSelector base class receives layout mode from CommandPanelController
- Adds IsNarrowLayout helper for selectors to check narrow mode
In narrow mode (iPad-like 4:3 aspect ratios), selectors can hide
hero/battalion pickers since users can select via the Resident
Heroes and Battalions panels instead.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add heroesColumn and battalionsColumn fields to March/Defend selectors
These GameObject fields allow hiding the hero and battalion selection
columns in narrow layout mode. Wire them up in Unity editor to the
appropriate column GameObjects.
Also fixes DefendCommandSelector import: UnityEditor -> UnityEngine
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Hide hero/battalion columns in narrow layout mode
In SetUpUI(), hide heroesColumn and battalionsColumn when IsNarrowLayout
is true. Users can still select heroes and battalions via the Resident
Heroes and Battalions side panels.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Wire up heroesColumn and battalionsColumn in Unity
Connect the column GameObjects to MarchCommandSelector and
DefendCommandSelector so they can be hidden in narrow layout mode.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add new icon.png for app icon
- Remove old unused image asset
- Update ProjectSettings for splash screen
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add turn mismatch detection with auto-reconnection
Detects when server reports YourTurn but client has no available
commands. After a 3-second grace period (to avoid false positives
during normal update processing), logs an error via ErrorHandler
and triggers ForceReconnect() to recover state.
This addresses intermittent bugs where exceptions or token mismatches
leave the connection status showing "Your Turn" but the UI doesn't
display any turn actions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Read ServerGameStatus from GameUpdate level, add BattleInProgress display
- Move ServerGameStatus reading from ActionResultResponse to GameUpdate
level so status updates work for all update types including Shardok
- Add "Battle in progress" display for the new BattleInProgress status
This fixes the turn mismatch false positives during Shardok battles -
the server now sends BattleInProgress instead of YourTurn during battles.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add new BATTLE_IN_PROGRESS status to ServerGameStatus enum
- Add server_game_status field to GameUpdate so any update type can
include status (not just ActionResultResponse)
- Report BATTLE_IN_PROGRESS when there are outstanding Shardok battles
instead of YOUR_TURN
- Set serverGameStatus on both GameUpdate and ActionResultResponse
for backwards compatibility
This fixes the issue where "Your Turn" was shown during battles,
including observed battles where the player isn't participating.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor widescreen layout to use enable/disable instead of reparenting
Instead of moving UI panels between containers at runtime, use duplicate
panels positioned in Unity and enable/disable the appropriate version
based on aspect ratio.
Changes:
- Add separate narrow/wide versions of FactionsTable, MovingArmiesTable,
and DominionPanel
- ArrangeLayout() now just enables/disables panels instead of reparenting
- Remove unused container GameObjects (factionsAndMovingArmiesRow, etc.)
- Both panel copies are kept in sync with model data
This approach is simpler to maintain and allows layouts to be fully
configured in the Unity editor.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update Unity scene with layout adjustments
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Trim map textures to remove excess water border
Crop all map textures (map_bw_labels.png and province textures 1-43.png)
from 4096x2064 to 3786x1834, removing:
- Left: 200px
- Top: 130px
- Right: 110px
- Bottom: 100px
This leaves a small water margin around the landmass edges.
Also update newMapPrefab.prefab to set AnchoredPosition and SizeDelta to 0
instead of previous offset values.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Trim rawGray.bytes to match trimmed map textures
Apply same crop (left 200, top 130, right 110, bottom 100) to the
province hit-test data so pixel lookups remain aligned with the
trimmed map images.
Dimensions: 4096x2064 → 3786x1834
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update texture import settings and RectTransforms for trimmed map
- Set Non-Power of 2 to "None" for map texture to preserve actual
dimensions (3786x1834) instead of scaling to 4096x2048
- Update RectTransforms to remove offsets now that images are trimmed
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
After moving ShardokGameController from ShardokCanvas to ShardokContainer,
several button onClick events lost their target references. Re-wire:
- EndTurn button (Back to Eagle / End Turn)
- DisplayActionsButtonClicked (9 action buttons)
- WarningPanelCommitClicked
- WarningPanelCancelClicked
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add .meta files for:
- BuildInfo.cs (build info feature)
- Editor/ folder and its scripts
- TouchAwareTooltip.cs
Also add ServerData/ to .gitignore to prevent local data from being tracked.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Move ShardokGameController from ShardokCanvas to ShardokContainer
When exiting a battle, ShardokGameController calls this.gameObject.SetActive(false).
Previously this deactivated ShardokCanvas, leaving it inactive for the next battle.
Since ShardokCanvas is now inside ShardokContainer, GetComponentInChildren couldn't
find the controller because the canvas was inactive.
Fix: Move ShardokGameController to ShardokContainer so SetActive(false) deactivates
the whole container. Also update code to use GetComponent instead of GetComponentInChildren.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use serialized eagleCommonTextures field instead of GetComponent
After moving ShardokGameController to ShardokContainer, EagleCommonTextures
was also moved. Replace runtime GetComponent calls with a serialized field
reference for better performance and clearer dependencies.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds explicit null checks to identify exactly which reference is null:
- Model
- shardokContainer
- shardokController (from GetComponentInChildren)
This replaces generic NullReferenceException with specific error messages.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add BuildInfo.cs with commit hash, short hash, and build timestamp
- Add BuildInfoGenerator.cs to auto-generate BuildInfo before builds
- Update ErrorHandler to include commit hash in error messages
This helps identify which exact build a production error came from.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The ShardokContainer reference was missing (fileID: 0), causing a
NullReferenceException when clicking "Return to Lobby" in settings.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Root cause: SwapModel() returns early when Model is null (line 504),
but goToBattleButton visibility is updated after that early return
(line 626). When StopAll() sets Model to null via SwapModel(), the
button was never hidden.
Fix: Hide the goToBattleButton in the early return path when Model
becomes null.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When the underlying GrpcChannel is disposed (e.g., during environment
switch), the PersistentClientConnection now invokes an OnChannelDead
callback instead of futilely attempting to reconnect on the dead channel.
ConnectionHandler sets this callback to recreate the EagleConnection
and PersistentClientConnection, properly recovering from channel disposal.
Fixes ObjectDisposedException spam when channel dies.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adjust the overlay mesh local position to align correctly with the
hex grid on widescreen displays.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add HorizontalLayoutGroup to Improve Panel to prevent overlapping
elements on ultrawide displays (e.g., 3440x1440). The improvement
type buttons were covering the lock toggle and hero dropdown when
the screen was wider than the reference resolution.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Create separate Overlay Canvas with smaller plane distance so text
labels render in front of 3D bridge objects.
Changes:
- Add ShardokContainer parent to hold both canvases
- Rename shardokCanvas references to shardokContainer
- Add overlayContent RectTransform field to HexGrid for label parenting
- Update all scripts to use shardokContainer for activation
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Only trigger on changes to:
- AddressableAssetsData config
- Music assets (the addressable content)
- BuildScript.cs
- Build scripts
Previously triggered on any Unity file change, which was too broad.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When placing default bridges, check for opposite pairs of land tiles
and prefer orientations where both endpoints touch land, rather than
just orienting toward the first land tile found.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The -buildTarget iOS flag doesn't reliably switch the active build
target before editor scripts run. The addressables were being built
for StandaloneOSX instead of iOS.
Added BuildiOSAddressables() method that explicitly calls
EditorUserBuildSettings.SwitchActiveBuildTarget() before building.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update ASSET_AUDIT.md: mark Terrain Hexes and Discord logo as verified
- Terrain Hexes: confirmed Unity Asset Store purchase
- Discord logo: complies with Discord brand guidelines
- Updated action items and recommendations to reflect remaining work
Remaining items:
- 6 music tracks (1 unknown source, 5 Audius non-CC license)
- 56 Shardok sound effects (unknown origin)
- 138 StrategyGameIcons (unknown source)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update ASSET_AUDIT.md: Medieval Victory Theme is CC0 Public Domain
Found source: RandomMind on Chosic, CC0 Public Domain license.
No attribution required, free for commercial use.
Remaining items:
- 5 Dima Koltsov/Audius tracks (non-CC license)
- 56 Shardok sound effects (unknown origin)
- 138 StrategyGameIcons (unknown source)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update ASSET_AUDIT.md: Dima Koltsov tracks mostly verified as CC BY 4.0
- No Time for Greatness: CC BY 4.0 (YouTube)
- Warriors of Demacia: CC BY 4.0 (YouTube)
- Valor: CC BY 4.0 (YouTube)
- Forest Queen Tale: Presumed CC BY 4.0 (not found on YouTube)
- Clouds: Presumed CC BY 4.0 (not found on YouTube)
Remaining items:
- 56 Shardok sound effects (unknown origin)
- 138 StrategyGameIcons (unknown source)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update ASSET_AUDIT.md: StrategyGameIcons verified as Asset Store purchase
Confirmed as "Strategy Game Icons" by REXARD from Unity Asset Store.
Remaining items:
- 56 Shardok sound effects (unknown origin)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update ASSET_AUDIT.md: identify 3 sounds from Zombie Monster Undead Collection
Verified from Unity Asset Store purchase:
- raise_undead.mp3
- undead_break_control.mp3
- undead_grew.wav
Also corrected count: 37 audio files (not 56 - was counting .meta files)
34 sound effects still unverified.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update ASSET_AUDIT.md: flag anybody.mp3 and runaway.mp3 for replacement
These two sound effects have licensing issues and must be replaced.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update ASSET_AUDIT.md: also flag burnination.mp3 for replacement
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update ASSET_AUDIT.md: mark remaining sound effects as presumed Asset Store
Owner believes 31 remaining sounds are from purchased Asset Store packs:
- Fantasy Interface Sounds
- Medieval Combat Sounds
- Magic Spells Sound Effects LITE
- Medieval Battle Sound Pack
Only 3 files need replacement: anybody.mp3, burnination.mp3, runaway.mp3
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Replace anybody.mp3 and burnination.mp3 with licensed alternatives
- anybody.mp3: replaced with Positive Effect 6.wav (Magic Spells Sound Effects LITE)
- burnination.mp3: replaced with Magic Element Fire 04.wav (Medieval Combat Sounds)
Still need replacements for:
- failure_horn.mp3
- runaway.mp3
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add bridge rotation based on builder position or adjacent land
Bridges now rotate to face meaningful directions:
- For bridges built by engineers: one end faces the builder's location
- For bridges present at battle start: one end faces adjacent land
Implementation:
- Add rotationDegrees parameter to HexGrid.SetCellModifier3D()
- Track bridge builder locations in _bridgeBuilderLocations dictionary
- Add GetHexNeighbors() to find adjacent hex cells
- Add CalculateBridgeRotation() to compute angle between cells
- Add CalculateBridgeRotationFromLand() to find land for initial bridges
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify bridge rotation to 3-orientation lookup table
Replace complex trigonometry with simple lookup based on hex grid
direction. Only 3 possible orientations exist:
- Horizontal (0°): left ↔ right
- Diagonal up (124°): down-left ↔ up-right
- Diagonal down (56°): up-left ↔ down-right
Add BridgeRotationCalculator with unit tests for all 6 hex directions
from both even and odd rows.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add iOS addressables build workflow
Creates a separate workflow to build and upload iOS addressables to CDN.
This is needed for iOS clients to load remote assets.
- Adds build_ios_addressables.sh script
- Adds ios_addressables_build.yml workflow
- Uploads to https://assets.eagle0.net/addressables/iOS/
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix iOS addressables build: add proto build step
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Document that bridge.png was replaced with AI-generated wooden rope
bridge icon created with ChatGPT/DALL-E 3.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
New 512x512 PNG icon featuring an isometric wooden rope bridge,
better suited for the fantasy game aesthetic. Generated with
ChatGPT/DALL-E, has transparent background.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace 2D bridge texture overlay with 3D bridge prefabs for more
visually appealing bridges on water tiles. The system now supports
an array of bridge prefabs for variety, selected deterministically
based on cell index.
- Add TerrainModifier3D field to HexGrid Cell struct
- Add SetCellModifier3D() and ClearCellModifier3Ds() methods
- Change ShardokGameController.bridgeImage to bridgePrefabs array
- Bridge positioned at true cell center using Geometry.AnchoredPosition
- Assign 6 bridge prefabs from TileableBridgePack in Gameplay scene
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
During blue-green deployments, warmup games may be invalidated while
LLM requests are in flight. The responses are correctly ignored, but
the log messages were noisy. Remove them since this is expected behavior.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The mobile shaders in TextMesh Pro/Shaders/ reference these include
files with relative paths but they only exist in Resources/Shaders/.
Copy them here to fix shader compilation errors.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Usage:
eagle-logs # Tail logs (follow mode)
eagle-logs -n 100 # Show last 100 lines and follow
eagle-logs --no-follow -n 50 # Last 50 lines without following
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Replace fire icon with UXWing flame and consolidate duplicates
Replaced the startFire.png icon with a clean flat-style flame from UXWing
(free for commercial use, no attribution required). Consolidated from two
duplicate copies to a single canonical copy in Assets/Images/, updating all
Unity scene references.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update ASSET_AUDIT.md with UXWing flame icon source
Document that startFire.png was replaced with the flame icon from UXWing
(https://uxwing.com/flame-icon/) which is free for commercial use with no
attribution required. Also note the consolidation from two duplicate copies
to one canonical copy.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Remove sudo symlink creation (deploy user lacks passwordless sudo)
- Simplify eagle-exec to read from /opt/eagle0/.active-instance file
- Update deploy-blue-green.sh to write active instance to file
- Keep fallback to checking running containers if file doesn't exist
Users can add their own alias:
alias eagle-exec='/opt/eagle0/scripts/eagle-exec.sh'
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add iOS/mobile support with touch-aware tooltips
- Add melee button to Shardok action bar (button 4 with [SHFT] label)
- Changed MeleeAttackGroup from -1 to 4 to show melee commands as button
- Updated Gameplay.unity to assign melee/charge commands to group 4
- Add touch-aware tooltip system using long-press pattern
- Updated HoveringTooltipTextProvider to detect touch vs mouse input
- On desktop: tooltips appear immediately on hover
- On mobile: tooltips appear after 0.4s long-press
- Created TouchAwareTooltip.cs as reusable component
- Add iOS build method to BuildScript.cs
- Generates Xcode project for building/signing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add dedicated melee button (button9) for mobile/tablet support
- Add button9 and buttonText9 fields to ShardokGameController
- Change MeleeAttackGroup from 4 to 9 for dedicated button
- Change EndTurnCommandGroup from 9 to 12 (handled separately)
- Update melee/charge commands in Gameplay.unity to group 9
- Update End Turn command to group 12
- Add null checks for unassigned buttons in UpdateActionButtons
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix melee/charge command groups in Gameplay.unity
Set commandGroup to 9 for melee and charge commands so they
appear on the dedicated melee button and indicators work correctly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds a helper script that automatically runs docker exec against the
active Eagle instance (blue or green), determined by checking nginx
config.
Usage:
eagle-exec printenv GEMINI_API_KEY
eagle-exec jcmd 1 VM.flags
eagle-exec sh
The script is deployed to /opt/eagle0/scripts/ and symlinked to
/usr/local/bin/eagle-exec for easy access.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When LlmProvider was set to "gemini", the validation code was missing a
case for extracting the GeminiModelName, causing it to default to an
empty string. This resulted in malformed API URLs like:
https://generativelanguage.googleapis.com/v1beta/models/:streamGenerateContent
which returned 404 errors.
Added the "gemini" case to properly extract GeminiModelName from
settings.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When switching LLM providers in the admin console, if the API key for
the target provider isn't configured, the validation would crash with
ExceptionInInitializerError. This fix:
- Wraps service creation in Try to catch initialization errors
- Logs the error with context (likely missing API key)
- Reports to Sentry for monitoring
- Returns a user-friendly error message
- Falls back to keeping the old setting unchanged
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The LLM settings validation in GameAdminServiceImpl was missing the
"gemini" provider case, causing "Unknown provider: gemini" errors when
trying to switch to Gemini from the admin console.
Changes:
- Add GeminiServiceImpl import
- Add "gemini" case to validateLlmSettings()
- Add "GeminiModelName" to llmSettingKeys set
- Add gemini_service_impl dep to BUILD.bazel
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace 13 heroes that referenced real people or copyrighted characters
with original alternatives that preserve gameplay stats and general archetypes:
Real people replaced:
- Jesse "The Body" Ventura → Magnus "The Boulder" Varnok
- Jeb! → Aldric the Overlooked
- Ringo → Tomlin the Tuneful
- Flea → Zander the Restless
Copyrighted characters replaced:
- Darth Plagueis → Voros the Undying
- Deckard Cain → Old Marek the Learned
- Skeletor → Karvax the Fleshless
- Trap Jaw → Ironmaw
- Shu Lien → Mei Shen
- Roger the Shrubber → Hedrick the Hedge-Merchant
- Kriss Kross → The Tumbler
- Daddy Mac → Silvio the Smooth
- Ul (removed Warcraft "Far Seer" reference)
Image files in eagle0-headshots bucket have been renamed to match.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor LLM update interface to use Scala domain types instead of protos
Replace proto types (GeneratedTextRequest, LLMResponse) with Scala domain
type (GeneratedTextRequestT) throughout the LLM streaming interface. This
eliminates unnecessary proto conversions and simplifies the code.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove unused llm_response_scala_proto dependency from llm_resolver
The LLMResponse proto is no longer used since we now pass Scala domain
types through the LLM update interface.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Filter LLM requests for deleted games before sending
Add a game validity check to LlmResolver to prevent sending LLM requests
for games that have been deleted (e.g., warmup games during blue-green
deployments). This avoids wasted API calls and eliminates the "Ignoring
LLM response for deleted game" log messages at startup.
Changes:
- Add LlmResolverGameDeleted result type to LlmResolver
- Add isGameValid callback parameter to LlmResolver constructor
- Check game validity before sending each request
- Handle LlmResolverGameDeleted in UnrequestedTextHandler
- Pass gameControllerInfos.contains check from GamesManager
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Move proto conversion inside TextGenerationSuccess case
The proto conversion is only needed for the success case where we
actually send the request. Moving it avoids unnecessary conversion
for dependency-blocked requests.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Move claude-3-5-haiku to the top of the dropdown since it's the
fastest model for streaming responses (~1.9s TTFT vs ~5.0s for sonnet).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add ANTHROPIC_API_KEY to docker-compose environment
Required for Claude LLM provider to work in production.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add ANTHROPIC_API_KEY to deployment workflow
Pass the secret to the deploy job and export it for docker-compose.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Only send reasoning_effort for reasoning models (o1, o3)
The reasoning_effort parameter is only valid for OpenAI reasoning models.
Sending it to non-reasoning models like gpt-5-mini causes a 400 Bad Request.
Now only includes reasoning_effort when the model name starts with "o1" or "o3".
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Treat models without -mini/-nano as reasoning models, add gpt-5.1 option
- Change reasoning model detection: models with -mini or -nano suffix are
non-reasoning; all others (gpt-5.1, gpt-5.2, o1, o3, etc.) get reasoning_effort
- Add gpt-5.1 to admin console dropdown options
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The previous change incorrectly set both total players and human players
to 1. This restores the intended behavior: 7 total players (1 human + 6 AI)
with 1 human player selected by default.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
OkHttp can call onFailure with a null Throwable when there's an HTTP
error response but no actual exception. Handle this case by:
- Null-checking before calling getMessage()
- Creating a synthetic RuntimeException when t is null
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
JFR profiling revealed that the first client connection after deploy was
experiencing 5+ second delays due to lazy initialization of the AWS S3 SDK
(profile loading, TLS handshake to DigitalOcean Spaces, etc.).
This change adds a warmup() method to S3Utils that:
- Forces initialization of the lazy transferManager
- Makes a lightweight headBucket call to complete TLS handshake
The warmup is called early in Main.scala before the server starts accepting
connections, moving the initialization cost to startup time rather than
first-request time.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The --gpt-model-name CLI argument was removed in the LLM model switching
PR (97b313f6), but docker-compose.prod.yml still passed it, causing the
server to fail with "Unknown option --gpt-model-name".
LLM provider and model are now configured via settings (admin console)
instead of CLI arguments.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add LLM model switching from admin console
Implement the ability to switch LLM provider and model dynamically from the
admin console with validation before applying changes.
Changes:
- Add String setting type support (StringSetting.scala, generator updates)
- Add LlmProvider, OpenAiModelName, ClaudeModelName settings
- Modify LlmResolver to read provider/model from settings dynamically
- Add recreateLlmCallers() to allow hot-swapping LLM configuration
- Add validation in GameAdminServiceImpl that tests API before applying
- Remove gptModelName command line flag (now configured via settings)
- Update AddSettingsResponse proto with success/errorMessage fields
The admin console now shows these settings and validates them by making
a test API call before applying. If validation fails, the error is returned
and settings remain unchanged.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add dropdown UI for LLM settings in admin console
Instead of showing LLM provider and model names as text inputs in the
general settings list, display them as dropdowns with common options:
- LlmProvider: openai, claude
- OpenAiModelName: gpt-5-mini, gpt-5.2, gpt-4.1, gpt-4.1-mini, etc.
- ClaudeModelName: claude-sonnet-4-20250514, claude-opus-4-20250514, etc.
This provides better UX by making it easy to switch between known models
without having to remember exact model name strings.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When loading a game, if a hero's name text (hn_X format) is missing from
the text store, the server would throw an exception requiring the game
to be deleted. This occasionally happened when a hero was created but
the name request was somehow lost before persistence.
Changes:
- New MissingHeroNameRecovery utility: Provides a reusable method to
check heroes for missing name texts and recreate GeneratedHeroName
requests. Can be called from game load or other contexts as needed.
- GamesManager: Uses the new utility during game loading to recover
missing hero names instead of throwing an exception.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add explicit length instructions to prompt (IMPORTANT prefix)
- Add reminder at end of prompt to be selective
- Increase default word count from 200 to 400 (appropriate for 1.5 pages)
- Instruct LLM to focus on 2-3 most significant events
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Clarifies that users can right-click a province on the map or use the
dropdown menu to select a destination.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Default to 1-player game in create game dropdown
- Auto-create 1-player game on first launch (skip lobby)
- Add sign-in tutorial for users without stored accounts
- Add lobby tutorial for users who reach the lobby
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The installer is built by installer_build.yml, not the Docker workflow.
Adding exclusion prevents unnecessary Docker builds when only installer
files change.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Generate 64-bit .syso resource file for the WebView installer build.
The previous resource file was 32-bit and caused link errors with the
64-bit CGO cross-compilation.
Changes:
- Add resource_windows_amd64.syso (64-bit COFF object with icon/version)
- Update genrule to copy the .syso file during build
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Fix genrule variable syntax for manifest public key
Genrules use Make variable syntax $(VAR), not curly brace syntax {VAR}.
The curly brace syntax only works in x_defs for go_binary rules.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use --action_env to pass MANIFEST_PUBLIC_KEY to genrule
Bazel genrules don't support workspace status variable substitution
in cmd strings. Instead, use --action_env to pass the environment
variable into the sandbox, where it can be referenced as a regular
shell variable ($$MANIFEST_PUBLIC_KEY in the genrule cmd).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Handle missing MANIFEST_PUBLIC_KEY env var in genrule
When building via 'bazel test //src/main/go/...', the genrule runs
without --action_env=MANIFEST_PUBLIC_KEY set. Use bash default value
syntax ${VAR:-} to default to empty string when the env var isn't set,
allowing the build to succeed with an empty public key.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify public key handling - let empty var expand naturally
Bazel doesn't support ${VAR:-} syntax in genrule cmd. Instead, just
use $MANIFEST_PUBLIC_KEY directly - if not set, it expands to empty
string which the installer handles gracefully.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add manual tag to exclude webview installer from wildcard builds
The webview installer genrule requires --action_env=MANIFEST_PUBLIC_KEY
to be set. Adding tags = ["manual"] excludes it from wildcard patterns
like "bazel test //src/main/go/..." so it won't fail in the test workflow.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
PR #5459 accidentally broke Shardok update streaming when refactoring
postBattleUpdate to make Engine protoless. The original code called
withHandledEngineAndResults() which invoked humanClientsAfterPostingResults
to push updates to connected clients. The refactored code updated the
history but skipped client notification.
This caused clients to only receive Shardok updates on initial subscription
or reconnect, not during ongoing gameplay. The heartbeat would detect sync
mismatches (client=X server=Y) but updates weren't pushed.
Fix: Call humanClientsAfterPostingResults in postBattleUpdate to push
Shardok results to connected human clients, restoring the behavior that
was lost during the refactor.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add stamp = 1 to the genrule so that {STABLE_MANIFEST_PUBLIC_KEY}
gets substituted with the actual value from workspace status.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When the client subscribes with ShardokViewStatuses empty (because
ShardokGameModels hasn't been populated yet), the server sends an
initial ShardokActionResultResponse as catch-up but doesn't know to
keep sending updates for that game.
The model is created when processing ShardokActionResultResponse, but
by then the subscription was already sent without it. The server
won't send further updates, causing sync mismatches detected by
heartbeat until the client reconnects.
Fix: When creating a new ShardokGameModel from ShardokActionResultResponse,
re-subscribe to tell the server to include this game in future updates.
This issue was exposed by commit fb2b0beb85 which added
ShardokGameModels.Clear() in HandleStartingState, ensuring models are
always empty when subscription is initially sent after reconnect.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Units are auto-placed in starting positions by default, so the
tutorial should explain how to move them rather than implying
you need to place them from the Unplaced Units panel first.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The manifest public key wasn't being injected properly because:
1. Workspace status variables need STABLE_ prefix for x_defs stamping
2. BUILD.bazel was using {MANIFEST_PUBLIC_KEY} but the variable
wasn't being output with the STABLE_ prefix
This caused the installer to try to base64 decode the literal string
"{MANIFEST_PUBLIC_KEY}" instead of the actual public key value.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a ShardokGameModel is first created via MakeGameModel, its
GameStatus property is null. It's only set later when history
entries are processed with a non-null gsvDiff.GameStatus.
The ShardokGameModelIsRunning check accessed sgm.GameStatus.State
without a null check, causing a NullReferenceException when
filtering ShardokGameModels. This caused RunningShardokGameModels
to fail and return empty, making the Observe Battle button not appear.
Fix: Add null check before accessing GameStatus.State, matching the
pattern already used in ShardokGameModel.InSetUp property.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When CurrentEagleToken or CurrentShardokToken is null (e.g., during
battles), TryPendingCommands was calling PostRequest which:
1. Adds the command back to _pendingCommands
2. Sends it to the server
3. Server returns BAD_TOKEN
4. Next game update triggers TryPendingCommands again
5. Command is still pending with null token → retry loop
This caused the same command to be posted multiple times, flooding the
server with BAD_TOKEN errors and potentially causing reconnect storms.
Fix: When token is null, add the command back to _pendingCommands
directly without calling PostRequest. The command stays pending until
we have a valid token to compare against.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Rename WebView installer output from EagleInstallerWebView.exe to Eagle0.exe
- Update CI workflow to build and deploy Eagle0.exe
- Update all references in authservice (download URLs, batch scripts, HTML)
- Update installer_build_handler default key and version content
- Update help text and versioninfo.json metadata
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add WebView GUI to Windows installer
Adds a native GUI window to the Windows installer using the webview
library which embeds Microsoft Edge WebView2. The installer now shows
a modern HTML/CSS/JS interface with progress bar and status updates.
Key changes:
- Add llvm-mingw toolchain for macOS → Windows CGO cross-compilation
- Add webview_go dependency for WebView2 integration
- Create UIInterface abstraction with ConsoleUI and WebViewUI impls
- Build tags select stub (non-Windows/non-CGO) vs real WebView impl
- Genrule handles CGO cross-compilation within Bazel sandbox
Build targets:
- eagle_installer_windows_amd64: Pure Go console version (5.8 MB)
- eagle_installer_windows_amd64_webview: CGO WebView GUI (13.7 MB)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix WebView genrule to use Bazel Go SDK
The CI doesn't have Go in PATH, so the genrule needs to use Bazel's
Go SDK explicitly. This change:
- Exposes @go_default_sdk via use_repo in MODULE.bazel
- Adds @go_default_sdk//:files as a srcs dependency to the genrule
- Uses absolute paths for GO and GOROOT so they work after cd
- Filters out Go SDK files when copying source files
- Updates go.mod version to 1.23 to match the Bazel SDK
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove continue-on-error from "Run tests" step so GitHub UI expands
the actual failing step instead of "Fail if tests failed".
The log collection and artifact upload steps use `if: always()` so
they still run after test failures.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Change hero selection in several commands to use HeroUtils.fatigue
(constitution - vigor) instead of absolute vigor. This is more
consistent with how other commands select heroes and properly accounts
for heroes having different max vigor (constitution).
Commands updated:
- HandleRiotCrackDown in CommandChoiceHelpers
- TruceOfferCommandSelector
- AllianceOfferCommandSelector
- Recon in MidGameAIClient
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add relationship context and personality guidance to prevent repetitive
"If you mean to kill me, do it swiftly" patterns:
- Detect if hero was previously in captor's faction (defection context)
- Note previous captures by the same faction
- Reference prior battles between the parties
- Use hero's personality words to guide tone
- Explicitly suggest varied emotional approaches and discourage clichés
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Match Unity client layout: left column (Discord, GitHub, Steam),
right column (Google, Apple, Twitch). Includes responsive fallback
to single column on narrow screens.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The library/ code is now fully protoless (zero Scala proto dependencies).
Transitive deps through C++/Go build tools (map generation) are expected.
Changes:
- Delete docs/DEPROTO_PLAN.md - migration complete
- Delete scripts/build_deps_baseline.txt - no longer needed
- Update check_build_deps.sh to enforce zero Scala proto deps in library/
(C++/Go proto deps are allowed as they're build-time tools)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The Windows installer has been rewritten in Go. Remove the old C#/.NET
installer code and the associated CI build script.
Deleted:
- src/main/csharp/net/eagle0/clients/win/ - .NET installer source
- ci/build_eagle_installer_win.sh - old manual build script
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
After the installer updates itself, the old version is left behind as
EagleInstaller.exe.backup. Now on startup, the installer checks for and
removes this backup file.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The perform_province_events_action_test and perform_vassal_commands_phase_action_test
had BUILD.bazel dependencies on proto_matchers but the Scala files don't actually
use ProtoMatchers or equalProto. Remove these dead dependencies.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove .NET installer build and old manifest support. The Go installer is
now the only version built and deployed. Both the installer and Unity
builds now update only the v2 manifest at installer/v2/eagle0_manifest.txt.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Two issues fixed:
1. Unity build workflow now updates BOTH manifests (old and v2)
- The v2 manifest was missing all game file hashes because only the
installer workflow was updating it
- Now unity_build.yml calls manifest_manager with both "unity3d" and
"unity3d-v2" to update both manifests
2. Go installer now verifies game exists before claiming "up to date"
- Previously, if manifests matched but files didn't exist, it would
claim success and try to launch a non-existent game
- Now checks if eagle0.exe exists; if not, forces redownload
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove all proto imports and proto_converters from ResolveBattleActionTest.
Use native Scala model types (ProvinceC, HeroC, FactionC, BattalionC,
GameState, etc.) and standard ScalaTest matchers instead of ProtoMatchers.
Key changes:
- Replace proto imports with Scala model imports
- Use inside() pattern for type-safe assertions
- Add resultsOfExecute() extension method using ActionResultApplierImpl
- Update BUILD.bazel to remove proto dependencies
- Add test visibility for battalion_view package
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This file was a proto-using duplicate of the test in battalion_suitability/
but was never built (no BUILD target referenced it).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add console UI with progress bar to Go installer
Adds a polished console-based UI to the Go installer with:
- Box-drawn banner header
- Status messages with ► prefix
- Success messages with ✓ prefix
- Error messages with ✗ prefix
- Warning messages with ⚠ prefix
- Progress bar with █ (filled) and ░ (empty) characters
- File count display during downloads
- Elapsed time on completion
The UI uses pure Go (no external dependencies) so the binary
size remains at 5.4MB.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add application icon to Go installer
- Copy eagle0.ico from .NET installer
- Add versioninfo.json for goversioninfo configuration
- Generate resource_windows.syso with embedded icon and version info
- Update BUILD.bazel to include the .syso resource in Windows build
Binary size increased from 5.4MB to 5.5MB.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Zone.Identifier removal in .NET installer
File.Exists() doesn't work with Windows Alternate Data Streams (ADS) -
it always returns false. Changed to unconditionally attempt deletion
and ignore errors if the ADS doesn't exist.
This fixes the self-update mechanism where SmartScreen was blocking
the new installer because Zone.Identifier was never actually removed.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Two-stage manifest for clean installer upgrade path
Old .NET installers have a bug where Zone.Identifier removal fails,
blocking SmartScreen from allowing the Go installer to run. This change
creates a two-stage upgrade path:
1. OLD manifest (installer/eagle0_manifest.txt) -> points to .NET installer
- Old .NET installers download new .NET installer (same format, works)
2. NEW manifest (installer/v2/eagle0_manifest.txt) -> points to Go installer
- New .NET installer checks v2 manifest, downloads Go installer
- Go installer also checks v2 manifest for future updates
Changes:
- .NET installer now checks installer/v2/ manifest path
- Go installer now checks installer/v2/ manifest path
- manifest_manager supports "installer-v2" section for v2 manifest
- CI workflow updates both manifests appropriately
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Delete unused AvailableCommandMatcher and SelectedCommandMatcher
These proto-based test matchers were defined but never used by any test.
Removing them eliminates direct proto imports from library test code.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove stale selected_command_matcher dependency
perform_vassal_commands_phase_action_test had a dep on selected_command_matcher
but didn't actually use it.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add console UI with progress bar to Go installer
Adds a polished console-based UI to the Go installer with:
- Box-drawn banner header
- Status messages with ► prefix
- Success messages with ✓ prefix
- Error messages with ✗ prefix
- Warning messages with ⚠ prefix
- Progress bar with █ (filled) and ░ (empty) characters
- File count display during downloads
- Elapsed time on completion
The UI uses pure Go (no external dependencies) so the binary
size remains at 5.4MB.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add application icon to Go installer
- Copy eagle0.ico from .NET installer
- Add versioninfo.json for goversioninfo configuration
- Generate resource_windows.syso with embedded icon and version info
- Update BUILD.bazel to include the .syso resource in Windows build
Binary size increased from 5.4MB to 5.5MB.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix Zone.Identifier removal in .NET installer
File.Exists() doesn't work with Windows Alternate Data Streams (ADS) -
it always returns false. Changed to unconditionally attempt deletion
and ignore errors if the ADS doesn't exist.
This fixes the self-update mechanism where SmartScreen was blocking
the new installer because Zone.Identifier was never actually removed.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The CI runner doesn't have s3cmd installed. Modified installer_build_handler
to accept an optional second argument for the S3 key, allowing it to upload
the Go installer to installer/EagleInstallerMini.exe.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate deferred test files to Scala types, delete unused utilities
- Migrate BattalionTypesTestData.scala to use Scala BattalionType
- Migrate NewRoundActionTest.scala to Scala types (FactionRelationship, MovingArmy, etc.)
- Migrate EndBattleAftermathPhaseActionTest.scala with complete rewrite
- Migrate ResolveBattleActionTest.scala (remove IDable dependency)
- Delete unused impl/package.scala and availability/package.scala
- Remove 48 dangling action_impl_pkg references from BUILD files
- Update DEPROTO_PLAN.md (24 migrated, 1 deferred, 3 deleted)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Delete unused IDable.scala and remove all :idable BUILD references
IDable.scala was listed as a BUILD dependency in 22 targets, but no code
actually imported or used it. The BUILD dependencies were stale.
This completes the library test deproto migration with zero deferred files.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove Zone.Identifier to fix Windows SmartScreen blocking
When the installer downloads a new version and launches it programmatically,
Windows SmartScreen blocks the execution because the file has a "Mark of the
Web" (Zone.Identifier alternate data stream) indicating it was downloaded
from the internet.
Remove the Zone.Identifier after download to allow the new installer to
launch without being blocked.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Zone.Identifier fix to .NET installer, deploy both versions
- Add Zone.Identifier removal to .NET installer's download logic
- Update CI to build both .NET and Go installers
- Deploy .NET installer to installer/EagleInstaller.exe
- Deploy Go installer to installer/EagleInstallerMini.exe
- Manifest points to EagleInstallerMini.exe (Go version)
Flow for existing users:
1. Old .NET installer → auto-updates to fixed .NET installer
2. Fixed .NET installer → downloads EagleInstallerMini.exe, strips Zone.Identifier, updates to Go
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Rewrite Windows installer in Go (73MB -> 5.4MB)
Rewrites the Eagle0 Windows installer from C#/.NET to Go, reducing the
binary size from 73MB to 5.4MB (93% reduction).
The Go implementation provides the same functionality:
- Downloads and verifies game files via manifest
- Ed25519 signature verification for manifest
- SHA256 verification for all downloaded files
- Parallel downloads (8 concurrent slots)
- Self-update mechanism for installer updates
- Console-based progress output
- Invitation code support
Changes:
- Add Go installer source code (main.go, updater.go, config.go)
- Add Bazel BUILD file with Windows cross-compilation target
- Update CI workflow to build Go binary instead of .NET
- Add MANIFEST_PUBLIC_KEY to workspace status for build-time injection
The installer is cross-compiled for Windows from Mac using Bazel.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix CI permission error when copying installer output
Remove any existing installer-output directory before copying
to avoid permission denied errors from previous runs.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Replace proto ActionResult with Scala ActionResultC
- Replace proto Date with Scala Date
- Replace proto RoundPhase with Scala RoundPhase
- Add InMemoryHistory.fromScalaResults() method for building history
from Scala ActionResultT objects without proto conversion
- Update visibility for action_result_concrete to allow library tests
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Corrects error from #5512 - the LLM prompt generators are actually
fully protoless. My grep was matching Scala utility imports like
`net.eagle0.common.JsonUtils`, not proto imports.
Verified with: grep -r "import net.eagle0.eagle.(internal|common|api|views)."
which returns zero matches in src/main/scala/net/eagle0/eagle/library/
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Enable remote catalog building in AddressableAssetSettings
- Switch Default Local Group to use remote build/load paths
- Set CDN URL to https://assets.eagle0.net/addressables/[BuildTarget]
- Update BuildScript to log bundle output location for upload
- Add graceful error handling in SoundManager when music fails to load
(e.g., offline and not cached) - game continues without music
After building, bundles in ServerData/[BuildTarget] are uploaded to the
CDN by CI (see PR #5509).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Mark Hostility proto migration as complete (all 4 files now use Scala enum)
- Correct status: LLM prompt generators still have 4 files with proto imports
- List specific LLM files and their proto dependencies
- Move LLM generators from "Fully Protoless" to "Remaining Proto Usage"
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add upload_addressables.sh script for Addressables bundles
- Add upload step to Mac and Windows build workflows
- Update mac_build_handler to use eagle0-assets bucket
- Update unity3d_windows_build_handler to use eagle0-assets bucket
- Update installer_build_handler to use eagle0-assets bucket
- Update manifest_manager to use eagle0-assets bucket
All uploads now go to eagle0-assets bucket, accessible via assets.eagle0.net.
Old eagle0-windows bucket can be deleted after DNS TTL expires (~1 week).
Addressables uploaded to:
- https://assets.eagle0.net/addressables/StandaloneOSX/
- https://assets.eagle0.net/addressables/StandaloneWindows64/
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit migrates the GameStateViewFilterTest to use native Scala
model types instead of protobuf types:
- Replace proto GameState with Scala GameState
- Use Scala concrete types: FactionC, HeroC, ProvinceC, UnaffiliatedHeroC
- Use Scala enums: Profession.NoProfession, RoundPhase, etc.
- Update ShardokBattle and ShardokPlayer with all required fields
- Replace proto assertions with field-specific assertions on view types
- Add visibility for shardok_battle to test package
- Add required view dependencies to BUILD.bazel
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This test file was left behind when RansomCommand was refactored into
the DiplomacyCommand system. The file:
- Had no BUILD target
- Used types that no longer exist (RansomAvailableCommand, RansomSelectedCommand)
- Called APIs that no longer exist (RansomCommand.make)
The ransom functionality is now tested in DiplomacyCommandTest.scala
using the current API.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Reapply "Convert SoundManager music loading to Addressables (#5475)" (#5494)
This reverts commit bda787c07e.
* Add BuildScript to build Addressables before player build
The Addressables change (PR #5475) was reverted because the Mac build
failed. The issue was that Addressables content must be built before the
player build, which the CI scripts weren't doing.
This commit:
- Reapplies the Addressables changes (revert of the revert)
- Adds Editor/BuildScript.cs with methods to build Addressables content
before building the player
- Updates CI build scripts to use -executeMethod BuildScript.Build*Player
instead of -buildWindows64Player/-buildOSXUniversalPlayer
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Documents the completion of Phase 10 library test migration:
- 18 test files successfully migrated to Scala types
- 8 files deferred with documented rationale (shared utilities, API
boundary tests, complex proto integration)
- 1 orphaned test file deleted
- Added migration patterns and key conversions reference
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert test assertions from using proto ActionResult types to native
Scala types (ActionResultT, ChangedProvinceC, ChangedHeroC, ChangedFactionC,
UnaffiliatedHeroC, etc.). Keep proto types for input data construction
with GameStateConverter.fromProto() conversion.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert test to use native Scala model types instead of ScalaPB protos:
- Replace proto GameState, Hero, Province, Faction, Battalion with Scala types
- Use Scala Gender, Profession, UnaffiliatedHeroType, RecruitmentInfo enums
- Use FactionRelationship.RelationshipLevel instead of proto enum
- Use native Scala quest types (WealthQuest, TruceCountQuest)
- Use BackstoryVersion from Scala model
- Remove all proto converters (DateConverter, FactionConverter, etc.)
- Remove 16 proto dependencies from BUILD.bazel
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert test to use native Scala model types instead of ScalaPB protos:
- Replace proto Date, Gender, Profession with Scala equivalents
- Use DeferredChange.* for BlizzardStarted, DroughtStarted, etc.
- Use ProvinceOrderType Scala enum
- Use RecruitmentInfo and UnaffiliatedHeroType enums
- Use GameState, ProvinceC, HeroC, FactionC, UnaffiliatedHeroC directly
- Remove 15 proto dependencies from BUILD.bazel
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Convert from proto types with ScalaPB lens syntax to native Scala
models with .copy():
- Replace proto GameState/Province/Faction/Hero with Scala types
- Replace proto FactionRelationship/PrestigeModifier with Scala types
- Replace proto DiplomacyOffer with Scala TruceOffer
- Replace proto UnaffiliatedHero with UnaffiliatedHeroC
- Replace proto RecruitmentInfo/Status with Scala RecruitmentInfo
- Remove GameStateConverter.fromProto() calls
- Remove IDable.mapifyFactions/mapifyHeroes/mapifyProvinces helpers
- Remove 13 proto dependencies from BUILD.bazel
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Tutorial improvements: queue-based system, skip Turn Cycle for AI
Refactored tutorial system to use a queue-based approach:
- Tutorials that can't be shown immediately are queued
- Queue is processed when current tutorial completes or is skipped
- Removes complex interruption logic and pending profession tracking
- Prerequisites checked when dequeuing (may have changed since enqueue)
Turn Cycle tutorial only shows for multiplayer games (>1 human player):
- Added TutorialManager.IsMultiplayerGame static property
- Set from ConnectionHandler when creating/joining games
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix profession tutorial timing after onboarding
- Remove ArePrerequisitesMet check from OnGameEvent so tutorials
can be queued during onboarding even when prerequisites aren't
met yet. ProcessQueue checks prerequisites when showing them.
- Process the tutorial queue when entering a hidden step, allowing
queued tutorials to show while onboarding waits for async events.
- Add re-entrancy guard to ProcessQueue to prevent nested calls
from StartSequence -> StopCurrentSequence -> ProcessQueue causing
tutorials to be started and immediately overwritten.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace proto imports with native Scala types:
- Use Date, ProvinceC, HeroC, BattalionC
- Use Army, MovingArmy, CombatUnit, Supplies
- Use BattalionType, BattalionTypeId
- Use GameState directly instead of via converter
Remove 8 proto dependencies from BUILD.bazel.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace proto imports with native Scala types:
- Use Date, ProvinceC, FactionC, HeroC, BattalionC
- Use Army, MovingArmy, Supplies for army types
- Use ProvinceEvent subtypes for events
- Use ProvinceEventRolls from action package
- Use GameState directly instead of via converter
Remove 9 proto dependencies from BUILD.bazel.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Replace proto imports with native Scala model types
- Use GameState, ProvinceC, FactionC, Army, MovingArmy, HostileArmyGroup
- Update FactionRelationship and RelationshipLevel imports
- Remove proto dependencies from BUILD.bazel
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace proto types with native Scala equivalents:
- GameState, Province, Hero, Faction, Date
- UnaffiliatedHero, RecruitmentInfo, UnaffiliatedHeroType
- Remove IDable helpers, GameStateConverter.fromProto
- Remove 6 proto dependencies from BUILD.bazel
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Replace proto types with native Scala equivalents:
- GameState, Province, Hero, Faction, Battalion
- Army, MovingArmy, HostileArmyGroup, CombatUnit
- Remove 10 proto dependencies from BUILD.bazel
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate PerformHostileArmySetupActionTest to pure Scala types
Replace proto types (GameState, Province, Army, MovingArmy, RoundPhase)
with their Scala equivalents. Construct GameState directly instead of
using GameStateConverter.fromProto. Remove dependency on IDable.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Refactor PerformHostileArmySetupActionTest for clarity
* Update PerformHostileArmySetupActionTest.scala
* Add Inside matcher import for tests
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Migrate FeastCommandTest from proto to Scala ActionResultType
Replace proto ActionResultType.FEAST import with Scala ActionResultType.Feast
and remove proto dependency from BUILD.bazel.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix assertion for actionResultType in FeastCommandTest
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Full migration of test data from proto types to native Scala model types.
Key changes:
- Replace proto Army, CombatUnit, Battalion with Scala equivalents
- Remove ProvinceConverter.fromProto - construct ProvinceC directly
- Remove IDable.mapifyFactions usage
- Remove 10 proto deps, 3 converter deps, and idable from BUILD.bazel
- Add Scala state type dependencies
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Full migration of test data from proto types (Province, Hero, Faction,
Battalion, GameState) to native Scala model types (ProvinceC, HeroC,
FactionC, BattalionC, GameState).
Key changes:
- Replace ScalaPB .update() lens syntax with Scala .copy() methods
- Remove GameStateConverter.fromProto usage
- Remove 6 proto deps from BUILD.bazel
- Add equivalent Scala type dependencies
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
BattalionTypesTestData, impl/package.scala, and IDable.scala are shared
test utilities that use proto types and have cascading dependencies.
These need to be migrated together with their dependent tests.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The questDetailsProto, questProto, and euh variables using proto types
were defined but never used in any test. Removed the dead code and
proto dependencies.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The residentHeroes variable using proto Hero was dead code (created but
never used, alongside commented-out test code). Removed the variable,
the commented code, and the proto dependency.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove unused proto imports and dead code (questDetailsProto, questProto).
The test already uses Scala model types; these proto variables were never used.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Remove deleted stock images section (already removed files)
- Document Market Day (RandomMind) and Shopping List (Komiku) as free-to-use
- Flag Medieval: Victory Theme as needing verification
- Flag Dima Koltsov Audius tracks for eventual replacement (Open Music License, not CC)
- Update action items and recommendations
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Mark production library/ code as complete
- Remove detailed history of completed phases
- Add Phase 10: Library Test Migration with 27 files to migrate
- Categorize files by proto import count (light/medium/heavy)
- Include key type conversions and recommended migration order
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert SoundManager music loading to Addressables
Replace synchronous Resources.LoadAll calls with async Addressables loading.
This enables music files to be updated independently via Addressables catalog,
reducing download sizes for game updates.
- Add LoadMusicAsync coroutine using Addressables.LoadAssetsAsync
- Load music by label (music-spring, music-summer, etc.) instead of path
- Add MusicLoaded property to check when music is ready
- Guard PlayMusic() against empty clips during async loading
Note: Requires Unity Editor setup to mark music files as Addressable assets
and assign the corresponding labels.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Auto-play music after async loading completes
If a music type was requested before loading finished, start playing
once the music is ready. This ensures music plays even if
CurrentMusicType was set during the async load window.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Prioritize Summer/Autumn music loading, parallelize rest
- Load Summer first (most likely starting season)
- Load Autumn second (next season after Summer)
- Load remaining music (Spring, Winter, Travel, Battle, Victory) in parallel
- Try to start playing after each priority load completes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix: Don't interrupt playing music after async load
TryStartMusicIfNotPlaying() only starts music if:
- Music type was requested (not MusicNone)
- Music isn't already playing
- The requested type's clips are loaded
This prevents interrupting Summer playback when Autumn finishes loading.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix missing Clipboard Button texture in Chronicle canvas
Replace broken Unity built-in sprite reference with btn_icon_copy.png
from GUI Pro Kit. Also change Image Type to Simple with Preserve Aspect
for proper icon rendering.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Revert "Fix missing Clipboard Button texture in Chronicle canvas"
This reverts commit c747e78ea2dbe03ed5215566c6f1a6f2ac8dbdb2.
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Verify code signature before attempting to staple (catches transfer corruption)
- Increase retry attempts from 5 to 10
- Increase wait between retries from 10s to 30s (total wait up to 5 minutes)
- Capture and display stapler error output for better debugging
- Show signature details if verification fails
This addresses intermittent stapling failures due to Apple CloudKit
propagation delays after notarization completes.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove stale proto exports from province:province
The province target was exporting province_event_scala_proto,
army_scala_proto, and unaffiliated_hero_scala_proto but no downstream
targets actually needed these through that path. Removing these stale
exports reduces library/ proto deps from 41 to 26.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix unused import and stale test dependency
Remove unused LlmRequestT import and stale unaffiliated_hero_scala_proto
dependency that were exposed by removing the province proto exports.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix test build failures from stale proto dependencies
- Remove unused ProtoMatchers from tests that don't use it
- Add missing unaffiliated_hero_quest_scala_proto dep to quest_fulfillment_utils_test
- Remove stale unaffiliated_hero_quest_scala_proto dep from check_for_fulfilled_quests_action_test
- Keep ProtoMatchers in ResolveBattleActionTest (uses equalProto matcher)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove unused eagle_unit deps from resolved_eagle_unit and
province_conquered_action. These files use ResolvedEagleUnit (which
uses Scala types), not EagleUnit (which uses proto types).
This breaks the transitive proto dependency chain, reducing library/
proto deps from 74 to 41.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Shardok battle tutorials
Adds three introductory tutorials for tactical combat:
1. Victory conditions - explains attacker/defender win conditions
2. Unit placement - how to position units before battle
3. Battle controls - click to select, click to move/attack, ctrl+click for specials
Tutorials chain with prerequisites and trigger at appropriate game states.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Split victory conditions into multi-step tutorial
Expands the victory conditions tutorial into three steps:
1. "Battle!" - introduces hex-grid combat, shows if player is attacker/defender
2. "Attacker Victory" - capture all castles + hold, or eliminate defenders, by Day 31
3. "Defender Victory" - eliminate attackers, or hold out past Day 31 (scattering works)
Adds IsDefender property to ShardokGameModel for role detection.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix profession tutorial bugs
- Fix duplicate hero name appearing multiple times by storing original
descriptions in a dictionary
- Fix Champion description: Duel, not Fear
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Migrate ResolvedShardokPlayer to use native Scala EndGameCondition
instead of the proto type. This removes 1 transitive proto dependency
from library/ (75 → 74).
Changes:
- Add Scala EndGameCondition sealed trait with Victory, AllyVictory,
Draw, and Loss cases
- Add DrawType enum for draw conditions
- Add EndGameConditionConverter in ShardokBattleConverter
- Update ResolvedShardokPlayer to use Scala EndGameCondition
- Move proto conversion to ShardokInterfaceGrpcClient boundary
- Update ResolveBattleAction to use Scala EndGameCondition API
- Update ResolveBattleActionTest to construct Scala EndGameCondition
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove stale proto_converter deps from library/ BUILD files
Library code should not depend on proto_converters - those belong at
the service layer boundary. Removed 9 stale proto_converter deps that
were no longer used by any Scala code.
The only remaining proto_converter dep is shardok_battle_converter,
which is actually used by ResolveBattleAction.scala (to be fixed in
a follow-up PR).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Move ShardokBattle boundary conversion to ShardokInterfaceGrpcClient
Previously, BattleResolution contained a proto ShardokBattle, requiring
ResolveBattleAction in library/ to depend on ShardokBattleConverter.
This violated the boundary principle where proto conversions should
happen at the service layer, not in library code.
This change:
- Updates BattleResolution.battle to use Scala ShardokBattle
- Moves the proto-to-Scala conversion into ShardokInterfaceGrpcClient
- Removes ShardokBattleConverter dependency from library/ code
- Updates tests to use the scalaBattle() helper for conversion
This completely removes proto_converters dependencies from library/
(the last one was shardok_battle_converter).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add linter check for library/ proto_converters boundary
Adds Rule 3 to check_build_deps.sh that verifies library/ code does not
depend on proto_converters. Proto conversions should happen at service
boundaries (ShardokInterfaceGrpcClient, EagleServiceImpl, etc.), not in
library code.
Also updates baseline from 167 to 75 proto deps reflecting recent cleanup.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Documents the design for implementing delta patches in the Mac auto-update
system to reduce download sizes from ~200MB to ~10-30MB per update.
Covers:
- Appcast XML structure with sparkle:deltas
- S3 storage layout for app bundles and deltas
- BinaryDelta tool usage
- Migration strategy and error handling
- Storage impact analysis (~3.25GB total for 85% bandwidth savings)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Use run-specific artifact names (signed-mac-app-{run_id}, notarized-mac-app-{run_id})
to prevent race conditions where a previous run's cleanup job might delete
artifacts from a concurrent run.
Root cause: When PR #5465 merged, the previous run (which used the old workflow
that deleted ALL artifacts by name) ran its cleanup job between when the new
run uploaded its artifact (06:13:20) and when it tried to download it (06:18:27).
Fix: Include the GitHub run ID in artifact names so each run's artifacts are
uniquely named and cleanup jobs can only delete their own artifacts.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Library code should not depend on proto_converters - those belong at
the service layer boundary. Removed 9 stale proto_converter deps that
were no longer used by any Scala code.
The only remaining proto_converter dep is shardok_battle_converter,
which is actually used by ResolveBattleAction.scala (to be fixed in
a follow-up PR).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Eagle server build to CI
Add eagle_build.yml workflow that builds the Eagle server when Scala
or Eagle proto files change. This ensures build failures in the Eagle
server are caught during PR review, similar to how shardok_build.yml
works for the C++ Shardok server.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix unused dependencies and simplify GameAdminServiceImpl
- Remove unused json4s and settings_loader deps from :service target
- Simplify GameAdminServiceImpl since GamesManager now returns
game_admin proto types directly (no conversion needed)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Read version from the built app's Info.plist instead of calculating
from git commit count at deploy time. This ensures the appcast version
always matches what's actually in the app bundle.
Previously, the deploy step used `git rev-list --count HEAD` which could
differ from the version baked into the Unity build if commits were added
between the build and deploy steps.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add first-encounter profession tutorials
Shows tutorial popup explaining each profession's abilities when player
first encounters a hero with that profession. Covers both strategic
(Eagle) and tactical (Shardok) abilities in natural language.
Triggers when viewing provinces with employed or free heroes.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update mage and paladin profession tutorial text
- Mage: Can create *or end* blizzards and droughts
- Paladin: Giving alms provides much larger support boost
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Now that the admin console uses the GameAdmin service (PR #5460),
remove the duplicate admin endpoints from eagle.proto and their
implementations from EagleServiceImpl.
Changes:
- Remove 14 admin RPC endpoints from Eagle service in eagle.proto
- Remove corresponding message types (AddSettings*, Get*, Convert*,
Reassign*, Rewind*, Import*, Delete*, CheckGame*, DownloadGame*)
- Remove admin method implementations from EagleServiceImpl.scala
- Update GamesManager to import admin response types from game_admin
proto instead of eagle proto
This completes the admin service separation:
- PR #5458: Create GameAdmin service
- PR #5460: Migrate admin console to GameAdmin
- This PR: Remove admin endpoints from Eagle
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
After the GameHistory split, EagleServiceImpl needs to access
shardokPlayerCount from controller.fullHistory instead of
controller.engine.history. Also export full_game_history from
game_controller so downstream targets can see the FullGameHistory type.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
With all proto imports removed from library/ Scala code, these proto
dependencies in BUILD.bazel files are no longer needed. This completes
the deproto milestone for the library/ layer.
- Update DEPROTO_PLAN.md to reflect 0 proto files in library/
- Remove unused proto deps from library/BUILD.bazel, util/, actions/impl/
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update admin_server.go to call the new GameAdmin gRPC service instead
of Eagle for all game management operations. This is step 2 of the
admin service separation:
- Replace grpcClient (EagleClient) with gameAdminClient (GameAdminClient)
- Update all game admin RPC calls to use gameadminpb types
- Add game_admin_go_proto dependency to BUILD.bazel
- Fix game_admin BUILD.bazel to use go_grpc instead of go_grpc_v2
(v2 only generates gRPC code, not message types)
Auth operations still use authpb (via Eagle's Auth service).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Create a separate GameAdmin service to handle admin-only game management
operations (RewindGame, DeleteGame, GetRunningGames, etc.) that were
previously exposed through the Eagle service alongside client endpoints.
This is the first step in separating admin and client APIs:
- Creates game_admin.proto with all 13 admin game endpoints
- Implements GameAdminServiceImpl calling the existing GamesManager
- Registers the new service in Main.scala on the same port as Eagle
The admin console will be migrated to use this service in a follow-up PR,
after which the admin endpoints can be removed from eagle.proto.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Go's XML unmarshaler doesn't preserve xmlns attributes correctly.
When the appcast was fetched and re-marshaled, the Sparkle namespace
was being set to empty string, which prevented Sparkle from parsing
version numbers and detecting updates.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Change from com.Shardok-Games.eagle0 to net.eagle0.eagle0 to match
the project's domain (net.eagle0).
Updated in:
- Unity ProjectSettings (applicationIdentifier for Standalone)
- inject_sparkle.sh (CFBundleURLName for URL scheme)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Sparkle's default check interval is 24 hours. If a user launches the app
shortly after a new version is deployed but before their scheduled check,
they won't see the update prompt.
This adds an explicit background check on every startup to ensure users
get updates promptly when a new version is available.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Make Engine API protoless by introducing Scala ActionResultView
Created Scala ActionResultView type to replace proto ActionResultView
in the Engine's public API. Proto conversion now happens at the gRPC
boundary in HumanPlayerClientConnectionState.
Changes:
- Created model/view/action_result/ActionResultView.scala
- Created proto_converters/view/action_result/ActionResultViewConverter.scala
- Updated ActionResultFilter to return Scala ActionResultView
- Updated Engine and EngineImpl to use Scala type
- Updated HumanPlayerClientConnectionState to convert to proto at gRPC boundary
- Removed proto dependencies from library/
This follows the architecture principle that the Engine should vend a
pure Scala model API, with proto conversions happening at the outer
boundary layer (GameController/GamesManager).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Split GameHistory into protoless GameHistory and FullGameHistory
- Create FullGameHistory trait in service/ that extends GameHistory
and adds Shardok-specific methods (shardokPlayerResultsSince,
shardokPlayerAvailableCommands)
- Remove receiveBattleUpdate from Engine trait (moved to GamesManager)
- Update GameController to hold fullHistory: FullGameHistory field
- Update PersistedHistory and InMemoryHistory to extend FullGameHistory
- Add extractFullHistory helper methods for safe type extraction
- Update tests to use FullGameHistory mocks
This continues the deproto effort by isolating proto-dependent
Shardok methods from the library layer's GameHistory trait.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Regenerate ICO file with proper format:
- Use PNG compression for 256x256 size (Windows Vista+ requirement)
- Use BMP format for smaller sizes (16, 24, 32, 48)
- Proper BITMAPINFOHEADER structure for BMP entries
The previous ICO may not have displayed correctly due to format issues.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Reduce build size: optimize Twitch button, remove unused profession icons
1. glitch_flat_purple.png (Twitch OAuth button):
- Reduce maxTextureSize from 2048 to 512
- Enable crunched compression
- Expected savings: ~8 MB
2. Delete Resources/Professions folder:
- These icons were unused (profession textures are assigned via
EagleCommonTextures component, not loaded from Resources)
- Expected savings: ~7 MB (5.3 MB paladin + others)
Total expected savings: ~15 MB
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Move actually-used icons from Unused to InUse folder
22 icons from 4000_Fantasy_Icons/Unused were actually referenced by
Gameplay.unity (for flame effects, UI elements, etc). Moving them
to an InUse folder makes the naming accurate.
Unity tracks assets by GUID, so moving files with their .meta files
preserves all scene/prefab references.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Uses the same eagle head icon that was added to the Unity/macOS app.
Converted from PNG to ICO format with multiple sizes (16-256px).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Unity creates .meta files for the native plugins which should also
be ignored since the plugins are built at CI time.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Created Scala ActionResultView type to replace proto ActionResultView
in the Engine's public API. Proto conversion now happens at the gRPC
boundary in HumanPlayerClientConnectionState.
Changes:
- Created model/view/action_result/ActionResultView.scala
- Created proto_converters/view/action_result/ActionResultViewConverter.scala
- Updated ActionResultFilter to return Scala ActionResultView
- Updated Engine and EngineImpl to use Scala type
- Updated HumanPlayerClientConnectionState to convert to proto at gRPC boundary
- Removed proto dependencies from library/
This follows the architecture principle that the Engine should vend a
pure Scala model API, with proto conversions happening at the outer
boundary layer (GameController/GamesManager).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add styled DMG with arrow pointing to Applications
- Add create-dmg as a bazel dependency for creating styled DMG installers
- Add background image with arrow pointing from app to Applications
- Update mac_build_handler to use create-dmg for styled DMG creation
- Update CI workflow to use the new DMG creation process
The DMG now shows a visual arrow guiding users to drag the app
to the Applications folder.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add --skip-jenkins flag for headless CI environment
The AppleScript that positions icons times out in CI environments
without a GUI session. Using --skip-jenkins skips the Finder styling
while still including the background image and Applications link.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Switch from create-dmg to dmgbuild for CI compatibility
dmgbuild generates .DS_Store files programmatically without needing
AppleScript or Finder access, making it work in headless CI environments.
- Remove create-dmg bazel dependency (relied on AppleScript)
- Use dmgbuild Python tool instead (pip install dmgbuild)
- Generates proper icon positions and background without GUI
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix DMG styling: lighter background, adjust icon positions
- Use light gray background so file/folder names are readable
- Move arrow up to align with icon centers
- Move Applications link further right (500 -> 520) to clear the arrow
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix arrow: move left, connect head to shaft properly
- Arrow now starts at x=200 (closer to app icon)
- Arrow ends at x=460 (further from Applications at x=520)
- Head and shaft now overlap for proper connection
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Created Scala ActionResultView type to replace proto ActionResultView
in the Engine's public API. Proto conversion now happens at the gRPC
boundary in HumanPlayerClientConnectionState.
Changes:
- Created model/view/action_result/ActionResultView.scala
- Created proto_converters/view/action_result/ActionResultViewConverter.scala
- Updated ActionResultFilter to return Scala ActionResultView
- Updated Engine and EngineImpl to use Scala type
- Updated HumanPlayerClientConnectionState to convert to proto at gRPC boundary
- Removed proto dependencies from library/
This follows the architecture principle that the Engine should vend a
pure Scala model API, with proto conversions happening at the outer
boundary layer (GameController/GamesManager).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove proto Date from GeneratorUtilities
All callers use Scala Date, so the proto Date overload is unused.
- Remove import of net.eagle0.eagle.common.date.Date
- Remove the proto Date overload of dateString()
- Keep only the Scala Date version
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Remove proto types from LLM prompt generators
Migrated 8 files to use Scala types instead of proto:
- BattalionDescriptions: uses BattalionTypeId, BattalionT, BattalionView
- ChronicleEventTextGenerator: uses ChronicleEvent sealed trait
- ChronicleUpdatePromptGenerator: removed ChronicleEventConverter
- HeroBackstoryUpdatePromptGenerator: removed BattalionViewConverter, QuestConverter
- QuestEndedGeneratorUtilities: uses QuestT/QuestC types
- QuestFailedPromptGenerator: removed QuestConverter
- QuestFulfilledPromptGenerator: removed QuestConverter
Updated DEPROTO_PLAN.md to accurately reflect completion of Phase 8
(LLM Prompt Generators) and Phase 8b (Hostility Migration).
All non-boundary library code is now protoless.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The IsRunningFromReadOnlyVolume and ShowDMGWarning functions were
not being exported from the native plugin, causing the DMG launch
check to fail silently.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Runs every 6 hours to check total artifact storage. Fails if storage
exceeds 500 MB and lists the largest artifacts for debugging.
Steady-state should be ~50 MB (logs and test results). This acts as
a safety net in case cleanup jobs fail to run properly.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The cleanup step was deleting ALL signed-mac-app artifacts across the
entire repo, not just the current run's. When two main builds ran
concurrently, one build's cleanup would delete the other's artifacts.
Fix: Change cleanup to only delete the current run's artifacts using
the runs/$run_id/artifacts API endpoint instead of deleting all
artifacts matching the name.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Adds a step to the bazel_test workflow that runs check_build_deps.sh
in strict mode. This enforces architectural boundaries:
- src/main should not depend on src/test
- library/ proto dependencies should not exceed baseline
Runs before tests so failures are caught early.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The test tutorials ("You've issued your first command!") were
accidentally left enabled in the Gameplay scene.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Replace Hostility proto with Scala Hostility enum
This removes direct proto imports from library/ for the Hostility type:
- FactionUtils.hostilityStatus now returns Scala Hostility
- ArmyStats.hostility now uses Scala Hostility type
- AvailableAttackDecisionCommandFactory no longer imports proto
- AvailableFreeForAllDecisionCommandFactory no longer imports proto
- AttackDecisionCommandChooser uses Hostility.Enemy instead of proto
- BattleFilter no longer needs protoToScalaHostility converter
- AvailableCommandConverter uses HostilityConverter.toProto
This completes Phase 8b of the deproto migration, reducing direct
proto imports in library/ from 8 files to 4 (LLM prompt generators).
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix AvailableCommandConverterTest to use Scala Hostility enum
Update test to use net.eagle0.eagle.model.state.Hostility.Self
instead of proto net.eagle0.common.hostility.Hostility.SELF_HOSTILITY.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add retention-days to all GitHub Actions artifacts
- Use retention-days: 1 for artifacts deployed to external storage
(sysroot, installer, mac app builds)
- Use retention-days: 3 for debug logs and test results
Prevents artifact storage quota from being exceeded.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Delete Mac build artifacts after successful deploy
Automatically deletes signed-mac-app and notarized-mac-app artifacts
after deployment completes. These ~250MB artifacts are only needed to
pass the app between workflow jobs; once deployed, they're redundant.
This prevents artifact storage from accumulating even if retention-days
doesn't expire them quickly enough.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Delete ALL old build artifacts after deploy (not just current run)
Both Mac and Windows installer workflows now delete ALL artifacts
with their respective names after successful deployment:
- Mac: signed-mac-app, notarized-mac-app
- Windows: eagle-installer
This ensures no artifact buildup even from failed/stale runs.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Increase retention for test/build logs to 7 days
These are small files useful for debugging, so keep them longer:
- test.json (~100KB each)
- editor_win.log / editor_mac.log (~60KB each)
- failed-test-logs
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use 5-day retention to match repository maximum
The repository has a 5-day maximum retention policy configured.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Delete old Mac builds from DigitalOcean when pruning appcast
The appcast keeps the last 10 versions, but the old DMG files were never
deleted from S3. Now when items are removed from the appcast, the
corresponding DMG files are also deleted from DigitalOcean Spaces.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add cleanup job that runs even when build fails
Previously, artifact cleanup only happened in the deploy job, which
doesn't run if earlier jobs fail. This left behind large artifacts
from failed builds, eventually hitting the storage quota.
The new cleanup job runs on ubuntu-latest with `if: always()` so it
executes regardless of whether build-and-sign, wait-notarization,
or deploy succeeded or failed.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add BUILD.bazel dependency linting
Adds tooling to enforce architectural boundaries in the codebase:
1. Shell script `scripts/check_build_deps.sh` with multiple modes:
- Default: runs all checks and shows dependency counts
- --ci: fails only on hard violations (src/main depends on src/test)
- --strict: also fails if proto deps in library/ increase from baseline
- --count: just show current dependency counts
- --update-baseline: update the baseline file for tracking
2. Bazel test `//:build_deps_test` for integration with test suite
3. Baseline file `scripts/build_deps_baseline.txt` tracking proto dep count
Current enforced rules:
- src/main must not depend on src/test (enforced now)
- library/ proto dependencies tracked (167 currently, deproto in progress)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update DEPROTO_PLAN.md with accurate proto import inventory
The previous status was inaccurate. This update:
- Corrects summary table: Availability, Command Choice Helpers,
and other utilities still have Hostility proto imports
- Adds Phase 8b documenting Hostility proto migration (4 files)
- Updates detailed inventory showing actual 13 files with direct
proto imports (vs 167 transitive deps from bazel query)
- Corrects success criteria checkboxes to reflect actual status
- Documents the grep command used to verify imports
The 167 proto deps from bazel query are transitive dependencies.
Only 13 files have direct proto imports:
- 5 boundary files (expected to keep proto)
- 4 files using Hostility proto (Phase 8b)
- 4 LLM prompt generator files (Phase 8)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Mark Phase 8 (LLM Prompt Generators) as complete following PR #5429.
All 34 prompt generator files and all request generators now use Scala
LlmRequestT types instead of proto types.
Summary:
- Total proto imports: ~7 (down from 149)
- All remaining proto usage is at system boundaries (gRPC, persistence)
- All success criteria met
- All open questions resolved
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Unity auto-updated audio import settings:
- Serialized version 6 → 8
- Quality setting adjusted
- Preload audio data setting moved
Font SDF asset regenerated.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Convert all remaining LLM prompt generators to Scala types
This converts all remaining prompt generators from proto types to Scala
types (LlmRequestT). Updates include:
- All prompt generators now use LlmRequestT.* instead of proto types
- LlmResolver uses Scala type matches for all generators
- Proto fallback cases removed (now throw exceptions for non-LLM types)
- Updated BUILD files with correct dependencies
- Fixed test file to use Scala types
Note: ChronicleUpdatePromptGeneratorTest still needs to be updated to use
Scala types in a follow-up PR.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix ChronicleUpdatePromptGeneratorTest to use Scala types
Update the test to use LlmRequestT.ChronicleUpdateMessage and related
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>
The deferred flag logic was inverted - we were collecting notifications
where deferred=true (hold for later) instead of deferred=false (send now).
This bug was introduced in commit 8d3bc914ed when migrating from proto
types to Scala types. The original proto-based code used notificationsToDeliver
which already handled the deferred distinction. The new Scala code used
NotificationConverter.toProto() which returns (proto, deferred) tuple,
but the .collect pattern incorrectly filtered for deferred=true.
Affected features:
- Divine command notifications (learning about quests)
- Quest completion/failure notifications
- Diplomacy event notifications (alliance, truce, etc.)
- All other immediate notifications
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add in-app warning when running from Downloads folder
Sparkle auto-update framework cannot update apps running from the
Downloads folder or other transient locations. This adds a native
macOS alert that warns users when they launch the app from an
invalid location and instructs them to move it to Applications.
The check detects:
- /Downloads/ - where macOS downloads files
- /tmp/ or /private/tmp/ - temporary directories
- /.Trash/ - deleted files
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix build: import AppKit for NSAlert
NSAlert is part of AppKit, not Foundation. Changed import to AppKit
which also includes Foundation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Offer to move app to /Applications and relaunch
Instead of just showing a warning, now offers to:
1. Copy the app to /Applications (removing existing if present)
2. Launch the new copy
3. Terminate the current instance
This provides a seamless experience - user just clicks "Move to
Applications" and the app handles everything automatically.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Switch Mac distribution from ZIP to DMG
Changes the Mac app distribution from a ZIP file to a DMG disk image.
This provides a better installation experience and enables enforcing
proper installation:
1. **DMG with Applications alias**: Users see the app and an
Applications folder alias, making drag-to-install intuitive.
2. **Read-only enforcement**: DMGs are mounted read-only, so if
users try to run the app directly from the DMG, we detect it
and show a blocking dialog telling them to install first.
3. **Sparkle works from any writable location**: Unlike the previous
Downloads-folder check, this approach allows the app to run from
anywhere writable (~/Desktop, ~/Games, /Applications, etc.).
Changes:
- SparklePlugin.m: Replace location warning with read-only volume check
- SparkleUpdater.cs: Update P/Invoke bindings for new functions
- SparkleInitializer.cs: Check for read-only volume instead of path
- mac_build_handler.go: Create DMG instead of ZIP using hdiutil
- invitation_handlers.go: Update download URL and instructions for DMG
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update BreakAllianceResolutionMessagePromptGenerator to accept
LlmRequestT.BreakAllianceResolutionMessage instead of the proto type.
Add Scala type matching in LlmResolver and update BUILD dependency
from proto to Scala types.
Also updates enum handling from proto DiplomacyOfferStatus to Scala Status.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update BreakAllianceMessagePromptGenerator to accept LlmRequestT.BreakAllianceMessage
instead of the proto type. Add Scala type matching in LlmResolver and update BUILD
dependency from proto to Scala types.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update AllianceResolutionMessagePromptGenerator to accept
LlmRequestT.AllianceOfferResolutionMessage instead of the proto type.
Add Scala type matching in LlmResolver and update BUILD dependency
from proto to Scala types.
Also updates enum handling from proto DiplomacyOfferStatus to Scala Status.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
docker_build.yml was missing TWITCH_CLIENT_ID and TWITCH_CLIENT_SECRET
in its env block and export statements. All other OAuth providers
(Discord, Google, GitHub, Apple) were present, but Twitch was omitted
when the provider was added.
This caused Twitch to not appear on the invitation landing page after
Eagle deployments, even though auth_build.yml had the secrets.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update AllianceOfferMessagePromptGenerator to accept LlmRequestT.AllianceOfferMessage
instead of the proto AllianceOfferMessage type. Add Scala type matching in LlmResolver
and update BUILD dependency from proto to Scala types.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update TruceResolutionMessagePromptGenerator to accept
LlmRequestT.TruceResolutionMessage and use the Scala Status enum
instead of proto DiplomacyOfferStatus. Add Scala type matching in
LlmResolver.promptGenerator before falling back to proto matching.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update TruceOfferMessagePromptGenerator to accept
LlmRequestT.TruceOfferMessage instead of the proto TruceOfferMessage
type. Add Scala type matching in LlmResolver.promptGenerator before
falling back to proto matching.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add --no-deps to nginx and admin docker compose commands to prevent
them from cascading to auth. The auth container has secrets (like
TWITCH_CLIENT_ID) that are only available in auth_build.yml, not in
docker_build.yml. Without --no-deps, docker compose would recreate
auth with blank env vars when it detected config changes.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Update DivineMessagePromptGenerator to accept LlmRequestT.DivineMessage
instead of the proto DivineMessage type. Add Scala type matching in
LlmResolver.promptGenerator before falling back to proto matching.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add sworn brotherhood guidance tutorial
- Add guidance_sworn_brotherhood tutorial that triggers when:
- Player has 4+ provinces, OR
- SwearBrotherhood command is available with good candidates (server-determined)
- Explains that sworn siblings become faction leaders for direct province control
- Mentions succession benefit if Warlord falls
Also update guidance_expand to note that vassals manage provinces without
a Warlord or sworn sibling present.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Improve sworn brotherhood tutorial with candidate detection
- Use SwornBrotherChooser logic to identify good candidates:
charisma >= 65, constitution >= 65, loyalty >= 95, ranked by power
- Dynamically update tutorial text to name the specific candidate
- Add 95 loyalty requirement and permanence info to description
- Keep 4+ provinces as alternative trigger condition
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix hero stat access - use int directly, not StatView
Charisma, Constitution, Strength, Agility, Wisdom are int properties,
only Loyalty uses StatView?.Stat pattern.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix TextEntry access - use .Text property for string value
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use gender-neutral language: Brotherhood → Kinship
- Command header: "Swear Kinship with Hero"
- Tutorial text uses "sworn siblings" and "swear kinship"
- Internal IDs updated: guidance_sworn_brotherhood → guidance_sworn_kinship
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Native plugin (SparklePlugin.m):
- Log app version and build number from Info.plist
- Log feed URL configuration
- Log whether public key is set
- Log auto-check plist setting
- Log current auto-check state after initialization
- Log last update check date
Unity initializer (SparkleInitializer.cs):
- Log Application.version and Unity version
- Log platform and product name
- Log AutomaticallyChecksForUpdates setting
This helps debug why Sparkle updates may not be working.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This is the next step in the incremental migration from proto to Scala types
in the LLM processing pipeline.
Changes:
- LlmRequestWithGameState now takes GeneratedTextRequestT instead of
GeneratedTextRequest (proto)
- LlmResolver.resolveLlmRequests returns Scala types in the result
- Proto conversion happens at boundaries:
- Converting to proto for LLMResponse when streaming
- Converting to proto for receiveStreamingLlmFailure
- Converting to proto in promptGenerator for individual prompt generators
- UnrequestedTextHandler no longer needs to convert to proto when calling
LlmResolver
The prompt generators still expect proto types - they will be updated
incrementally in follow-up changes.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The nginx config is already deployed by docker_build.yml (which has
nginx/** in its trigger paths). Adding it to auth_build.yml was
redundant and broken (the production server isn't a git repo).
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When a user tries to sign in with an OAuth provider but doesn't have an
existing account, the auth service returns InvitationRequired status.
Previously this fell through to the default case and threw a generic
"Unknown OAuth status" exception, providing a poor user experience.
Now displays a friendly message explaining that an invitation is required
to create a new account, and suggests using an invitation link or signing
in with the original provider if they already have an account.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add nginx/nginx.conf and docker-compose.prod.yml to trigger paths
- Pull latest nginx config and restart nginx during deployment
This ensures nginx config changes (like OAuth callback routes) are
automatically deployed when merged to main.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Store Scala types in ClientTextStore instead of proto
This change updates the ClientTextStore layer to store the native Scala
type `GeneratedTextRequestT` in memory instead of the proto type
`GeneratedTextRequest`. Proto conversion now only happens at persistence
boundaries (when saving to/loading from disk).
Key changes:
- ClientText.scala: Changed llmRequest field to use GeneratedTextRequestT
- ClientTextStore.scala: Updated interface to accept Scala types
- ClientTextStoreImpl.scala: Added toProto/fromProto conversion at persistence
- GameController.scala: Removed proto conversion in allNewLlmRequests
- UnrequestedTextHandler.scala: Uses Scala types internally, converts to proto
only when passing to LlmResolver
- GamesManager.scala: Uses Scala types directly when creating UnrequestedClientText
- Updated BUILD files for new dependencies and visibility
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix tests for ClientTextStore Scala types migration
- Update UnrequestedTextHandlerTest to use Scala types instead of proto
- Add GeneratedTextRequestConverter import for proto conversions in test mocks
- Change test requests from FixedHeroName to LlmRequestT.DivineMessage to properly test LLM resolution path
- Add visibility for test packages in BUILD files
- Remove unused proto dependency from generator_utilities_test
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
During blue-green deployment, the old (active) instance should not wait
for the flush marker - it would be waiting for itself to stop, which
causes a 30s timeout warning.
Now compares deployment timestamp to instance start time:
- If deployment started AFTER instance: skip wait (we're the old instance)
- If deployment started BEFORE instance: wait (we're the new instance)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Steam OpenID 2.0 redirects back to /oauth/steam/callback after
authentication, but nginx was not configured to proxy this path to the
auth service, resulting in 404 errors.
Added the location block alongside the existing /oauth/callback and
/oauth/apple/callback blocks.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add GitHub, Apple, Steam, and Twitch to handleLoginStart switch
- Add sign-in buttons with icons for all providers to login.html
- Add CSS styles for new provider buttons
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add Steam and Twitch sign-in buttons to Unity client
- Add steamLoginButton and twitchLoginButton fields
- Add steamProviderIcon and twitchProviderIcon for stored accounts
- Wire up button click handlers in SetupOAuthUI()
- Add icon mapping for stored account display
Note: Unity scene needs Steam/Twitch button GameObjects and icons wired up.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Add Steam and Twitch OAuth button assets
- Add Steam and Twitch (glitch) logo icons for OAuth buttons
- Consolidate Discord icon to OAuth Buttons folder
- Remove unused Google OAuth button variants
- Update Gameplay.unity with new button references
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The TWITCH_CLIENT_ID and TWITCH_CLIENT_SECRET were added to
docker-compose.prod.yml but not to the GitHub Actions workflow
that deploys the auth service.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add game state guidance tutorials
Add four contextual tutorials that trigger based on game state analysis:
1. guidance_loyalty_danger - November warning when heroes have low loyalty
Suggests using Feast or Give Gift to boost loyalty before year end
2. guidance_neighbor_danger - Alert when province borders hostile faction
Suggests Organizing Troops or seeking Diplomacy
3. guidance_recruit_heroes - When stable province has recruitable heroes
Suggests using Travel, Divine, and Recruit commands
4. guidance_expand - When player has stable province but only one territory
Suggests using March to claim new provinces
These provide strategic guidance for new players based on their game state.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix compilation errors in guidance trigger checks
- MarchAvailableCommand.OneProvinceCommands to access destination provinces
- FactionRelationshipView.TargetFactionId instead of FactionId
- province.FullInfo.Gold instead of model.CurrentGold
- Net.Eagle0.Eagle.Common.RecruitmentStatus instead of Views namespace
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use Any() instead of ToList().Count for efficiency
Any() short-circuits on first match rather than materializing entire list.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Simplify recruit heroes gold check
Just check for 100 gold - January was when this would naturally be true,
not an alternative condition.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
docker-compose validates the entire file even when deploying just auth.
Remove the :? validation since docker_build.yml already validates
SHARDOK_ADDRESS explicitly before deploying Eagle.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Document that WarlordRow, VassalRow1, VassalRow2 are registered dynamically
by HeroesAndBattalionsPanelController when hero rows are created from prefabs.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Steam: OpenID 2.0 authentication (no API key required)
- Twitch: Standard OAuth 2.0 with user:read:email scope
- Add TWITCH_CLIENT_ID and TWITCH_CLIENT_SECRET to deployment config
- Add Steam/Twitch buttons to invitation landing page
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add fromProto methods to LlmRequestConverter and GeneratedTextRequestConverter
to enable converting proto types back to Scala types. This is preparation for
storing Scala types in-memory in ClientTextStore, with proto conversion only
happening at persistence boundaries.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Clear all children of storedAccountsContainer and disable them
immediately before destroying (fixes visual duplication from
deferred Destroy)
- Add delete button to StoredAccountButton to remove saved accounts
- Wire up delete button to OAuthManager.RemoveStoredAccount
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Divine reveals what you must do to impress a hero and recruit them,
not their stats/abilities.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Security improvement: Eagle no longer has access to JWT private keys.
- JwtService.scala: Simplified to validation-only (removed signing methods)
- Loads public key from /etc/eagle0/keys/public.pem (shared volume from auth)
- Falls back to extracting public key from JWT_PRIVATE_KEY for backward compat
- AuthServiceImpl.scala: Local fallbacks throw UNIMPLEMENTED (signing in Go auth)
- docker-compose.prod.yml: Removed JWT_PRIVATE_KEY from Eagle, volume now read-only
Auth service unchanged - still bootstraps PEM files from JWT_PRIVATE_KEY env var.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Clicking a selected unit during setup would trigger a placement action
instead of deselecting the unit, because HasPlacementAction returned
true for the unit's own tile. Now the placement check excludes the
currently selected tile, allowing the deselection logic to run.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Change version scheme from git describe (which picked up unrelated tags
like busybox-1.35.0) to commit-count based versions like 1.0.9548.
This gives automatic, always-incrementing version numbers that Sparkle
can properly compare for updates.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Update DEPROTO_PLAN.md with recent completions
- Mark Phase 9 (Utilities) as complete - MapGenerator now protoless
- Mark Phase 10 (History APIs) as complete - PersistedHistory accepts Scala types
- Add PRs #5373, #5378, #5381, #5390 to recent completions
- Update proto import inventory with current counts
- Update success criteria checklist
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Make DiplomacyResolutionLlmRequestGenerator protoless
Replace proto types with Scala equivalents:
- DiplomacyOfferStatus -> Status
- GeneratedTextRequest -> LlmRequestT
- Proto message types -> LlmRequestT enum cases
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Use ditto instead of cp -R to copy Sparkle.framework (preserves bundle structure)
- Skip individual signing of Sparkle's internal XPC services, apps, and executables
- Use --deep flag when signing Sparkle.framework to handle its internal components
- Verify cached Sparkle.framework has proper symlink structure, re-download if corrupted
Fixes "bundle format unrecognized" error during codesigning.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
After investigating a bad deployment where Eagle connected to "shardok"
instead of the actual Hetzner address, found the root cause: GitHub
Actions secrets can occasionally fail to load, causing the heredoc to
expand with default values instead of the actual secrets.
Changes:
- Add validation at deploy start to check critical env vars are present
- Add network connectivity check to Shardok before blue-green deployment
(blocks deployment if Shardok is unreachable)
- Remove the misleading "shardok:40042" default - now SHARDOK_ADDRESS
must be explicitly set, making configuration errors immediately obvious
- docker-compose.prod.yml now uses :? syntax to require SHARDOK_ADDRESS
The env var validation will abort deployment early with a clear error
message if secrets failed to load, rather than deploying with broken
config.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add a new apply overload that takes Vector[ActionResultT] directly,
avoiding proto conversion at the call site. The conversion to proto
for storage still happens internally, but callers like NewGameCreation
can now pass Scala types directly.
Removed the unused proto-based apply overload since all callers that
need proto types use the case class constructor directly.
This makes NewGameCreation fully protoless.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The logout button was wired to QuitButtonClicked in the Unity scene,
causing the app to exit instead of returning to the connection panel.
- Change scene to wire logout button to OnLogoutClicked
- Make OnLogoutClicked public so it's visible in Unity inspector
- Remove redundant programmatic listener setup
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Add loading indicator when switching environments in lobby
Shows "Switching environment..." text while waiting for the server
to respond after changing the environment dropdown. Text is cleared
when the lobby data arrives and populates the game lists.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* add lobby connection status and fix 16:10 aspect ratio
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Make NewGameCreation protoless
Convert NewGameCreation to build Scala ActionResultC internally instead of
protobuf ActionResult. Proto conversion now only happens at the boundary
when storing to PersistedHistory.
Changes:
- NewGameCreation.scala: Use ActionResultC, Scala RoundPhase, ChronicleEntry,
FactionC, FactionRelationship instead of proto equivalents
- StartGameActionResultUtils.scala: Change from proto ActionResult to
ActionResultC throughout
- GameParametersUtils.scala: Add provinceWithOverrideScala method
- Add withId methods to HeroT/HeroC and BattalionT/BattalionC traits
- Update BUILD.bazel files with visibility for new_game_creation package
- Update test to use Scala types
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* empty check
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The toProto method was not serializing newProvinces, causing provinces
to be lost when persisting game state. This led to stack overflow during
game creation as the phase advancer would loop infinitely with no provinces.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Export proto types from converters for caller visibility
ActionResultProtoConverter and GameStateConverter return proto types,
so those proto dependencies need to be exported for callers to use
the return values.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix missing proto exports and ServerSetupHelpers warnings
- Export shardok_battle_scala_proto from ShardokBattleConverter
- Export game_state_scala_proto and shardok_battle_scala_proto from GamesManager
- Remove unused ManagedChannelBuilder import from ServerSetupHelpers
- Remove unused default parameter from private newShardokInterface method
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add @unused annotation to parameters that are intentionally unused
(userName and name parameters that need future verification)
- Add missing game_state_scala_proto dependency to BUILD.bazel
- Add CustomBattleManagerTest with tests for CustomBattleGameController
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Change EngineAndResults.results to return Vector[ActionResultT] (Scala) instead of Vector[ActionResult] (proto)
- Update PostResults to use Scala ActionResultT and ShardokBattle types
- Update GameController methods to use Scala types internally
- Add GeneratedTextRequestConverter for Scala-to-proto conversion when needed for LLM requests
- Add recipientFactionIds field to ClientTextVisibilityExtensionT trait
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Remove verbose debug logs that were added during Apple Sign-In debugging:
- Token exchange client_id/redirect_uri
- id_token length
- Parsed user info (id/email)
Error logging is retained for troubleshooting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Apple inconsistently returns email_verified and is_private_email as
either boolean (true/false) or string ("true"/"false"). Added custom
AppleBool type that unmarshals both formats.
Also added missing claims from Apple's documentation:
- nonce, nonce_supported, c_hash, auth_time
See: https://developer.apple.com/forums/thread/121411🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Log the id_token length and any parse errors to help debug
"Failed to parse user info" errors.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The key is stored as base64-encoded PEM. The previous code base64
decoded it but then tried to use the result directly as key bytes.
The fix: after base64 decoding, PEM decode to extract the actual
key bytes.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
MapGenerator now returns Scala ProvinceT instead of proto Province.
GameParametersUtils works with Scala types internally, converting to
proto only in NewGameCreation when needed for ActionResult.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
docker-compose ignores AUTH_IMAGE env var for unknown reasons.
Fix with multiple approaches:
1. Tag pulled image as :latest locally (belt)
2. Pass AUTH_IMAGE explicitly on command line (suspenders)
3. Add debug output to diagnose .env file issues
4. Verify by comparing image digests, not tags
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
BattalionTypeLoader now returns Scala BattalionType instead of proto.
GamesManager and NewGameCreation updated to use Scala types internally,
with conversion to proto only when needed for GameState updates.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Exclude auth paths from docker_build.yml triggers so eagle builds
don't trigger when only auth code changes
- Remove `docker compose up -d auth` from docker_build.yml - auth is
deployed exclusively by auth_build.yml now
- Delete unused deploy/update-env.sh and deploy/env.template
This prevents docker_build.yml from reverting auth to stale images
when it runs after auth_build.yml.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Make GameHistory APIs return Scala types
The GameHistory trait now returns ActionResultWithResultingState (pure
Scala) instead of ActionWithResultingState (proto-based). Internal
storage in InMemoryHistory and PersistedHistory still uses proto types
for file persistence, but the public API is now protoless.
Changes:
- GameHistory.all, since, sinceDate, last, recentResultsForRound now
return Vector[ActionResultWithResultingState]
- withNewResults accepts Scala types and converts to proto for storage
- Added toScala() helper in history implementations
- Updated callers: EngineImpl, EagleServiceImpl, ChronicleEventGenerator,
ActionResultFilter, GamesManager
- SavedGameUtils uses internal ActionWithProtoState for proto reflection
- Fixed Option[Date] comparisons with .forall() pattern
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Delete unused SavedGameUtils
This file was a standalone debugging utility that was never integrated
into the build (BUILD target was commented out). Removing it as cleanup.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The crane+docker-load approach was causing image tagging issues where the
loaded image didn't have the expected registry tag, causing docker-compose
to not find the correct image.
Changes:
- Use simple `docker pull` instead of crane pull + docker load (since
deploy server is already logged into the registry)
- Add verification that the running container is using the expected image
- Fail the workflow if container is running the wrong image
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add comprehensive logging to Apple callback handler
- Fetch GitHub email from /user/emails when not in main response
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
When user clicks an OAuth login button:
- Game window minimizes (Windows) or exits fullscreen (macOS/Linux)
- Browser opens and is immediately visible to user
- After auth completes (success or failure), game returns to foreground
- Fullscreen mode is restored if it was enabled
Adds WindowFocusManager utility with platform-specific native calls:
- Windows: P/Invoke to user32.dll (ShowWindow, SetForegroundWindow)
- macOS: Placeholder for native plugin, falls back to fullscreen toggle
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
- Add precomputedScalaActionResult parameter and lazy scalaActionResult property to
ActionWithResultingState, following the same pattern as scalaGameState
- Update GameHistory.withNewResultsScala to preserve Scala ActionResultT to avoid
re-conversion
- Convert ActionResultFilter.includeForPlayer to use Scala types (ActionResultT,
NotificationT, ActionResultType) instead of proto types
- Add UNIVERSALLY_VISIBLE_TYPES_SCALA constant with Scala ActionResultType values
This continues Phase 10 of the deproto migration, moving internal logic to use
Scala types while keeping proto for boundaries.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Use single quotes in .env to handle JSON and special chars
Double quotes don't work when values contain embedded quotes
(like JWT_PRIVATE_KEY JSON). Single quotes treat content literally.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use exported env vars instead of .env file for deployment
The .env file approach was fragile for complex values like JSON
(JWT_PRIVATE_KEY) and base64 (APPLE_SIGNIN_PRIVATE_KEY).
Export environment variables directly in the deploy script so
docker compose reads them from the shell environment.
Also adds Apple Sign-In credentials to auth_build.yml workflow.
Note: Delete /opt/eagle0/.env on the server before deploying.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fetch GitHub email from /user/emails endpoint
GitHub only returns email in the /user endpoint if the user has made
their email public. For private emails, we need to call /user/emails.
This ensures we get the user's primary verified email even when they
have their email set to private on GitHub.
🤖 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 PR completes Phase 7 of the deproto migration by making the view
filter and differ components work with Scala types internally, converting
to proto only at boundaries.
## New Scala Types
- `GameStateView` - full game state view
- `GameStateViewDiff` - diff between two game state views
- `ProvinceViewDiff` - diff for province views
- `HeroViewDiff` - diff for hero views
- `FactionViewDiff` - diff for faction views
- `FullProvinceInfoDiff` - diff for full province info
- `ShardokBattleView` - battle view type
- `Hostility` - enum for hostility levels
## New Converters
- `GameStateViewConverter` - converts Scala GameStateView to proto
- `GameStateViewDiffConverter` - converts Scala GameStateViewDiff to proto
- `ProvinceViewDiffConverter` - converts Scala ProvinceViewDiff to proto
- `HeroViewDiffConverter` - converts Scala HeroViewDiff to proto
- `FactionViewDiffConverter` - converts Scala FactionViewDiff to proto
- `ShardokBattleViewConverter` - converts Scala ShardokBattleView to proto
- `HostilityConverter` - converts Scala Hostility to proto
## Updated Components
- `GameStateViewFilter` - now returns Scala `GameStateView`
- `GameStateViewDiffer` - now uses Scala diff types internally
- `ActionResultFilter` - converts Scala diff to proto at boundary
- `HumanPlayerClientConnectionState` - converts Scala GameStateView to proto
## Test Updates
- Updated tests to use Scala `ProvinceOrderType` instead of proto
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Fail fast with a clear error message if a province has an empty
hexMapName when a battle is being created. Previously this would
fail downstream in Shardok with a generic "Must include map path"
error, making it harder to diagnose.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Use single quotes in .env to handle JSON and special chars
Double quotes don't work when values contain embedded quotes
(like JWT_PRIVATE_KEY JSON). Single quotes treat content literally.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Use exported env vars instead of .env file for deployment
The .env file approach was fragile for complex values like JSON
(JWT_PRIVATE_KEY) and base64 (APPLE_SIGNIN_PRIVATE_KEY).
Export environment variables directly in the deploy script so
docker compose reads them from the shell environment.
Also adds Apple Sign-In credentials to auth_build.yml workflow.
Note: Delete /opt/eagle0/.env on the server before deploying.
🤖 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>
* Include state parameter in OAuth callback redirect
When redirecting from /oauth/callback to /invite/{code}/callback,
include the state parameter so the invitation handler can look up
the OAuth result.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Quote values in .env file to handle special characters
Base64-encoded values contain / characters that break unquoted
.env parsing. Wrap all values in double quotes.
🤖 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>
Pass APPLE_SIGNIN_CLIENT_ID, APPLE_TEAM_ID, APPLE_SIGNIN_KEY_ID,
and APPLE_SIGNIN_PRIVATE_KEY to the auth container during deployment.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Add connectionBackgroundLayer field to ConnectionHandler that can be
linked in the Unity Editor. The layer is hidden when entering the lobby
and shown when returning to the connection screen.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* Remove invitation code handling from Unity client
Account creation now happens on the web landing page, so the client
no longer needs to handle invitation codes.
Removed:
- InvitationCodeManager.cs (entire file)
- Invitation code parameter from AuthClient.GetOAuthUrlAsync()
- OAuthStatus.InvitationRequired handling in AuthClient
- OnInvitationRequired event and handlers in OAuthManager
- Invitation code panel UI fields in ConnectionHandler
- OnInvitationRequired and OnSubmitInvitationCodeClicked methods
The display name panel is retained for edge cases where a user
somehow doesn't have a display name set.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Fix OAuth issues on landing page
- Fix Apple OAuth redirect_uri mismatch in token exchange
The token exchange was using /oauth/callback but the auth request
uses /oauth/apple/callback, causing redirect_uri mismatch error
- Add nginx route for /oauth/apple/callback
Apple OAuth uses form_post response mode which posts to a separate
callback path that wasn't proxied through nginx
- Add credential validation in GetAuthURL
Only show OAuth buttons if provider credentials are configured,
preventing broken auth URLs when client ID/secret are missing
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Pass GitHub and Apple OAuth credentials to auth container
The GH_OAUTH_CLIENT_ID/SECRET and Apple Sign-In credentials were
set in GitHub secrets but not passed to the auth service container
in docker-compose.prod.yml, causing the OAuth providers to appear
unconfigured.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* Update OAuth tests to expect error on empty credentials
The credential validation now returns an error for empty client ID
or client secret, so update the test to expect this behavior.
🤖 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>
RansomOfferHelpers had no production callers - it was only tested.
The ransom offer logic in CommandChoiceHelpers already uses
Scala DiplomacyOptionType.Ransom and Scala RansomOfferDetails.
This removes the last 3 proto imports from command_choice_helpers/.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The doctl --format output was being incorrectly parsed, causing the script
to read size values (18.67 MB) as dates. This resulted in all images being
deleted, including those with the 'latest' tag.
Switch to --output json with jq parsing for reliable field extraction.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Move account creation from Unity client to the web landing page.
Users now complete OAuth sign-in and display name selection in the
browser before downloading the client.
New flow:
1. User visits /invite/{code} → sees OAuth buttons
2. User clicks provider → OAuth flow
3. Existing user: redirect to download page (code not consumed)
4. New user: show display name form → create account → redeem code
5. Download page with platform-specific installer links
Changes:
- Landing page shows OAuth buttons instead of download buttons
- New routes: /invite/{code}/auth/{provider}, /invite/{code}/callback,
/invite/{code}/set-name, /invite/{code}/download
- HMAC-signed cookies for session management between OAuth and form
- 4 new HTML templates: landing, display name form, download, error
- Legacy .bat/.command handlers retained for backwards compatibility
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Pass GH_OAUTH_CLIENT_ID and GH_OAUTH_CLIENT_SECRET through both
auth_build.yml and docker_build.yml to enable GitHub OAuth login.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The stats() method that returns proto ArmyStats has no callers.
Removing it eliminates both proto imports from IncomingArmyUtils.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
The providerToString and stringToProvider functions were missing
cases for GITHUB and APPLE providers, causing them to return
"unknown" which resulted in "unsupported provider" errors.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 19:14:34 -08:00
3871 changed files with 82268 additions and 33994 deletions
@@ -110,6 +110,10 @@ bazel run gazelle # Update Go build files
1.**If you modified any BUILD.bazel file:** Run `bazel run gazelle` and stage any changes it makes
2.**If you modified C++ or C# files:** Run `clang-format -i` on the modified files
3.**If you modified Scala files:** scalafmt will run automatically via pre-commit hook
4.**ALWAYS run the full test suite for your language changes before pushing:**
- For Scala changes: `bazel test //src/test/scala/...`
- For C++ changes: `bazel test //src/test/cpp/...`
- This catches exhaustive pattern match errors and other compile-time failures that single-target builds miss
The pre-commit hook runs gazelle but only checks if it succeeds - it does NOT verify the BUILD files are in canonical format. The `gazelle_test` will fail if deps are not alphabetically sorted. **Always run gazelle manually after BUILD file changes.**
# This switches Unity to iOS target and builds addressables for CDN upload
set -euxo pipefail
# Read Unity version from project file
UNITY_VERSION=$(grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //')
# Use runner-specific build directory if EAGLE0_BUILD_DIR is set, otherwise default
UNITY_VERSION=$(grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //')
# Build iOS Unity player (generates Xcode project)
set -euxo pipefail
# Read Unity version from project file
UNITY_VERSION=$(grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //')
# Use runner-specific build directory if EAGLE0_BUILD_DIR is set, otherwise default
BUILD_BASE="${EAGLE0_BUILD_DIR:-/tmp/eagle0}"
WORKSPACE=$(pwd)
BUILD_PATH=${1:-"${BUILD_BASE}/eagle0iOS"}
LOG_PATH="${BUILD_BASE}/editor_ios.log"
# Generate unique build number from git commit count
# This ensures each build has a unique number for TestFlight
BUILD_NUMBER=$(git rev-list --count HEAD)
echo"Build number (git commit count): $BUILD_NUMBER"
UNITY_VERSION=$(grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //')
This document describes how to add new quest types to Eagle. Quests are tasks that unaffiliated heroes want factions to complete before they'll join.
## Deployment Strategy: Three-PR Approach
When adding new quest types, use a three-PR strategy to ensure clients never see quests they don't understand:
1.**PR 1 - Types Only (Server)**: Add proto definitions, Scala types, converters, and all handling logic (fulfillment, failure, LLM prompts). Do NOT generate the quests yet.
2.**PR 2 - Client Handling**: Add client-side display code (C#/Unity) that can render the new quest type.
3.**PR 3 - Quest Generation (Server)**: Add the actual quest creation logic to `QuestCreationUtils.scala`.
**PR Dependencies:**
```
PR 1 (types)
├── PR 2 (client)
└── PR 3 (generation)
```
PR 2 and PR 3 both depend on PR 1, but are independent of each other. They can be developed in parallel after PR 1 is merged, and merged in either order. The key constraint is that PR 3 should not be deployed before PR 2, so clients understand the quest type before the server starts generating it.
Add a case to `didFulfillQuest` that returns `true` when the quest conditions are met:
```scala
caseMyNewQuest(someField,anotherField)=>
// Return true if quest is fulfilled
someConditionIsMet(province,someField)
```
For `ComponentQuest` subclasses, the base case `case q: ComponentQuest => q.componentsFulfilled >= q.componentCount` handles fulfillment automatically.
Add a case to `ShortQuestString` for displaying the quest in the UI:
```csharp
caseSealedValueOneofCase.MyNewQuest:{
vardetails=quest.Details.MyNewQuest;
return$"Do something with {details.SomeField}";
}
```
For quests involving heroes (where you want dynamic name updates), add a case to `SetQuestText` instead.
### Build Files
After making changes, run:
```bash
bazel run gazelle
```
This updates BUILD.bazel files with any new dependencies.
## Quest Type Categories
### Simple Quests
Quests with fixed completion conditions (e.g., `AllianceQuest`, `SuppressRiotByForceQuest`).
### Component Quests
Quests with multi-part completion that track progress via `componentsFulfilled` / `componentCount` (e.g., `AlmsToProvinceQuest`, `DevelopProvincesQuest`). Extend `ComponentQuest` and implement `withComponentsFulfilled`.
### Action Quests
Quests completed by specific player actions rather than state checks. Return `false` in `didFulfillQuest` and handle completion in the relevant command handler (e.g., `ExecutePrisonerQuest` is fulfilled in prisoner management command).
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)
| No Time for Greatness | Dima Koltsov | CC BY 4.0 | [YouTube](https://www.youtube.com/watch?v=cQh0OWIFdgM) |
| Warriors of Demacia | Dima Koltsov | CC BY 4.0 | [YouTube](https://www.youtube.com/watch?v=yktSUMJn9ao) |
| Forest Queen Tale | Dima Koltsov | Presumed CC BY 4.0 | Not found on YouTube; other Dima Koltsov tracks are CC BY 4.0 |
| Valor | Dima Koltsov | CC BY 4.0 | [YouTube](https://www.youtube.com/watch?v=uoHYJRPcS2Y) |
| Clouds | Dima Koltsov | Presumed CC BY 4.0 | Not found on YouTube; other Dima Koltsov tracks are CC BY 4.0 |
**Note on Dima Koltsov tracks:** 3 of 5 tracks confirmed CC BY 4.0 via YouTube. 2 remaining tracks (Forest Queen Tale, Clouds) presumed same license but not verified.
| ~~`Assets/Shardok/commandImages/bridge.png`~~ | **REPLACED** (2026-01-23) with AI-generated wooden rope bridge icon (ChatGPT/DALL-E 3, 512x512 PNG). No licensing restrictions - AI-generated for this project. |
|~~`Assets/Images/startFire.png`~~ | **REPLACED** (2026-01-23) with "Flame Icon" from [UXWing](https://uxwing.com/flame-icon/) (free for commercial use, no attribution required). Consolidated duplicate removed. |
| ~~`Assets/Shardok/commandImages/startFire.png`~~ | **DELETED** (2026-01-23) - duplicate removed, all references updated to use `Assets/Images/startFire.png` |
### Shardok Sound Effects
- **Location:** `Assets/Shardok/soundEffects/`
- **Count:** 56 MP3 files
- **Count:** 37 audio files (was incorrectly counted as 56 including .meta files)
4.**Terrain Hexes** - 85 hex tiles of unknown source
- **TODO:** Confirm if this is a Unity Asset Store purchase (owner believes it is)
3.**Dima Koltsov tracks** - 2 of 5 not verified: `Forest Queen Tale`, `Clouds` (presumed CC BY 4.0 like his other tracks)
5.**StrategyGameIcons** - 138 icons of unknown source
- **TODO:** Investigate origin - check Unity Asset Store purchase history
### Already Resolved:
6.**AUDIUS music tracks** - Verify Dima Koltsov tracks are licensed for commercial use
4.~~**Free Icons**~~ - **RESOLVED** (2026-01-27): All replaced with Asset Store equivalents, folder deleted
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
5.~~**Terrain Hexes**~~ - **VERIFIED** (2026-01-23): Confirmed Unity Asset Store purchase
6.~~**StrategyGameIcons**~~ - **VERIFIED** (2026-01-23): Unity Asset Store purchase (REXARD)
7.~~**Medieval: Victory Theme**~~ - **VERIFIED** (2026-01-23): CC0 Public Domain by RandomMind ([Chosic](https://www.chosic.com/download-audio/28492/))
8.~~**Discord logo**~~ - **OK** (2026-01-23): Usage complies with Discord brand guidelines for "Login with Discord" button
### Already Safe:
@@ -229,12 +284,23 @@ NuGet packages in `Assets/Packages/` all include LICENSE.TXT files:
## Recommendation
Before removing HTTP basic auth:
**Remaining before public release:**
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
None! All required items resolved.
**Low priority:**
2. Verify 2 Dima Koltsov tracks (`Forest Queen Tale`, `Clouds`) - presumed CC BY 4.0
**Already resolved:**
- ~~Clip art images~~ **DONE** - replaced with properly licensed alternatives
- ~~Terrain Hexes~~ **DONE** - confirmed Asset Store purchase
- ~~StrategyGameIcons~~ **DONE** - Unity Asset Store (REXARD)
- ~~Medieval: Victory Theme~~ **DONE** - CC0 Public Domain
- ~~3 other Dima Koltsov tracks~~ **DONE** - confirmed CC BY 4.0
**Protocol buffers should only be used at the edges** — for network serialization (gRPC) and disk persistence. Inside the Eagle game engine, all logic should operate on native Scala models.
**Migration strategy**: These files generate text for LLM prompts. They can be migrated to accept Scala types (`HeroT`, `FactionT`, `ProvinceT`) with converters at the call sites if needed.
**Priority**: Medium - These don't block other migrations and are isolated.
### Phase 9: Remaining Utilities (~9 proto imports in 3 files)
- View filters (3 imports in 2 files) - In progress
- LLM prompt generators (56 imports in 34 files) - Medium priority
- Some utility files (13 imports in 5 files) - Low priority
- History API internals - Low priority
---
## Estimated Remaining Effort
| Component | Files | Imports | Priority |
|-----------|-------|---------|----------|
| View Filters | 2 | 3 | High |
| LLM Prompt Generators | 34 | 56 | Medium |
| Utility files | 3 | 9 | Low |
| Root Library (boundary) | 3 | 9 | Keep |
| **Total** | **42** | **~77** | |
---
## Success Criteria
### Code Quality
- [x] Zero proto imports in `/ai/`
- [x] Zero proto imports in `/library/actions/impl/command/`
- [x] Zero proto imports in `/library/actions/availability/`
- [x] Zero proto imports in `/library/util/command_choice_helpers/` (except RansomOfferHelpers)
- [x] Zero proto imports in `/library/actions/impl/action/`
- [ ] Zero proto imports in `/library/util/view_filters/` (except BattleFilter, GameStateViewDiffer)
- [ ] Zero proto imports in `/library/actions/llm_prompt_generators/`
- [ ] Zero proto imports in `/library/util/` (except view filters)
### Architecture
- [x] Clear separation: Scala models (internal) vs Proto (boundaries)
- [x] Converters as the only bridge between domains
- [x] CommandFactory accepts/returns Scala types
- [x] AI layer fully protoless
- [x] FactionViewFilter returns Scala types
- [x] HeroViewFilter returns Scala types
- [x] ProvinceViewFilter returns Scala types
- [ ] All view filters return Scala types
- [ ] LLM layer uses Scala types
- [ ] No "proto creep" into business logic
---
## Open Questions
1.**LLM Prompt Generators**: Should these accept Scala types directly, or is it acceptable to have proto usage here since they're generating text (not core game logic)?
2.**GameStateViewDiffer**: This works with view protos for client updates. Should it remain proto-based since it's generating client-facing data?
3.**History Serialization**: Keep proto for persistence (good for schema evolution) or consider alternatives?
---
## Proto Import Inventory (Detailed)
**Current: ~80 proto imports across 42 files** (as of 2026-01-14)
These files generate LLM prompts and primarily use `internal.generated_text_request.*` types plus some common enums like `DiplomacyOfferStatus` and `BattalionTypeId`.
**Migration strategy**: Can be migrated to Scala types when convenient, but low priority as they're isolated from core game logic.
---
## 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`, 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
This document compares streaming latency (time-to-first-token) and pricing across OpenAI, Anthropic (Claude), and Google (Gemini) models for use in Eagle's narrative text generation.
## Test Methodology
All tests were performed locally using curl with streaming enabled. Each model was tested 3 times with the same prompt:
> "Write a short paragraph about a brave knight who discovers a hidden cave. Make it vivid and descriptive."
Time-to-first-token (TTFT) was measured from request initiation to the first text content appearing in the stream.
This document proposes 5 new hero professions for Eagle0. Each profession has abilities for both the Eagle (strategic) and Shardok (tactical) game layers.
`ActionResultFilter.filterForPlayer` generates per-action-result game state diffs, which is expensive. Profiling shows significant time spent in `filteredGameStateDiff` → `GameStateViewFilter.filteredGameState` (called twice per result) → `GameStateViewDiffer.diff`.
A potential optimization is to batch diffs: instead of computing N diffs for N action results, compute one combined diff representing the final state change. However, this is blocked by how client notification generators work.
## Current Architecture
### Server Side
1.`ActionResultFilter.filterForPlayer` processes each action result
2. For each result, computes `filteredGameStateDiff(before, after, factionId)`
3. Returns `Vector[ActionResultView]` where each view has its own `gameStateDiff`
### Client Side
1.`EagleGameModel.HandleNewHistoryEntry` processes each `ActionResultView`:
2. Notification generators receive `(ActionResultView, IGameModel)` and look up display data from the model:
```csharp
var province = currentModel.Provinces[details.ProvinceId];
var factionName = currentModel.FactionName(details.FactionId);
var hero = currentModel.Heroes[heroId];
var affectedProvinces = currentModel.ProvincesForFaction(factionId);
```
### The Problem
Notification for action N sees model state after actions 1..N-1 have been applied. If we batch diffs, notification N would see model state before ANY actions, potentially showing stale data.
Example:
1. Action 1: Province X conquered by Faction B (was Faction A)
2. Action 2: Notification needs to show Province X's current ruler
With batching, action 2's notification would incorrectly show Faction A.
- ~50 generators need updating (mechanical but tedious)
- Server must know what display data each notification type needs
## Alternative Approaches Considered
### A: Selective Per-Action Diffs
Only generate individual diffs for action results with notifications that need mutable model state. Requires tracking which notification types need which state.
**Rejected because:** Fragile; easy to add a new generator that breaks the invariant.
### B: State-Change-Triggered Diffs
If batch contains state-changing action types (ProvinceConquered, etc.), generate individual diffs from that point. Otherwise batch.
**Rejected because:** Still conservative; many batches would fall back to individual diffs.
## Status
**Deferred** - Current performance is acceptable. This doc captures the analysis for future reference if optimization becomes necessary.
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
This document outlines opportunities to modernize the Eagle0 codebase to use Scala 3 best practices and features. The migration to Scala 3 is complete, but the code still uses many Scala 2 patterns that can be improved.
## Modernization Opportunities
### 1. **Convert Sealed Traits to Enums** 🎯 HIGH IMPACT
**Benefits**: Better performance, more concise syntax, improved exhaustiveness checking
Be able to support a small (10-50 user) private alpha, including with strangers.
Larger set of goals in [The Big Eagle TODO](https://docs.google.com/document/d/17RTt3-4Wl2AAVMRLodaC3a84E4de6xuQTWBPvCRM484/edit?pli=1&tab=t.0), but most of that is not necessary for MVP.
## Required
### Gameplay Productionization
- [x]~~All functionality works on production eagle / shardok servers~~
- [x]~~Acceptable latency in all regions~~
- [x]~~Shardok performance similar to QA~~
- [x]~~Error logging & alerting~~
- [x]~~Fix long disconnects on deployments~~
- [x] Fix the Mac installer
- [x]~~Still not reconnecting after deployments~~
- [ ] Notify about client updates, button to come directly back
- [x] Generatedtext healing
- [x] Kill outstanding shardok requests when game is deleted
### Other Productionization
- [x]~~Oauth sign-in~~
- [x]~~Add Google, others?~~
- [ ] User management
- [x]~~Invite codes~~
- [ ] Link accounts
- [x]~~Choose display name~~
- [x]~~Just do account setup from the landing page?~~
- [x]~~Download client directly from DO, avoid basic auth/my home network~~
- [x] Support plan (Discord + in-client bug reporting + email contact)
### Alpha Tester Support
- [x] Feedback channel (Discord server)
- [x] Bug report form in Unity client (Settings > Report Bug, sends to Discord webhook)
- [ ] Known issues doc (so testers don't report the same things)
### IP / Legal
- [x] Document and make available licenses for art & music (attributions panel)
- [x] Required open source disclosures (attributions panel)
- [x] Audit assets for anything we don't have rights to and replace it
- [x] Replace heroes that are based on real 20th or 21st century people or IP
This document outlines the implementation plan for adding delta update support to the Eagle0 macOS auto-update system using Sparkle's BinaryDelta feature.
### Current State
- Full DMG downloads (~200MB) for every update
-`mac_build_handler.go` creates DMG, signs it, uploads to S3, updates appcast.xml
- Keeps last 10 versions in appcast, deletes older DMGs
- Users must download full app even for small changes
### Goals
- Reduce update download size from ~200MB to ~10-30MB (85% reduction)
- Maintain backward compatibility with full DMG downloads
- Automatic fallback for users who are many versions behind
## Sparkle Delta Update Architecture
Sparkle supports binary delta updates through the `<sparkle:deltas>` element in the appcast. When a user updates, Sparkle:
1. Checks if a delta patch exists from their current version to the new version
2. If found, downloads the smaller delta patch instead of the full DMG
3. Applies the patch locally to create the new app version
4. Falls back to full DMG if no matching delta exists
importscala.reflect.runtime.universe// Not available in Scala 3
```
### Solution Applied
**Deleted the test entirely** as it was redundant. The test was verifying that auto-generated Scala objects (created by Bazel from proto enum values) matched their source proto values - something already guaranteed by the build system. Since the objects are generated directly from the proto definitions, this test provided no value.
In Scala 2, singleton objects are accessed via `ClassName$.MODULE$()`, but in Scala 3, they're accessed directly via `ClassName$` field. Additionally, `scala.reflect.runtime.universe` is not available in Scala 3.
### Solution Applied
**Completely eliminated reflection** by auto-generating the entire `SettingsLoader.scala` file from BUILD.bazel definitions:
1.**Created generator**: `src/main/go/net/eagle0/build/settings_loader_generator/settings_loader_generator.go` - parses BUILD.bazel and generates complete SettingsLoader.scala with pattern matching for all 272 settings
2.**Added genrule**: In `src/main/scala/net/eagle0/eagle/library/settings/loaders/BUILD.bazel`:
json4s automatic case class serialization uses reflection that tries to access Scala 3 metaprogramming classes (`scala.quoted.staging.package$`) which aren't available at runtime.
#### Solution Applied
Replaced automatic json4s serialization with ScalaPB's built-in JSON support:
```scala
// Old (reflection-based):
// implicit val formats: DefaultFormats.type = DefaultFormats
1.**✅ COMPLETED**: ShardokMapInfo json4s reflection issue resolved with manual parsing
2.**Monitor remaining json4s usage**: Watch for runtime failures in HeroNameFetcher, JsonUtils, and HexMapJsonUtils during full Scala 3 migration
3.**Consider ScalaPB for new JSON needs**: For new functionality, prefer ScalaPB's JSON support to avoid reflection entirely
4.**Apply manual parsing pattern**: If other json4s case class extractions cause runtime failures, use the same manual parsing approach demonstrated in ShardokMapInfo
## Key Learnings
- **Scala 3 reflection changes**: Major differences in singleton object access patterns
- **json4s compatibility**: Automatic case class extraction doesn't work well with Scala 3 metaprogramming
- **ScalaPB advantage**: Using ScalaPB's JSON support avoids reflection issues entirely
- **Systematic approach**: Many issues followed patterns that could be fixed with scripts across multiple files
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.