Compare commits

...
Author SHA1 Message Date
adminandClaude Opus 4.5 a0817c9652 WIP: Local Unity changes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 16:28:13 -08:00
14308026ea Update ASSET_AUDIT.md: mark Terrain Hexes and Discord logo as verified (#5580)
* 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>
2026-01-23 15:44:53 -08:00
4b7a1791e0 Add bridge rotation based on builder position or adjacent land (#5577)
* 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>
2026-01-23 15:34:36 -08:00
a6a3e26853 Add iOS addressables build workflow (#5579)
* 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>
2026-01-23 13:18:07 -08:00
dc1c9e8284 Update ASSET_AUDIT.md with new AI-generated bridge icon (#5578)
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>
2026-01-23 13:17:23 -08:00
21eb5ff79a Replace bridge icon with AI-generated wooden rope bridge (#5576)
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>
2026-01-23 12:32:34 -08:00
2cd0583bb0 Add 3D bridge prefab support for hex map terrain modifiers (#5575)
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>
2026-01-23 12:09:33 -08:00
e45ad8fc14 Silence log messages for LLM responses to deleted warmup games (#5573)
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>
2026-01-23 11:23:00 -08:00
d41535ffc0 Add missing TMP shader include files to Shaders directory (#5572)
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>
2026-01-23 11:17:06 -08:00
c0eff139bf Add eagle-logs helper for tailing active instance logs (#5571)
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>
2026-01-23 11:00:16 -08:00
4338600896 Replace fire icon with UXWing flame and consolidate duplicates (#5570)
* 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>
2026-01-23 10:59:51 -08:00
1ac7477597 Fix eagle-exec deployment and simplify implementation (#5569)
- 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>
2026-01-23 10:47:16 -08:00
23cad2f383 Add iOS/mobile support with touch-aware tooltips (#5550)
* 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>
2026-01-23 10:45:29 -08:00
84c1bf61b0 Add eagle-exec helper for blue-green deployments (#5567)
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>
2026-01-23 10:31:53 -08:00
21dfa12197 Fix Gemini 404 errors by adding missing model name case (#5566)
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>
2026-01-23 10:12:33 -08:00
de4c568aa0 Handle LLM validation errors gracefully (#5565)
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>
2026-01-23 09:53:05 -08:00
edccde9a90 Fix Gemini provider validation in admin settings (#5564)
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>
2026-01-23 09:09:50 -08:00
0f7f019f14 Replace characters that could cause IP/publicity issues (#5563)
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>
2026-01-23 08:45:11 -08:00
adcafe3224 Refactor LLM update interface to use Scala domain types instead of protos (#5562)
* 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>
2026-01-23 08:38:34 -08:00
89ad8371b5 Add Gemini LLM support with model comparison documentation (#5561)
This adds Google Gemini as a third LLM provider option alongside OpenAI
and Anthropic (Claude). Based on streaming latency tests, Gemini 2.5
Flash-Lite shows the fastest time-to-first-token (~0.6s) at the lowest
cost ($0.10/$0.40 per 1M tokens).

Changes:
- Add GeminiServiceImpl.scala following OpenAI/Claude pattern
- Add GeminiModelName setting (default: gemini-2.5-flash-lite)
- Update LlmResolver to support "gemini" provider
- Update admin console dropdowns with Gemini options
- Reorder dropdown options to show recommended models first:
  - OpenAI: gpt-4.1-mini (was gpt-5-mini)
  - Claude: claude-3-5-haiku (already first)
  - Gemini: gemini-2.5-flash-lite
- Add GEMINI_API_KEY to docker-compose and deployment workflow
- Add docs/LLM_MODEL_COMPARISON.md with latency/pricing comparison

Streaming TTFT results from testing:
- Gemini 2.5 Flash-Lite: ~0.6s (fastest, cheapest)
- gpt-4.1-mini: ~1.7s
- claude-3-5-haiku: ~1.9s
- Gemini 2.5 Flash: ~6.8s (surprisingly slow)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 08:23:38 -08:00
b8c584572d Filter LLM requests for deleted games before sending (#5560)
* 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>
2026-01-23 08:10:11 -08:00
acb411c4d2 Reorder Claude models to put haiku first (#5559)
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>
2026-01-23 08:00:31 -08:00
25f92c7832 Add ANTHROPIC_API_KEY to deployment pipeline (#5558)
* 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>
2026-01-23 07:25:04 -08:00
0f0d2433bb Fix reasoning_effort for non-reasoning models, add gpt-5.1 option (#5557)
* 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>
2026-01-22 22:00:53 -08:00
eea61f1c6a Fix create game defaults: 7 total players, 1 human (#5556)
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>
2026-01-22 21:59:29 -08:00
559f051efa Fix NPE when OkHttp SSE onFailure receives null Throwable (#5555)
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>
2026-01-22 21:49:25 -08:00
a2e136af3d Eagerly initialize S3 client at startup to reduce first-connection latency (#5552)
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>
2026-01-22 21:43:03 -08:00
c4f887255b Remove --gpt-model-name from docker-compose (#5554)
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>
2026-01-22 21:41:44 -08:00
97b313f6d5 Add LLM model switching from admin console (#5551)
* 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>
2026-01-22 21:31:04 -08:00
13e9fb5dad Add self-healing for missing hero name text during game load (#5547)
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>
2026-01-21 22:25:46 -08:00
6936c36fc1 Limit chronicle length to ~1.5 pages (#5549)
- 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>
2026-01-21 22:20:38 -08:00
2db4db0ba3 Update March tutorial to mention right-click on map (#5548)
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>
2026-01-21 22:10:58 -08:00
5abfdc1f09 Improve first-run experience for new users (#5546)
- 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>
2026-01-21 21:38:41 -08:00
5c5cf2ce43 Exclude Windows installer from Docker build trigger (#5545)
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>
2026-01-21 21:34:20 -08:00
a31aa0a46d Add icon to WebView installer (#5544)
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>
2026-01-21 21:28:38 -08:00
63f0119a02 Fix genrule variable syntax for manifest public key (#5542)
* 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>
2026-01-21 21:12:47 -08:00
8436106c7b Fix server not pushing Shardok updates to connected clients (#5543)
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>
2026-01-21 21:08:30 -08:00
62f4d40ef3 Enable stamp substitution for WebView installer genrule (#5541)
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>
2026-01-21 20:49:15 -08:00
5a650cb9b7 Fix Shardok updates not received after initial subscription (#5540)
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>
2026-01-21 20:49:03 -08:00
643765a2ba Fix unit placement tutorial text to reflect default placement (#5539)
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>
2026-01-21 20:39:26 -08:00
91553a0eec Fix installer manifest public key injection (#5537)
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>
2026-01-21 20:29:25 -08:00
e1384fffd1 Fix Observe Battle button not appearing due to null GameStatus (#5538)
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>
2026-01-21 20:28:16 -08:00
1f3c4bfef0 Fix pending command retry loop when token is null (#5536)
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>
2026-01-21 20:25:13 -08:00
bfd48bd628 Rename installer to Eagle0.exe and use WebView GUI (#5535)
- 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>
2026-01-21 20:08:56 -08:00
12bd6a78b1 Add WebView GUI to Windows installer (#5532)
* 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>
2026-01-21 19:36:21 -08:00
420edb15ae Fix Bazel test workflow to show failure on correct step (#5533)
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>
2026-01-21 19:22:05 -08:00
8c0ac6e0fa Use fatigue instead of absolute vigor for hero selection (#5534)
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>
2026-01-21 19:18:44 -08:00
81ce184e17 Improve captured hero plea prompt variety (#5531)
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>
2026-01-21 17:00:16 -08:00
9a4324e302 Change OAuth buttons to two-column layout (#5530)
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>
2026-01-21 16:49:58 -08:00
473f83e5f6 Complete deproto migration: delete plan and update linter (#5529)
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>
2026-01-21 16:26:57 -08:00
7d7fa53f46 Remove deprecated .NET installer code (#5528)
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>
2026-01-21 15:57:53 -08:00
56df35a3b7 Clean up backup installer after successful self-update (#5527)
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>
2026-01-21 15:55:47 -08:00
b93a2ad1ce Remove stale proto_matchers dependencies from library tests (#5526)
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>
2026-01-21 15:52:45 -08:00
8213c733a5 Simplify installer: Go only, v2 manifest only (#5525)
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>
2026-01-21 15:50:32 -08:00
f2bc2f995e Fix v2 manifest missing Unity files and add game existence check (#5523)
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>
2026-01-21 15:40:32 -08:00
c2f00244b0 Migrate ResolveBattleActionTest to Scala model types (#5524)
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>
2026-01-21 15:26:48 -08:00
b1e70c443c Delete dead BattalionSuitabilityTest.scala file (#5521)
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>
2026-01-21 15:08:51 -08:00
c29485cb89 Two-stage manifest for clean installer upgrade path (#5522)
* 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>
2026-01-21 15:07:47 -08:00
32631879a0 Delete unused AvailableCommandMatcher and SelectedCommandMatcher (#5520)
* 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>
2026-01-21 15:02:40 -08:00
a818577a59 Add console UI with progress bar to Go installer (#5518)
* 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>
2026-01-21 14:57:46 -08:00
d116ad0105 Fix installer CI: use installer_build_handler instead of s3cmd (#5519)
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>
2026-01-21 14:41:46 -08:00
41732c8f98 Migrate deferred test files to Scala types, delete unused utilities (#5517)
* 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>
2026-01-21 14:40:29 -08:00
f096cdead1 Remove Zone.Identifier to fix Windows SmartScreen blocking (#5516)
* 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>
2026-01-21 13:54:37 -08:00
1f2bc115d0 Rewrite Windows installer in Go (73MB -> 5.4MB) (#5515)
* 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>
2026-01-21 13:41:32 -08:00
de727b1810 Migrate EngineImplTest to Scala types (#5488)
- 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>
2026-01-21 13:15:30 -08:00
c83ea25658 Fix DEPROTO_PLAN.md: Production library code IS protoless (#5513)
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>
2026-01-21 12:20:16 -08:00
4d4056324c Configure Addressables for remote loading (#5508)
- 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>
2026-01-21 12:18:43 -08:00
4d2e91a54d Update DEPROTO_PLAN.md: Hostility complete, LLM generators remain (#5512)
- 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>
2026-01-21 12:09:51 -08:00
a242237534 Migrate all CI uploads to eagle0-assets bucket (#5509)
- 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>
2026-01-21 11:25:03 -08:00
916c01978b Update DEPROTO_PLAN.md to reflect additional test migrations (#5511)
- EngineImplTest.scala migrated (#5488)
- GameStateViewFilterTest.scala migrated (#5510)
- Updated counts: 20 migrated, 6 deferred (was 18/8)
- Removed "API boundary tests" category - these test Scala APIs, not proto

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 11:00:46 -08:00
1fc25fc93a Migrate GameStateViewFilterTest from proto to Scala model types (#5510)
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>
2026-01-21 10:31:47 -08:00
9da6ddc899 Delete orphaned RansomCommandTest.scala (#5492)
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>
2026-01-21 09:35:01 -08:00
d6f6bcb002 Fix Addressables music loading for CI builds (#5503)
* 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>
2026-01-21 08:53:47 -08:00
0f4d2ce2aa Update DEPROTO_PLAN.md with completed library test migration status (#5507)
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>
2026-01-21 08:53:37 -08:00
d0e3d8eeb6 Migrate NewRoundActionTest from proto to Scala model types (#5506)
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>
2026-01-21 08:51:54 -08:00
1b38b81cf3 Migrate ProvinceConqueredActionTest.scala from proto to Scala types (#5505)
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>
2026-01-21 08:51:26 -08:00
4455c3fb6d Migrate EndPlayerCommandsPhaseActionTest.scala from proto to Scala types (#5504)
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>
2026-01-21 08:50:38 -08:00
c04cb72326 Migrate AvailableDiplomacyCommandsFactoryTest to Scala models (#5502)
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>
2026-01-21 07:36:51 -08:00
1981f537ee Make entire Running Game row clickable instead of just Go button (#5501)
Simplifies the UI by making the whole row a button, improving click targets.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 07:29:31 -08:00
c0d4ebe425 Tutorial improvements: Skip Turn Cycle for AI, fix profession timing (#5483)
* 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>
2026-01-21 07:21:20 -08:00
ad43a1a247 Migrate PerformFoodConsumptionPhaseActionTest to Scala models (#5500)
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>
2026-01-21 07:19:35 -08:00
87d8650883 Migrate PerformProvinceEventsActionTest to Scala models (#5499)
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>
2026-01-21 07:14:27 -08:00
c615cd8ae0 Migrate AvailableCommandsFactoryTest.scala from proto to Scala types (#5498)
- 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>
2026-01-21 07:07:24 -08:00
bbb520f140 Migrate AvailablePleaseRecruitMeCommandFactoryTest to Scala models (#5497)
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>
2026-01-21 07:06:53 -08:00
50af7d64dc Migrate PerformReconResolutionActionTest to Scala models (#5496)
Replace proto types with native Scala equivalents:
- GameState, Province, Hero, Faction, Date, BackstoryVersion
- IncomingEndTurnAction, IncomingRecon, ProvinceOrderType
- Remove 10 proto dependencies from BUILD.bazel
- Remove GameStateConverter.fromProto() calls

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 06:46:15 -08:00
d329fafc80 Migrate AttackDecisionCommandChooserTest to Scala models (#5495)
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>
2026-01-21 06:44:11 -08:00
9f4c02b82c Migrate PerformHostileArmySetupActionTest to pure Scala types (#5489)
* 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>
2026-01-21 06:41:47 -08:00
84ad215491 Migrate FeastCommandTest from proto to Scala ActionResultType (#5484)
* 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>
2026-01-21 06:38:10 -08:00
54ba129d2b Migrate AvailableMarchCommandFactoryTest to Scala types (#5493)
- Replace proto Hero, Battalion, Province, GameState with HeroC, BattalionC, ProvinceC, GameState
- Replace proto BattalionType, BattalionTypeId with Scala equivalents
- Replace proto Date, BlizzardEvent with Scala Date, ProvinceEvent.BlizzardEvent
- Replace proto Neighbor with Scala Neighbor
- Remove GameStateConverter.fromProto usage - construct GameState directly
- Remove availability/package.scala dependency (HeroMap/BattalionMap inlined)
- Remove 7 proto deps, 1 converter dep
- Add Scala model deps

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-21 06:33:12 -08:00
85fe20171d Migrate ProvinceHeldActionTest.scala from proto to Scala types (#5491)
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>
2026-01-21 06:31:31 -08:00
adminandGitHub bda787c07e Revert "Convert SoundManager music loading to Addressables (#5475)" (#5494)
This reverts commit 6757f23c5d.
2026-01-21 06:29:49 -08:00
a5bb630e65 Migrate NewYearActionTest.scala from proto to Scala types (#5490)
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>
2026-01-21 06:29:42 -08:00
6daa44cb13 Document test utilities with cascading proto dependencies (#5487)
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>
2026-01-21 06:25:17 -08:00
0051b08c55 Remove unused proto imports from DeclineQuestCommandTest (#5486)
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>
2026-01-21 06:24:21 -08:00
27e55a1fc5 Remove unused proto Hero import from FriendlyMoveActionTest (#5485)
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>
2026-01-21 05:31:32 -08:00
062a09bd20 Remove proto imports from QuestFulfillmentUtilsTest (#5482)
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>
2026-01-20 22:28:14 -08:00
ad4b9e88f6 Update asset audit: clean up deleted items, document music licenses (#5481)
- 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>
2026-01-20 22:27:52 -08:00
bf7f736a4a Update DEPROTO_PLAN.md for library test migration (#5480)
- 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>
2026-01-20 22:12:33 -08:00
6757f23c5d Convert SoundManager music loading to Addressables (#5475)
* 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>
2026-01-20 22:09:31 -08:00
c41306912d Improve Mac notarization stapling reliability (#5479)
- 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>
2026-01-20 22:06:03 -08:00
c6e1fe10ea Remove stale proto exports from province:province (#5478)
* 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>
2026-01-20 22:05:24 -08:00
6b92d14d1d Remove stale eagle_unit dependencies from library/ (#5477)
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>
2026-01-20 21:25:18 -08:00
5705fb9403 Add Shardok battle tutorials (#5468)
* 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>
2026-01-20 21:22:40 -08:00
658bdac97f Add alpha tester support, legal, and onboarding items to TODO (#5476)
- Alpha Tester Support: feedback channel, crash reporting, known issues
- Legal: privacy policy, ToS, data deletion
- Onboarding: narrative hook, first-session goals, early victories

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 17:08:51 -08:00
5599d07247 Replace EndGameCondition proto with Scala sealed trait (#5473)
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>
2026-01-20 17:06:12 -08:00
b7afd6d6ad Add Small Eagle TODO checklist for closed alpha (#5474)
Tracks remaining work for 10-50 user private alpha release:
- Gameplay productionization (Mac installer, client updates, etc.)
- User management (link accounts, MOTD)
- IP/Legal (licenses, asset audit)
- Basic gameplay (tutorials, goals/ending)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 17:02:12 -08:00
6e7611d547 Move ShardokBattle boundary conversion to ShardokInterfaceGrpcClient (#5471)
* 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>
2026-01-20 06:27:57 -08:00
8975514e7c Add Sparkle delta updates implementation plan (#5469)
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>
2026-01-19 22:35:53 -08:00
75f08812db Fix Mac build artifact race condition with run-specific names (#5470)
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>
2026-01-19 22:35:29 -08:00
6639ef62fc Remove stale proto_converter deps from library/ BUILD files (#5467)
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>
2026-01-19 22:07:56 -08:00
c8c7c1da42 Add Eagle server build to CI (#5466)
* 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>
2026-01-19 22:04:42 -08:00
e03fc90787 Fix appcast version mismatch with actual app bundle (#5465)
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>
2026-01-19 22:04:05 -08:00
b7034918a2 Add first-encounter profession tutorials (#5464)
* 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>
2026-01-19 22:02:32 -08:00
5201e4c737 Remove admin endpoints from Eagle service (#5463)
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>
2026-01-19 21:12:04 -08:00
f395bde15c Fix FullGameHistory access in EagleServiceImpl (#5462)
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>
2026-01-19 16:38:30 -08:00
af854ea2cc Remove stale proto deps from library/ BUILD.bazel files (#5461)
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>
2026-01-19 16:30:37 -08:00
113b6eee27 Migrate admin console to use GameAdmin service (#5460)
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>
2026-01-19 16:29:34 -08:00
3d89fcd9de Add GameAdmin gRPC service for admin game management (#5458)
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>
2026-01-19 16:04:24 -08:00
621e2fc128 Fix Sparkle appcast namespace after XML unmarshal (#5456)
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>
2026-01-19 16:02:41 -08:00
fb5ca109b7 Update bundle identifier to net.eagle0.eagle0 (#5455)
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>
2026-01-19 16:00:31 -08:00
a56c4feaf2 Check for Sparkle updates on every app startup (#5457)
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>
2026-01-19 15:56:36 -08:00
d10fda1b41 Split GameHistory into protoless GameHistory and FullGameHistory (#5459)
* 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>
2026-01-19 15:55:31 -08:00
c6d958dd9e Fix installer icon format for proper Windows display (#5454)
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>
2026-01-19 15:40:58 -08:00
ce521e534b Move actually-used icons from Unused to InUse folder (#5452)
* 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>
2026-01-19 15:38:42 -08:00
945d5f05b0 Fix installer self-update URL construction (#5453)
The installer download URL was incorrectly prepending "assets/" to the
installer path, resulting in a 403 error when trying to download the
new installer from:
  https://assets.eagle0.net/assets/installer/EagleInstaller.exe

The correct URL is:
  https://assets.eagle0.net/installer/EagleInstaller.exe

This bug prevented installed copies from auto-updating to newer versions.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 15:38:01 -08:00
d5f8b08018 Add application icon to Windows installer (#5451)
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>
2026-01-19 15:14:19 -08:00
c80761a3f9 Reduce build size: optimize Twitch button, remove unused profession icons (#5450)
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>
2026-01-19 14:52:50 -08:00
d22f360d1f Ignore native plugin .meta files in gitignore (#5449)
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>
2026-01-19 14:20:27 -08:00
9d07984b2e Make Engine API protoless by introducing Scala ActionResultView (#5447)
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>
2026-01-19 13:48:13 -08:00
bc763d0590 Add app icon for macOS build (#5448)
Eagle head facing crowned king - represents the strategic conflict
theme of the game.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-19 13:46:55 -08:00
10933d3c8b Add styled DMG with arrow pointing to Applications (#5445)
* 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>
2026-01-19 13:06:37 -08:00
56312414ec Make Engine API protoless by introducing Scala ActionResultView (#5446)
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>
2026-01-19 13:04:40 -08:00
9b2315aaff Remove proto types from LLM prompt generators (#5444)
* 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>
2026-01-19 10:29:25 -08:00
d7f1f27ea7 Export DMG warning symbols from SparklePlugin (#5443)
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>
2026-01-19 10:15:43 -08:00
08d9e04428 Add periodic artifact storage monitoring workflow (#5442)
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>
2026-01-19 07:56:23 -08:00
62c7c45ef1 Fix Mac build failures from concurrent artifact deletion (#5441)
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>
2026-01-19 07:55:21 -08:00
4da6675b91 Add BUILD.bazel dependency linting to CI (#5439)
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>
2026-01-19 07:44:18 -08:00
57ffabdb67 Disable test tutorial popups (#5440)
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>
2026-01-19 07:43:44 -08:00
916501cbf8 Replace Hostility proto with Scala Hostility enum (#5438)
* 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>
2026-01-19 07:35:38 -08:00
2d58f0231c Add retention-days to all GitHub Actions artifacts (#5436)
* 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>
2026-01-19 07:30:58 -08:00
7ab8fc796b Add BUILD.bazel dependency linting (#5437)
* 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>
2026-01-19 07:28:58 -08:00
adminandGitHub a0bf6050b4 Reduce Unity build size (~84MB savings) (#5433) 2026-01-19 07:16:25 -08:00
adminandGitHub 8fd95fc596 Auto-size tutorial panel to fit content (#5430) 2026-01-19 07:16:11 -08:00
adminandGitHub e91da453a1 Remove ActionWithResultingState, create PersistedActionResult (#5434) 2026-01-19 06:54:39 -08:00
8981298394 Update DEPROTO_PLAN.md - all business logic now protoless (#5432)
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>
2026-01-18 21:45:01 -08:00
0836636fda Update Unity audio compression settings and font asset (#5435)
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>
2026-01-18 21:42:42 -08:00
f0bfcb005a Convert all remaining LLM prompt generators to Scala types (#5429)
* 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>
2026-01-18 07:47:33 -08:00
c3b882f178 Fix notification filtering to send immediate notifications to clients (#5431)
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>
2026-01-18 07:47:13 -08:00
8c322eb812 Switch Mac distribution from ZIP to DMG (#5427)
* 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>
2026-01-18 07:27:00 -08:00
485da704b4 Convert BreakAllianceResolutionMessagePromptGenerator to Scala types (#5425)
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>
2026-01-18 06:16:40 -08:00
ecda346773 Convert BreakAllianceMessagePromptGenerator to Scala types (#5424)
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>
2026-01-17 22:48:48 -08:00
97bb3789b3 Convert AllianceResolutionMessagePromptGenerator to Scala types (#5423)
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>
2026-01-17 22:45:03 -08:00
fdf42c812f Add Twitch OAuth secrets to docker_build.yml (#5428)
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>
2026-01-17 22:34:19 -08:00
1b3ccc9e5d Convert AllianceOfferMessagePromptGenerator to Scala types (#5422)
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>
2026-01-17 22:32:54 -08:00
54cf54ad83 Convert TruceResolutionMessagePromptGenerator to Scala types (#5421)
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>
2026-01-17 22:28:45 -08:00
81d1eaa875 Convert TruceOfferMessagePromptGenerator to Scala types (#5420)
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>
2026-01-17 21:52:00 -08:00
0e9a06cc88 Fix deploy script to not cascade to auth container (#5426)
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>
2026-01-17 21:44:38 -08:00
3fffdfa506 Convert DivineMessagePromptGenerator to Scala types (#5419)
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>
2026-01-17 21:42:32 -08:00
6fa324f5ef Add sworn kinship guidance tutorial (#5411)
* 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>
2026-01-17 14:24:42 -08:00
455e8c0367 Add detailed Sparkle logging for debugging (#5418)
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>
2026-01-17 14:18:45 -08:00
db7f19d7ae Update LlmResolver to use Scala types instead of proto (#5417)
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>
2026-01-17 14:17:47 -08:00
f5d89e67e4 Revert nginx deployment from auth workflow (#5416)
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>
2026-01-17 14:11:26 -08:00
ae2bf05e7d Handle InvitationRequired OAuth status gracefully in Unity client (#5414)
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>
2026-01-17 14:02:19 -08:00
3e1919a715 Update auth workflow to deploy nginx config changes (#5412)
- Add nginx/nginx.conf and docker-compose.prod.yml to trigger paths
- Pull latest nginx config and restart nginx during deployment

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

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

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

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

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

* Fix tests for ClientTextStore Scala types migration

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

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

---------

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

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

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

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

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

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

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

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

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

* Add Steam and Twitch OAuth button assets

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

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

---------

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

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

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

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

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

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

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

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

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

* Fix compilation errors in guidance trigger checks

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

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

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

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

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

* Simplify recruit heroes gold check

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Make DiplomacyResolutionLlmRequestGenerator protoless

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

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

---------

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

Fixes "bundle format unrecognized" error during codesigning.

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

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

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

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

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

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

This makes NewGameCreation fully protoless.

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

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

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

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

* and the github icon

* add the apple button

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

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

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

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

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

---------

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

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

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

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

* empty check

---------

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

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

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

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

* Fix missing proto exports and ServerSetupHelpers warnings

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

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

---------

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

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

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

Error logging is retained for troubleshooting.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Delete unused SavedGameUtils

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

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

---------

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

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

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

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

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 07:34:46 -08:00
cc5c6422bd Improve OAuth flow by minimizing game before opening browser (#5363)
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>
2026-01-15 07:22:23 -08:00
c71bf10c0e Add scalaActionResult to ActionWithResultingState and use Scala types in ActionResultFilter (#5362)
- 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>
2026-01-15 07:18:52 -08:00
fd94d2ac8b Fetch GitHub email from /user/emails endpoint (#5358)
* 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>
2026-01-14 21:40:10 -08:00
46f3c52174 Make GameStateViewFilter and GameStateViewDiffer protoless (#5360)
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>
2026-01-14 21:39:37 -08:00
8e952c3611 Add validation for empty hexMapName when creating battles (#5359)
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>
2026-01-14 21:23:12 -08:00
a61be38ea6 Use exported env vars instead of .env file for deployment (#5357)
* 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>
2026-01-14 20:49:35 -08:00
4512 changed files with 40938 additions and 16769 deletions
@@ -0,0 +1,37 @@
name: Artifact Storage Check
on:
schedule:
# Run every 6 hours
- cron: '0 */6 * * *'
workflow_dispatch:
jobs:
check-storage:
runs-on: ubuntu-latest
steps:
- name: Check artifact storage size
env:
GH_TOKEN: ${{ github.token }}
run: |
# Calculate total artifact storage
total_bytes=$(gh api "repos/${{ github.repository }}/actions/artifacts" \
--paginate -q '.artifacts[].size_in_bytes' | awk '{sum+=$1} END {print sum}')
total_mb=$((total_bytes / 1024 / 1024))
echo "Total artifact storage: ${total_mb} MB"
# Fail if over 500MB
if [ "$total_mb" -gt 500 ]; then
echo "::error::Artifact storage is ${total_mb} MB, which exceeds the 500 MB threshold!"
echo ""
echo "Largest artifacts:"
gh api "repos/${{ github.repository }}/actions/artifacts" \
--paginate -q '.artifacts[] | "\(.size_in_bytes)\t\(.name)\t\(.created_at)"' | \
sort -rn | head -20 | \
awk -F'\t' '{printf "%d MB\t%s\t%s\n", $1/1024/1024, $2, $3}'
exit 1
fi
echo "Storage is within acceptable limits."
+58 -40
View File
@@ -106,6 +106,12 @@ jobs:
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
GH_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }}
GH_OAUTH_CLIENT_SECRET: ${{ secrets.GH_OAUTH_CLIENT_SECRET }}
APPLE_SIGNIN_CLIENT_ID: ${{ secrets.APPLE_SIGNIN_CLIENT_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_SIGNIN_KEY_ID: ${{ secrets.APPLE_SIGNIN_KEY_ID }}
APPLE_SIGNIN_PRIVATE_KEY: ${{ secrets.APPLE_SIGNIN_PRIVATE_KEY }}
TWITCH_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }}
TWITCH_CLIENT_SECRET: ${{ secrets.TWITCH_CLIENT_SECRET }}
JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}
FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }}
FASTMAIL_FROM_EMAIL: ${{ secrets.FASTMAIL_FROM_EMAIL }}
@@ -114,16 +120,6 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Copy update-env script to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.DO_DROPLET_IP }}
username: deploy
key: ${{ secrets.DO_SSH_KEY }}
source: "deploy/update-env.sh,deploy/env.template"
target: /opt/eagle0/
strip_components: 1
- name: Deploy auth service to production
uses: appleboy/ssh-action@v1.0.3
with:
@@ -131,53 +127,75 @@ jobs:
username: deploy
key: ${{ secrets.DO_SSH_KEY }}
script_stop: true
envs: AUTH_IMAGE,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GH_OAUTH_CLIENT_ID,GH_OAUTH_CLIENT_SECRET,JWT_PRIVATE_KEY,FASTMAIL_API_TOKEN,FASTMAIL_FROM_EMAIL,FASTMAIL_FROM_NAME
envs: AUTH_IMAGE,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GH_OAUTH_CLIENT_ID,GH_OAUTH_CLIENT_SECRET,APPLE_SIGNIN_CLIENT_ID,APPLE_TEAM_ID,APPLE_SIGNIN_KEY_ID,APPLE_SIGNIN_PRIVATE_KEY,TWITCH_CLIENT_ID,TWITCH_CLIENT_SECRET,JWT_PRIVATE_KEY,FASTMAIL_API_TOKEN,FASTMAIL_FROM_EMAIL,FASTMAIL_FROM_NAME
script: |
set -x
cd /opt/eagle0
# Update env vars using shared script (preserves vars set by other workflows)
chmod +x update-env.sh
./update-env.sh \
"AUTH_IMAGE=${AUTH_IMAGE}" \
"DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}" \
"DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET}" \
"GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}" \
"GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}" \
"GH_OAUTH_CLIENT_ID=${GH_OAUTH_CLIENT_ID}" \
"GH_OAUTH_CLIENT_SECRET=${GH_OAUTH_CLIENT_SECRET}" \
"JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY}" \
"FASTMAIL_API_TOKEN=${FASTMAIL_API_TOKEN}" \
"FASTMAIL_FROM_EMAIL=${FASTMAIL_FROM_EMAIL}" \
"FASTMAIL_FROM_NAME=${FASTMAIL_FROM_NAME}"
# Export env vars for docker compose (appleboy/ssh-action sets them but doesn't export)
export AUTH_IMAGE="${AUTH_IMAGE}"
export DISCORD_CLIENT_ID="${DISCORD_CLIENT_ID}"
export DISCORD_CLIENT_SECRET="${DISCORD_CLIENT_SECRET}"
export GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID}"
export GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET}"
export GH_OAUTH_CLIENT_ID="${GH_OAUTH_CLIENT_ID}"
export GH_OAUTH_CLIENT_SECRET="${GH_OAUTH_CLIENT_SECRET}"
export APPLE_SIGNIN_CLIENT_ID="${APPLE_SIGNIN_CLIENT_ID}"
export APPLE_TEAM_ID="${APPLE_TEAM_ID}"
export APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
export APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
export TWITCH_CLIENT_ID="${TWITCH_CLIENT_ID}"
export TWITCH_CLIENT_SECRET="${TWITCH_CLIENT_SECRET}"
export JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
export FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
# Login to registry
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
echo "Deploying auth service: $AUTH_IMAGE"
# Use crane to pull image
echo "Installing crane..."
curl -sL https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz | tar xzf - crane
chmod +x crane
# Pull the image directly (docker is already logged in)
echo "Pulling Auth image..."
docker pull "${AUTH_IMAGE}" || { echo "ERROR: Failed to pull auth image"; exit 1; }
echo "Pulling Auth image with crane..."
./crane pull "${AUTH_IMAGE}" auth.tar || { echo "ERROR: Failed to pull auth image"; exit 1; }
echo "Loading Auth image into Docker..."
docker load -i auth.tar
rm auth.tar
rm ./crane
# Tag as :latest locally so any fallback uses correct image
docker tag "${AUTH_IMAGE}" registry.digitalocean.com/eagle0/auth-server:latest
# Only recreate the auth container (not eagle, shardok, etc.)
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate auth
# Debug: check environment and .env file
echo "DEBUG: AUTH_IMAGE=$AUTH_IMAGE"
env | grep AUTH || echo "AUTH_IMAGE not in env output"
if [ -f .env ]; then
echo "DEBUG: .env file contents related to AUTH:"
grep AUTH .env || echo "No AUTH in .env"
fi
# Recreate auth container - pass AUTH_IMAGE explicitly on command line
AUTH_IMAGE="${AUTH_IMAGE}" docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate auth
# Wait for health check
sleep 5
docker compose -f docker-compose.prod.yml ps auth
# Verify container is using correct image
# Verify container is using the correct image
# Note: docker-compose may use :latest tag (which we tagged to the correct image)
echo "=== Verifying auth container image ==="
docker compose -f docker-compose.prod.yml images auth
RUNNING_IMAGE=$(docker inspect auth-server --format '{{.Config.Image}}')
RUNNING_DIGEST=$(docker inspect auth-server --format '{{.Image}}')
EXPECTED_DIGEST=$(docker inspect "${AUTH_IMAGE}" --format '{{.Id}}')
echo "Expected image: ${AUTH_IMAGE}"
echo "Running image: ${RUNNING_IMAGE}"
echo "Expected digest: ${EXPECTED_DIGEST}"
echo "Running digest: ${RUNNING_DIGEST}"
if [ "$RUNNING_DIGEST" != "$EXPECTED_DIGEST" ]; then
echo "ERROR: Container is running wrong image!"
exit 1
fi
echo "Image digests match - correct image is running"
# Show container status
docker compose -f docker-compose.prod.yml ps auth
# Cleanup old images
docker image prune -f
+4 -5
View File
@@ -33,9 +33,9 @@ jobs:
uses: actions/checkout@v4
with:
lfs: false
- name: Check BUILD.bazel dependencies
run: ./scripts/check_build_deps.sh --strict
- name: Run tests
id: test
continue-on-error: true
run: bazel test --build_event_json_file=test.json //src/test/... //src/main/go/...
- name: Collect failed test logs
if: always()
@@ -75,6 +75,7 @@ jobs:
with:
name: test.json
path: test.json
retention-days: 5
- name: Archive failed test logs
if: always()
uses: actions/upload-artifact@v4
@@ -82,6 +83,4 @@ jobs:
name: failed-test-logs
path: failed_test_logs/
if-no-files-found: ignore
- name: Fail if tests failed
if: steps.test.outcome == 'failure'
run: exit 1
retention-days: 5
+2
View File
@@ -37,6 +37,7 @@ jobs:
with:
name: ubuntu-noble-sysroot-amd64
path: tools/sysroot/output/
retention-days: 1
- name: Install AWS CLI
run: |
@@ -98,6 +99,7 @@ jobs:
with:
name: ubuntu-noble-sysroot-arm64
path: tools/sysroot/output/
retention-days: 1
- name: Install AWS CLI
run: |
+121 -63
View File
@@ -5,9 +5,17 @@ on:
branches: [ "main" ]
paths:
# Note: C++ changes trigger shardok_arm64_build.yml instead
# Note: Auth changes trigger auth_build.yml instead
# Note: Windows installer changes trigger installer_build.yml instead
- 'src/main/go/**'
- '!src/main/go/net/eagle0/authservice/**'
- '!src/main/go/net/eagle0/authcli/**'
- '!src/main/go/net/eagle0/clients/**'
- 'src/main/scala/**'
- 'src/main/protobuf/**'
- '!src/main/protobuf/net/eagle0/eagle/api/auth.proto'
- '!src/main/protobuf/net/eagle0/eagle/api/admin/**'
- '!src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
- 'src/main/resources/**'
- 'ci/BUILD.bazel'
- 'MODULE.bazel'
@@ -153,6 +161,8 @@ jobs:
ADMIN_IMAGE: ${{ needs.build-all.outputs.admin_image_tag }}
JFR_SIDECAR_IMAGE: ${{ needs.build-all.outputs.jfr_sidecar_image_tag }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GPT_MODEL_NAME: ${{ secrets.GPT_MODEL_NAME }}
EAGLE_ENABLE_S3: ${{ secrets.EAGLE_ENABLE_S3 }}
DO_SPACES_ACCESS_KEY: ${{ secrets.DO_SPACES_ACCESS_KEY }}
@@ -168,6 +178,8 @@ jobs:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_SIGNIN_KEY_ID: ${{ secrets.APPLE_SIGNIN_KEY_ID }}
APPLE_SIGNIN_PRIVATE_KEY: ${{ secrets.APPLE_SIGNIN_PRIVATE_KEY }}
TWITCH_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }}
TWITCH_CLIENT_SECRET: ${{ secrets.TWITCH_CLIENT_SECRET }}
SHARDOK_ADDRESS: ${{ secrets.SHARDOK_ADDRESS }}
SHARDOK_AUTH_TOKEN: ${{ secrets.SHARDOK_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
@@ -198,16 +210,15 @@ jobs:
# Create directory structure on remote
ssh -i ~/.ssh/deploy_key deploy@"$DO_DROPLET_IP" bash -s << 'SETUP_DIRS'
set -e
mkdir -p /opt/eagle0/scripts/bin /opt/eagle0/nginx /opt/eagle0/deploy
mkdir -p /opt/eagle0/scripts/bin /opt/eagle0/nginx
rm -f /opt/eagle0/scripts/bin/warmup
SETUP_DIRS
# Copy files
scp -i ~/.ssh/deploy_key docker-compose.prod.yml deploy@"$DO_DROPLET_IP":/opt/eagle0/
scp -i ~/.ssh/deploy_key nginx/nginx.conf deploy@"$DO_DROPLET_IP":/opt/eagle0/nginx/
scp -i ~/.ssh/deploy_key scripts/deploy-blue-green.sh scripts/warmup-eagle.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/
scp -i ~/.ssh/deploy_key scripts/deploy-blue-green.sh scripts/warmup-eagle.sh scripts/eagle-exec.sh scripts/eagle-logs.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/
scp -i ~/.ssh/deploy_key scripts/bin/warmup deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/bin/
scp -i ~/.ssh/deploy_key deploy/env.template deploy/update-env.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/deploy/
- name: Deploy to production droplet
run: |
@@ -215,69 +226,93 @@ jobs:
set -ex
cd /opt/eagle0
# Environment variables passed via heredoc
EAGLE_IMAGE="${EAGLE_IMAGE}"
ADMIN_IMAGE="${ADMIN_IMAGE}"
JFR_SIDECAR_IMAGE="${JFR_SIDECAR_IMAGE}"
OPENAI_API_KEY="${OPENAI_API_KEY}"
GPT_MODEL_NAME="${GPT_MODEL_NAME}"
EAGLE_ENABLE_S3="${EAGLE_ENABLE_S3}"
DO_SPACES_ACCESS_KEY="${DO_SPACES_ACCESS_KEY}"
DO_SPACES_SECRET_KEY="${DO_SPACES_SECRET_KEY}"
JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
DISCORD_CLIENT_ID="${DISCORD_CLIENT_ID}"
DISCORD_CLIENT_SECRET="${DISCORD_CLIENT_SECRET}"
GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID}"
GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET}"
GH_OAUTH_CLIENT_ID="${GH_OAUTH_CLIENT_ID}"
GH_OAUTH_CLIENT_SECRET="${GH_OAUTH_CLIENT_SECRET}"
APPLE_SIGNIN_CLIENT_ID="${APPLE_SIGNIN_CLIENT_ID}"
APPLE_TEAM_ID="${APPLE_TEAM_ID}"
APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
SHARDOK_ADDRESS="${SHARDOK_ADDRESS}"
SHARDOK_AUTH_TOKEN="${SHARDOK_AUTH_TOKEN}"
SENTRY_DSN="${SENTRY_DSN}"
FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
DO_REGISTRY_TOKEN="${DO_REGISTRY_TOKEN}"
# =================================================================
# CRITICAL: Validate environment variables before proceeding
# This catches GitHub Actions secret store hiccups early
# =================================================================
validate_env() {
local var_name="\$1"
local var_value="\$2"
local default_value="\${3:-}"
if [ -z "\${var_value}" ]; then
echo "ERROR: \${var_name} is empty. GitHub Actions secrets may have failed to load."
echo "Please retry the workflow."
return 1
fi
# Check if we got a default value instead of the real secret
if [ -n "\${default_value}" ] && [ "\${var_value}" = "\${default_value}" ]; then
echo "ERROR: \${var_name} has default value '\${default_value}' instead of the actual secret."
echo "This indicates GitHub Actions secrets failed to load. Please retry the workflow."
return 1
fi
return 0
}
echo "Validating critical environment variables..."
# These are the raw values from GitHub Actions (before export)
# We check them before exporting to catch issues early
VALIDATION_FAILED=0
validate_env "SHARDOK_ADDRESS" "${SHARDOK_ADDRESS}" "" || VALIDATION_FAILED=1
validate_env "EAGLE_IMAGE" "${EAGLE_IMAGE}" "" || VALIDATION_FAILED=1
validate_env "JWT_PRIVATE_KEY" "${JWT_PRIVATE_KEY}" "" || VALIDATION_FAILED=1
validate_env "DO_REGISTRY_TOKEN" "${DO_REGISTRY_TOKEN}" "" || VALIDATION_FAILED=1
if [ "\${VALIDATION_FAILED}" -eq 1 ]; then
echo ""
echo "========================================="
echo "DEPLOYMENT ABORTED: Missing critical secrets"
echo "This is likely a transient GitHub Actions issue."
echo "Please retry the workflow."
echo "========================================="
exit 1
fi
echo "All critical environment variables validated successfully."
# =================================================================
# Export environment variables for docker compose
# These are passed via heredoc and exported so child processes (docker compose) can access them
export EAGLE_IMAGE="${EAGLE_IMAGE}"
export ADMIN_IMAGE="${ADMIN_IMAGE}"
export JFR_SIDECAR_IMAGE="${JFR_SIDECAR_IMAGE}"
export OPENAI_API_KEY="${OPENAI_API_KEY}"
export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}"
export GEMINI_API_KEY="${GEMINI_API_KEY}"
export GPT_MODEL_NAME="${GPT_MODEL_NAME:-gpt-4o}"
export EAGLE_ENABLE_S3="${EAGLE_ENABLE_S3:-false}"
export DO_SPACES_ACCESS_KEY="${DO_SPACES_ACCESS_KEY}"
export DO_SPACES_SECRET_KEY="${DO_SPACES_SECRET_KEY}"
export JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
export DISCORD_CLIENT_ID="${DISCORD_CLIENT_ID}"
export DISCORD_CLIENT_SECRET="${DISCORD_CLIENT_SECRET}"
export GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID}"
export GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET}"
export GH_OAUTH_CLIENT_ID="${GH_OAUTH_CLIENT_ID}"
export GH_OAUTH_CLIENT_SECRET="${GH_OAUTH_CLIENT_SECRET}"
export APPLE_SIGNIN_CLIENT_ID="${APPLE_SIGNIN_CLIENT_ID}"
export APPLE_TEAM_ID="${APPLE_TEAM_ID}"
export APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
export APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
export TWITCH_CLIENT_ID="${TWITCH_CLIENT_ID}"
export TWITCH_CLIENT_SECRET="${TWITCH_CLIENT_SECRET}"
export SHARDOK_ADDRESS="${SHARDOK_ADDRESS}"
export SHARDOK_AUTH_TOKEN="${SHARDOK_AUTH_TOKEN}"
export SENTRY_DSN="${SENTRY_DSN}"
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
export FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
export DO_REGISTRY_TOKEN="${DO_REGISTRY_TOKEN}"
# Check Docker has IPv6 support
if ! cat /etc/docker/daemon.json 2>/dev/null | grep -q '"ip6tables"'; then
echo "WARNING: Docker IPv6 not configured. Eagle may not reach Hetzner Shardok."
fi
# Update env vars
chmod +x deploy/update-env.sh
cd deploy && ./update-env.sh \
"EAGLE_IMAGE=\${EAGLE_IMAGE}" \
"ADMIN_IMAGE=\${ADMIN_IMAGE}" \
"JFR_SIDECAR_IMAGE=\${JFR_SIDECAR_IMAGE}" \
"OPENAI_API_KEY=\${OPENAI_API_KEY}" \
"GPT_MODEL_NAME=\${GPT_MODEL_NAME:-gpt-4o}" \
"EAGLE_ENABLE_S3=\${EAGLE_ENABLE_S3:-false}" \
"DO_SPACES_ACCESS_KEY=\${DO_SPACES_ACCESS_KEY}" \
"DO_SPACES_SECRET_KEY=\${DO_SPACES_SECRET_KEY}" \
"JWT_PRIVATE_KEY=\${JWT_PRIVATE_KEY}" \
"DISCORD_CLIENT_ID=\${DISCORD_CLIENT_ID}" \
"DISCORD_CLIENT_SECRET=\${DISCORD_CLIENT_SECRET}" \
"GOOGLE_CLIENT_ID=\${GOOGLE_CLIENT_ID}" \
"GOOGLE_CLIENT_SECRET=\${GOOGLE_CLIENT_SECRET}" \
"GH_OAUTH_CLIENT_ID=\${GH_OAUTH_CLIENT_ID}" \
"GH_OAUTH_CLIENT_SECRET=\${GH_OAUTH_CLIENT_SECRET}" \
"APPLE_SIGNIN_CLIENT_ID=\${APPLE_SIGNIN_CLIENT_ID}" \
"APPLE_TEAM_ID=\${APPLE_TEAM_ID}" \
"APPLE_SIGNIN_KEY_ID=\${APPLE_SIGNIN_KEY_ID}" \
"APPLE_SIGNIN_PRIVATE_KEY=\${APPLE_SIGNIN_PRIVATE_KEY}" \
"SHARDOK_ADDRESS=\${SHARDOK_ADDRESS:-shardok:40042}" \
"SHARDOK_AUTH_TOKEN=\${SHARDOK_AUTH_TOKEN}" \
"SENTRY_DSN=\${SENTRY_DSN}" \
"FASTMAIL_API_TOKEN=\${FASTMAIL_API_TOKEN}" \
"FASTMAIL_FROM_EMAIL=\${FASTMAIL_FROM_EMAIL}" \
"FASTMAIL_FROM_NAME=\${FASTMAIL_FROM_NAME}"
cd ..
# Login to registry
echo "\$DO_REGISTRY_TOKEN" | docker login registry.digitalocean.com -u "\$DO_REGISTRY_TOKEN" --password-stdin
@@ -305,20 +340,43 @@ jobs:
echo "All images pulled successfully"
# =================================================================
# Verify Shardok connectivity before proceeding with deployment
# This catches network/firewall issues early
# =================================================================
echo "Verifying Shardok connectivity..."
SHARDOK_HOST=\$(echo "\${SHARDOK_ADDRESS}" | cut -d: -f1)
SHARDOK_PORT=\$(echo "\${SHARDOK_ADDRESS}" | cut -d: -f2)
# Try to connect to Shardok (timeout after 10 seconds)
if nc -z -w 10 "\${SHARDOK_HOST}" "\${SHARDOK_PORT}" 2>/dev/null; then
echo "Shardok connectivity verified: \${SHARDOK_ADDRESS} is reachable"
else
echo ""
echo "========================================="
echo "ERROR: Cannot reach Shardok at \${SHARDOK_ADDRESS}"
echo "This may indicate:"
echo " - Shardok server is not running on Hetzner"
echo " - Network/firewall issues between DigitalOcean and Hetzner"
echo " - Incorrect SHARDOK_ADDRESS configuration"
echo ""
echo "DEPLOYMENT ABORTED: Shardok must be reachable for battles to work."
echo "========================================="
exit 1
fi
# Stop local shardok container if running (now runs on Hetzner)
docker stop shardok-server 2>/dev/null || true
docker rm shardok-server 2>/dev/null || true
# Deploy Eagle with blue-green (handles eagle, nginx, admin, jfr-sidecar)
# Note: Shardok runs on Hetzner, deployed separately via shardok_arm64_build.yml
# Note: Auth is deployed separately via auth_build.yml - do NOT touch auth here
chmod +x /opt/eagle0/scripts/*.sh
[ -f "/opt/eagle0/scripts/bin/warmup" ] && chmod +x /opt/eagle0/scripts/bin/warmup
GIT_SHA=\$(echo "\${EAGLE_IMAGE}" | sed 's/.*://')
/opt/eagle0/scripts/deploy-blue-green.sh "\${GIT_SHA}"
# Ensure auth is running
docker compose -f docker-compose.prod.yml up -d auth
# Verify
sleep 10
docker compose -f docker-compose.prod.yml ps
+37
View File
@@ -0,0 +1,37 @@
name: Eagle Build
on:
push:
branches: [ "main" ]
paths:
- 'src/main/scala/**'
- 'src/main/protobuf/net/eagle0/eagle/**'
- 'src/main/protobuf/net/eagle0/common/**'
- 'WORKSPACE'
- 'MODULE.bazel'
- 'BUILD.bazel'
- '.github/workflows/eagle_build.yml'
pull_request:
paths:
- 'src/main/scala/**'
- 'src/main/protobuf/net/eagle0/eagle/**'
- 'src/main/protobuf/net/eagle0/common/**'
- 'WORKSPACE'
- 'MODULE.bazel'
- 'BUILD.bazel'
- '.github/workflows/eagle_build.yml'
permissions:
contents: read
jobs:
build:
runs-on: [self-hosted, bazel]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
- name: Build Eagle server
run: bazel build //src/main/scala/net/eagle0/eagle:eagle_server
+52 -38
View File
@@ -5,15 +5,16 @@ on:
branches: [ "main" ]
paths:
- ".github/workflows/installer_build.yml"
- "src/main/csharp/net/eagle0/clients/win/installer/**"
- "src/main/go/net/eagle0/clients/win/installer/**"
pull_request:
paths:
- ".github/workflows/installer_build.yml"
- "src/main/csharp/net/eagle0/clients/win/installer/**"
- "src/main/go/net/eagle0/clients/win/installer/**"
workflow_dispatch:
permissions:
contents: read
actions: write # Required to delete artifacts after deploy
jobs:
build-installer:
@@ -25,47 +26,47 @@ jobs:
lfs: false
clean: false
- name: Setup .NET 8
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Inject manifest public key
- name: Build Go installer for Windows
env:
MANIFEST_PUBLIC_KEY: ${{ secrets.MANIFEST_PUBLIC_KEY }}
run: |
if [ -n "$MANIFEST_PUBLIC_KEY" ]; then
CONFIG_FILE="src/main/csharp/net/eagle0/clients/win/installer/EagleInstaller/configuration.txt"
echo "manifest_public_key = $MANIFEST_PUBLIC_KEY" >> "$CONFIG_FILE"
echo "Injected manifest public key into configuration.txt"
cat "$CONFIG_FILE"
else
echo "MANIFEST_PUBLIC_KEY not set, skipping injection"
# Require manifest public key for production builds
if [ -z "$MANIFEST_PUBLIC_KEY" ]; then
echo "ERROR: MANIFEST_PUBLIC_KEY secret is not set"
echo "The installer requires a public key for manifest signature verification"
exit 1
fi
- name: Restore dependencies
run: dotnet restore src/main/csharp/net/eagle0/clients/win/installer/EagleInstaller/EagleInstaller.csproj
# Build Windows installer with WebView GUI (uses CGO cross-compilation)
# Use --action_env to pass the signing key into the genrule sandbox
bazel build //src/main/go/net/eagle0/clients/win/installer:eagle_installer_windows_amd64_webview --stamp --action_env=MANIFEST_PUBLIC_KEY
- name: Build installer
run: dotnet publish src/main/csharp/net/eagle0/clients/win/installer/EagleInstaller/EagleInstaller.csproj -c Release -r win-x64 --self-contained true --output ./installer-output
# Copy to output directory
rm -rf ./installer-output
mkdir -p ./installer-output
cp bazel-bin/src/main/go/net/eagle0/clients/win/installer/Eagle0.exe ./installer-output/Eagle0.exe
echo "Go installer size: $(ls -lh ./installer-output/Eagle0.exe | awk '{print $5}')"
- name: Archive installer binary
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: eagle-installer
path: ./installer-output/EagleInstaller.exe
path: ./installer-output/
retention-days: 1
- name: Verify installer exists
if: success()
run: |
if [ ! -f "./installer-output/EagleInstaller.exe" ]; then
echo "ERROR: EagleInstaller.exe not found at expected location"
echo "Directory contents:"
ls -la ./installer-output/
echo "=== Installer output directory ==="
ls -lh ./installer-output/
if [ ! -f "./installer-output/Eagle0.exe" ]; then
echo "ERROR: Eagle0.exe not found"
exit 1
fi
echo "Installer found at correct location"
echo "Installer found"
- name: Deploy installer
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
@@ -73,25 +74,24 @@ jobs:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: |
INSTALLER_PATH="$(pwd)/installer-output/EagleInstaller.exe"
echo "Using absolute path: $INSTALLER_PATH"
bazel run //src/main/go/net/eagle0/build/installer_build_handler:installer_build_handler -- "$INSTALLER_PATH"
INSTALLER_PATH="$(pwd)/installer-output/Eagle0.exe"
echo "Deploying Go installer to installer/Eagle0.exe"
bazel run //src/main/go/net/eagle0/build/installer_build_handler:installer_build_handler -- "$INSTALLER_PATH" "installer/Eagle0.exe"
- name: Update unified manifest
- name: Update manifest
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
MANIFEST_SIGNING_KEY: ${{ secrets.MANIFEST_SIGNING_KEY }}
run: |
# Create installer manifest content
INSTALLER_SHA=$(sha256sum ./installer-output/EagleInstaller.exe | cut -d' ' -f1)
INSTALLER_SHA=$(sha256sum ./installer-output/Eagle0.exe | cut -d' ' -f1)
echo "installer_version=$INSTALLER_SHA" > /tmp/installer_manifest.txt
echo "installer_url=installer/EagleInstaller.exe" >> /tmp/installer_manifest.txt
echo "installer_url=installer/Eagle0.exe" >> /tmp/installer_manifest.txt
echo "=== Installer manifest content ==="
echo "=== Manifest content ==="
cat /tmp/installer_manifest.txt
echo "=================================="
echo "========================"
# Write signing key to temp file (if available)
SIGNING_ARGS=""
@@ -104,8 +104,22 @@ jobs:
echo "Warning: MANIFEST_SIGNING_KEY not set, manifest will be unsigned"
fi
# Update the unified manifest (with optional signing)
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- installer /tmp/installer_manifest.txt $SIGNING_ARGS
# Update the v2 manifest at installer/v2/eagle0_manifest.txt
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- installer-v2 /tmp/installer_manifest.txt $SIGNING_ARGS
# Cleanup
rm -f /tmp/manifest_signing_key
rm -f /tmp/manifest_signing_key
- name: Delete all installer artifacts
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
env:
GH_TOKEN: ${{ github.token }}
run: |
# Delete ALL eagle-installer artifacts to free up storage
echo "Fetching all eagle-installer artifacts..."
artifact_ids=$(gh api "repos/${{ github.repository }}/actions/artifacts" \
--paginate -q '.artifacts[] | select(.name == "eagle-installer") | .id')
for id in $artifact_ids; do
echo "Deleting artifact ID: $id"
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$id" || true
done
echo "Cleanup complete"
@@ -0,0 +1,62 @@
name: iOS Addressables Build
on:
push:
branches: [ "main" ]
paths:
- ".github/workflows/ios_addressables_build.yml"
- "src/main/csharp/net/eagle0/clients/unity/**"
- "ci/github_actions/build_ios_addressables.sh"
- "ci/github_actions/upload_addressables.sh"
pull_request:
paths:
- ".github/workflows/ios_addressables_build.yml"
- "src/main/csharp/net/eagle0/clients/unity/**"
- "ci/github_actions/build_ios_addressables.sh"
- "ci/github_actions/upload_addressables.sh"
workflow_dispatch:
permissions:
contents: read
jobs:
build-ios-addressables:
runs-on: [self-hosted, macOS, unity-mac]
steps:
- uses: actions/checkout@v4
with:
lfs: true
clean: true
- name: Pull LFS files
run: git lfs pull
- name: Restore Library/
env:
UNITY_CACHE_PLATFORM: ios
run: ./ci/github_actions/restore_library.sh
- name: Build iOS Addressables
run: ./ci/github_actions/build_ios_addressables.sh "/tmp/eagle0/editor_ios_addressables.log"
- name: Persist Library/
if: success()
env:
UNITY_CACHE_PLATFORM: ios
run: ./ci/github_actions/persist_library.sh
- name: Upload Addressables to CDN
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: ./ci/github_actions/upload_addressables.sh iOS
- name: Archive Build Log
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: editor_ios_addressables.log
path: /tmp/eagle0/editor_ios_addressables.log
retention-days: 5
+65 -6
View File
@@ -20,6 +20,7 @@ on:
- "scripts/notarize_wait.sh"
- "ci/github_actions/build_mac.sh"
- "ci/github_actions/build_unity_mac.sh"
- "ci/github_actions/upload_addressables.sh"
- "ci/mac/**"
pull_request:
paths:
@@ -38,6 +39,7 @@ on:
- "scripts/notarize_wait.sh"
- "ci/github_actions/build_mac.sh"
- "ci/github_actions/build_unity_mac.sh"
- "ci/github_actions/upload_addressables.sh"
- "ci/mac/**"
workflow_dispatch:
inputs:
@@ -49,6 +51,7 @@ on:
permissions:
contents: read
actions: write # Required to delete artifacts after deploy
jobs:
build-and-sign:
@@ -81,6 +84,13 @@ jobs:
UNITY_CACHE_PLATFORM: mac
run: ./ci/github_actions/persist_library.sh
- name: Upload Addressables to CDN
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: ./ci/github_actions/upload_addressables.sh StandaloneOSX
- name: Inject Sparkle Framework
if: success()
env:
@@ -166,7 +176,7 @@ jobs:
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
uses: actions/upload-artifact@v4
with:
name: signed-mac-app
name: signed-mac-app-${{ github.run_id }}
path: /tmp/eagle0/eagle0MAC/eagle0.app.zip
retention-days: 1
@@ -176,6 +186,7 @@ jobs:
with:
name: editor_mac.log
path: /tmp/eagle0/editor_mac.log
retention-days: 5
wait-notarization:
needs: build-and-sign
@@ -193,7 +204,7 @@ jobs:
- name: Download signed app
uses: actions/download-artifact@v4
with:
name: signed-mac-app
name: signed-mac-app-${{ github.run_id }}
path: /tmp/eagle0/eagle0MAC
- name: Unzip signed app
@@ -221,7 +232,7 @@ jobs:
- name: Upload notarized app
uses: actions/upload-artifact@v4
with:
name: notarized-mac-app
name: notarized-mac-app-${{ github.run_id }}
path: /tmp/eagle0/eagle0MAC/eagle0.app.zip
retention-days: 1
@@ -241,7 +252,7 @@ jobs:
- name: Download notarized app
uses: actions/download-artifact@v4
with:
name: notarized-mac-app
name: notarized-mac-app-${{ github.run_id }}
path: /tmp/eagle0/eagle0MAC
- name: Unzip notarized app
@@ -261,13 +272,61 @@ jobs:
echo "$SPARKLE_EDDSA_PRIVATE_KEY" > "$SPARKLE_PRIVATE_KEY_PATH"
chmod 600 "$SPARKLE_PRIVATE_KEY_PATH"
VERSION=$(git describe --tags --always)
BUILD_NUMBER=$(git rev-list --count HEAD)
# Install dmgbuild (creates .DS_Store programmatically, no AppleScript needed)
pip3 install dmgbuild
# Background image for styled DMG
BACKGROUND_PATH="$(pwd)/ci/mac/dmg/background.png"
# Read version from the built app's Info.plist to ensure appcast matches the actual app
APP_PATH="/tmp/eagle0/eagle0MAC/eagle0.app"
BUILD_NUMBER=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$APP_PATH/Contents/Info.plist")
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$APP_PATH/Contents/Info.plist")
bazel run //src/main/go/net/eagle0/build/mac_build_handler:mac_build_handler -- \
"/tmp/eagle0/eagle0MAC/eagle0.app" \
"$VERSION" \
"$BUILD_NUMBER" \
"$BACKGROUND_PATH" \
"$SPARKLE_PRIVATE_KEY_PATH"
rm "$SPARKLE_PRIVATE_KEY_PATH"
- name: Delete this run's Mac app artifacts
env:
GH_TOKEN: ${{ github.token }}
run: |
# Delete this run's artifacts (names include run ID to avoid conflicts)
for artifact_name in signed-mac-app-${{ github.run_id }} notarized-mac-app-${{ github.run_id }}; do
echo "Deleting artifact: $artifact_name"
artifact_id=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
-q ".artifacts[] | select(.name == \"$artifact_name\") | .id")
if [ -n "$artifact_id" ]; then
echo "Deleting artifact ID: $artifact_id"
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" || true
fi
done
echo "Cleanup complete"
# Cleanup job runs regardless of success/failure to prevent artifact accumulation
cleanup:
needs: [build-and-sign, wait-notarization, deploy]
if: always()
runs-on: ubuntu-latest
steps:
- name: Delete this run's Mac app artifacts
env:
GH_TOKEN: ${{ github.token }}
run: |
# Delete this run's artifacts (names include run ID to avoid conflicts)
for artifact_name in signed-mac-app-${{ github.run_id }} notarized-mac-app-${{ github.run_id }}; do
echo "Deleting artifact: $artifact_name"
artifact_id=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
-q ".artifacts[] | select(.name == \"$artifact_name\") | .id")
if [ -n "$artifact_id" ]; then
echo "Deleting artifact ID: $artifact_id"
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" || true
fi
done
echo "Cleanup complete"
+12 -3
View File
@@ -17,6 +17,7 @@ on:
- "ci/github_actions/build_unity.sh"
- "ci/github_actions/restore_library.sh"
- "ci/github_actions/persist_library.sh"
- "ci/github_actions/upload_addressables.sh"
- "MODULE.bazel"
- "WORKSPACE"
workflow_dispatch:
@@ -35,6 +36,7 @@ on:
- "ci/github_actions/build_unity.sh"
- "ci/github_actions/restore_library.sh"
- "ci/github_actions/persist_library.sh"
- "ci/github_actions/upload_addressables.sh"
- "MODULE.bazel"
- "WORKSPACE"
@@ -63,6 +65,12 @@ jobs:
env:
UNITY_CACHE_PLATFORM: windows
run: ./ci/github_actions/persist_library.sh
- name: Upload Addressables to CDN
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: ./ci/github_actions/upload_addressables.sh StandaloneWindows64
- name: Deploy Windows unity
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
env:
@@ -88,8 +96,8 @@ jobs:
echo "Warning: MANIFEST_SIGNING_KEY not set, manifest will be unsigned"
fi
# Update the unified manifest (with optional signing)
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- unity3d /tmp/unity_manifest.txt $SIGNING_ARGS
# Update the v2 manifest at installer/v2/eagle0_manifest.txt
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- unity3d-v2 /tmp/unity_manifest.txt $SIGNING_ARGS
# Cleanup
rm -f /tmp/manifest_signing_key
@@ -98,4 +106,5 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: editor_win.log
path: /tmp/eagle0/editor_win.log
path: /tmp/eagle0/editor_win.log
retention-days: 5
+12
View File
@@ -32,3 +32,15 @@ nogo(
vet = True,
visibility = ["//visibility:public"],
)
# Dependency constraint tests
# These verify architectural boundaries are maintained
sh_test(
name = "build_deps_test",
srcs = ["scripts/check_build_deps.sh"],
args = ["--ci"],
tags = [
"local", # Needs bazel query access
"no-sandbox",
],
)
+15
View File
@@ -107,6 +107,7 @@ bazel_dep(name = "gazelle", version = "0.47.0", repo_name = "bazel_gazelle")
go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
go_sdk.download(version = "1.23.3")
use_repo(go_sdk, "go_default_sdk")
go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
@@ -118,6 +119,7 @@ use_repo(
"com_github_aws_aws_sdk_go_v2_service_s3",
"com_github_golang_jwt_jwt_v5",
"com_github_google_uuid",
"com_github_webview_webview_go",
"org_golang_google_grpc",
"org_golang_google_protobuf",
)
@@ -336,6 +338,19 @@ http_file(
executable = True,
)
# LLVM MinGW toolchain for Windows cross-compilation from macOS
# This provides a complete toolchain for building Windows executables including
# the MinGW-w64 libraries needed for CGO cross-compilation
LLVM_MINGW_VERSION = "20250305"
http_archive(
name = "llvm_mingw",
build_file = "@//external:BUILD.llvm_mingw",
sha256 = "32c24fc62fc8b9f8a900bf2c730b78b36767688f816f9d21e97a168289ff44e0",
strip_prefix = "llvm-mingw-%s-ucrt-macos-14.4.1-universal" % LLVM_MINGW_VERSION,
urls = ["https://github.com/fathonix/llvm-mingw-arm64ec-macos/releases/download/%s/llvm-mingw-%s-ucrt-macos-14.4.1-universal.tar.xz" % (LLVM_MINGW_VERSION, LLVM_MINGW_VERSION)],
)
#
# Toolchain Registration
#
+561
View File
@@ -611,6 +611,445 @@
]
}
},
"@@aspect_rules_js~//npm:extensions.bzl%pnpm": {
"general": {
"bzlTransitiveDigest": "T22SdPzhLxF1CM+j9RD/Rq03yJ0NDfH2eE6hAbUcOII=",
"usagesDigest": "6rWte4KDbiluq1s7w98bc4+2NjA8w67DKHDj4+DNw/Y=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"pnpm": {
"bzlFile": "@@aspect_rules_js~//npm/private:npm_import.bzl",
"ruleClassName": "npm_import_rule",
"attributes": {
"package": "pnpm",
"version": "8.6.7",
"root_package": "",
"link_workspace": "",
"link_packages": {},
"integrity": "sha512-vRIWpD/L4phf9Bk2o/O2TDR8fFoJnpYrp2TKqTIZF/qZ2/rgL3qKXzHofHgbXsinwMoSEigz28sqk3pQ+yMEQQ==",
"url": "",
"commit": "",
"patch_args": [
"-p0"
],
"patches": [],
"custom_postinstall": "",
"npm_auth": "",
"npm_auth_basic": "",
"npm_auth_username": "",
"npm_auth_password": "",
"lifecycle_hooks": [],
"extra_build_content": "load(\"@aspect_rules_js//js:defs.bzl\", \"js_binary\")\njs_binary(name = \"pnpm\", data = glob([\"package/**\"]), entry_point = \"package/dist/pnpm.cjs\", visibility = [\"//visibility:public\"])",
"generate_bzl_library_targets": false,
"extract_full_archive": true,
"exclude_package_contents": [],
"system_tar": "auto"
}
},
"pnpm__links": {
"bzlFile": "@@aspect_rules_js~//npm/private:npm_import.bzl",
"ruleClassName": "npm_import_links",
"attributes": {
"package": "pnpm",
"version": "8.6.7",
"dev": false,
"root_package": "",
"link_packages": {},
"deps": {},
"transitive_closure": {},
"lifecycle_build_target": false,
"lifecycle_hooks_env": [],
"lifecycle_hooks_execution_requirements": [
"no-sandbox"
],
"lifecycle_hooks_use_default_shell_env": false,
"bins": {},
"package_visibility": [
"//visibility:public"
],
"replace_package": "",
"exclude_package_contents": []
}
}
},
"recordedRepoMappingEntries": [
[
"aspect_bazel_lib~",
"bazel_lib",
"bazel_lib~"
],
[
"aspect_bazel_lib~",
"bazel_skylib",
"bazel_skylib~"
],
[
"aspect_bazel_lib~",
"bazel_tools",
"bazel_tools"
],
[
"aspect_bazel_lib~",
"tar.bzl",
"tar.bzl~"
],
[
"aspect_rules_js~",
"aspect_bazel_lib",
"aspect_bazel_lib~"
],
[
"aspect_rules_js~",
"aspect_rules_js",
"aspect_rules_js~"
],
[
"aspect_rules_js~",
"bazel_features",
"bazel_features~"
],
[
"aspect_rules_js~",
"bazel_skylib",
"bazel_skylib~"
],
[
"aspect_rules_js~",
"bazel_tools",
"bazel_tools"
],
[
"bazel_features~",
"bazel_features_globals",
"bazel_features~~version_extension~bazel_features_globals"
],
[
"bazel_features~",
"bazel_features_version",
"bazel_features~~version_extension~bazel_features_version"
],
[
"bazel_lib~",
"bazel_skylib",
"bazel_skylib~"
],
[
"bazel_lib~",
"bazel_tools",
"bazel_tools"
],
[
"tar.bzl~",
"aspect_bazel_lib",
"aspect_bazel_lib~"
],
[
"tar.bzl~",
"bazel_skylib",
"bazel_skylib~"
],
[
"tar.bzl~",
"tar.bzl",
"tar.bzl~"
]
]
}
},
"@@aspect_rules_ts~//ts:extensions.bzl%ext": {
"general": {
"bzlTransitiveDigest": "h1hftyFCdJgiHD9blfFcMAiEk5ltEoUdNkuHPIkg/hM=",
"usagesDigest": "v0aTa4/gasWF2bvssXYr1bqcYWc3kjV48hcj0z2QVT0=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"npm_typescript": {
"bzlFile": "@@aspect_rules_ts~//ts/private:npm_repositories.bzl",
"ruleClassName": "http_archive_version",
"attributes": {
"bzlmod": true,
"version": "5.8.3",
"integrity": "",
"build_file": "@@aspect_rules_ts~//ts:BUILD.typescript",
"build_file_substitutions": {
"bazel_worker_version": "5.4.2",
"google_protobuf_version": "3.20.1"
},
"urls": [
"https://registry.npmjs.org/typescript/-/typescript-{}.tgz"
]
}
}
},
"recordedRepoMappingEntries": [
[
"aspect_rules_ts~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@cel-spec~//:extensions.bzl%non_module_dependencies": {
"general": {
"bzlTransitiveDigest": "/2uyuQa5purSharolRXYOGYMGSGjDeByo6JfQDWsraA=",
"usagesDigest": "2f6juplOpWu+UdD1kVgi773xavnFQ+OcH0PRuQduDxY=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"com_google_googleapis": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"sha256": "bd8e735d881fb829751ecb1a77038dda4a8d274c45490cb9fcf004583ee10571",
"strip_prefix": "googleapis-07c27163ac591955d736f3057b1619ece66f5b99",
"urls": [
"https://github.com/googleapis/googleapis/archive/07c27163ac591955d736f3057b1619ece66f5b99.tar.gz"
]
}
}
},
"recordedRepoMappingEntries": [
[
"cel-spec~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@cel-spec~//:googleapis_ext.bzl%googleapis_ext": {
"general": {
"bzlTransitiveDigest": "yun2jmsomFi3bs5bjQWXApBzqQf66zBJ39JEBYigzdc=",
"usagesDigest": "OK8FsLndSl2AbwGM1Npe5NdHR1kDAebcw7Ee+KkekE0=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"com_google_googleapis_imports": {
"bzlFile": "@@cel-spec~~non_module_dependencies~com_google_googleapis//:repository_rules.bzl",
"ruleClassName": "switched_rules",
"attributes": {
"rules": {
"proto_library_with_info": [
"",
""
],
"moved_proto_library": [
"",
""
],
"java_proto_library": [
"",
""
],
"java_grpc_library": [
"",
""
],
"java_gapic_library": [
"",
""
],
"java_gapic_test": [
"",
""
],
"java_gapic_assembly_gradle_pkg": [
"",
""
],
"py_proto_library": [
"",
""
],
"py_grpc_library": [
"",
""
],
"py_gapic_library": [
"",
""
],
"py_test": [
"",
""
],
"py_gapic_assembly_pkg": [
"",
""
],
"py_import": [
"",
""
],
"go_proto_library": [
"",
""
],
"go_library": [
"",
""
],
"go_test": [
"",
""
],
"go_gapic_library": [
"",
""
],
"go_gapic_assembly_pkg": [
"",
""
],
"cc_proto_library": [
"native.cc_proto_library",
""
],
"cc_grpc_library": [
"",
""
],
"cc_gapic_library": [
"",
""
],
"php_proto_library": [
"",
"php_proto_library"
],
"php_grpc_library": [
"",
"php_grpc_library"
],
"php_gapic_library": [
"",
"php_gapic_library"
],
"php_gapic_assembly_pkg": [
"",
"php_gapic_assembly_pkg"
],
"nodejs_gapic_library": [
"",
"typescript_gapic_library"
],
"nodejs_gapic_assembly_pkg": [
"",
"typescript_gapic_assembly_pkg"
],
"ruby_proto_library": [
"",
""
],
"ruby_grpc_library": [
"",
""
],
"ruby_ads_gapic_library": [
"",
""
],
"ruby_cloud_gapic_library": [
"",
""
],
"ruby_gapic_assembly_pkg": [
"",
""
],
"csharp_proto_library": [
"",
""
],
"csharp_grpc_library": [
"",
""
],
"csharp_gapic_library": [
"",
""
],
"csharp_gapic_assembly_pkg": [
"",
""
]
}
}
}
},
"recordedRepoMappingEntries": [
[
"cel-spec~",
"com_google_googleapis",
"cel-spec~~non_module_dependencies~com_google_googleapis"
]
]
}
},
"@@envoy_api~//bazel:repositories.bzl%non_module_deps": {
"general": {
"bzlTransitiveDigest": "6TqmRfVELxZJRPQYuJpC4JBX4QvdrHqTDOUJGOGODSo=",
"usagesDigest": "IivjlawPvqhHPUJ3c6dLiPsH22mn/g/dJtlmv3zdimM=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"prometheus_metrics_model": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/prometheus/client_model/archive/v0.6.1.tar.gz"
],
"sha256": "b9b690bc35d80061f255faa7df7621eae39fe157179ccd78ff6409c3b004f05e",
"strip_prefix": "client_model-0.6.1",
"build_file_content": "\nload(\"@envoy_api//bazel:api_build_system.bzl\", \"api_cc_py_proto_library\")\nload(\"@io_bazel_rules_go//proto:def.bzl\", \"go_proto_library\")\n\napi_cc_py_proto_library(\n name = \"client_model\",\n srcs = [\n \"io/prometheus/client/metrics.proto\",\n ],\n visibility = [\"//visibility:public\"],\n)\n\ngo_proto_library(\n name = \"client_model_go_proto\",\n importpath = \"github.com/prometheus/client_model/go\",\n proto = \":client_model\",\n visibility = [\"//visibility:public\"],\n)\n"
}
},
"com_github_bufbuild_buf": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/bufbuild/buf/releases/download/v1.49.0/buf-Linux-x86_64.tar.gz"
],
"sha256": "ee8da9748249f7946d79191e36469ce7bc3b8ba80019bff1fa4289a44cbc23bf",
"strip_prefix": "buf",
"build_file_content": "\npackage(\n default_visibility = [\"//visibility:public\"],\n)\n\nfilegroup(\n name = \"buf\",\n srcs = [\n \"@com_github_bufbuild_buf//:bin/buf\",\n ],\n tags = [\"manual\"], # buf is downloaded as a linux binary; tagged manual to prevent build for non-linux users\n)\n"
}
},
"envoy_toolshed": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/envoyproxy/toolshed/archive/bazel-v0.2.2.tar.gz"
],
"sha256": "443fe177aba0cef8c17b7a48905c925c67b09005b10dd70ff12cd9f729a72d51",
"strip_prefix": "toolshed-bazel-v0.2.2/bazel"
}
}
},
"recordedRepoMappingEntries": [
[
"envoy_api~",
"bazel_tools",
"bazel_tools"
],
[
"envoy_api~",
"envoy_api",
"envoy_api~"
]
]
}
},
"@@googleapis~//:extensions.bzl%switched_rules": {
"general": {
"bzlTransitiveDigest": "vG6fuTzXD8MMvHWZEQud0MMH7eoC4GXY0va7VrFFh04=",
@@ -779,6 +1218,37 @@
"recordedRepoMappingEntries": []
}
},
"@@pybind11_bazel~//:internal_configure.bzl%internal_configure_extension": {
"general": {
"bzlTransitiveDigest": "CyAKLVVonohnkTSqg9II/HA7M49sOlnMkgMHL3CmDuc=",
"usagesDigest": "mFrTHX5eCiNU/OIIGVHH3cOILY9Zmjqk8RQYv8o6Thk=",
"recordedFileInputs": {
"@@pybind11_bazel~//MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34"
},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"pybind11": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file": "@@pybind11_bazel~//:pybind11-BUILD.bazel",
"strip_prefix": "pybind11-2.12.0",
"urls": [
"https://github.com/pybind/pybind11/archive/v2.12.0.zip"
]
}
}
},
"recordedRepoMappingEntries": [
[
"pybind11_bazel~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@rules_foreign_cc~//foreign_cc:extensions.bzl%tools": {
"general": {
"bzlTransitiveDigest": "a7qnESofmIRYId6wwGNPJ9kvExU80KrkxL281P3+lBE=",
@@ -1119,6 +1589,97 @@
]
}
},
"@@rules_fuzzing~//fuzzing/private:extensions.bzl%non_module_dependencies": {
"general": {
"bzlTransitiveDigest": "hVgJRQ3Er45/UUAgNn1Yp2Khcp/Y8WyafA2kXIYmQ5M=",
"usagesDigest": "YnIrdgwnf3iCLfChsltBdZ7yOJh706lpa2vww/i2pDI=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"platforms": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz",
"https://github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz"
],
"sha256": "8150406605389ececb6da07cbcb509d5637a3ab9a24bc69b1101531367d89d74"
}
},
"rules_python": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"sha256": "d70cd72a7a4880f0000a6346253414825c19cdd40a28289bdf67b8e6480edff8",
"strip_prefix": "rules_python-0.28.0",
"url": "https://github.com/bazelbuild/rules_python/releases/download/0.28.0/rules_python-0.28.0.tar.gz"
}
},
"bazel_skylib": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"sha256": "cd55a062e763b9349921f0f5db8c3933288dc8ba4f76dd9416aac68acee3cb94",
"urls": [
"https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz",
"https://github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz"
]
}
},
"com_google_absl": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/abseil/abseil-cpp/archive/refs/tags/20240116.1.zip"
],
"strip_prefix": "abseil-cpp-20240116.1",
"integrity": "sha256-7capMWOvWyoYbUaHF/b+I2U6XLMaHmky8KugWvfXYuk="
}
},
"rules_fuzzing_oss_fuzz": {
"bzlFile": "@@rules_fuzzing~//fuzzing/private/oss_fuzz:repository.bzl",
"ruleClassName": "oss_fuzz_repository",
"attributes": {}
},
"honggfuzz": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file": "@@rules_fuzzing~//:honggfuzz.BUILD",
"sha256": "6b18ba13bc1f36b7b950c72d80f19ea67fbadc0ac0bb297ec89ad91f2eaa423e",
"url": "https://github.com/google/honggfuzz/archive/2.5.zip",
"strip_prefix": "honggfuzz-2.5"
}
},
"rules_fuzzing_jazzer": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_jar",
"attributes": {
"sha256": "ee6feb569d88962d59cb59e8a31eb9d007c82683f3ebc64955fd5b96f277eec2",
"url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer/0.20.1/jazzer-0.20.1.jar"
}
},
"rules_fuzzing_jazzer_api": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_jar",
"attributes": {
"sha256": "f5a60242bc408f7fa20fccf10d6c5c5ea1fcb3c6f44642fec5af88373ae7aa1b",
"url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer-api/0.20.1/jazzer-api-0.20.1.jar"
}
}
},
"recordedRepoMappingEntries": [
[
"rules_fuzzing~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@rules_java~//java:rules_java_deps.bzl%compatibility_proxy": {
"general": {
"bzlTransitiveDigest": "KIX40nDfygEWbU+rq3nYpt3tVgTK/iO8PKh5VMBlN7M=",
-28
View File
@@ -1,28 +0,0 @@
#!/usr/bin/env bash
/bin/mkdir -p win_output
/usr/bin/dotnet publish src/main/csharp/net/eagle0/clients/win/installer/EagleInstaller.sln -o win_output
SHA=`sha256sum /tmp/EagleInstaller.exe | awk '{print $1 }'`
ZIP_FILE="updater__$SHA.zip"
/usr/bin/zip win_output/$ZIP_FILE win_output/EagleInstaller.exe
rm win_output/EagleInstaller.exe
rm win_output/EagleInstaller.pdb
DATE=`date +"%Y-%m-%d %T"`
cat > win_output/updater.html <<-EOF
<html>
<head>
<title>Download Eagle Updater</title>
</head>
<body>
<a href="http://eagle0.net/assets/$ZIP_FILE">$ZIP_FILE</a> (updated $DATE)
</body>
</html>
EOF
SSH_KEY_FILE=$1
SSH_USER_NAME=$2
/usr/bin/rsync -r --copy-links -e "/usr/bin/ssh -i $SSH_KEY_FILE -p 9022" win_output/ $SSH_USER_NAME@eagle0.net:/www/assets/
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Build iOS Addressables only (no player build)
# This switches Unity to iOS target and builds addressables for CDN upload
set -euxo pipefail
. ./ci/unity_version.sh
WORKSPACE=$(pwd)
echo "Building protos"
./scripts/build_protos.sh
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
LOG_PATH=${1:-"/tmp/eagle0/editor_ios_addressables.log"}
echo "Building iOS Addressables"
mkdir -p "$(dirname "$LOG_PATH")"
# Build Addressables for iOS target
# -buildTarget iOS switches the editor to iOS before running
${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
-nographics \
-batchmode \
-quit \
-buildTarget iOS \
-executeMethod BuildScript.BuildAddressables \
-logFile "$LOG_PATH" \
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
echo "iOS Addressables build complete"
echo "Bundles should be in: $WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0/ServerData/iOS/"
+3 -1
View File
@@ -15,10 +15,12 @@ echo "Cleaning up $BUILD_DIR"
/bin/rm -rf "$BUILD_DIR"
/bin/mkdir -p "$BUILD_DIR"
# Use custom build script that builds Addressables before the player
${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
-nographics \
-batchmode \
-quit \
-buildOSXUniversalPlayer "$BUILD_DIR/eagle0.app" \
-executeMethod BuildScript.BuildMacPlayer \
-buildPath "$BUILD_DIR/eagle0.app" \
-logFile "$LOG_PATH" \
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
+3 -1
View File
@@ -15,10 +15,12 @@ echo "Cleaning up $1"
/bin/rm -rf $1
/bin/mkdir -p $1
# Use custom build script that builds Addressables before the player
${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
-nographics \
-batchmode \
-quit \
-buildWindows64Player $BUILD_DIR/eagle0.exe \
-executeMethod BuildScript.BuildWindowsPlayer \
-buildPath "$BUILD_DIR/eagle0.exe" \
-logFile $LOG_PATH \
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Upload Addressables bundles to DigitalOcean Spaces
# Usage: ./upload_addressables.sh <build_target>
# Example: ./upload_addressables.sh StandaloneOSX
#
# Required environment variables:
# ACCESS_KEY_ID - DigitalOcean Spaces access key (same as other deploys)
# SECRET_KEY - DigitalOcean Spaces secret key (same as other deploys)
set -euxo pipefail
BUILD_TARGET=$1
WORKSPACE=$(pwd)
UNITY_PROJECT="$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
SERVER_DATA="$UNITY_PROJECT/ServerData/$BUILD_TARGET"
# DigitalOcean Spaces configuration (same region as other eagle0 buckets)
DO_ENDPOINT="https://sfo3.digitaloceanspaces.com"
DO_BUCKET="eagle0-assets"
if [ ! -d "$SERVER_DATA" ]; then
echo "No Addressables bundles found at $SERVER_DATA"
echo "Skipping upload (this is expected if Addressables are bundled locally)"
exit 0
fi
echo "Uploading Addressables bundles from $SERVER_DATA"
echo "Target: s3://$DO_BUCKET/addressables/$BUILD_TARGET/"
# Configure AWS CLI for DigitalOcean Spaces
export AWS_ACCESS_KEY_ID="$ACCESS_KEY_ID"
export AWS_SECRET_ACCESS_KEY="$SECRET_KEY"
# Sync bundles to Spaces
# --delete removes files in destination that don't exist in source
# --acl public-read makes files publicly accessible
aws s3 sync "$SERVER_DATA" "s3://$DO_BUCKET/addressables/$BUILD_TARGET/" \
--endpoint-url "$DO_ENDPOINT" \
--acl public-read \
--delete
echo "Addressables upload complete"
echo "Files available at: https://assets.eagle0.net/addressables/$BUILD_TARGET/"
Binary file not shown.
-40
View File
@@ -1,40 +0,0 @@
# Environment template for production deployment
# This file defines all env vars used by docker-compose.prod.yml
# Workflows should update their specific vars without overwriting others
# Container images (managed by respective build workflows)
# Note: Shardok runs on Hetzner, deployed via shardok_arm64_build.yml
EAGLE_IMAGE=registry.digitalocean.com/eagle0/eagle-server:latest
ADMIN_IMAGE=registry.digitalocean.com/eagle0/admin-server:latest
JFR_SIDECAR_IMAGE=registry.digitalocean.com/eagle0/jfr-sidecar:latest
AUTH_IMAGE=registry.digitalocean.com/eagle0/auth-server:latest
# OpenAI / LLM
OPENAI_API_KEY=
GPT_MODEL_NAME=gpt-4o
# DigitalOcean Spaces (S3-compatible storage)
EAGLE_ENABLE_S3=false
DO_SPACES_ACCESS_KEY=
DO_SPACES_SECRET_KEY=
# JWT authentication
JWT_PRIVATE_KEY=
# OAuth providers
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Shardok connection (Hetzner ARM64 server)
SHARDOK_ADDRESS=
SHARDOK_AUTH_TOKEN=
# Monitoring
SENTRY_DSN=
# Email (Fastmail JMAP)
FASTMAIL_API_TOKEN=
FASTMAIL_FROM_EMAIL=
FASTMAIL_FROM_NAME=
-61
View File
@@ -1,61 +0,0 @@
#!/bin/bash
# Update .env file without losing other variables
# Usage: ./update-env.sh KEY1=value1 KEY2=value2 ...
#
# This script:
# 1. Creates .env from template if it doesn't exist
# 2. Updates only the specified KEY=value pairs
# 3. Preserves all other existing values
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="${ENV_FILE:-/opt/eagle0/.env}"
TEMPLATE_FILE="${TEMPLATE_FILE:-$SCRIPT_DIR/env.template}"
# Create .env from template if it doesn't exist
if [ ! -f "$ENV_FILE" ]; then
if [ -f "$TEMPLATE_FILE" ]; then
echo "Creating .env from template..."
grep -v '^#' "$TEMPLATE_FILE" | grep -v '^$' > "$ENV_FILE"
else
echo "Creating empty .env..."
touch "$ENV_FILE"
fi
chmod 600 "$ENV_FILE"
fi
# Process each KEY=VALUE argument
for arg in "$@"; do
# Skip empty args
[ -z "$arg" ] && continue
# Parse KEY=VALUE
KEY="${arg%%=*}"
VALUE="${arg#*=}"
# Skip if no key
[ -z "$KEY" ] && continue
# Skip setting empty values (keeps existing value)
if [ -z "$VALUE" ]; then
echo "Skipping $KEY (empty value)"
continue
fi
# Remove existing line for this key and add new one
# Values are quoted to handle special characters (/, =, etc.)
if grep -q "^${KEY}=" "$ENV_FILE" 2>/dev/null; then
# Key exists, remove old line and add new one (sed with special chars is fragile)
grep -v "^${KEY}=" "$ENV_FILE" > "$ENV_FILE.tmp" && mv "$ENV_FILE.tmp" "$ENV_FILE"
echo "${KEY}=\"${VALUE}\"" >> "$ENV_FILE"
echo "Updated $KEY"
else
# Key doesn't exist, add it
echo "${KEY}=\"${VALUE}\"" >> "$ENV_FILE"
echo "Added $KEY"
fi
done
chmod 600 "$ENV_FILE"
echo "Done updating $ENV_FILE"
+15 -14
View File
@@ -18,23 +18,22 @@ services:
image: ${EAGLE_IMAGE:-registry.digitalocean.com/eagle0/eagle-server:latest}
container_name: eagle-blue
command:
- "--gpt-model-name"
- "${GPT_MODEL_NAME:-gpt-5.1}"
- "--shardok-interface-remote-address"
- "${SHARDOK_ADDRESS:-shardok:40042}"
- "${SHARDOK_ADDRESS}"
- "--auth-service-url"
- "auth:40033"
ports:
- "40032:40032"
environment:
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
GEMINI_API_KEY: "${GEMINI_API_KEY:-}"
EAGLE_ENABLE_S3: "${EAGLE_ENABLE_S3:-false}"
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}"
DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}"
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
# JWT public key for token validation (auth service handles signing)
# Reads from /etc/eagle0/keys/public.pem via shared volume
# Auth token for Shardok on Hetzner (required)
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
# Use persistent volume for save data (users, games, etc.)
@@ -47,7 +46,7 @@ services:
- ./archived:/app/archived # Archived completed games
- ./jfr:/app/jfr # JFR recordings - dump with: docker exec eagle-blue jcmd 1 JFR.dump filename=/app/jfr/profile.jfr
- jvm-tmp:/tmp # Shared with jfr-sidecar for JVM attach socket files
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
- jwt-keys:/etc/eagle0/keys:ro # JWT public key from auth service (read-only)
depends_on:
- auth
restart: unless-stopped
@@ -68,23 +67,22 @@ services:
container_name: eagle-green
profiles: ["blue-green"] # Only started during blue-green deployment
command:
- "--gpt-model-name"
- "${GPT_MODEL_NAME:-gpt-5.1}"
- "--shardok-interface-remote-address"
- "${SHARDOK_ADDRESS:-shardok:40042}"
- "${SHARDOK_ADDRESS}"
- "--auth-service-url"
- "auth:40033"
ports:
- "40034:40032" # Different host port for staging
environment:
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
GEMINI_API_KEY: "${GEMINI_API_KEY:-}"
EAGLE_ENABLE_S3: "${EAGLE_ENABLE_S3:-false}"
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}"
DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}"
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
# JWT public key for token validation (auth service handles signing)
# Reads from /etc/eagle0/keys/public.pem via shared volume
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
EAGLE_SAVE_DIR: "/app/saves"
EAGLE_ARCHIVE_DIR: "/app/archived"
@@ -95,7 +93,7 @@ services:
- ./archived:/app/archived # Same archive directory as blue
- ./jfr:/app/jfr # JFR recordings (same as blue)
- jvm-tmp:/tmp # Shared with jfr-sidecar-green for JVM attach socket files
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
- jwt-keys:/etc/eagle0/keys:ro # JWT public key from auth service (read-only)
depends_on:
- auth
restart: "no" # Don't auto-restart during deployment
@@ -140,6 +138,9 @@ services:
APPLE_TEAM_ID: "${APPLE_TEAM_ID:-}"
APPLE_SIGNIN_KEY_ID: "${APPLE_SIGNIN_KEY_ID:-}"
APPLE_SIGNIN_PRIVATE_KEY: "${APPLE_SIGNIN_PRIVATE_KEY:-}"
# Twitch OAuth credentials
TWITCH_CLIENT_ID: "${TWITCH_CLIENT_ID:-}"
TWITCH_CLIENT_SECRET: "${TWITCH_CLIENT_SECRET:-}"
# Server base URL for OAuth callbacks
SERVER_BASE_URL: "${SERVER_BASE_URL:-https://prod.eagle0.net}"
# JWT keys - PEM files in volume, bootstrapped from JWK on first run
+67 -46
View File
@@ -109,11 +109,20 @@ All 26 tracks have CC licenses with proper attribution:
| Dragon Castle | Makai Symphony | CC BY-SA 3.0 |
| Durandal | Makai Symphony | CC BY-SA 3.0 |
**Tracks without specific license (verify):**
- Market Day
- Shopping List
- Medieval: Victory Theme
- Tracks by Dima Koltsov (AUDIUS): No Time for Greatness, Warriors of Demacia, Forest Queen Tale, Valor, Clouds
**Tracks with non-CC licenses:**
| Track | Artist | License | Source |
|-------|--------|---------|--------|
| Market Day | RandomMind | Free without attribution | [Chosic](https://www.chosic.com/download-audio/27016/) |
| Shopping List | Komiku | Free without attribution | [Chosic](https://www.chosic.com/download-audio/24714/) |
| Medieval: Victory Theme | RandomMind | CC0 Public Domain | [Chosic](https://www.chosic.com/download-audio/28492/) |
| 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.
---
@@ -129,42 +138,55 @@ All 26 tracks have CC licenses with proper attribution:
## 4. Potentially Problematic Assets (Review Needed)
### Stock Images (Possible License Issues)
These appear to be stock images that may have been used as placeholders:
| File | Concern |
|------|---------|
| ~~`Assets/Eagle/79066358-stock-illustration-raster-illustration-medieval-purse-bag...jpg`~~ | **DELETED** (2025-01-04) |
| ~~`Assets/Images/kisspng-hammer-hand-saws-tool-clip-art...jpg`~~ | **DELETED** (2025-01-04) |
| ~~`Assets/Images/lee-ermy-cropped.jpg`~~ | **DELETED** (2025-01-04) |
| ~~`Assets/Eagle/images.jpeg`~~ | **DELETED** (2025-01-04) |
### Clip Art (Unknown License)
| File | Concern |
|------|---------|
| `Assets/Shardok/commandImages/bridge.png` | Clip art style wooden bridge, unknown source - **needs replacement** |
| `Assets/Images/startFire.png` | Icon, unknown source - **needs verification or replacement** |
### ~~Clip Art (Unknown License)~~ RESOLVED
| File | Status |
|------|--------|
| ~~`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)
- **Contents:** Spell effects, movement, combat sounds
- **Status:** Unknown origin - may be custom or need verification
**Verified from [Zombie Monster - Undead Collection](https://assetstore.unity.com/packages/audio/sound-fx/creatures/zombie-monster-undead-collection-70662) (Unity Asset Store):**
- `raise_undead.mp3`
- `undead_break_control.mp3`
- `undead_grew.wav`
**⚠️ MUST REPLACE:**
- ~~`anybody.mp3`~~ - **REPLACED** (2026-01-23) with `Positive Effect 6.wav` from Magic Spells Sound Effects LITE
- ~~`burnination.mp3`~~ - **REPLACED** (2026-01-23) with `Magic Element Fire 04.wav` from Medieval Combat Sounds
- `failure_horn.mp3` - licensing issue, no replacement found in purchased assets
- `runaway.mp3` - licensing issue, no replacement found in purchased assets
**Presumed from Unity Asset Store purchases (31 files):**
Owner believes these are from: Fantasy Interface Sounds, Medieval Combat Sounds, Magic Spells Sound Effects LITE, and/or Medieval Battle Sound Pack.
- `archery.mp3`, `battle_shout.mp3`, `boo.mp3`, `braved_water.mp3`
- `build_bridge.mp3`, `build_bridge_failure.mp3`, `charge.mp3`
- `dismiss_unit.mp3`, `duel_challenged.mp3`, `failure_horn.mp3`, `fear.mp3`, `fear_failed.mp3`
- `fire_extinguish.mp3`, `fire_spread.mp3`, `fire_start.mp3`, `fire_start_failure.mp3`
- `freeze.mp3`, `holy_wave.mp3`, `holy_wave_damage.mp3`, `jail_door.mp3`, `lightning.mp3`
- `melee.mp3`, `meteor.mp3`, `mind_control.mp3`, `move.mp3`, `move 1.mp3`
- `raging_fire.mp3`, `reduce.mp3`, `repair.mp3`, `repair_failed.mp3`, `splash.mp3`
### Free Icons
- **Location:** `Assets/free_icons/`
- **Count:** 8 PNG weather icons
- **Status:** Verify "free" means commercially usable
### Terrain Hexes
### ~~Terrain Hexes~~ VERIFIED
- **Location:** `Assets/Terrain Hexes/`
- **Count:** 85 PNG files
- **Status:** Unknown source - verify licensing
- **Status:** ✓ Confirmed Unity Asset Store purchase (2026-01-23)
### StrategyGameIcons
### ~~StrategyGameIcons~~ VERIFIED
- **Location:** `Assets/StrategyGameIcons/`
- **Count:** 138 PNG files
- **Status:** Unknown source - verify licensing
- **Publisher:** REXARD
- **Asset Store Link:** https://assetstore.unity.com/packages/2d/gui/icons/strategy-game-icons-64816
- **Status:** ✓ Confirmed Unity Asset Store purchase (2026-01-23)
---
@@ -196,26 +218,24 @@ NuGet packages in `Assets/Packages/` all include LICENSE.TXT files:
### Must Verify Before Opening Public Access:
1. ~~**Stock images** - The JPG files with stock image filenames need review.~~ **DONE** - Deleted lee-ermy, kisspng, stock-illustration, Yosemite Sam, and images.jpeg (2025-01-04)
1. ~~**Clip art images**~~ - **RESOLVED** (2025-01-23): Replaced with game-icons.net CC BY 3.0 icons
2. **Clip art images** - Unknown license, need replacement with properly licensed alternatives:
- `Assets/Shardok/commandImages/bridge.png` - wooden bridge icon
- `Assets/Images/startFire.png` - fire icon
2. **Shardok sound effects** - 3 files must be replaced:
- `anybody.mp3` - licensing issue
- `burnination.mp3` - licensing issue
- `runaway.mp3` - licensing issue
3. **Shardok sound effects** - 56 MP3 files of unknown origin. Either:
- Document their source
- Replace with known-licensed alternatives
- Confirm they were custom-created
Remaining 31 files presumed from Asset Store purchases; 3 verified from Zombie Monster Undead Collection.
4. **Terrain Hexes** - 85 hex tiles of unknown source
- **TODO:** Confirm if this is a Unity Asset Store purchase (owner believes it is)
3. ~~**Terrain Hexes**~~ - **VERIFIED** (2026-01-23): Confirmed Unity Asset Store purchase
5. **StrategyGameIcons** - 138 icons of unknown source
- **TODO:** Investigate origin - check Unity Asset Store purchase history
4. ~~**StrategyGameIcons**~~ - **VERIFIED** (2026-01-23): Unity Asset Store purchase (REXARD)
6. **AUDIUS music tracks** - Verify Dima Koltsov tracks are licensed for commercial use
5. ~~**Medieval: Victory Theme**~~ - **VERIFIED** (2026-01-23): CC0 Public Domain by RandomMind ([Chosic](https://www.chosic.com/download-audio/28492/))
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
6. ~~**Dima Koltsov tracks**~~ - **MOSTLY VERIFIED** (2026-01-23): 3 of 5 confirmed CC BY 4.0 via YouTube. 2 remaining (Forest Queen Tale, Clouds) presumed same license.
7. ~~**Discord logo**~~ - **OK** (2026-01-23): Usage complies with Discord brand guidelines for "Login with Discord" button
### Already Safe:
@@ -229,12 +249,13 @@ NuGet packages in `Assets/Packages/` all include LICENSE.TXT files:
## Recommendation
Before removing HTTP basic auth:
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
1. ~~Replace clip art images (`bridge.png`, `startFire.png`) with properly licensed alternatives~~ **DONE** - see Section 4
2. ~~Verify source of `Assets/Shardok/soundEffects/` MP3s~~ **MOSTLY DONE** - 3 files flagged for replacement, rest presumed Asset Store
3. ~~Verify source of `Assets/Terrain Hexes/`~~ **DONE** - confirmed Asset Store purchase
4. ~~Verify source of `Assets/StrategyGameIcons/`~~ **DONE** - Unity Asset Store (REXARD)
5. ~~Replace Dima Koltsov Audius tracks~~ **MOSTLY DONE** - 3/5 confirmed CC BY 4.0, 2 presumed same
6. ~~Find source of Medieval: Victory Theme or replace~~ **DONE** - CC0 Public Domain by RandomMind
The bulk of your assets (95%+) are properly licensed Asset Store purchases or CC content.
-279
View File
@@ -1,279 +0,0 @@
# Deproto Migration Plan
## Vision
**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.
```
┌─────────────────────────────────────────────────────────────────────┐
│ GRPC BOUNDARY │
│ EagleServiceImpl.scala ←→ Proto Messages ←→ Unity Client │
└─────────────────────────────────────────────────────────────────────┘
GameStateConverter
┌─────────────────────────────────────────────────────────────────────┐
│ SCALA ENGINE │
│ │
│ GameStateC ───→ Actions ───→ ActionResultT ───→ New GameStateC │
│ ↑ │ │
│ │ (Pure Scala models) │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ HeroC, FactionC, ProvinceC, BattalionC, ArmyC, etc. │
└─────────────────────────────────────────────────────────────────────┘
GameStateConverter
┌─────────────────────────────────────────────────────────────────────┐
│ PERSISTENCE BOUNDARY │
│ GameHistory.scala ←→ Proto Messages ←→ File/Database │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Current State (January 2026)
### Summary
| Area | Proto Imports | Status |
|------|---------------|--------|
| AI (`/ai/`) | **0** | ✅ **Complete** |
| Actions (`/library/actions/impl/action/`) | **0** | ✅ **Complete** |
| Commands (`/library/actions/impl/command/`) | **0** | ✅ **Complete** |
| Availability (`/library/actions/availability/`) | **0** | ✅ **Complete** |
| Command Choice Helpers (`/library/util/command_choice_helpers/`) | **0** | ✅ **Complete** |
| View Filters (`/library/util/view_filters/`) | **3** | ⏳ In progress (2 files) |
| LLM Prompt Generators (`/library/actions/llm_prompt_generators/`) | **56** | ⏳ Remaining work (34 files) |
| Other Utilities (`/library/util/`) | **7** | ⏳ Remaining work (2 files) |
| Root Library (`/library/`) | **9** | Boundary code (3 files) |
**Total: ~75 proto imports remaining** (down from 149)
### Completed Phases
| Phase | Status | Summary |
|-------|--------|---------|
| Phase 1: GameStateC | **Complete** | Scala `GameState` model with 22 fields |
| Phase 2: EngineImpl | **Complete** | Holds Scala `GameState` internally |
| Phase 3: GameHistory | **Complete** | `stateAfter` returns Scala GameState |
| Phase 4: ActionResultT | **Complete** | All 59 actions return `ActionResultT` |
| Phase 5: Action Base Classes | **Complete** | All actions converted to T-type base classes |
| Phase 5b-d: RoundPhaseAdvancer | **Complete** | Uses Scala GameState throughout |
| Phase 6: Core Engine | **Complete** | ActionResultApplier, RandomStateSequencer protoless |
| Phase 6b: CommandFactory | **Complete** | Uses Scala SelectedCommand and AvailableCommand |
| Phase 6c: AI Clients | **Complete** | AIClient, command choosers all protoless |
| Phase 6d: CommandSelection | **Complete** | Renamed ScalaCommandSelection → CommandSelection |
### Recent Completions (PRs #5326, #5330, #5332, #5333, #5334, #5336, #5341, #5342)
1. **CommandFactory protoless** - Now accepts/returns Scala `SelectedCommand` types
2. **AI clients protoless** - `AIClient`, `MidGameAIClient`, all command choosers use Scala types
3. **CommandSelection cleanup** - Deleted legacy proto-based `CommandSelection`, renamed `ScalaCommandSelection``CommandSelection`
4. **AvailableCommandsFactory protoless** - Returns Scala `OneProvinceAvailableCommands`
5. **All CommandChoiceHelpers protoless** - All selector/chooser files converted
6. **ChronicleEventGenerator protoless** (PR #5332) - Removed last proto enum import from Actions layer
7. **ProvinceUtils protoless** (PR #5333) - Converted to use Scala `BattalionType`
8. **FactionViewFilter protoless** (PR #5334) - Returns Scala `FactionView`, converts to proto at edge
9. **HeroViewFilter protoless** (PR #5336) - Returns Scala `HeroView`, converts to proto at edge
10. **ProvinceViewFilter protoless** (PR #5341) - `ProvinceView.knownEvents` uses Scala `ProvinceEvent`, converts at edge
11. **StatWithConditionUtils & ProvinceEventUtils protoless** (PR #5342) - Deleted unused proto overloads
12. **IncomingArmyUtils protoless** (PR #5348) - Deleted unused `stats()` method
13. **RansomOfferHelpers deleted** (PR #5351) - Deleted unused dead code file
---
## Remaining Work
### Phase 7: View Filters (~3 proto imports remaining in 2 files)
| File | Status | Migration Path |
|------|--------|----------------|
| `FactionViewFilter.scala` | ✅ **Complete** | Returns Scala `FactionView` (PR #5334) |
| `HeroViewFilter.scala` | ✅ **Complete** | Returns Scala `HeroView` (PR #5336) |
| `ProvinceViewFilter.scala` | ✅ **Complete** | Returns Scala `ProvinceView` with Scala events (PR #5341) |
| `GameStateViewFilter.scala` | Partial | Converts views at edge; uses `FactionViewProto`, `GameStateView` |
| `BattleFilter.scala` | Keep | Uses `ShardokBattleView` (boundary code for battles) |
**Pattern established**: Create Scala view type → Create converter → Update filter to return Scala type → Convert at edge in `GameStateViewFilter`.
### Phase 8: LLM Prompt Generators (~56 proto imports in 34 files)
The LLM prompt generators still use proto types for hero/faction/province data:
| File Category | Files | Proto Usage |
|---------------|-------|-------------|
| Diplomacy prompts | 12 | `Hero`, `Faction`, `Province` protos |
| Quest prompts | 4 | `Hero`, `Faction` protos |
| Chronicle prompts | 3 | `Hero`, `Faction`, `Province` protos |
| Other prompts | 15 | Various proto types |
**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)
| File | Proto Usage | Migration Path |
|------|-------------|----------------|
| `ProvinceEventUtils.scala` | ✅ **0** | Deleted unused proto overloads (PR #5342) |
| `StatWithConditionUtils.scala` | ✅ **0** | Deleted unused proto overloads (PR #5342) |
| `MapGenerator.scala` | 1 import | Convert to Scala types |
| `IncomingArmyUtils.scala` | ✅ **0** | Deleted unused `stats()` method (PR #5348) |
| `GameStateViewDiffer.scala` | 7 imports | Keep - works with client view protos (boundary) |
### Phase 10: History APIs
```
src/main/scala/net/eagle0/eagle/service/InMemoryHistory.scala
src/main/scala/net/eagle0/eagle/service/PersistedHistory.scala
```
Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versions.
---
## Architecture Summary
### Fully Protoless Areas ✅
- **AI layer** (`/ai/`) - All AI clients and command choosers
- **Actions** (`/library/actions/impl/action/`) - All 52 action files
- **Commands** (`/library/actions/impl/command/`) - CommandFactory and all commands
- **Availability** (`/library/actions/availability/`) - AvailableCommandsFactory and all factories
- **Command helpers** (`/library/util/command_choice_helpers/`) - All selectors and choosers
- **Core types** - `CommandSelection`, `SelectedCommand`, `AvailableCommand`, `OneProvinceAvailableCommands`
- **View types** - `FactionView`, `HeroView`, `ProvinceView` (return Scala, convert at edge)
### Proto at Boundaries (Expected) ✅
- `EagleServiceImpl.scala` - gRPC boundary
- `GameController.scala` - Client communication
- `GameStateViewFilter.scala` - Converts Scala views to proto for client
- `*Converter.scala` - Explicit conversion utilities
- `PersistedHistory.scala` - Disk persistence
### Remaining Proto Usage ⏳
- View filters (3 imports in 2 files) - In progress
- LLM prompt generators (56 imports in 34 files) - Medium priority
- Some utility files (11 imports in 4 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 | 2 | 8 | Low |
| Root Library (boundary) | 3 | 9 | Keep |
| **Total** | **40** | **~75** | |
---
## 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/`
- [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: ~75 proto imports across 40 files** (as of 2026-01-14)
### Summary by Directory
| Directory | Files | Imports | Status |
|-----------|-------|---------|--------|
| `ai/` | 0 | 0 | ✅ Clean |
| `actions/availability/` | 0 | 0 | ✅ Clean |
| `actions/impl/action/` | 0 | 0 | ✅ Clean |
| `actions/impl/command/` | 0 | 0 | ✅ Clean |
| `actions/llm_prompt_generators/` | 34 | 56 | LLM request types |
| `util/command_choice_helpers/` | 0 | 0 | ✅ Clean |
| `util/view_filters/` | 2 | 3 | View boundary |
| `util/` (other) | 2 | 8 | Mixed |
| Root (`library/`) | 3 | 9 | Boundary code |
### util/view_filters/ (3 imports, 2 files)
| File | Import | Notes |
|------|--------|-------|
| `GameStateViewFilter.scala` | `views.faction_view.FactionView` | Output type |
| `GameStateViewFilter.scala` | `views.game_state_view.GameStateView` | Output type |
| `BattleFilter.scala` | `views.shardok_battle_view.{...}` | Battle view boundary |
### util/ other files (8 imports, 2 files)
| File | Imports | Notes |
|------|---------|-------|
| `GameStateViewDiffer.scala` | 7 | View diff for client (boundary code) |
| `MapGenerator.scala` | 1 | Map generation |
### Root library/ (9 imports, 3 files)
| File | Imports | Notes |
|------|---------|-------|
| `ActionResultFilter.scala` | 3 | Action result filtering (boundary) |
| `Engine.scala` | 1 | Trait interface |
| `EngineImpl.scala` | 5 | Returns proto for persistence (boundary) |
### actions/llm_prompt_generators/ (56 imports, 34 files)
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
4. Delete `Legacy*` files when no longer needed
### Deleted Legacy Files (no production callers)
- `LegacyProvinceDistances`
- `LegacyBattalionSuitability`
- `LegacyFoodConsumptionUtils`
- `LegacyHandleRiotUtils`
+94
View File
@@ -0,0 +1,94 @@
# LLM Model Comparison
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.
## Streaming Latency Results (January 2026)
| Model | Run 1 | Run 2 | Run 3 | Average TTFT |
|-------|-------|-------|-------|--------------|
| **Gemini 2.5 Flash-Lite** | 0.76s | 0.54s | 0.53s | **~0.6s** |
| **gpt-4.1-mini** | 1.65s | 1.72s | 1.68s | **~1.7s** |
| **claude-3-5-haiku** | 1.85s | 1.92s | 1.88s | **~1.9s** |
| gpt-5.2 | 3.25s | 3.38s | 3.32s | **~3.3s** |
| gpt-5-mini | 2.52s | 5.82s | 3.12s | **~3.8s** (high variance) |
| Gemini 3 Flash Preview | 4.11s | 4.77s | 4.28s | **~4.4s** |
| claude-sonnet-4 | 4.89s | 5.12s | 4.98s | **~5.0s** |
| Gemini 2.5 Flash | 5.60s | 7.00s | 7.79s | **~6.8s** |
## Pricing Comparison (per 1M tokens)
| Model | Input Price | Output Price | Notes |
|-------|-------------|--------------|-------|
| **Gemini 2.5 Flash-Lite** | $0.10 | $0.40 | Cheapest and fastest |
| Gemini 2.5 Flash | $0.15 | $0.60 | |
| gpt-5-mini | $0.25 | $2.00 | |
| **gpt-4.1-mini** | $0.40 | $1.60 | Best OpenAI value |
| Gemini 3 Flash Preview | $0.50 | $3.00 | Includes thinking tokens |
| **claude-3-5-haiku** | $0.80 | $4.00 | Best Anthropic value |
| gpt-5.2 | ~$1.00 | ~$10.00 | Full reasoning model |
| Gemini 2.5 Pro | $1.25 | $10.00 | |
| Gemini 3 Pro Preview | $2.00 | $12.00 | ≤200K context |
| claude-sonnet-4 | $3.00 | $15.00 | |
## Recommendations
### For Narrative Text Generation (Default)
**Gemini 2.5 Flash-Lite** is recommended as the default:
- Fastest TTFT (~0.6s) - nearly 3x faster than alternatives
- Cheapest pricing ($0.10/$0.40 per 1M tokens)
- Quality is acceptable for short narrative snippets
### Alternative Options
| Priority | Model | When to Use |
|----------|-------|-------------|
| Speed + Cost | Gemini 2.5 Flash-Lite | Default for most use cases |
| Speed + Quality | gpt-4.1-mini | When you need OpenAI quality with good speed |
| Instruction Following | claude-3-5-haiku | Complex multi-step prompts, consistent tone |
| Maximum Quality | claude-sonnet-4 or gpt-5.2 | When output quality is paramount |
### Quality Trade-offs
For short narrative snippets (1-3 paragraphs):
- **Flash-Lite vs Haiku/4.1-mini**: Minor quality difference, significant speed gain
- **Haiku vs Sonnet**: Noticeable quality difference in creative writing variety
- **gpt-4.1-mini vs gpt-5.2**: Moderate quality difference, significant cost savings
## Configuration
LLM settings can be changed at runtime via the admin console:
1. Navigate to Admin Console → Settings
2. Change `LlmProvider` to select vendor (gemini, openai, claude)
3. Change the corresponding model name setting:
- `GeminiModelName` (default: gemini-2.5-flash-lite)
- `OpenAiModelName` (default: gpt-4.1-mini)
- `ClaudeModelName` (default: claude-3-5-haiku-20241022)
Changes take effect on the next LLM request.
## Environment Variables
For production deployment, ensure API keys are set:
```bash
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GEMINI_API_KEY=AIza...
```
## Notes
- **gpt-5-mini** showed high latency variance (2.5s - 5.8s) in testing
- **Gemini 2.5 Flash** was surprisingly slower than Flash-Lite, possibly due to internal reasoning overhead
- **Gemini 3 Flash** is a frontier model with better quality but higher latency than 2.5 Flash-Lite
- All Gemini models have a generous free tier (up to 1,000 daily requests)
+78
View File
@@ -0,0 +1,78 @@
# The Small Eagle TODO
## Goals
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~~
- [ ] Fix the Mac installer
- [x] ~~Still not reconnecting after deployments~~
- [ ] Notify about client updates, button to come directly back
- [ ] Generatedtext healing
- [ ] 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~~
- [ ] "Message of the day"
- [ ] Support plan
### Alpha Tester Support
- [ ] Feedback channel (Discord server? Bug report form?)
- [ ] Crash reporting from Unity client
- [ ] Known issues doc (so testers don't report the same things)
### IP / Legal
- [ ] Document and make available licenses for art & music
- [ ] Required open source disclosures
- [ ] 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
- [ ] Privacy policy (collecting accounts, OAuth data, gameplay data)
- [ ] Terms of service (basic liability protection)
- [ ] Data deletion capability (user requests account removal)
### Basic Gameplay
- [ ] Tutorial
- [ ] In the Your Warlord panel, say what the profession is
- [x] ~~And separate panels for each profession when you encounter one~~
- [x] ~~Command tutorial for each command the first time it's clicked~~
- [x] ~~Time to recruit / expand~~
- [x] ~~And how expansion works~~
- [ ] Province events
- [ ] Running low on food
- [x] ~~Time to swear brotherhood~~
- [x] ~~When you get large, or~~
- [x] ~~When you get a good candidate~~
- [ ] Shardok tutorial!
- [ ] Basic Shardok AI stuff fixed
- [ ] Lobby fixes
- [ ] Have goals / ending
- [ ] First-session onboarding (beyond mechanics tutorial)
- [ ] Narrative hook in first few minutes - why should I care about my warlord?
- [ ] Clear first-session goal ("try to capture your first province" or similar)
- [ ] Early small victory to build momentum
- [ ] Guided first scenario vs. overwhelming sandbox?
## Nice to have
<!-- Add nice-to-have items here as they come up -->
+568
View File
@@ -0,0 +1,568 @@
# Sparkle Delta Updates Implementation Plan
## Overview
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
### Appcast XML Structure with Deltas
```xml
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>Eagle0</title>
<link>https://assets.eagle0.net/mac/appcast.xml</link>
<description>Eagle0 game updates</description>
<language>en</language>
<item>
<title>Version 1.0.9615</title>
<pubDate>Sun, 19 Jan 2026 12:00:00 -0800</pubDate>
<sparkle:version>9615</sparkle:version>
<sparkle:shortVersionString>1.0.9615</sparkle:shortVersionString>
<enclosure
url="https://assets.eagle0.net/mac/builds/eagle0-1.0.9615.dmg"
length="200000000"
type="application/octet-stream"
sparkle:edSignature="..." />
<sparkle:deltas>
<enclosure
url="https://assets.eagle0.net/mac/deltas/9614-9615.delta"
sparkle:deltaFrom="9614"
length="15000000"
type="application/octet-stream"
sparkle:edSignature="..." />
<enclosure
url="https://assets.eagle0.net/mac/deltas/9613-9615.delta"
sparkle:deltaFrom="9613"
length="18000000"
type="application/octet-stream"
sparkle:edSignature="..." />
<enclosure
url="https://assets.eagle0.net/mac/deltas/9612-9615.delta"
sparkle:deltaFrom="9612"
length="22000000"
type="application/octet-stream"
sparkle:edSignature="..." />
</sparkle:deltas>
</item>
<!-- older versions... -->
</channel>
</rss>
```
## Implementation Plan
### Phase 1: Add S3 Utility Functions
**File:** `src/main/go/net/eagle0/util/aws/bucket_basics.go`
Add two new functions to support delta generation:
```go
// ListObjectsWithPrefix returns all object keys matching the given prefix
func (bb BucketBasics) ListObjectsWithPrefix(bucket, prefix string) ([]string, error) {
var keys []string
paginator := s3.NewListObjectsV2Paginator(bb.S3Client, &s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
Prefix: aws.String(prefix),
})
for paginator.HasMorePages() {
page, err := paginator.NextPage(context.TODO())
if err != nil {
return nil, err
}
for _, obj := range page.Contents {
keys = append(keys, *obj.Key)
}
}
return keys, nil
}
// DownloadFile downloads an object to a local file path
func (bb BucketBasics) DownloadFile(bucket, key, localPath string) error {
result, err := bb.S3Client.GetObject(context.TODO(), &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
if err != nil {
return err
}
defer result.Body.Close()
file, err := os.Create(localPath)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, result.Body)
return err
}
```
### Phase 2: Store App Bundles for Delta Generation
**File:** `src/main/go/net/eagle0/build/mac_build_handler/mac_build_handler.go`
Add storage paths:
```go
var appsRoot = "mac/apps/" // Zipped app bundles for delta generation
var deltasRoot = "mac/deltas/" // Delta patches
```
After DMG creation, upload the zipped app bundle:
```go
func uploadAppBundle(bb aws.BucketBasics, appPath string, buildNumber string) error {
appZipPath := filepath.Join("/tmp", fmt.Sprintf("eagle0-%s.app.zip", buildNumber))
// Create zip of app bundle using ditto (preserves metadata)
cmd := exec.Command("ditto", "-c", "-k", "--keepParent", appPath, appZipPath)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to zip app: %s: %w", string(output), err)
}
defer os.Remove(appZipPath)
// Upload to S3
remotePath := appsRoot + fmt.Sprintf("eagle0-%s.app.zip", buildNumber)
log.Printf("Uploading app bundle to S3: %s", remotePath)
return bb.UploadFilePublic(bucketName, remotePath, appZipPath)
}
```
### Phase 3: Add Delta XML Structures
**File:** `src/main/go/net/eagle0/build/mac_build_handler/mac_build_handler.go`
Add new structs for delta representation:
```go
// Delta represents a delta patch from a previous version
type Delta struct {
XMLName xml.Name `xml:"enclosure"`
URL string `xml:"url,attr"`
DeltaFrom string `xml:"sparkle:deltaFrom,attr"`
Length int64 `xml:"length,attr"`
Type string `xml:"type,attr"`
EdSig string `xml:"sparkle:edSignature,attr"`
}
// Deltas wraps the sparkle:deltas element
type Deltas struct {
XMLName xml.Name `xml:"sparkle:deltas"`
Items []Delta `xml:"enclosure"`
}
// Update Item struct to include Deltas
type Item struct {
Title string `xml:"title"`
PubDate string `xml:"pubDate"`
SparkleVersion string `xml:"sparkle:version"`
SparkleShortVersion string `xml:"sparkle:shortVersionString"`
Description string `xml:"description,omitempty"`
Enclosure Enclosure `xml:"enclosure"`
Deltas *Deltas `xml:"sparkle:deltas,omitempty"`
}
```
### Phase 4: Generate Delta Patches
**File:** `src/main/go/net/eagle0/build/mac_build_handler/mac_build_handler.go`
```go
// Maximum number of versions to generate deltas from
const maxDeltaVersions = 5
// generateDeltas creates delta patches from previous versions to the new version
func generateDeltas(bb aws.BucketBasics, newBuildNumber string, newAppPath string, privateKeyPath string) ([]Delta, error) {
var deltas []Delta
// Ensure BinaryDelta tool is available
binaryDeltaPath, err := ensureBinaryDelta()
if err != nil {
return nil, fmt.Errorf("failed to get BinaryDelta: %w", err)
}
// List available app bundles
appKeys, err := bb.ListObjectsWithPrefix(bucketName, appsRoot+"eagle0-")
if err != nil {
log.Printf("Warning: failed to list app bundles: %v", err)
return deltas, nil // Continue without deltas
}
// Parse build numbers from keys and sort descending
var buildNumbers []string
for _, key := range appKeys {
// Extract build number from "mac/apps/eagle0-9614.app.zip"
base := filepath.Base(key)
if strings.HasPrefix(base, "eagle0-") && strings.HasSuffix(base, ".app.zip") {
bn := strings.TrimSuffix(strings.TrimPrefix(base, "eagle0-"), ".app.zip")
if bn != newBuildNumber {
buildNumbers = append(buildNumbers, bn)
}
}
}
// Sort descending (most recent first) and limit to maxDeltaVersions
sort.Sort(sort.Reverse(sort.StringSlice(buildNumbers)))
if len(buildNumbers) > maxDeltaVersions {
buildNumbers = buildNumbers[:maxDeltaVersions]
}
// Generate delta from each previous version
for _, oldBuild := range buildNumbers {
delta, err := generateSingleDelta(bb, binaryDeltaPath, oldBuild, newBuildNumber, newAppPath, privateKeyPath)
if err != nil {
log.Printf("Warning: failed to generate delta from %s: %v", oldBuild, err)
continue // Skip this delta but continue with others
}
deltas = append(deltas, delta)
}
return deltas, nil
}
func generateSingleDelta(bb aws.BucketBasics, binaryDeltaPath, oldBuild, newBuild, newAppPath, privateKeyPath string) (Delta, error) {
// Download old app bundle
oldAppZipKey := appsRoot + fmt.Sprintf("eagle0-%s.app.zip", oldBuild)
oldAppZipLocal := filepath.Join("/tmp", fmt.Sprintf("eagle0-%s.app.zip", oldBuild))
defer os.Remove(oldAppZipLocal)
if err := bb.DownloadFile(bucketName, oldAppZipKey, oldAppZipLocal); err != nil {
return Delta{}, fmt.Errorf("failed to download old app: %w", err)
}
// Unzip old app
oldAppDir := filepath.Join("/tmp", fmt.Sprintf("old-app-%s", oldBuild))
defer os.RemoveAll(oldAppDir)
cmd := exec.Command("ditto", "-x", "-k", oldAppZipLocal, oldAppDir)
if output, err := cmd.CombinedOutput(); err != nil {
return Delta{}, fmt.Errorf("failed to unzip old app: %s: %w", string(output), err)
}
oldAppPath := filepath.Join(oldAppDir, "eagle0.app")
// Generate delta
deltaPath := filepath.Join("/tmp", fmt.Sprintf("%s-%s.delta", oldBuild, newBuild))
defer os.Remove(deltaPath)
cmd = exec.Command(binaryDeltaPath, "create", oldAppPath, newAppPath, deltaPath)
if output, err := cmd.CombinedOutput(); err != nil {
return Delta{}, fmt.Errorf("failed to create delta: %s: %w", string(output), err)
}
// Get delta size
deltaSize, err := getFileSize(deltaPath)
if err != nil {
return Delta{}, fmt.Errorf("failed to get delta size: %w", err)
}
log.Printf("Delta %s->%s size: %d bytes (%.1f MB)", oldBuild, newBuild, deltaSize, float64(deltaSize)/1024/1024)
// Sign delta
signature, err := signWithSparkle(deltaPath, privateKeyPath)
if err != nil {
return Delta{}, fmt.Errorf("failed to sign delta: %w", err)
}
// Upload delta
deltaKey := deltasRoot + fmt.Sprintf("%s-%s.delta", oldBuild, newBuild)
if err := bb.UploadFilePublic(bucketName, deltaKey, deltaPath); err != nil {
return Delta{}, fmt.Errorf("failed to upload delta: %w", err)
}
deltaURL := fmt.Sprintf("https://assets.eagle0.net/%s", deltaKey)
return Delta{
URL: deltaURL,
DeltaFrom: oldBuild,
Length: deltaSize,
Type: "application/octet-stream",
EdSig: signature,
}, nil
}
func ensureBinaryDelta() (string, error) {
binaryDeltaPath := "/tmp/sparkle-cache/Sparkle-2.6.4/bin/BinaryDelta"
if _, err := os.Stat(binaryDeltaPath); os.IsNotExist(err) {
log.Println("Sparkle BinaryDelta not found, downloading...")
cmd := exec.Command("bash", "-c", `
mkdir -p /tmp/sparkle-cache
curl -sL https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz | tar -xJ -C /tmp/sparkle-cache
`)
if output, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("failed to download Sparkle: %s: %w", string(output), err)
}
}
return binaryDeltaPath, nil
}
```
### Phase 5: Update Main Deploy Flow
**File:** `src/main/go/net/eagle0/build/mac_build_handler/mac_build_handler.go`
Modify `main()` to integrate delta generation:
```go
func main() {
// ... existing argument parsing ...
// Create DMG (existing)
if err := createDMG(appPath, dmgPath, "Eagle0"); err != nil {
log.Fatalf("Failed to create DMG: %v", err)
}
// ... existing DMG upload ...
if privateKeyPath != "" {
// Upload app bundle for future delta generation (NEW)
log.Println("Uploading app bundle for delta generation...")
if err := uploadAppBundle(bb, appPath, buildNumber); err != nil {
log.Printf("Warning: failed to upload app bundle: %v", err)
// Continue - delta generation is optional
}
// Generate deltas from previous versions (NEW)
log.Println("Generating delta patches...")
deltas, err := generateDeltas(bb, buildNumber, appPath, privateKeyPath)
if err != nil {
log.Printf("Warning: failed to generate deltas: %v", err)
} else {
log.Printf("Generated %d delta patches", len(deltas))
}
// Update appcast with deltas
log.Println("Updating appcast.xml...")
appcast, err := fetchAppcast(bb)
if err != nil {
log.Fatalf("Failed to fetch appcast: %v", err)
}
// Create new item with deltas
newItem := Item{
Title: fmt.Sprintf("Version %s", version),
PubDate: time.Now().Format(time.RFC1123Z),
SparkleVersion: buildNumber,
SparkleShortVersion: version,
Description: "",
Enclosure: Enclosure{
URL: downloadURL,
Length: fileSize,
Type: "application/octet-stream",
EdSig: signature,
},
}
// Add deltas if any were generated
if len(deltas) > 0 {
newItem.Deltas = &Deltas{Items: deltas}
}
// ... rest of appcast handling ...
}
}
```
### Phase 6: Cleanup Old Artifacts
**File:** `src/main/go/net/eagle0/build/mac_build_handler/mac_build_handler.go`
When pruning old versions from appcast, also delete associated artifacts:
```go
// In the appcast pruning section, after removing old items:
if len(appcast.Channel.Items) > 10 {
oldItems := appcast.Channel.Items[10:]
for _, item := range oldItems {
oldBuild := item.SparkleVersion
// Delete old DMG (existing)
dmgKey := strings.TrimPrefix(item.Enclosure.URL, "https://assets.eagle0.net/")
log.Printf("Deleting old build: %s", dmgKey)
bb.DeleteObject(bucketName, dmgKey)
// Delete old app bundle (NEW)
appKey := appsRoot + fmt.Sprintf("eagle0-%s.app.zip", oldBuild)
log.Printf("Deleting old app bundle: %s", appKey)
bb.DeleteObject(bucketName, appKey)
// Delete deltas TO this version (NEW)
deltaKeys, _ := bb.ListObjectsWithPrefix(bucketName, deltasRoot)
for _, key := range deltaKeys {
if strings.HasSuffix(key, fmt.Sprintf("-%s.delta", oldBuild)) {
log.Printf("Deleting old delta: %s", key)
bb.DeleteObject(bucketName, key)
}
}
}
appcast.Channel.Items = appcast.Channel.Items[:10]
}
```
## S3 Storage Structure
After implementation, the S3 bucket will have this structure:
```
eagle0-windows/
├── mac/
│ ├── appcast.xml # Update feed with delta info
│ ├── builds/ # Full DMG downloads
│ │ ├── eagle0-1.0.9620.dmg
│ │ ├── eagle0-1.0.9619.dmg
│ │ ├── ...
│ │ └── eagle0-latest.dmg # Symlink to latest
│ ├── apps/ # Zipped app bundles (NEW)
│ │ ├── eagle0-9620.app.zip
│ │ ├── eagle0-9619.app.zip
│ │ ├── eagle0-9618.app.zip
│ │ ├── eagle0-9617.app.zip
│ │ └── eagle0-9616.app.zip # Keep last 5 for delta gen
│ └── deltas/ # Delta patches (NEW)
│ ├── 9619-9620.delta
│ ├── 9618-9620.delta
│ ├── 9617-9620.delta
│ ├── 9616-9620.delta
│ ├── 9615-9620.delta
│ ├── 9618-9619.delta
│ ├── 9617-9619.delta
│ └── ...
```
## Storage Impact Analysis
### Current Storage (without deltas)
- 10 DMGs × 200MB = **~2GB**
### Estimated Storage (with deltas)
- 10 DMGs × 200MB = 2GB
- 5 app bundles × 150MB = 0.75GB (zip compression)
- ~25 delta files × 20MB avg = 0.5GB
- **Total: ~3.25GB**
### Trade-offs
- **+1.25GB storage** (~60% increase)
- **-170MB per user update** (~85% bandwidth savings)
- Break-even: ~8 user updates to recoup storage cost
## Bandwidth Savings
| Scenario | Without Deltas | With Deltas | Savings |
|----------|---------------|-------------|---------|
| 1 version behind | 200MB | ~15MB | 92% |
| 2 versions behind | 200MB | ~20MB | 90% |
| 3 versions behind | 200MB | ~25MB | 87% |
| 5 versions behind | 200MB | ~35MB | 82% |
| 6+ versions behind | 200MB | 200MB (full) | 0% |
## Migration Strategy
The implementation is backward-compatible and requires no changes to existing clients:
1. **First deploy after implementation:**
- Stores app bundle for the first time
- No deltas generated (no previous app bundles exist)
- Appcast has no `<sparkle:deltas>` element
2. **Second deploy:**
- Generates delta from previous version
- Appcast now has `<sparkle:deltas>` with one entry
- Users on previous version get delta update
3. **Subsequent deploys:**
- Generate deltas from last 5 versions
- Users within 5 versions get delta updates
- Users more than 5 versions behind get full DMG
4. **Client behavior:**
- Sparkle automatically checks for matching delta
- Falls back to full DMG if no delta matches
- No client code changes required
## Error Handling
The implementation handles failures gracefully:
1. **S3 list/download fails:** Skip delta generation, use full DMG
2. **BinaryDelta fails for one version:** Log warning, continue with other versions
3. **Signing fails:** Skip that delta, continue with others
4. **Upload fails:** Skip that delta, continue with others
The deploy never fails due to delta issues - deltas are optional enhancements.
## Verification Plan
### Manual Testing
1. **Deploy version N:**
- Verify app bundle uploaded to `mac/apps/eagle0-N.app.zip`
- Verify appcast has no deltas (first deploy)
2. **Deploy version N+1:**
- Verify delta generated at `mac/deltas/N-(N+1).delta`
- Verify appcast contains `<sparkle:deltas>` element
- Verify delta signature is valid
3. **Test update from N to N+1:**
- Install version N manually
- Check for updates
- Monitor download size in Player.log (should be ~15-30MB, not 200MB)
- Verify app updated successfully
4. **Test fresh install:**
- Download latest DMG directly
- Verify installation works normally
5. **Test fallback scenario:**
- Install a version more than 5 versions behind
- Update should download full DMG
### Automated Verification
Add to CI workflow (optional):
```yaml
- name: Verify delta generation
run: |
# Check app bundle exists
aws s3 ls s3://eagle0-windows/mac/apps/ | grep eagle0-${BUILD_NUMBER}.app.zip
# Check deltas exist (after second deploy)
aws s3 ls s3://eagle0-windows/mac/deltas/ | head -5
# Verify appcast has deltas
curl -s https://assets.eagle0.net/mac/appcast.xml | grep "sparkle:deltas"
```
## Security Considerations
1. **All deltas are EdDSA signed:** Same signature verification as full DMG
2. **BinaryDelta is Sparkle's official tool:** Well-audited, production-ready
3. **App bundles in S3 are public:** Same as DMGs, no additional exposure
4. **Cleanup removes old artifacts:** No indefinite storage of old versions
## Future Enhancements
1. **Parallel delta generation:** Generate multiple deltas concurrently
2. **Delta size threshold:** Skip uploading deltas larger than X% of full DMG
3. **Delta metrics:** Track delta download rates vs full DMG
4. **Configurable delta count:** Allow adjusting how many versions to keep
+48
View File
@@ -0,0 +1,48 @@
# LLVM MinGW toolchain for Windows cross-compilation
# Provides x86_64-w64-mingw32 target compiler and libraries
package(default_visibility = ["//visibility:public"])
filegroup(
name = "all_files",
srcs = glob(["**/*"]),
)
# Compiler binaries
filegroup(
name = "compiler_files",
srcs = glob([
"bin/x86_64-w64-mingw32-*",
"bin/clang*",
"bin/llvm-*",
"bin/lld*",
]),
)
# Windows x86_64 sysroot (headers and libraries)
filegroup(
name = "windows_x86_64_sysroot",
srcs = glob([
"x86_64-w64-mingw32/**/*",
"generic-w64-mingw32/include/**/*",
]),
)
# All library files needed for linking
filegroup(
name = "linker_files",
srcs = glob([
"bin/x86_64-w64-mingw32-*",
"bin/lld*",
"bin/ld.lld*",
"lib/**/*",
"x86_64-w64-mingw32/lib/**/*",
]),
)
# The main C compiler wrapper script path for CGO
# CGO needs CC to point to the cross-compiler
exports_files([
"bin/x86_64-w64-mingw32-clang",
"bin/x86_64-w64-mingw32-clang++",
])
+1
View File
@@ -11,6 +11,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.36.3
)
+2
View File
@@ -41,6 +41,8 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 h1:VQpB2SpK88C6B5lPHTuSZKb2Qee1QWwiFlC5CKY4AW0=
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+7
View File
@@ -119,6 +119,13 @@ http {
proxy_set_header X-Real-IP $remote_addr;
}
# Steam OAuth callback (Steam uses OpenID 2.0)
location /oauth/steam/callback {
proxy_pass http://auth:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Invitation landing page (proxied to Go auth service)
location /invite/ {
proxy_pass http://auth:8080;
+129
View File
@@ -0,0 +1,129 @@
#!/bin/bash
# Check BUILD.bazel dependency constraints
# This script enforces architectural boundaries in the codebase.
#
# Usage:
# ./scripts/check_build_deps.sh # Check all rules
# ./scripts/check_build_deps.sh --ci # CI mode (fail on any violation)
# ./scripts/check_build_deps.sh --count # Just count current violations (for tracking progress)
# ./scripts/check_build_deps.sh --strict # Same as --ci (strict enforcement)
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
MODE="${1:-check}"
EXIT_CODE=0
# Rule 1: src/main should not depend on src/test
check_main_depends_on_test() {
echo -e "${YELLOW}Checking: src/main should not depend on src/test...${NC}"
violations=$(bazel query 'deps(//src/main/...) intersect //src/test/...' 2>/dev/null || true)
if [ -n "$violations" ]; then
echo -e "${RED}VIOLATION: src/main depends on src/test:${NC}"
echo "$violations"
return 1
else
echo -e "${GREEN}✓ No violations${NC}"
return 0
fi
}
# Rule 2: library/ should not depend on Scala proto types
# C++/Go proto deps are allowed (they're build-time deps for map generation tools)
check_library_depends_on_scala_proto() {
echo -e "${YELLOW}Checking: library/ should not depend on Scala proto types...${NC}"
violations=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | grep "_scala_proto" || true)
if [ -z "$violations" ]; then
count=0
else
count=$(echo "$violations" | grep -c "^//" || true)
fi
if [ "$count" -gt 0 ]; then
echo -e "${RED}VIOLATION: Found $count Scala proto dependencies in library/:${NC}"
echo "$violations"
return 1
else
echo -e "${GREEN}✓ No Scala proto dependencies in library/${NC}"
return 0
fi
}
# Rule 3: library/ should not depend on proto_converters
# Proto conversions should happen at service boundaries, not in library code
check_library_depends_on_proto_converters() {
echo -e "${YELLOW}Checking: library/ should not depend on proto_converters...${NC}"
violations=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/scala/net/eagle0/eagle/model/proto_converters/...' 2>/dev/null | grep "^//" || true)
if [ -n "$violations" ]; then
count=$(echo "$violations" | wc -l | tr -d ' ')
echo -e "${RED}VIOLATION: library/ depends on $count proto_converters targets:${NC}"
echo "$violations"
echo ""
echo "Proto conversions should happen at service boundaries (ShardokInterfaceGrpcClient,"
echo "EagleServiceImpl, etc.), not in library code."
return 1
else
echo -e "${GREEN}✓ No proto_converters dependencies in library/${NC}"
return 0
fi
}
# Count proto deps for informational purposes
count_proto_deps() {
echo -e "${YELLOW}=== Proto dependency counts ===${NC}"
scala_proto_results=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | grep "_scala_proto" || true)
if [ -z "$scala_proto_results" ]; then
scala_proto_count=0
else
scala_proto_count=$(echo "$scala_proto_results" | wc -l | tr -d ' ')
fi
echo "library/ Scala proto deps: $scala_proto_count"
# C++/Go proto deps are expected (map generation tools)
all_proto_count=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | wc -l | tr -d ' ')
echo "library/ all proto deps (includes C++/Go build tools): $all_proto_count"
}
echo "=== BUILD.bazel Dependency Check ==="
echo ""
case "$MODE" in
--count)
count_proto_deps
;;
--ci|--strict)
check_main_depends_on_test || EXIT_CODE=1
check_library_depends_on_scala_proto || EXIT_CODE=1
check_library_depends_on_proto_converters || EXIT_CODE=1
;;
*)
check_main_depends_on_test || EXIT_CODE=1
check_library_depends_on_scala_proto || EXIT_CODE=1
check_library_depends_on_proto_converters || EXIT_CODE=1
echo ""
count_proto_deps
;;
esac
echo ""
if [ $EXIT_CODE -eq 0 ]; then
echo -e "${GREEN}=== All checks passed ===${NC}"
else
echo -e "${RED}=== Some checks failed ===${NC}"
fi
exit $EXIT_CODE
+25 -6
View File
@@ -39,32 +39,51 @@ find "$APP_PATH" -name "*.bundle" -print0 | while IFS= read -r -d '' item; do
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign XPC services (inside Sparkle framework)
# Sign XPC services (but skip ones inside Sparkle.framework - they're already signed)
find "$APP_PATH" -name "*.xpc" -print0 | while IFS= read -r -d '' item; do
if [[ "$item" == *"Sparkle.framework"* ]]; then
echo "Skipping Sparkle XPC service (pre-signed): $item"
continue
fi
echo "Signing XPC service: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign nested apps (like Sparkle's Updater.app)
# Sign nested apps (but skip ones inside Sparkle.framework - they're already signed)
find "$APP_PATH" -path "*/Frameworks/*.app" -print0 | while IFS= read -r -d '' item; do
if [[ "$item" == *"Sparkle.framework"* ]]; then
echo "Skipping Sparkle nested app (pre-signed): $item"
continue
fi
echo "Signing nested app: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign standalone executables inside frameworks (like Autoupdate)
# Sign standalone executables inside frameworks (but skip Sparkle.framework internals)
find "$APP_PATH" -path "*/Frameworks/*/Versions/*/Autoupdate" -type f -print0 | while IFS= read -r -d '' item; do
if [[ "$item" == *"Sparkle.framework"* ]]; then
echo "Skipping Sparkle executable (pre-signed): $item"
continue
fi
echo "Signing executable: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign all frameworks (after their contents are signed)
# Use --deep for Sparkle.framework to handle its XPC services
find "$APP_PATH" -name "*.framework" -print0 | while IFS= read -r -d '' item; do
echo "Signing framework: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
if [[ "$item" == *"Sparkle.framework" ]]; then
echo "Signing Sparkle framework with --deep: $item"
codesign --deep --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
else
echo "Signing framework: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
fi
done
echo "=== Signing main app bundle ==="
+9 -2
View File
@@ -35,6 +35,7 @@ WARMUP_SCRIPT="${SCRIPT_DIR}/warmup-eagle.sh"
SAVES_DIR="${APP_DIR}/saves"
FLUSH_MARKER="${SAVES_DIR}/.flush_complete"
DEPLOYMENT_IN_PROGRESS="${SAVES_DIR}/.deployment_in_progress"
ACTIVE_INSTANCE_FILE="${APP_DIR}/.active-instance"
# Colors for output
RED='\033[0;31m'
@@ -264,7 +265,8 @@ main() {
fi
# Recreate nginx to pick up new config
docker compose -f "${COMPOSE_FILE}" up -d --force-recreate nginx
# Use --no-deps to prevent cascading to auth (which has secrets not available here)
docker compose -f "${COMPOSE_FILE}" up -d --force-recreate --no-deps nginx
# Verify nginx picked up the correct config
local nginx_backend
@@ -297,6 +299,10 @@ main() {
create_flush_marker "${deploy_id}" "eagle-${staging}"
log_info "[DEPLOY:${deploy_id}] Flush marker created - server will auto-invalidate stale cache"
# Write active instance file for eagle-exec helper
echo "eagle-${staging}" > "${ACTIVE_INSTANCE_FILE}"
log_info "[DEPLOY:${deploy_id}] Active instance file updated: eagle-${staging}"
# Update .env for admin service
local env_file="${APP_DIR}/.env"
if [ "$staging" = "green" ]; then
@@ -312,8 +318,9 @@ main() {
fi
# Restart admin to pick up new .env
# Use --no-deps to prevent cascading to auth (which has secrets not available here)
log_info "Restarting admin service..."
docker compose -f "${COMPOSE_FILE}" up -d --force-recreate admin
docker compose -f "${COMPOSE_FILE}" up -d --force-recreate --no-deps admin
# Clean up old instance
log_info "Cleaning up old eagle-${active}..."
+46
View File
@@ -0,0 +1,46 @@
#!/bin/bash
#
# Helper to run docker exec against the active Eagle instance.
# Reads the active instance from /opt/eagle0/.active-instance (set by deploy-blue-green.sh).
#
# Usage:
# eagle-exec printenv GEMINI_API_KEY
# eagle-exec jcmd 1 VM.flags
# eagle-exec sh # Get a shell
#
# To create an alias, add to ~/.bashrc:
# alias eagle-exec='/opt/eagle0/scripts/eagle-exec.sh'
#
set -euo pipefail
APP_DIR="${APP_DIR:-/opt/eagle0}"
ACTIVE_FILE="${APP_DIR}/.active-instance"
# Read active instance from file, with fallback
if [ -f "$ACTIVE_FILE" ]; then
ACTIVE=$(cat "$ACTIVE_FILE")
else
# Fallback: check which container is actually running
if docker inspect --format='{{.State.Running}}' eagle-blue 2>/dev/null | grep -q true; then
ACTIVE="eagle-blue"
elif docker inspect --format='{{.State.Running}}' eagle-green 2>/dev/null | grep -q true; then
ACTIVE="eagle-green"
else
ACTIVE=""
fi
fi
if [ -z "$ACTIVE" ]; then
echo "Error: No active Eagle instance found" >&2
exit 1
fi
if [ $# -eq 0 ]; then
echo "Active instance: $ACTIVE"
echo "Usage: $0 <command> [args...]"
echo "Example: $0 printenv GEMINI_API_KEY"
exit 0
fi
exec docker exec "$ACTIVE" "$@"
+44
View File
@@ -0,0 +1,44 @@
#!/bin/bash
#
# Helper to tail logs from the active Eagle instance.
# Reads the active instance from /opt/eagle0/.active-instance (set by deploy-blue-green.sh).
#
# Usage:
# eagle-logs # Tail logs (follow mode)
# eagle-logs -n 100 # Show last 100 lines and follow
# eagle-logs --no-follow -n 50 # Show last 50 lines without following
#
# To create an alias, add to ~/.bashrc:
# alias eagle-logs='/opt/eagle0/scripts/eagle-logs.sh'
#
set -euo pipefail
APP_DIR="${APP_DIR:-/opt/eagle0}"
ACTIVE_FILE="${APP_DIR}/.active-instance"
# Read active instance from file, with fallback
if [ -f "$ACTIVE_FILE" ]; then
ACTIVE=$(cat "$ACTIVE_FILE")
else
# Fallback: check which container is actually running
if docker inspect --format='{{.State.Running}}' eagle-blue 2>/dev/null | grep -q true; then
ACTIVE="eagle-blue"
elif docker inspect --format='{{.State.Running}}' eagle-green 2>/dev/null | grep -q true; then
ACTIVE="eagle-green"
else
ACTIVE=""
fi
fi
if [ -z "$ACTIVE" ]; then
echo "Error: No active Eagle instance found" >&2
exit 1
fi
# Default to follow mode if no args provided
if [ $# -eq 0 ]; then
exec docker logs -f "$ACTIVE"
else
exec docker logs "$@" "$ACTIVE"
fi
+47 -20
View File
@@ -27,33 +27,59 @@ if [ -z "${SPARKLE_EDDSA_PUBLIC_KEY:-}" ]; then
exit 1
fi
# Download Sparkle if not cached
# Always use a fresh Sparkle download to avoid cache corruption issues
SPARKLE_DIR="$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION"
if [ ! -d "$SPARKLE_DIR/Sparkle.framework" ]; then
echo "=== Downloading Sparkle $SPARKLE_VERSION ==="
mkdir -p "$SPARKLE_CACHE_DIR"
SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
curl -L "$SPARKLE_URL" | tar -xJ -C "$SPARKLE_CACHE_DIR"
mv "$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION" "$SPARKLE_DIR" 2>/dev/null || true
# If the extracted directory doesn't match version pattern, it may just be "Sparkle"
if [ ! -d "$SPARKLE_DIR" ]; then
mkdir -p "$SPARKLE_DIR"
mv "$SPARKLE_CACHE_DIR/Sparkle.framework" "$SPARKLE_DIR/" 2>/dev/null || true
mv "$SPARKLE_CACHE_DIR/bin" "$SPARKLE_DIR/" 2>/dev/null || true
fi
echo "=== Clearing Sparkle cache and downloading fresh copy ==="
rm -rf "$SPARKLE_DIR"
mkdir -p "$SPARKLE_DIR"
SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
echo "Downloading from: $SPARKLE_URL"
curl -L "$SPARKLE_URL" -o /tmp/sparkle.tar.xz
tar -xJf /tmp/sparkle.tar.xz -C "$SPARKLE_DIR"
rm /tmp/sparkle.tar.xz
# Show what was extracted
echo "=== Extracted contents ==="
ls -la "$SPARKLE_DIR/"
# The tarball extracts files directly, not into a subdirectory
# Verify the framework has proper symlink structure
echo "=== Verifying Sparkle.framework structure ==="
ls -la "$SPARKLE_DIR/Sparkle.framework/"
if [ ! -L "$SPARKLE_DIR/Sparkle.framework/Sparkle" ]; then
echo "ERROR: Sparkle.framework/Sparkle is not a symlink"
file "$SPARKLE_DIR/Sparkle.framework/Sparkle"
exit 1
fi
if [ ! -L "$SPARKLE_DIR/Sparkle.framework/Versions/Current" ]; then
echo "ERROR: Sparkle.framework/Versions/Current is not a symlink"
ls -la "$SPARKLE_DIR/Sparkle.framework/Versions/"
exit 1
fi
echo "Sparkle framework structure verified OK"
echo "=== Injecting Sparkle framework ==="
FRAMEWORKS_DIR="$APP_PATH/Contents/Frameworks"
mkdir -p "$FRAMEWORKS_DIR"
# Copy Sparkle framework
cp -R "$SPARKLE_DIR/Sparkle.framework" "$FRAMEWORKS_DIR/"
# Remove any existing Sparkle.framework in the app
rm -rf "$FRAMEWORKS_DIR/Sparkle.framework"
# Also copy the XPC services if present
if [ -d "$SPARKLE_DIR/Sparkle.framework/Versions/B/XPCServices" ]; then
echo "Sparkle XPC services present"
# Copy Sparkle framework (use ditto to preserve symlinks and bundle structure)
ditto "$SPARKLE_DIR/Sparkle.framework" "$FRAMEWORKS_DIR/Sparkle.framework"
# Verify the copied framework still has proper structure
echo "=== Verifying copied Sparkle.framework structure ==="
ls -la "$FRAMEWORKS_DIR/Sparkle.framework/"
if [ ! -L "$FRAMEWORKS_DIR/Sparkle.framework/Sparkle" ]; then
echo "ERROR: Copied framework lost symlink structure"
exit 1
fi
echo "Copied framework structure OK"
echo "=== Updating Info.plist ==="
PLIST_PATH="$APP_PATH/Contents/Info.plist"
@@ -69,8 +95,9 @@ PLIST_PATH="$APP_PATH/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Add :SUEnableAutomaticChecks bool true" "$PLIST_PATH"
# Set bundle version from git for Sparkle version comparison
VERSION=$(git describe --tags --always 2>/dev/null || echo "1.0.0")
# Use commit count for automatic incrementing versions (e.g., 1.0.9548)
BUILD_NUMBER=$(git rev-list --count HEAD 2>/dev/null || echo "1")
VERSION="1.0.${BUILD_NUMBER}"
echo "Setting version: $VERSION (build $BUILD_NUMBER)"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$PLIST_PATH" 2>/dev/null || \
@@ -83,7 +110,7 @@ echo "=== Adding URL scheme for invitation codes ==="
/usr/libexec/PlistBuddy -c "Delete :CFBundleURLTypes" "$PLIST_PATH" 2>/dev/null || true
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes array" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0 dict" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLName string 'com.Shardok-Games.eagle0'" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLName string 'net.eagle0.eagle0'" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes array" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string 'eagle0'" "$PLIST_PATH"
+18 -6
View File
@@ -50,22 +50,34 @@ if [ "$STATUS" != "Accepted" ]; then
exit 1
fi
echo "=== Verifying code signature before stapling ==="
if ! codesign --verify --deep --strict "$APP_PATH" 2>&1; then
echo "ERROR: Code signature verification failed - app may have been damaged during transfer"
echo "Attempting to show signature details:"
codesign -dvvv "$APP_PATH" 2>&1 || true
exit 1
fi
echo "Code signature verified successfully"
echo "=== Stapling notarization ticket to app ==="
# Retry stapling - Apple's CloudKit can have a brief delay after notarization completes
MAX_STAPLE_ATTEMPTS=5
# Retry stapling - Apple's CloudKit can take several minutes to propagate the ticket
MAX_STAPLE_ATTEMPTS=10
STAPLE_WAIT_SECONDS=30
STAPLE_ATTEMPT=1
while [ $STAPLE_ATTEMPT -le $MAX_STAPLE_ATTEMPTS ]; do
echo "Stapling attempt $STAPLE_ATTEMPT/$MAX_STAPLE_ATTEMPTS..."
if xcrun stapler staple "$APP_PATH"; then
if STAPLE_OUTPUT=$(xcrun stapler staple "$APP_PATH" 2>&1); then
echo "$STAPLE_OUTPUT"
echo "Stapling successful"
break
fi
echo "Stapler output: $STAPLE_OUTPUT"
if [ $STAPLE_ATTEMPT -eq $MAX_STAPLE_ATTEMPTS ]; then
echo "ERROR: Stapling failed after $MAX_STAPLE_ATTEMPTS attempts"
echo "ERROR: Stapling failed after $MAX_STAPLE_ATTEMPTS attempts (total wait: $((MAX_STAPLE_ATTEMPTS * STAPLE_WAIT_SECONDS)) seconds)"
exit 1
fi
echo "Stapling failed, waiting 10 seconds before retry..."
sleep 10
echo "Stapling failed, waiting $STAPLE_WAIT_SECONDS seconds before retry..."
sleep $STAPLE_WAIT_SECONDS
STAPLE_ATTEMPT=$((STAPLE_ATTEMPT + 1))
done
@@ -87,7 +87,6 @@
<Compile Include="Assets/Modern UI Pack/Scripts/Demo/LaunchURL.cs" />
<Compile Include="Assets/Shardok/HexCoordinates.cs" />
<Compile Include="Assets/Eagle/Table Rows/OrganizeTroopsTableRow.cs" />
<Compile Include="Assets/Bluetooth/PickerRowController.cs" />
<Compile Include="Assets/TouchHandler.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/TravelCommandSelector.cs" />
<Compile Include="Assets/Eagle/ConnectionStatusUI.cs" />
@@ -98,7 +97,6 @@
<Compile Include="Assets/HoveringTooltipTextProvider.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/util/KeyModifiedAmount.cs" />
<Compile Include="Assets/HoveringTooltip.cs" />
<Compile Include="Assets/Bluetooth/DiceInterface.cs" />
<Compile Include="Assets/ButtonColors.cs" />
<Compile Include="Assets/Eagle/MapController.cs" />
<Compile Include="Assets/common/DisclosureTriangle.cs" />
@@ -148,13 +146,12 @@
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/RadialSlider.cs" />
<Compile Include="Assets/Eagle/Notifications/WeatherForcedTurnBackNotificationGenerator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManager.cs" />
<Compile Include="Assets/common/GUIUtils/TouchAwareTooltip.cs" />
<Compile Include="Assets/Eagle/GeneratedTextUpdater.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/OutlawApprehendedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/HexMetrics.cs" />
<Compile Include="Assets/Bluetooth/OneDiceRollController.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Switch/SwitchManager.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ProvinceHeldDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Bluetooth/DieInfo.cs" />
<Compile Include="Assets/common/ResourceFetcher.cs" />
<Compile Include="Assets/Tutorial/Content/TutorialStep.cs" />
<Compile Include="Assets/Auth/AuthClient.cs" />
@@ -162,12 +159,11 @@
<Compile Include="Assets/Eagle/Notifications/RiotSuppressedNotificationGenerator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Modal Window/ModalWindowManagerEditor.cs" />
<Compile Include="Assets/common/GUIUtils/AutoScrollingText.cs" />
<Compile Include="Assets/Bluetooth/NativeDiceInterfaceImports.cs" />
<Compile Include="Assets/Eagle/Notifications/ProvinceConqueredNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/Table Rows/ExtraTroopsRowController.cs" />
<Compile Include="Assets/Auth/JwtAuthInterceptor.cs" />
<Compile Include="Assets/Eagle/CommandPanelController.cs" />
<Compile Include="Assets/Tutorial/Content/TutorialContentDefinitions.cs" />
<Compile Include="Assets/Eagle/CommandPanelController.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ShatteredArmyDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/TurnHistoryPanelController.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/HeroDepartureDetailsNotificationGenerator.cs" />
@@ -179,9 +175,9 @@
<Compile Include="Assets/Shardok/BattalionTypeManager.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/DefendCommandSelector.cs" />
<Compile Include="Assets/Eagle/HeroesAndBattalionsPanelController.cs" />
<Compile Include="Assets/common/HexMapStorage.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasic.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/BreakAllianceAmbassadorImprisonedNotificationGenerator.cs" />
<Compile Include="Assets/common/HexMapStorage.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/PleaseRecruitMeCommandSelector.cs" />
<Compile Include="Assets/Eagle/Notifications/RiotAvertedNotificationGenerator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Notification/NotificationStacking.cs" />
@@ -212,7 +208,6 @@
<Compile Include="Assets/Eagle/CommandSelectors/ExileVassalCommandSelector.cs" />
<Compile Include="Assets/Tutorial/TutorialState.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicWithIconEditor.cs" />
<Compile Include="Assets/Bluetooth/DiceVectors.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerProgressBarLoop.cs" />
<Compile Include="Assets/Eagle/MovingArmiesTableController.cs" />
<Compile Include="Assets/ConnectionHandler/ConnectionHandler.cs" />
@@ -233,9 +228,11 @@
<Compile Include="Assets/Eagle/CommandSelectors/DiplomacyCommandSelector.cs" />
<Compile Include="Assets/Tutorial/Triggers/TutorialTriggerRegistry.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ResolveBreakAllianceCommandSelector.cs" />
<Compile Include="Assets/common/WindowFocusManager.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ResolveAllianceCommandSelector.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/WithdrewForTruceDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/MarchCommandSelector.cs" />
<Compile Include="Assets/Tutorial/TutorialTargetRegistry.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/TrainCommandSelector.cs" />
<Compile Include="Assets/Eagle/CustomFileLogger.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/InvitationAcceptedDetailsNotificationGenerator.cs" />
@@ -269,7 +266,6 @@
<Compile Include="Assets/Shardok/AnimationTestController.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/TradeCommandSelector.cs" />
<Compile Include="Assets/Eagle/Table Rows/DominionTableRowController.cs" />
<Compile Include="Assets/Bluetooth/RollPanelController.cs" />
<Compile Include="Assets/Shardok/ChargeAnimator.cs" />
<Compile Include="Assets/Eagle/ClientTextProvider.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManager.cs" />
@@ -299,11 +295,13 @@
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerEditor.cs" />
<Compile Include="Assets/UI/Scripts/SceneMigrationHelper.cs" />
<Compile Include="Assets/Eagle/Table Rows/UnitSelectorHeroRowController.cs" />
<Compile Include="Assets/Shardok/BridgeRotationCalculator.cs" />
<Compile Include="Assets/ConnectionHandler/CustomBattleHandler.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/RansomPaidDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/ControlAnimator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Horizontal Selector/HorizontalSelector.cs" />
<Compile Include="Assets/Eagle/Table Rows/OrganizeTroopsResultRow.cs" />
<Compile Include="Assets/Eagle/SparkleUpdater.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/SliderManagerEditor.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SwearBrotherhoodDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/PersistentClientConnection.cs" />
@@ -322,10 +320,10 @@
<Compile Include="Assets/Shardok/ShardokGameModel.cs" />
<Compile Include="Assets/common/GUIUtils/GeneralClickDetector.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerContextMenu.cs" />
<Compile Include="Assets/Eagle/SparkleInitializer.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/TruceAmbassadorImprisonedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicWithIcon.cs" />
<Compile Include="Assets/Tutorial/UI/TutorialHintIndicator.cs" />
<Compile Include="Assets/Bluetooth/UnityDieColors.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/OrganizeTroopsCommandSelector.cs" />
<Compile Include="Assets/Shardok/ExtinguishAnimator.cs" />
<Compile Include="Assets/Eagle/PanelPositions.cs" />
@@ -338,7 +336,6 @@
<Compile Include="Assets/EagleConnection.cs" />
<Compile Include="Assets/GUI Pro Kit Fantasy RPG/Scripts/CtrPanel.cs" />
<Compile Include="Assets/UI/Scripts/SceneLoadTester.cs" />
<Compile Include="Assets/Bluetooth/RollFetcher.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/HandleRiotGiveCommandSelector.cs" />
<Compile Include="Assets/Eagle/ProvinceUtils.cs" />
<Compile Include="Assets/common/GUIUtils/ProvinceStatUtils.cs" />
@@ -372,16 +369,15 @@
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SuppressBeastsFailedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Terrain Hexes/Example Scene/BasicHexArranger.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ManagePrisonersCommandSelector.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/SendSuppliesCommandSelector.cs" />
<Compile Include="Assets/ConnectionHandler/StoredAccountButton.cs" />
<Compile Include="Assets/Tutorial/UI/TutorialUIManager.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/SendSuppliesCommandSelector.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Window/WindowManager.cs" />
<Compile Include="Assets/Auth/InvitationCodeManager.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/CapturedHeroExiledDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SuppressBeastsSucceededNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/ReservesTableController.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/CommandSelector.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/RestCommandSelector.cs" />
<Compile Include="Assets/Bluetooth/DiceConfigurationPanelController.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerDropdown.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerButton.cs" />
<Compile Include="Assets/common/GUIUtils/GUIUtils.cs" />
@@ -417,10 +413,12 @@
<None Include="Assets/Packages/Microsoft.Extensions.Options.8.0.0/lib/netstandard2.1/Microsoft.Extensions.Options.xml" />
<None Include="Assets/TextMesh Pro/Shaders/TMPro_Mobile.cginc" />
<None Include="Assets/TextMesh Pro/Shaders/TMP_SDF SSD.shader" />
<None Include="Assets/TextMesh Pro/Shaders/TMPro_Properties.cginc" />
<None Include="Assets/Shardok/Painted Medieval Fantasy Items 1/medieval_items1.txt" />
<None Include="Assets/TextMesh Pro/Resources/Shaders/TMP_SDF-Surface-Mobile.shader" />
<None Include="Assets/Eagle/maskShader.shader" />
<None Include="Assets/GUI Pro Kit Fantasy RPG/Extensions/UIParticle/hidden particle.shader" />
<None Include="Assets/TextMesh Pro/Shaders/TMPro.cginc" />
<None Include="Assets/Terrain Hexes/readme.txt" />
<None Include="Assets/Modern UI Pack/Read Me.txt" />
<None Include="Assets/TextMesh Pro/Shaders/SDFFunctions.hlsl" />
@@ -450,7 +448,7 @@
<None Include="Assets/Packages/System.ComponentModel.Annotations.5.0.0/useSharedDesignerContext.txt" />
<None Include="Assets/Packages/System.Runtime.CompilerServices.Unsafe.6.0.0/useSharedDesignerContext.txt" />
<None Include="Assets/TextMesh Pro/Resources/Shaders/TMP_SDF-Mobile Masking.shader" />
<None Include="Assets/Resources/Music/Music Credits.txt" />
<None Include="Assets/Music/Music Credits.txt" />
<None Include="Assets/Packages/System.Diagnostics.DiagnosticSource.8.0.0/useSharedDesignerContext.txt" />
<None Include="Assets/Packages/Microsoft.Extensions.Logging.8.0.0/useSharedDesignerContext.txt" />
<None Include="Assets/Packages/Microsoft.Extensions.DependencyInjection.Abstractions.8.0.0/useSharedDesignerContext.txt" />
@@ -871,9 +869,6 @@
<Reference Include="Microsoft.Extensions.Logging.Abstractions">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Packages/Microsoft.Extensions.Logging.Abstractions.8.0.0/lib/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.Annotations">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Packages/System.ComponentModel.Annotations.5.0.0/lib/netstandard2.1/System.ComponentModel.Annotations.dll</HintPath>
</Reference>
<Reference Include="nunit.framework">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/PackageCache/com.unity.ext.nunit@d8c07649098d/net40/unity-custom/nunit.framework.dll</HintPath>
</Reference>
@@ -928,6 +923,15 @@
<Reference Include="Unity.Analytics.StandardEvents">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/PackageCache/com.unity.analytics@c9d14a6bdec6/AnalyticsStandardEvents/Unity.Analytics.StandardEvents.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.iOS.Extensions.Xcode">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/PlaybackEngines/iOSSupport/UnityEditor.iOS.Extensions.Xcode.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.iOS.Extensions.Common">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/PlaybackEngines/iOSSupport/UnityEditor.iOS.Extensions.Common.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.Apple.Extensions.Common">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/PlaybackEngines/iOSSupport/UnityEditor.Apple.Extensions.Common.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.iOS.Extensions.Xcode">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/PlaybackEngines/MacStandaloneSupport/UnityEditor.iOS.Extensions.Xcode.dll</HintPath>
</Reference>
@@ -1312,6 +1316,9 @@
<Reference Include="Unity.TextMeshPro">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.TextMeshPro.dll</HintPath>
</Reference>
<Reference Include="Unity.ResourceManager">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.ResourceManager.dll</HintPath>
</Reference>
<Reference Include="Unity.VisualStudio.Editor">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.VisualStudio.Editor.dll</HintPath>
</Reference>
@@ -1324,9 +1331,15 @@
<Reference Include="Unity.AI.Navigation">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.AI.Navigation.dll</HintPath>
</Reference>
<Reference Include="Unity.Profiling.Core">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.Profiling.Core.dll</HintPath>
</Reference>
<Reference Include="Unity.Timeline.Editor">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.Timeline.Editor.dll</HintPath>
</Reference>
<Reference Include="Unity.ScriptableBuildPipeline">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.ScriptableBuildPipeline.dll</HintPath>
</Reference>
<Reference Include="Cysharp.Net.Http.YetAnotherHttpHandler">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Cysharp.Net.Http.YetAnotherHttpHandler.dll</HintPath>
</Reference>
@@ -1351,12 +1364,18 @@
<Reference Include="Unity.AI.Navigation.Updater">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.AI.Navigation.Updater.dll</HintPath>
</Reference>
<Reference Include="Unity.Multiplayer.Center.Editor">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.Multiplayer.Center.Editor.dll</HintPath>
<Reference Include="Unity.ScriptableBuildPipeline.Editor">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.ScriptableBuildPipeline.Editor.dll</HintPath>
</Reference>
<Reference Include="Unity.PlasticSCM.Editor">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.PlasticSCM.Editor.dll</HintPath>
</Reference>
<Reference Include="Unity.Addressables">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.Addressables.dll</HintPath>
</Reference>
<Reference Include="Unity.Multiplayer.Center.Editor">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.Multiplayer.Center.Editor.dll</HintPath>
</Reference>
<Reference Include="Unity.Services.Core.Components">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.Services.Core.Components.dll</HintPath>
</Reference>
@@ -1372,6 +1391,9 @@
<Reference Include="Unity.AI.Navigation.Editor.ConversionSystem">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.AI.Navigation.Editor.ConversionSystem.dll</HintPath>
</Reference>
<Reference Include="Unity.Addressables.Editor">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.Addressables.Editor.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.UI">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/UnityEditor.UI.dll</HintPath>
</Reference>
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 7fe0b660c2ed64da8bbf0e128adfb34e
guid: fb38e3be95e9247c5a451d60236def15
folderAsset: yes
DefaultImporter:
externalObjects: {}

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