Compare commits

...
Author SHA1 Message Date
admin 5d55bb8436 Fix: check secret in shell instead of workflow if condition 2026-01-12 12:27:30 -08:00
admin 7a31d34aba Inject MANIFEST_PUBLIC_KEY into installer at build time 2026-01-12 12:25:38 -08:00
admin 9a4dda0eea Add workflow_dispatch to installer_build.yml 2026-01-12 12:24:01 -08:00
353eb3907c Sign manifest with Ed25519 at build time (#5226)
Add optional Ed25519 signing to the manifest_manager. When a signing key
is provided, the manifest is signed and the signature is prepended as
a header comment that clients can verify.

Changes:
- manifest_manager: Accept optional private key file as 3rd argument
- manifest_manager: Sign manifest content and prepend signature line
- installer_build.yml: Pass MANIFEST_SIGNING_KEY to manifest_manager
- unity_build.yml: Pass MANIFEST_SIGNING_KEY to manifest_manager
- Add generate_manifest_keys.go script to create key pairs

The signature line format is: # signature=<base64-encoded-ed25519-signature>

To enable signing:
1. Run: go run scripts/generate_manifest_keys.go
2. Add the private key as GitHub secret MANIFEST_SIGNING_KEY
3. Embed the public key in the installer for verification (PR 4)

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 12:08:54 -08:00
ded06ba815 Verify installer SHA256 after download (#5224)
* Verify installer SHA256 after download

The Windows installer was downloading and launching new installer updates
without verifying the SHA256 hash, which could allow a corrupted or
tampered installer to run. This adds SHA256 verification after download
and before launching the new installer.

- Add expectedSha parameter to DownloadAndLaunchNewInstaller
- Compute SHA256 of downloaded file and compare to manifest value
- Delete the file and fail if SHA doesn't match
- Log verification success on match

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

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

* Remove accidentally committed node_modules cache files

* Add node_modules to .gitignore

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 11:58:03 -08:00
70392b109a Stream downloads to disk with incremental SHA256 hashing (#5225)
Previously, FetchAndWriteOne() would download entire files into memory,
then compute the SHA256, then write to disk. This doubled memory usage
for each concurrent download.

Now the function streams directly to a temp file while computing SHA256
incrementally using TransformBlock. The temp file is renamed to the
final location only after SHA verification passes.

Benefits:
- Eliminates memory buffering of entire files
- Safer atomic writes using temp file + rename pattern
- Temp files cleaned up on failure

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 11:57:01 -08:00
821e3f1a4a Add retry loop for stapler after notarization (#5222)
Apple's CloudKit can have a brief delay after notarization completes
before the ticket is available for stapling. This adds a retry loop
with 10-second delays, up to 5 attempts.

Error was:
  CloudKit query for eagle0.app failed due to "Record not found".
  The staple and validate action failed! Error 65.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 10:13:43 -08:00
ebe819a5a1 Update Bazel dependencies to latest compatible versions (#5218)
Updates the following dependencies:
- bazel_skylib: 1.8.1 → 1.9.0
- rules_pkg: 1.1.0 → 1.2.0
- rules_go: 0.56.1 → 0.59.0
- gazelle: 0.45.0 → 0.47.0
- rules_oci: 2.2.6 → 2.2.7
- aspect_bazel_lib: 2.16.0 → 2.22.4
- rules_jvm_external: 6.3 → 6.9

Not updated (compatibility issues):
- googletest 1.17.0.bcr.2: pulls abseil-cpp incompatible with protobuf 29.2
- rules_scala 7.1.6: protobuf gencode/runtime version mismatch
- flatbuffers 25.9.23: C++ API changes break existing code
- grpc/grpc-java: requires rules_swift 3.x which conflicts with rules_apple

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 09:54:35 -08:00
3e868d0305 Fix nginx failing to reload during blue-green deployment (#5219)
The previous configuration used an upstream block with a static hostname:
  upstream eagle_grpc { server eagle-blue:40032; }

This caused nginx -s reload to fail when eagle-blue was stopped because
nginx tries to resolve all upstream hostnames at config load time.

Changed to use a map directive with a variable:
  map $host $eagle_backend { default "eagle-blue:40032"; }
  grpc_pass grpc://$eagle_backend;

This pattern (already used for auth backend) resolves the hostname at
request time, allowing nginx to reload even when the backend is down.
Requests to a stopped backend will get 502 errors instead of failing
to reload nginx entirely.

Tradeoff: Loses keepalive 100; setting, but deployment reliability
is more important than connection pooling optimization.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 09:53:38 -08:00
e46867333c Add mac_build_handler to Mac Build workflow triggers (#5220)
Changes to the Go upload tool should trigger the Mac build workflow.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 09:51:54 -08:00
2d59d4ff51 Fix rsync exit code 23 in persist_library.sh (#5221)
Unity's temporary .traceevents files can vanish during rsync, causing
exit code 23 ("partial transfer due to error"). This is acceptable for
the Library cache, so treat exit code 23 as success.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 09:51:19 -08:00
d493182184 Fix Sparkle sign_update flag: use -f for file path (#5217)
The -s flag expects the private key as a string argument, not a file
path. Changed to -f which correctly reads the key from a file.

Error was: "Failed to decode base64 encoded key data from: /tmp/sparkle_private_key"

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 09:05:36 -08:00
8ce4e4c4a9 Add rule: Claude must never merge PRs (#5216)
User explicitly stated: "never ever ever ever merge a PR for me.
You create PRs. I merge them."

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 08:41:55 -08:00
9729110ea8 Consolidate Docker Build into single job for better runner utilization (#5215)
Previously, docker_build.yml had 4 separate jobs (build-eagle, build-shardok,
build-admin, build-jfr-sidecar) that competed for runner slots. With 3 runners
and 6+ workflows triggering on main push, these jobs serialized rather than
running in parallel.

Now consolidated into a single `build-all` job that:
- Builds all 4 images with one `bazel build` command (Bazel parallelizes internally)
- Uses 1 runner slot instead of 4, freeing runners for other workflows
- Shares Bazel cache warming across all builds
- Pushes all images sequentially (fast, network-bound)

Expected improvement: Docker Build workflow goes from ~8.5m (4 serialized jobs)
to ~3-4m (1 consolidated job with internal parallelism).

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 08:37:07 -08:00
adminandGitHub 2224e78a36 Improve Sparkle sign_update error reporting (#5209)
Improves Sparkle sign_update error reporting by capturing stderr, and allows full signing/notarization/deploy pipeline on feature branches via workflow_dispatch.
2026-01-12 08:36:41 -08:00
007a57eea2 Convert CommandSelection and AI clients to use Scala types (#5160)
Add toScala() method to CommandSelection that converts proto-based
command selection to ScalaCommandSelection. This simplifies the action
files that were previously doing manual conversion from proto to Scala
types.

Updated files:
- CommandSelection.scala: Added toScala() method
- EndHandleRiotsPhaseAction.scala: Use toScala() instead of manual conversion
- PerformVassalCommandsPhaseAction.scala: Use toScala() instead of manual conversion
- PerformVassalDefenseDecisionsAction.scala: Use toScala() instead of manual conversion
- Removed unused AvailableCommandTypeMap and SelectedCommandConverter imports

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 08:31:35 -08:00
3febf2a6cd Use DigitalOcean Spaces for busybox binary (private repo fix) (#5213)
GitHub release URLs don't work for private repos without authentication.
Bazel's http_file can't use GitHub auth, so we need a public URL.

Uploaded the busybox binary to DigitalOcean Spaces alongside the sysroots.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:43:30 -08:00
26204cc879 Add runtime Canvas UI builder for tutorial modal (#5208)
* Add runtime Canvas UI builder for tutorial modal

Implements TutorialCanvasBuilder to construct Canvas-based tutorial modal
UI at runtime, replacing the IMGUI fallback when no prefab is assigned.

- TutorialCanvasBuilder creates complete Canvas UI hierarchy:
  - Modal blocker (dark overlay)
  - Panel with title, description, icon, progress bar
  - Continue, Skip, and Skip All buttons
  - Fantasy RPG color scheme matching game style
- TutorialUIManager auto-builds Canvas UI if ModalPanel not assigned
- TutorialModalPanel click handlers made public for external setup
- Updated TUTORIAL_PLAN.md to reflect Canvas UI completion

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

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

* Fix tutorial Canvas UI issues

- Auto-load Stoke font if not assigned (searches Resources and loaded assets)
- Increase panel height (550px) and use flexible spacer for layout
- Fix text truncation by using Overflow mode instead of Ellipsis
- Replace "Province Selected" tutorial with proper welcome intro
- Intro tutorial triggers immediately on game start
- Skip buttons hidden when AllowSkip=false (intro is non-skippable)

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

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

* Remove font search logic - use CanvasFont field instead

Font should be assigned in TutorialUIManager inspector (CanvasFont field).
Removed unnecessary auto-search logic from TutorialCanvasBuilder.

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

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

* Fix TutorialTestSetup compile errors

- Use HasCompletedTutorial instead of HasSeenTutorial
- Use OnGameEvent instead of TriggerEvent

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

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

* Trigger intro tutorial when entering game, not lobby

- Remove immediate trigger from TutorialTestSetup.Start()
- Trigger "game_started" event from TutorialManager.Initialize()
  when EagleGameController is passed (actual game entry)

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

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

* Update TUTORIAL_PLAN.md with current status and future work

- Document completed phases (foundation, Canvas UI, triggers, test setup)
- Add Unity setup instructions (font assignments)
- Add future work: lobby tutorial helper, overlay system, hints
- Add lobby tutorial section to planned contextual tutorials
- Update testing instructions

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

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

* Assign Stoke-Regular-SDF font to TutorialUIManager

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

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

* Mark font assignment complete in TUTORIAL_PLAN.md

🤖 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-12 07:36:29 -08:00
7032260a31 Make Engine.postCommand use Scala SelectedCommand (#5211)
Updates Engine.postCommand to accept Scala SelectedCommand instead of
the proto version. The conversion from proto to Scala now happens at
the GameController layer, keeping the Engine interface proto-free.

Changes:
- Engine.scala: Import Scala SelectedCommand instead of proto
- EngineImpl.scala: Remove proto import and converter, use Scala directly
- GameController.scala: Convert proto→Scala before calling engine.postCommand
- Update test files to use Scala SelectedCommand for mock expectations

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:26:51 -08:00
23ab715ffc Add GitHub mirror for busybox binary to fix CI download failures (#5210)
The busybox.net server is frequently unavailable or slow, causing CI
builds to fail with download timeouts. This adds a GitHub release
mirror as the primary download source with busybox.net as fallback.

The binary is hosted at:
https://github.com/nolen777/eagle0/releases/tag/busybox-1.35.0

SHA256 verified: 6e123e7f3202a8c1e9b1f94d8941580a25135382b99e8d3e34fb858bba311348

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:17:08 -08:00
73f42d5b51 Add Mac download support to invitation landing page (#5207)
* Allow Mac signing/notarization/deploy on manual workflow triggers

The signing, notarization, and deploy steps were only running on
push events. Now they also run on workflow_dispatch (manual triggers)
when targeting main branch.

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

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

* Add Mac download support to invitation landing page

- Detect Mac vs Windows from User-Agent header
- Add /invite/{code}/install.sh endpoint for Mac shell installer
- Shell script creates invitation.json, downloads app ZIP, extracts to /Applications
- Update HTML template with platform-specific instructions
- Add MAC_INSTALLER_URL environment variable (default: assets.eagle0.net/mac/builds/eagle0-latest.zip)
- Show link to other platform at bottom of page

🤖 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-12 07:03:28 -08:00
3cb61f69f4 Make CommandFactory use Scala types instead of proto types (#5205)
Update TCommandFactory and CommandFactory to accept Scala AvailableCommand
and SelectedCommand types directly, eliminating proto dependencies from
the command execution path.

Key changes:
- TCommandFactory.makeTCommand now takes Scala command types
- CommandFactory pattern matching updated for all ~40 command types
- Helper methods updated (attackDecision, improvementTypeMap, etc.)
- Removed proto converter imports from CommandFactory
- Updated vassal/riot phase actions to convert proto→Scala at call sites
- Added exports for Scala command types from t_command_factory

All 175 library tests pass.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 06:58:05 -08:00
62fff3c0ea Fix warmup authentication for Docker bridge network connections (#5206)
The warmup tool sends X-Warmup-User header for authentication during
blue/green deployments. This only worked when connecting from true
localhost (127.0.0.1), but when running warmup from the host machine
to a Docker container, the connection appears to come from the Docker
bridge network (172.17.x.x), causing the warmup header to be ignored.

The CreateGameRequest then fails because the user is unauthenticated
(null username), causing the warmup to timeout.

Fix: Expand the localhost check to also accept Docker bridge network
addresses (10.x.x.x, 172.16-31.x.x, 192.168.x.x) using Java's
isSiteLocalAddress(). These are all private network addresses that
can only come from the same machine or local network.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 06:47:23 -08:00
ca86926a25 Fix AI to chase scattering defenders instead of holding empty castles (#5203)
When defenders scatter (flee from castles), the attacker AI was still
using HOLD_CASTLES strategy. This caused the AI to park units on
empty castles instead of chasing fleeing defenders, even though
eliminating all defenders wins via LAST_PLAYER_STANDING.

The fix adds a new condition to the strategy selector: if defenders
exist but none are on castles, use ATTACK_UNITS strategy to chase
them down.

New strategy selection flow:
1. Consider fleeing (if combat odds are bad)
2. Consider crossing rivers (if needed)
3. If attacker can't hold all castles → ATTACK_UNITS
4. If any defender is on a castle → ATTACK_CASTLES
5. NEW: If defenders exist but none on castles → ATTACK_UNITS
6. If no defenders remain → HOLD_CASTLES

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 06:34:47 -08:00
1d334c91ae Allow Mac signing/notarization/deploy on manual workflow triggers (#5204)
The signing, notarization, and deploy steps were only running on
push events. Now they also run on workflow_dispatch (manual triggers)
when targeting main branch.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 06:32:24 -08:00
64e1e46e0a Fix crane installation and use crane in blue-green deploys (#5202)
1. Fix crane installation - don't try to mv crane to itself
   (tar extracts to cwd which is already /opt/eagle0)
2. Keep crane binary after deploy for blue-green script to use
3. Update blue-green deploy to use crane instead of docker pull
   (fixes OCI/Docker digest mismatch issue)

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 23:09:04 -08:00
4722a40384 Fix crane binary disappearing during Docker deploy (#5201)
Use absolute path for crane binary and add verification that it
was installed correctly. Also add ls -la output for debugging.

The crane binary was mysteriously disappearing between the Eagle
and Shardok image pulls.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 23:02:20 -08:00
6653a14660 Use Scala types for command matching in EngineImpl (#5200)
Refactors postCommand to:
- Get Scala commands directly from AvailableCommandsFactory
- Convert proto SelectedCommand to Scala for matching
- Use CommandType-based matching (no more proto dependency in AvailableCommandTypeMap)
- Convert back to proto for CommandFactory (temporary until full migration)

AvailableCommandTypeMap now uses only Scala types - matching is simply:
  availableCommands.find(_.commandType == selectedCommand.commandType)

Also adds:
- ScalaCommandSelection case class for future migration of command selectors
- Improved visibility for command converter targets

This is an incremental step toward eliminating proto commands from library/.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 22:59:01 -08:00
056571651e Upgrade rules_apple to 4.3.3 and rules_swift to 2.4.0 (#5198)
* Upgrade rules_apple to 4.3.3 and rules_swift to 2.4.0

These are the latest compatible versions (rules_apple 4.3.3 depends
on rules_swift 2.4.0 with compatibility level 2).

Note: rules_swift 3.x uses compatibility level 3 and is not
compatible with current rules_apple versions.

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

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

* Fix zipApp to handle symlinks in Sparkle.framework

Sparkle.framework contains symlinks like Headers -> Versions/Current/Headers.
The previous code used filepath.Walk which follows symlinks, causing it to
try to read a directory as a file.

Now uses filepath.WalkDir with os.Lstat to detect symlinks and store them
properly in the zip archive.

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

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

* Revert "Fix zipApp to handle symlinks in Sparkle.framework"

This reverts commit ad2f2e40d4562ced1b4001e13abc95d8901c8f0e.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 22:48:57 -08:00
d8224e7886 Fix zipApp to handle symlinks in Sparkle.framework (#5199)
Sparkle.framework contains symlinks like Headers -> Versions/Current/Headers.
The previous code used filepath.Walk which follows symlinks, causing it to
try to read a directory as a file.

Now uses filepath.WalkDir with os.Lstat to detect symlinks and store them
properly in the zip archive.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 22:48:29 -08:00
9cc9a65e4a Fix codesign script to sign all Sparkle framework components (#5195)
* Fix codesign script to sign all Sparkle framework components

Sign XPC services, nested apps (Updater.app), and standalone
executables (Autoupdate) before signing the framework itself.
Apple notarization requires all nested binaries to be signed
with Developer ID certificate and secure timestamp.

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

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

* Add missing path triggers for pull_request in Mac build workflow

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

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

* Re-add rules_swift for Mac GoDice plugin build

The DarwinGodiceBundle requires rules_swift to build.
This was inadvertently removed in #5194.

🤖 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-11 22:24:55 -08:00
025563b607 Add retry logic to docker pull in blue-green deploy (#5197)
Handles intermittent Docker registry digest mismatch errors like:
"failed commit on ref: unexpected commit digest"

This is a known Docker/containerd issue that can occur due to:
- Registry caching
- Network/proxy issues
- Race conditions during push

Now retries up to 3 times with 5 second delays.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 22:23:18 -08:00
030f6b2c2e Fix warmup tool and PostCommandResponse status (#5196)
Warmup tool fixes:
- Process GameUpdates while waiting for PostCommandResponse (action
  results arrive BEFORE PostCommandResponse, not after)
- Don't wait for SubscriptionAck before ActionResultResponse (they
  arrive in reverse order)
- Use recommended_hero_id from ImproveAvailableCommand instead of
  hardcoding 0 (which doesn't exist)
- Verify we receive new AvailableCommands after posting command
- Add detailed logging for debugging

Server fix:
- Return PostCommandResponse.Status.SUCCESS instead of default UNKNOWN

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 14:54:41 -08:00
1d5dd7b971 Remove swift_proto_library targets and rules_swift dependency (#5194)
The Mac history editor that used these targets was previously removed.
Cleaning up the unused Swift proto infrastructure.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 14:24:54 -08:00
649e80c4ac Add Mac build pipeline with Sparkle auto-updates (#5193)
- Unity Mac build scripts (build_mac.sh, build_unity_mac.sh)
- Code signing with Developer ID certificate
- Apple notarization for Gatekeeper compliance
- Sparkle framework injection for delta auto-updates
- Go build handler for S3 upload and appcast.xml generation
- Update InvitationCodeManager.cs for Mac platform paths
- GitHub Actions workflow triggered on main branch pushes

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 14:22:52 -08:00
a1b4e41553 Add CommandType enum to replace SelectedCommand in ActionResult and Province (#5191)
* Add CommandType enum to replace SelectedCommand in ActionResult and Province

Create a compile-time safe CommandType enum that provides exhaustive matching
when adding new command types. This replaces SelectedCommand in:
- ActionResult.lastCommandTypeForActingProvince
- Province.lastCommand

Key changes:
- New CommandType.scala enum with exhaustive converters from SelectedCommand
  and AvailableCommand
- New command_type.proto with 40 command types + UNKNOWN
- CommandTypeConverter for proto<->Scala conversion
- Simplified shouldFollow in AvailableCommandsFactory to compare CommandType
  values directly
- Updated RandomStateSequencer.withTCommandAndLastCommand to use CommandType

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

* Add backwards compatibility for CommandType in saved games

Preserve the deprecated SelectedCommand field (field 28) alongside the
new CommandType field (field 43) in action_result.proto. When loading
saved games, first check the new CommandType field; if not set, fall
back to the deprecated SelectedCommand and convert it.

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

* Use parameterized enums for SelectedCommand/AvailableCommand to CommandType mapping

Refactors the dependency direction: instead of CommandType.from(SelectedCommand),
each enum case now has a built-in `val commandType: CommandType` parameter.

Benefits:
- Compile-time safety: can't add a new command without specifying its CommandType
- Simpler access: just call .commandType instead of a converter method
- Better dependency direction: richer types depend on simpler types, not vice versa

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 13:58:34 -08:00
1c24474a0b Add critical git rules to top of CLAUDE.md (#5192)
Prevent future accidental pushes directly to main by putting
explicit rules at the very top of the file.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 13:31:36 -08:00
adminandClaude Opus 4.5 26eec6cabc Add tutorial system plan document
Documents current implementation status, architecture, and remaining work.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 13:15:27 -08:00
d134f40438 Add tutorial test setup with IMGUI fallback UI (#5173)
* Add tutorial test setup with IMGUI fallback UI

Create TutorialTestSetup component that:
- Registers test tutorials programmatically on startup
- Triggers on first province selection, battle entry, and command issued
- Allows testing without Unity Editor asset creation

Add fallback IMGUI modal in TutorialUIManager:
- Renders when no ModalPanel prefab is assigned
- Shows title, description, progress, and action buttons
- Enables end-to-end testing without UI prefab setup

To test:
1. Add TutorialTestSetup component to TutorialManager GameObject
2. Ensure TutorialManager has UIManager reference
3. Play game and select a province to see test tutorial

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

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

* Silence missing onboarding sequence warning

Change LogWarning to debug log when no onboarding sequence is assigned.
This is a valid configuration state, not an error.

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

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

* Improve IMGUI fallback modal styling

- Add FallbackFont field (assign Stoke-Regular.ttf in Unity)
- Increase font sizes: title 24, description 20, buttons 18
- Make modal window larger (600x300)
- Make buttons taller (40px height)

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

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

* Double IMGUI modal size and font sizes

- Window: 1200x600 (was 600x300)
- Title: 48pt, Description: 40pt, Progress: 32pt, Buttons: 36pt
- Buttons: 200-240x80 (was 100-120x40)
- Add TutorialManager GameObject to Gameplay scene

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

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

* Add Reset Tutorials button to Settings panel

- Add resetTutorialsButton field to SettingsPanelController
- Add OnResetTutorialsClick() handler that calls TutorialManager.ResetAllProgress()

To wire up in Unity:
1. Add a Button to the Settings panel
2. Assign it to resetTutorialsButton field
3. Set OnClick to SettingsPanelController.OnResetTutorialsClick

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

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

* Fix ambiguous Debug reference

Use UnityEngine.Debug.Log to resolve conflict with System.Diagnostics.Debug

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

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

* Apply window style to IMGUI modal title

Pass windowStyle to GUI.Window so title uses 48pt font

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

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

* Add more spacing between title and description

Increase top spacing from 30 to 60 pixels

🤖 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-11 11:38:21 -08:00
a638dd26ca Fix deploy: remove warmup before scp, safer container cleanup (#5190)
Three fixes:
1. Remove existing warmup binary before scp - Bazel outputs it with
   read-only permissions (r-xr-xr-x), causing scp to fail on overwrite.

2. Remove the `docker compose up -d --remove-orphans` step which was
   causing "container name already in use by service {}" errors due to
   Docker Compose state conflicts.

3. Add `docker container prune -f` as a safer alternative - removes
   stopped containers without trying to reconcile compose state.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 07:46:29 -08:00
5e93b0eaa0 Add null safety to RunningGamePlayerInfo in admin console (#5189)
Prevent NPE when listing games with corrupt faction/leader data.
This defensive fix handles three cases:
- Hero might not exist for the faction head ID
- Faction name might be null
- Leader nameTextId might be null

The warmup tool created games with unexpected null values, causing
the admin console to crash when listing running games.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 07:22:12 -08:00
6940312d3f Add /invite/ route to nginx config (#5188)
The invitation landing page is served by the auth service on port 8080,
but nginx wasn't configured to proxy requests to it, resulting in 404.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 07:18:26 -08:00
243f0d43e7 Fix scp to preserve directory structure for deploy scripts (#5187)
scp flattens paths - it was copying scripts/bin/warmup to /opt/eagle0/warmup
instead of /opt/eagle0/scripts/bin/warmup. Fixed by:
1. Creating directory structure on remote first
2. Copying files to their correct locations

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 22:44:25 -08:00
1ddd015449 Unify .env management across workflows with shared update script (#5186)
Both docker_build.yml and auth_build.yml now use a shared update-env.sh
script that updates only the variables each workflow is responsible for,
without overwriting values set by other workflows.

Changes:
- Add deploy/env.template with all environment variables
- Add deploy/update-env.sh to safely update individual env vars
- Update docker_build.yml to use update-env.sh instead of rm/recreate
- Update auth_build.yml to use update-env.sh instead of grep/sed chain
- Add FASTMAIL_* vars to docker_build.yml deploy job

This fixes the bug where docker_build.yml was overwriting FASTMAIL env
vars set by auth_build.yml because it recreated .env from scratch.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 22:42:08 -08:00
da85d0983a Fix deploy order: start jfr-sidecar after eagle-blue (#5185)
jfr-sidecar has `pid: "service:eagle-blue"` to share PID namespace for
JFR profiling. It must be started after eagle-blue exists, not before.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 22:31:12 -08:00
ae574e6ff5 Add delete user and delete invitation functionality to admin panel (#5181)
- Add DeleteUser and DeleteInvitation RPCs to admin.proto
- Add Delete methods to UserService and InvitationService
- Add delete handlers in admin_handlers.go and admin_server.go
- Add delete buttons to users and invitations admin UI templates
- Users can be deleted permanently (with self-deletion prevention)
- Only non-pending invitations (revoked, expired, redeemed) can be deleted

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 22:23:03 -08:00
c1ac57b929 Build warmup tool in deploy job before scp (#5183)
The warmup binary was being built in build-eagle but each job has its
own checkout, so the binary wasn't available in the deploy job when
we tried to scp it to the droplet.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 22:22:21 -08:00
be51daa148 Remove EagleGameHistoryViewer Mac app (#5182)
This Swift/SwiftUI app for viewing game history is no longer needed.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 22:21:25 -08:00
97605d0a63 Remove mac history editor build workflow (#5180)
The mac history editor is no longer needed now that we have the Admin
console for game management.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 22:17:48 -08:00
9cca922b70 Add blue-green deployment infrastructure for Eagle server (#5162)
* Add blue-green deployment infrastructure for Eagle server

- Add ReloadGames RPC to eagle.proto for reloading game state from disk
- Implement reloadAllGames() and flushToDisk() in GamesManager.scala
- Add reloadGames() handler to EagleServiceImpl.scala
- Update docker-compose.prod.yml with eagle-blue/green services
- Update nginx.conf for switchable upstream
- Create scripts/deploy-blue-green.sh for zero-downtime deployment
- Create Go warmup tool (src/main/go/net/eagle0/warmup) that:
  - Uses bidirectional streaming to create test games
  - Posts Improve command and verifies ActionResults
  - Cleans up test game after warmup
- Create scripts/warmup-eagle.sh wrapper that uses Go tool or falls back to grpcurl
- Update docker_build.yml to build and deploy warmup binary

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

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

* Add X-Warmup-User header support with localhost restriction

- AuthorizationInterceptor: Accept X-Warmup-User header for warmup authentication
- Only allow X-Warmup-User from localhost connections (security)
- Go warmup tool: Send x-warmup-user metadata header
- Add grpc/metadata dependency to warmup BUILD

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

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

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 22:13:15 -08:00
6eab9ffc3c Add invitation landing page with one-click installer (#5165)
Instead of asking users to copy/paste a PowerShell command, emails now
link to a landing page at /invite/{code} that:
- Shows a branded "Accept Invitation" page
- Offers a "Download & Install" button that downloads a .bat file
- The .bat file downloads the installer and runs it with --code=XXX
- Shows clear instructions for running the .bat file
- Displays error messages for invalid/expired/redeemed codes
- Falls back to showing the invitation code for manual entry

Changes:
- Add invitation_handlers.go with landing page and .bat download routes
- Update main.go to register the new HTTP routes
- Simplify email template to just link to the landing page

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 22:10:09 -08:00
08eaffb927 Remove debug logging from combat animators (#5172)
Remove Debug.Log calls from ArrowVolleyAnimator and MeleeAnimator.
Keep Debug.LogWarning calls that indicate configuration issues.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:51:28 -08:00
a2bc55e0f5 Remove debug logging from MeleeAnimator (#5179)
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:51:05 -08:00
19861377a6 Use hex center for animation positions (#5178)
Changed all animators to use GetCellCenterPosition instead of
GetCellLocalPosition so animations start and end at the actual
hex center rather than offset towards the top.

- MoveAnimator: footprints centered
- MeleeAnimator: weapon animations centered
- CatapultAnimator: projectile source now also centered (target already was)

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:21:14 -08:00
cc70087efc Fix captured hero visibility in AvailableCommandConverter (#5177)
The converter was using factionId = Some(viewingFactionId) which
applied visibility restrictions to captured heroes. The old proto-based
factory used factionId = None (with comment "can see unaffiliated
heroes") which always showed full hero info.

When handling captured heroes, the player needs to see all hero stats
to make informed decisions about recruiting, imprisoning, etc.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 21:14:33 -08:00
b2383c8384 Fix Move animation direction for observed battles (#5176)
When observing battles, Move animations were playing backwards because
the model is fully updated before animations play. The animation code
was using the unit's current location (post-move) as the source, but
for multi-step moves this caused animations to go from the final
position back to intermediate positions.

Fix: Track source coordinates in ShardokGameModel.MoveSourceCoords
before applying each diff, then use these stored coordinates for
animation source positions.

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

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 20:33:58 -08:00
03958b6914 Fix ArmyStats missing hostility and originProvinceId fields (#5175)
Add hostility and originProvinceId fields to Scala ArmyStats that were
present in the proto definition but missing from the Scala model. This
fixes the AttackDecisionCommandChooser which uses hostility to determine
friendly vs enemy armies.

Unlike #5174, this uses required fields instead of defaults, ensuring
all call sites explicitly provide the values rather than relying on
potentially incorrect defaults.

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 20:03:49 -08:00
266 changed files with 6047 additions and 52903 deletions
+22 -30
View File
@@ -112,6 +112,16 @@ 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:
@@ -124,36 +134,18 @@ jobs:
set -x
cd /opt/eagle0
# Update AUTH_IMAGE in .env file (preserve other vars)
if [ -f .env ]; then
# Remove old AUTH_IMAGE line and add new one
grep -v '^AUTH_IMAGE=' .env > .env.tmp || true
echo "AUTH_IMAGE=${AUTH_IMAGE}" >> .env.tmp
# Also update OAuth credentials
grep -v '^DISCORD_CLIENT_ID=' .env.tmp > .env.tmp2 || true
echo "DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID:-}" >> .env.tmp2
grep -v '^DISCORD_CLIENT_SECRET=' .env.tmp2 > .env.tmp3 || true
echo "DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET:-}" >> .env.tmp3
grep -v '^GOOGLE_CLIENT_ID=' .env.tmp3 > .env.tmp4 || true
echo "GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}" >> .env.tmp4
grep -v '^GOOGLE_CLIENT_SECRET=' .env.tmp4 > .env.tmp5 || true
echo "GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}" >> .env.tmp5
# Update JWT private key (JWK format for auth service bootstrap)
grep -v '^JWT_PRIVATE_KEY=' .env.tmp5 > .env.tmp6 || true
echo "JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY:-}" >> .env.tmp6
# Update Fastmail JMAP credentials for email sending
grep -v '^FASTMAIL_API_TOKEN=' .env.tmp6 > .env.tmp7 || true
echo "FASTMAIL_API_TOKEN=${FASTMAIL_API_TOKEN:-}" >> .env.tmp7
grep -v '^FASTMAIL_FROM_EMAIL=' .env.tmp7 > .env.tmp8 || true
echo "FASTMAIL_FROM_EMAIL=${FASTMAIL_FROM_EMAIL:-}" >> .env.tmp8
grep -v '^FASTMAIL_FROM_NAME=' .env.tmp8 > .env || true
echo "FASTMAIL_FROM_NAME=${FASTMAIL_FROM_NAME:-}" >> .env
rm -f .env.tmp .env.tmp2 .env.tmp3 .env.tmp4 .env.tmp5 .env.tmp6 .env.tmp7 .env.tmp8
chmod 600 .env
else
echo "ERROR: .env file not found. Run main deploy first."
exit 1
fi
# 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}" \
"JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY}" \
"FASTMAIL_API_TOKEN=${FASTMAIL_API_TOKEN}" \
"FASTMAIL_FROM_EMAIL=${FASTMAIL_FROM_EMAIL}" \
"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
+169 -416
View File
@@ -26,196 +26,71 @@ permissions:
contents: read
jobs:
build-eagle:
# Single consolidated build job - builds all images with one bazel invocation
# This uses 1 runner slot instead of 4, and Bazel parallelizes internally
build-all:
runs-on: self-hosted
outputs:
image_tag: ${{ steps.push-eagle.outputs.image_tag }}
eagle_image_tag: ${{ steps.push-images.outputs.eagle_image_tag }}
shardok_image_tag: ${{ steps.push-images.outputs.shardok_image_tag }}
admin_image_tag: ${{ steps.push-images.outputs.admin_image_tag }}
jfr_sidecar_image_tag: ${{ steps.push-images.outputs.jfr_sidecar_image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
- name: Build Eagle Docker image
id: build-eagle
run: |
set -ex
bazel build --platforms=//:linux_x86_64 //ci:eagle_server_image
# Save the resolved path before any other bazel command changes bazel-bin symlink
IMAGE_PATH=$(readlink -f bazel-bin/ci/eagle_server_image)
echo "Image path: $IMAGE_PATH"
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
- name: Login to DigitalOcean Container Registry
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
run: |
mkdir -p ~/.docker
AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
# Also set for current directory in case Bazel uses different home
mkdir -p .docker
cp ~/.docker/config.json .docker/
- name: Push Eagle image to DO registry
id: push-eagle
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DOCKER_CONFIG: ${{ github.workspace }}/.docker
- name: Build all Docker images
id: build-all
run: |
set -ex
# Use cross-compiled image path from build step
EAGLE_IMAGE="${{ steps.build-eagle.outputs.image_path }}"
echo "Using Eagle image: $EAGLE_IMAGE"
if [ -z "$EAGLE_IMAGE" ] || [ ! -d "$EAGLE_IMAGE" ]; then
echo "ERROR: Eagle image not found at: $EAGLE_IMAGE"
exit 1
fi
# Debug: show OCI layout contents
echo "=== OCI Layout Contents ==="
cat "$EAGLE_IMAGE/index.json"
echo ""
echo "=== Blobs ==="
ls -la "$EAGLE_IMAGE/blobs/sha256/" | head -20
# Verify OCI layout consistency before pushing
echo "=== Verifying OCI layout consistency ==="
for digest in $(cat "$EAGLE_IMAGE/index.json" | grep -o '"sha256:[^"]*"' | tr -d '"'); do
blob_path="$EAGLE_IMAGE/blobs/${digest/://}"
if [ ! -f "$blob_path" ]; then
echo "ERROR: Blob not found: $blob_path"
exit 1
fi
actual_digest="sha256:$(shasum -a 256 "$blob_path" | cut -d' ' -f1)"
if [ "$digest" != "$actual_digest" ]; then
echo "ERROR: Digest mismatch for $blob_path"
echo " Index says: $digest"
echo " Actual: $actual_digest"
exit 1
fi
echo "✓ Verified: $digest"
done
# Build the push target to get crane in runfiles
bazel build //ci:eagle_server_push
# Use crane directly for push (avoids OCI->Docker digest mismatch)
CRANE="bazel-bin/ci/push_eagle_server_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
echo "Using crane: $CRANE"
# Push with SHA tag
GIT_SHA=$(git rev-parse --short=8 HEAD)
IMAGE_TAG="registry.digitalocean.com/eagle0/eagle-server:${GIT_SHA}"
echo "Pushing eagle image: $IMAGE_TAG"
$CRANE push "$EAGLE_IMAGE" "$IMAGE_TAG"
# Verify push by checking what's in the registry
echo "=== Verifying push ==="
$CRANE manifest "$IMAGE_TAG" | head -50
PUSHED_DIGEST=$($CRANE digest "$IMAGE_TAG")
echo "Registry reports digest: $PUSHED_DIGEST"
# Output the full image tag for deploy step
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
# Also update :latest for convenience (but deploy won't use it)
echo "Copying to :latest tag"
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/eagle-server:latest"
build-shardok:
runs-on: self-hosted
outputs:
image_tag: ${{ steps.push-shardok.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
- name: Build Shardok binary (cross-compile for Linux)
run: |
set -ex
# Step 1: Build JUST the binary with cross-compilation
# We need --extra_toolchains to force the Linux toolchain to be used
# because toolchains_llvm registers with dev_dependency=True
echo "=== Building shardok-server binary for linux-x86_64 ==="
# Build ALL images in a single bazel command - Bazel parallelizes internally
# This is much more efficient than 4 separate GitHub Actions jobs
echo "=== Building all Docker images ==="
bazel build \
--platforms=//:linux_x86_64 \
--extra_toolchains=@llvm_toolchain_linux//:all \
//src/main/cpp/net/eagle0/shardok:shardok-server
//ci:eagle_server_image \
//ci:shardok_server_image \
//ci:admin_server_image \
//ci:jfr_sidecar_image \
//src/main/go/net/eagle0/warmup:warmup_linux_amd64
# Step 2: Check the binary directly from bazel-bin
# bazel-bin is a symlink that points to the correct output directory
LINUX_BIN="bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server"
echo "=== Checking binary at: $LINUX_BIN ==="
# Copy warmup binary to scripts/ for deployment
mkdir -p scripts/bin
cp bazel-bin/src/main/go/net/eagle0/warmup/warmup_linux_amd64_/warmup_linux_amd64 scripts/bin/warmup
if [ ! -f "$LINUX_BIN" ]; then
echo "ERROR: Binary not found at $LINUX_BIN"
exit 1
fi
# Save all image paths before any other bazel command changes bazel-bin symlink
EAGLE_PATH=$(readlink -f bazel-bin/ci/eagle_server_image)
SHARDOK_PATH=$(readlink -f bazel-bin/ci/shardok_server_image)
ADMIN_PATH=$(readlink -f bazel-bin/ci/admin_server_image)
JFR_PATH=$(readlink -f bazel-bin/ci/jfr_sidecar_image)
# Debug: show what bazel-bin points to
echo "bazel-bin symlink target: $(readlink bazel-bin || echo 'not a symlink')"
echo "eagle_path=$EAGLE_PATH" >> $GITHUB_OUTPUT
echo "shardok_path=$SHARDOK_PATH" >> $GITHUB_OUTPUT
echo "admin_path=$ADMIN_PATH" >> $GITHUB_OUTPUT
echo "jfr_path=$JFR_PATH" >> $GITHUB_OUTPUT
# Step 3: Verify it's ELF (Linux) not Mach-O (macOS)
echo "=== Verifying binary format ==="
MAGIC=$(head -c 4 "$LINUX_BIN" | xxd -p)
echo "Binary magic bytes: $MAGIC"
echo "=== Image paths ==="
echo "Eagle: $EAGLE_PATH"
echo "Shardok: $SHARDOK_PATH"
echo "Admin: $ADMIN_PATH"
echo "JFR Sidecar: $JFR_PATH"
if [ "$MAGIC" = "7f454c46" ]; then
echo "SUCCESS: Binary is ELF format (Linux)"
elif [ "$MAGIC" = "cfaeedfe" ] || [ "$MAGIC" = "cffaedfe" ]; then
echo "ERROR: Binary is Mach-O format (macOS) - cross-compilation failed!"
echo ""
echo "Debug info:"
echo "- bazel-bin points to: $(readlink bazel-bin)"
file "$LINUX_BIN" || true
exit 1
else
echo "WARNING: Unknown binary format: $MAGIC"
file "$LINUX_BIN" || true
fi
- name: Build Shardok Docker image
id: build-shardok
run: |
set -ex
# Build the OCI image with cross-compilation flags
bazel build \
--platforms=//:linux_x86_64 \
--extra_toolchains=@llvm_toolchain_linux//:all \
//ci:shardok_server_image
# The image is output to bazel-bin which is a symlink.
# Resolve it now before any other bazel commands change where it points.
IMAGE_PATH=$(readlink -f bazel-bin/ci/shardok_server_image)
echo "Image path: $IMAGE_PATH"
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
# Verify the binary inside the tar layer is ELF
echo "=== Verifying binary in image tar ==="
# Verify Shardok binary is ELF (Linux) format
echo "=== Verifying Shardok binary format ==="
BINARY_TAR="bazel-bin/ci/shardok_binary_layer.tar"
if [ -f "$BINARY_TAR" ]; then
echo "Checking binary in $BINARY_TAR"
# Extract just the first 4 bytes of the binary from the tar
MAGIC=$(tar -xOf "$BINARY_TAR" app/shardok-server 2>/dev/null | head -c 4 | xxd -p)
echo "Binary magic in tar: $MAGIC"
if [ "$MAGIC" = "7f454c46" ]; then
echo "SUCCESS: Binary in tar is ELF format (Linux)"
echo "SUCCESS: Binary is ELF format (Linux)"
else
echo "ERROR: Binary in tar is NOT ELF format!"
echo "This means pkg_tar is packaging the wrong binary."
echo "ERROR: Binary is NOT ELF format!"
exit 1
fi
else
echo "WARNING: Could not find $BINARY_TAR"
fi
- name: Login to DigitalOcean Container Registry
@@ -226,210 +101,79 @@ jobs:
mkdir -p ~/.docker
AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
# Also set for current directory in case Bazel uses different home
mkdir -p .docker
cp ~/.docker/config.json .docker/
- name: Push Shardok image to DO registry
id: push-shardok
- name: Push all images to DO registry
id: push-images
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DOCKER_CONFIG: ${{ github.workspace }}/.docker
run: |
set -ex
# Use cross-compiled image path from build step
CROSS_IMAGE="${{ steps.build-shardok.outputs.image_path }}"
echo "Using cross-compiled image: $CROSS_IMAGE"
GIT_SHA=$(git rev-parse --short=8 HEAD)
if [ -z "$CROSS_IMAGE" ] || [ ! -d "$CROSS_IMAGE" ]; then
echo "ERROR: Cross-compiled image not found at: $CROSS_IMAGE"
exit 1
fi
# Get crane from Eagle push target (which doesn't need cross-compilation)
# This gives us a macOS crane binary we can actually run.
# We can't build shardok_server_push with platform flags because it would
# download a Linux crane that can't run on macOS.
# Get crane from push target runfiles
bazel build //ci:eagle_server_push
CRANE="bazel-bin/ci/push_eagle_server_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
# Find the Darwin crane binary (may be a symlink)
RUNFILES="bazel-bin/ci/push_eagle_server_push.sh.runfiles"
CRANE=$(find "$RUNFILES" -path "*darwin*" -name crane 2>/dev/null | head -1)
if [ -z "$CRANE" ]; then
# Fallback to any crane
CRANE=$(find "$RUNFILES" -name crane 2>/dev/null | head -1)
if [ ! -e "$CRANE" ]; then
# Fallback: find any Darwin crane
RUNFILES="bazel-bin/ci/push_eagle_server_push.sh.runfiles"
CRANE=$(find "$RUNFILES" -path "*darwin*" -name crane 2>/dev/null | head -1)
fi
if [ -z "$CRANE" ] || [ ! -e "$CRANE" ]; then
echo "ERROR: crane not found. Listing runfiles:"
find "$RUNFILES" -name crane 2>/dev/null || true
echo "ERROR: crane not found"
exit 1
fi
echo "Using crane: $CRANE"
# Push the cross-compiled image with SHA tag
GIT_SHA=$(git rev-parse --short=8 HEAD)
IMAGE_TAG="registry.digitalocean.com/eagle0/shardok-server:${GIT_SHA}"
echo "Pushing shardok image: $IMAGE_TAG"
$CRANE push "$CROSS_IMAGE" "$IMAGE_TAG"
# Push Eagle image
EAGLE_IMAGE="${{ steps.build-all.outputs.eagle_path }}"
EAGLE_TAG="registry.digitalocean.com/eagle0/eagle-server:${GIT_SHA}"
echo "Pushing Eagle: $EAGLE_TAG"
$CRANE push "$EAGLE_IMAGE" "$EAGLE_TAG"
$CRANE copy "$EAGLE_TAG" "registry.digitalocean.com/eagle0/eagle-server:latest"
echo "eagle_image_tag=$EAGLE_TAG" >> $GITHUB_OUTPUT
# Output the full image tag for deploy step
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
# Push Shardok image
SHARDOK_IMAGE="${{ steps.build-all.outputs.shardok_path }}"
SHARDOK_TAG="registry.digitalocean.com/eagle0/shardok-server:${GIT_SHA}"
echo "Pushing Shardok: $SHARDOK_TAG"
$CRANE push "$SHARDOK_IMAGE" "$SHARDOK_TAG"
$CRANE copy "$SHARDOK_TAG" "registry.digitalocean.com/eagle0/shardok-server:latest"
echo "shardok_image_tag=$SHARDOK_TAG" >> $GITHUB_OUTPUT
# Also update :latest for convenience (but deploy won't use it)
echo "Copying to :latest tag"
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/shardok-server:latest"
# Push Admin image
ADMIN_IMAGE="${{ steps.build-all.outputs.admin_path }}"
ADMIN_TAG="registry.digitalocean.com/eagle0/admin-server:${GIT_SHA}"
echo "Pushing Admin: $ADMIN_TAG"
$CRANE push "$ADMIN_IMAGE" "$ADMIN_TAG"
$CRANE copy "$ADMIN_TAG" "registry.digitalocean.com/eagle0/admin-server:latest"
echo "admin_image_tag=$ADMIN_TAG" >> $GITHUB_OUTPUT
build-admin:
runs-on: self-hosted
outputs:
image_tag: ${{ steps.push-admin.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
# Push JFR Sidecar image
JFR_IMAGE="${{ steps.build-all.outputs.jfr_path }}"
JFR_TAG="registry.digitalocean.com/eagle0/jfr-sidecar:${GIT_SHA}"
echo "Pushing JFR Sidecar: $JFR_TAG"
$CRANE push "$JFR_IMAGE" "$JFR_TAG"
$CRANE copy "$JFR_TAG" "registry.digitalocean.com/eagle0/jfr-sidecar:latest"
echo "jfr_sidecar_image_tag=$JFR_TAG" >> $GITHUB_OUTPUT
- name: Build Admin Server Docker image
id: build-admin
run: |
set -ex
# Build admin server image (Go binary has explicit goos/goarch in BUILD.bazel)
bazel build //ci:admin_server_image
# Save the resolved path before any other bazel command changes bazel-bin symlink
IMAGE_PATH=$(readlink -f bazel-bin/ci/admin_server_image)
echo "Image path: $IMAGE_PATH"
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
- name: Login to DigitalOcean Container Registry
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
run: |
mkdir -p ~/.docker
AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
mkdir -p .docker
cp ~/.docker/config.json .docker/
- name: Push Admin image to DO registry
id: push-admin
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DOCKER_CONFIG: ${{ github.workspace }}/.docker
run: |
set -ex
ADMIN_IMAGE="${{ steps.build-admin.outputs.image_path }}"
echo "Using Admin image: $ADMIN_IMAGE"
if [ -z "$ADMIN_IMAGE" ] || [ ! -d "$ADMIN_IMAGE" ]; then
echo "ERROR: Admin image not found at: $ADMIN_IMAGE"
exit 1
fi
# Build the push target to get crane in runfiles
bazel build //ci:admin_server_push
# Use crane directly for push
CRANE="bazel-bin/ci/push_admin_server_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
echo "Using crane: $CRANE"
# Push with SHA tag
GIT_SHA=$(git rev-parse --short=8 HEAD)
IMAGE_TAG="registry.digitalocean.com/eagle0/admin-server:${GIT_SHA}"
echo "Pushing admin image: $IMAGE_TAG"
$CRANE push "$ADMIN_IMAGE" "$IMAGE_TAG"
# Output the full image tag for deploy step
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
# Also update :latest for convenience
echo "Copying to :latest tag"
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/admin-server:latest"
build-jfr-sidecar:
runs-on: self-hosted
outputs:
image_tag: ${{ steps.push-jfr-sidecar.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
- name: Build JFR Sidecar Docker image
id: build-jfr-sidecar
run: |
set -ex
# Build JFR sidecar image (Go binary has explicit goos/goarch in BUILD.bazel)
bazel build //ci:jfr_sidecar_image
# Save the resolved path before any other bazel command changes bazel-bin symlink
IMAGE_PATH=$(readlink -f bazel-bin/ci/jfr_sidecar_image)
echo "Image path: $IMAGE_PATH"
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
- name: Login to DigitalOcean Container Registry
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
run: |
mkdir -p ~/.docker
AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
mkdir -p .docker
cp ~/.docker/config.json .docker/
- name: Push JFR Sidecar image to DO registry
id: push-jfr-sidecar
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DOCKER_CONFIG: ${{ github.workspace }}/.docker
run: |
set -ex
JFR_IMAGE="${{ steps.build-jfr-sidecar.outputs.image_path }}"
echo "Using JFR Sidecar image: $JFR_IMAGE"
if [ -z "$JFR_IMAGE" ] || [ ! -d "$JFR_IMAGE" ]; then
echo "ERROR: JFR Sidecar image not found at: $JFR_IMAGE"
exit 1
fi
# Build the push target to get crane in runfiles
bazel build //ci:jfr_sidecar_push
# Use crane directly for push
CRANE="bazel-bin/ci/push_jfr_sidecar_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
echo "Using crane: $CRANE"
# Push with SHA tag
GIT_SHA=$(git rev-parse --short=8 HEAD)
IMAGE_TAG="registry.digitalocean.com/eagle0/jfr-sidecar:${GIT_SHA}"
echo "Pushing JFR sidecar image: $IMAGE_TAG"
$CRANE push "$JFR_IMAGE" "$IMAGE_TAG"
# Output the full image tag for deploy step
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
# Also update :latest for convenience
echo "Copying to :latest tag"
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/jfr-sidecar:latest"
echo "=== All images pushed successfully ==="
deploy:
runs-on: self-hosted
needs: [build-eagle, build-shardok, build-admin, build-jfr-sidecar]
needs: [build-all]
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
environment: production
env:
EAGLE_IMAGE: ${{ needs.build-eagle.outputs.image_tag }}
SHARDOK_IMAGE: ${{ needs.build-shardok.outputs.image_tag }}
ADMIN_IMAGE: ${{ needs.build-admin.outputs.image_tag }}
JFR_SIDECAR_IMAGE: ${{ needs.build-jfr-sidecar.outputs.image_tag }}
EAGLE_IMAGE: ${{ needs.build-all.outputs.eagle_image_tag }}
SHARDOK_IMAGE: ${{ needs.build-all.outputs.shardok_image_tag }}
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 }}
GPT_MODEL_NAME: ${{ secrets.GPT_MODEL_NAME }}
EAGLE_ENABLE_S3: ${{ secrets.EAGLE_ENABLE_S3 }}
@@ -443,6 +187,9 @@ jobs:
SHARDOK_ADDRESS: ${{ secrets.SHARDOK_ADDRESS }}
SHARDOK_AUTH_TOKEN: ${{ secrets.SHARDOK_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }}
FASTMAIL_FROM_EMAIL: ${{ secrets.FASTMAIL_FROM_EMAIL }}
FASTMAIL_FROM_NAME: ${{ secrets.FASTMAIL_FROM_NAME }}
DO_DROPLET_IP: ${{ secrets.DO_DROPLET_IP }}
DO_REGISTRY_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
steps:
@@ -456,11 +203,27 @@ jobs:
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -H "$DO_DROPLET_IP" >> ~/.ssh/known_hosts 2>/dev/null || true
- name: Build warmup tool
run: |
bazel build //src/main/go/net/eagle0/warmup:warmup_linux_amd64
mkdir -p scripts/bin
cp bazel-bin/src/main/go/net/eagle0/warmup/warmup_linux_amd64_/warmup_linux_amd64 scripts/bin/warmup
- name: Copy config files to droplet
run: |
scp -i ~/.ssh/deploy_key \
docker-compose.prod.yml nginx/nginx.conf \
deploy@"$DO_DROPLET_IP":/opt/eagle0/
# 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
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/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: |
@@ -486,113 +249,103 @@ jobs:
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}"
# Check Docker has IPv6 support for connecting to Hetzner Shardok
# (One-time setup: sudo tee /etc/docker/daemon.json <<< '{"ipv6": true, "ip6tables": true, "experimental": true, "fixed-cidr-v6": "fd00::/80"}' && sudo systemctl restart docker)
# 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."
echo "Run: sudo tee /etc/docker/daemon.json <<< '{\"ipv6\": true, \"ip6tables\": true, \"experimental\": true, \"fixed-cidr-v6\": \"fd00::/80\"}' && sudo systemctl restart docker"
fi
# Write env vars to .env file for docker-compose
# Preserve AUTH_IMAGE if it exists (managed by auth_build.yml)
EXISTING_AUTH_IMAGE=""
if [ -f .env ]; then
EXISTING_AUTH_IMAGE=\$(grep '^AUTH_IMAGE=' .env | cut -d= -f2- || true)
fi
rm -f .env 2>/dev/null || true
cat > .env << EOF
EAGLE_IMAGE=\${EAGLE_IMAGE}
SHARDOK_IMAGE=\${SHARDOK_IMAGE}
ADMIN_IMAGE=\${ADMIN_IMAGE}
JFR_SIDECAR_IMAGE=\${JFR_SIDECAR_IMAGE}
AUTH_IMAGE=\${EXISTING_AUTH_IMAGE:-registry.digitalocean.com/eagle0/auth-server:latest}
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:-}
SHARDOK_ADDRESS=\${SHARDOK_ADDRESS:-shardok:40042}
SHARDOK_AUTH_TOKEN=\${SHARDOK_AUTH_TOKEN:-}
SENTRY_DSN=\${SENTRY_DSN:-}
EOF
chmod 600 .env
# Update env vars
chmod +x deploy/update-env.sh
cd deploy && ./update-env.sh \
"EAGLE_IMAGE=\${EAGLE_IMAGE}" \
"SHARDOK_IMAGE=\${SHARDOK_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}" \
"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
# Use exact image tags passed from build jobs (no :latest fallback)
# Note: AUTH_IMAGE is managed separately by auth_build.yml
echo "Using images: \$EAGLE_IMAGE, \$SHARDOK_IMAGE, \$ADMIN_IMAGE, \$JFR_SIDECAR_IMAGE"
# Use crane to pull images (handles OCI format correctly) then load into Docker
# This avoids digest mismatch from DO registry's OCI->Docker format conversion
# Install crane for pulling OCI images
echo "Installing crane..."
rm -f 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
# crane uses Docker config for auth
echo "Pulling Eagle image with crane..."
./crane pull "\${EAGLE_IMAGE}" eagle.tar || { echo "ERROR: Failed to pull eagle image"; exit 1; }
echo "Loading Eagle image into Docker..."
docker load -i eagle.tar
rm eagle.tar
# Pull and load all images
echo "Pulling Eagle image..."
./crane pull "\${EAGLE_IMAGE}" eagle.tar && docker load -i eagle.tar && rm eagle.tar
echo "Pulling Shardok image with crane..."
./crane pull "\${SHARDOK_IMAGE}" shardok.tar || { echo "ERROR: Failed to pull shardok image"; exit 1; }
echo "Loading Shardok image into Docker..."
docker load -i shardok.tar
rm shardok.tar
echo "Pulling Shardok image..."
./crane pull "\${SHARDOK_IMAGE}" shardok.tar && docker load -i shardok.tar && rm shardok.tar
echo "Pulling Admin image with crane..."
./crane pull "\${ADMIN_IMAGE}" admin.tar || { echo "ERROR: Failed to pull admin image"; exit 1; }
echo "Loading Admin image into Docker..."
docker load -i admin.tar
rm admin.tar
echo "Pulling Admin image..."
./crane pull "\${ADMIN_IMAGE}" admin.tar && docker load -i admin.tar && rm admin.tar
echo "Pulling JFR Sidecar image with crane..."
./crane pull "\${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar || { echo "ERROR: Failed to pull jfr-sidecar image"; exit 1; }
echo "Loading JFR Sidecar image into Docker..."
docker load -i jfr-sidecar.tar
rm jfr-sidecar.tar
echo "Pulling JFR Sidecar image..."
./crane pull "\${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar && docker load -i jfr-sidecar.tar && rm jfr-sidecar.tar
rm ./crane
# Also pull other compose images
# Pull other compose images
docker pull nginx:alpine || true
docker pull certbot/certbot || true
echo "All images pulled successfully"
# Recreate only the services we're updating (not auth - managed by auth_build.yml)
# This prevents restarting auth service during every Eagle deploy
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle shardok admin jfr-sidecar
# Recreate non-Eagle services
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate shardok admin
# Ensure auth is running (but don't force-recreate it)
# Deploy Eagle with blue-green
if [ -x "/opt/eagle0/scripts/deploy-blue-green.sh" ]; then
echo "Using blue-green deployment for Eagle..."
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}" || {
echo "Blue-green failed, falling back to direct restart"
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle-blue
}
else
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle-blue
fi
# Start jfr-sidecar after eagle-blue
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate jfr-sidecar
# Ensure auth is running
docker compose -f docker-compose.prod.yml up -d auth
# Restart nginx to pick up new container IPs
# (nginx caches DNS at startup, so it needs restart after eagle/shardok)
# Restart nginx
docker compose -f docker-compose.prod.yml up -d --force-recreate nginx
# Cleanup orphaned containers
docker compose -f docker-compose.prod.yml up -d --remove-orphans
# Wait for health checks
# Verify
sleep 10
docker compose -f docker-compose.prod.yml ps
# Verify containers are using correct images
echo "=== Verifying container image tags ==="
docker compose -f docker-compose.prod.yml images
# Cleanup old images
# Cleanup
docker container prune -f
docker image prune -f
DEPLOY_SCRIPT
+34 -5
View File
@@ -10,6 +10,7 @@ on:
paths:
- ".github/workflows/installer_build.yml"
- "src/main/csharp/net/eagle0/clients/win/installer/**"
workflow_dispatch:
permissions:
contents: read
@@ -29,6 +30,19 @@ jobs:
with:
dotnet-version: '8.0.x'
- name: Inject manifest public key
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"
fi
- name: Restore dependencies
run: dotnet restore src/main/csharp/net/eagle0/clients/win/installer/EagleInstaller/EagleInstaller.csproj
@@ -68,15 +82,30 @@ jobs:
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
# Create installer manifest content
INSTALLER_SHA=$(sha256sum ./installer-output/EagleInstaller.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 manifest content ==="
cat /tmp/installer_manifest.txt
echo "=================================="
# Update the unified manifest
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- installer /tmp/installer_manifest.txt
# Write signing key to temp file (if available)
SIGNING_ARGS=""
if [ -n "$MANIFEST_SIGNING_KEY" ]; then
echo "$MANIFEST_SIGNING_KEY" > /tmp/manifest_signing_key
chmod 600 /tmp/manifest_signing_key
SIGNING_ARGS="/tmp/manifest_signing_key"
echo "Manifest signing key available"
else
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
# Cleanup
rm -f /tmp/manifest_signing_key
+154
View File
@@ -0,0 +1,154 @@
name: Mac Build
on:
push:
branches: [ "main" ]
paths:
- ".github/workflows/mac_build.yml"
- "src/main/csharp/net/eagle0/clients/unity/**"
- "src/main/proto/**"
- "src/main/go/net/eagle0/build/mac_build_handler/**"
- "scripts/build_protos.sh"
- "scripts/build_mac_plugin.sh"
- "scripts/inject_sparkle.sh"
- "scripts/codesign_mac_app.sh"
- "scripts/notarize_mac_app.sh"
- "ci/github_actions/build_mac.sh"
- "ci/github_actions/build_unity_mac.sh"
- "ci/mac/**"
pull_request:
paths:
- ".github/workflows/mac_build.yml"
- "src/main/csharp/net/eagle0/clients/unity/**"
- "src/main/go/net/eagle0/build/mac_build_handler/**"
- "scripts/build_mac_plugin.sh"
- "scripts/inject_sparkle.sh"
- "scripts/codesign_mac_app.sh"
- "scripts/notarize_mac_app.sh"
- "ci/github_actions/build_mac.sh"
- "ci/github_actions/build_unity_mac.sh"
- "ci/mac/**"
workflow_dispatch:
inputs:
skip_signing:
description: 'Skip code signing, notarization, and deploy (build only)'
required: false
default: 'false'
type: boolean
permissions:
contents: read
jobs:
mac-unity:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
with:
lfs: true
clean: false
fetch-depth: 0 # For version numbering from git history
- name: Pull LFS files
run: git lfs pull
- name: Restore Library/
run: ./ci/github_actions/restore_library.sh
- name: Build Mac Unity
run: ./ci/github_actions/build_unity_mac.sh "/tmp/eagle0/eagle0MAC"
- name: Persist Library/
run: ./ci/github_actions/persist_library.sh
- name: Inject Sparkle Framework
if: success()
env:
SPARKLE_EDDSA_PUBLIC_KEY: ${{ secrets.SPARKLE_EDDSA_PUBLIC_KEY }}
run: |
chmod +x ./scripts/inject_sparkle.sh
./scripts/inject_sparkle.sh "/tmp/eagle0/eagle0MAC/eagle0.app"
- name: Import Code Signing Certificate
if: success() && ((github.event_name == 'workflow_dispatch' && github.event.inputs.skip_signing != 'true') || (github.event_name == 'push' && github.ref == 'refs/heads/main'))
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
run: |
# Generate random keychain password (only used within this workflow run)
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> $GITHUB_ENV
# Decode certificate
echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
# Delete any existing keychain from previous runs
security delete-keychain build.keychain 2>/dev/null || true
# Create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
# Import certificate
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
# Allow codesign to access keychain
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
# Clean up
rm certificate.p12
- name: Code Sign App
if: success() && ((github.event_name == 'workflow_dispatch' && github.event.inputs.skip_signing != 'true') || (github.event_name == 'push' && github.ref == 'refs/heads/main'))
env:
SIGNING_IDENTITY: ${{ secrets.SIGNING_IDENTITY }}
run: |
chmod +x ./scripts/codesign_mac_app.sh
./scripts/codesign_mac_app.sh "/tmp/eagle0/eagle0MAC/eagle0.app" "ci/mac/eagle0.entitlements"
- name: Notarize App
if: success() && ((github.event_name == 'workflow_dispatch' && github.event.inputs.skip_signing != 'true') || (github.event_name == 'push' && github.ref == 'refs/heads/main'))
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
chmod +x ./scripts/notarize_mac_app.sh
./scripts/notarize_mac_app.sh "/tmp/eagle0/eagle0MAC/eagle0.app"
- name: Deploy Mac Build
if: success() && ((github.event_name == 'workflow_dispatch' && github.event.inputs.skip_signing != 'true') || (github.event_name == 'push' && github.ref == 'refs/heads/main'))
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
SPARKLE_EDDSA_PRIVATE_KEY: ${{ secrets.SPARKLE_EDDSA_PRIVATE_KEY }}
run: |
# Write private key to temp file for signing
SPARKLE_PRIVATE_KEY_PATH="/tmp/sparkle_private_key"
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)
bazel run //src/main/go/net/eagle0/build/mac_build_handler:mac_build_handler -- \
"/tmp/eagle0/eagle0MAC/eagle0.app" \
"$VERSION" \
"$BUILD_NUMBER" \
"$SPARKLE_PRIVATE_KEY_PATH"
rm "$SPARKLE_PRIVATE_KEY_PATH"
- name: Cleanup Keychain
if: always()
run: |
security delete-keychain build.keychain 2>/dev/null || true
- name: Archive Build Log
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: editor_mac.log
path: /tmp/eagle0/editor_mac.log
-29
View File
@@ -1,29 +0,0 @@
name: Mac History Editor Build
on:
push:
branches: [ "main" ]
paths:
- ".github/workflows/mac_history_build.yml"
- "src/main/swift/net/eagle0/EagleGameHistoryViewer/**"
- "src/main/protobuf/net/eagle0/eagle/**"
pull_request:
paths:
- ".github/workflows/mac_history_build.yml"
- "src/main/swift/net/eagle0/EagleGameHistoryViewer/**"
- "src/main/protobuf/net/eagle0/eagle/**"
permissions:
contents: read
jobs:
mac-history-build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
with:
lfs: false
clean: false
- name: Build the mac history
run: ./ci/github_actions/build_mac_history.sh
+18 -1
View File
@@ -63,7 +63,24 @@ jobs:
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- unity3d /tmp/unity_manifest.txt
MANIFEST_SIGNING_KEY: ${{ secrets.MANIFEST_SIGNING_KEY }}
run: |
# Write signing key to temp file (if available)
SIGNING_ARGS=""
if [ -n "$MANIFEST_SIGNING_KEY" ]; then
echo "$MANIFEST_SIGNING_KEY" > /tmp/manifest_signing_key
chmod 600 /tmp/manifest_signing_key
SIGNING_ARGS="/tmp/manifest_signing_key"
echo "Manifest signing key available"
else
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
# Cleanup
rm -f /tmp/manifest_signing_key
- name: Archive build log
if: success() || failure()
uses: actions/upload-artifact@v4
+1
View File
@@ -38,3 +38,4 @@ scripts/refresh_name_layers/refresh_name_layers.zip
api_keys.txt
src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/Packages/com.unity.dedicated-server/
node_modules/
+18
View File
@@ -1,5 +1,23 @@
# CLAUDE.md
## CRITICAL GIT RULES (NEVER VIOLATE)
**NEVER push directly to main/master.** No exceptions. Not for "small changes." Not for docs. Not ever.
**NEVER merge PRs.** You create PRs. The user merges them. No exceptions.
**ALWAYS use this workflow:**
1. Create a feature branch from origin/main
2. Commit to that branch
3. Create a PR with `gh pr create`
4. Wait for user to merge (DO NOT run `gh pr merge`)
If you catch yourself about to run `git push origin main` or `git push origin <branch>:main`, STOP. You are about to violate a critical rule. Create a PR instead.
If you catch yourself about to run `gh pr merge`, STOP. Only the user merges PRs.
---
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
+19 -12
View File
@@ -13,8 +13,8 @@ AWS_SDK_VERSION = "2.28.1"
# Core Build Tools
#
bazel_dep(name = "bazel_skylib", version = "1.8.1")
bazel_dep(name = "rules_pkg", version = "1.1.0")
bazel_dep(name = "bazel_skylib", version = "1.9.0")
bazel_dep(name = "rules_pkg", version = "1.2.0")
#
# Language Support - Scala
@@ -102,8 +102,8 @@ sysroot(
# Language Support - Go
#
bazel_dep(name = "rules_go", version = "0.56.1", repo_name = "io_bazel_rules_go")
bazel_dep(name = "gazelle", version = "0.45.0", repo_name = "bazel_gazelle")
bazel_dep(name = "rules_go", version = "0.59.0", repo_name = "io_bazel_rules_go")
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")
@@ -127,8 +127,8 @@ use_repo(
#
bazel_dep(name = "apple_support", version = "1.21.1", repo_name = "build_bazel_apple_support")
bazel_dep(name = "rules_apple", version = "3.16.1", repo_name = "build_bazel_rules_apple")
bazel_dep(name = "rules_swift", version = "2.3.1", repo_name = "build_bazel_rules_swift")
bazel_dep(name = "rules_apple", version = "4.3.3", repo_name = "build_bazel_rules_apple")
bazel_dep(name = "rules_swift", version = "2.4.0", repo_name = "build_bazel_rules_swift")
#
# Protocol Buffers & RPC
@@ -149,8 +149,8 @@ bazel_dep(name = "googletest", version = "1.17.0")
# Container Images (OCI)
#
bazel_dep(name = "rules_oci", version = "2.2.6")
bazel_dep(name = "aspect_bazel_lib", version = "2.16.0")
bazel_dep(name = "rules_oci", version = "2.2.7")
bazel_dep(name = "aspect_bazel_lib", version = "2.22.4")
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
@@ -186,7 +186,7 @@ use_repo(oci, "alpine_linux", "alpine_linux_linux_amd64", "eclipse_temurin_17",
# Java/Scala Dependencies
#
bazel_dep(name = "rules_jvm_external", version = "6.3")
bazel_dep(name = "rules_jvm_external", version = "6.9")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(
@@ -294,11 +294,15 @@ http_archive(
)
# Busybox static binary for Docker health checks (provides nc, wget, etc.)
# https://busybox.net/downloads/binaries/
# Primary: DigitalOcean Spaces (public, reliable)
# Fallback: busybox.net (can be unreliable/slow)
http_file(
name = "busybox_x86_64",
sha256 = "6e123e7f3202a8c1e9b1f94d8941580a25135382b99e8d3e34fb858bba311348",
urls = ["https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox"],
urls = [
"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/busybox/busybox-1.35.0-x86_64-linux-musl",
"https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox",
],
downloaded_file_path = "busybox",
executable = True,
)
@@ -306,7 +310,10 @@ http_file(
http_file(
name = "busybox_aarch64",
sha256 = "141adb1b625a6f44c4b114f76b4387b4ea4f7ab802b88eb40e0d2f6adcccb1c3",
urls = ["https://busybox.net/downloads/binaries/1.35.0-aarch64-linux-musl/busybox"],
urls = [
# TODO: Upload aarch64 binary to GitHub release when needed
"https://busybox.net/downloads/binaries/1.35.0-aarch64-linux-musl/busybox",
],
downloaded_file_path = "busybox",
executable = True,
)
+79 -22
View File
@@ -27,9 +27,9 @@
"https://bcr.bazel.build/modules/aspect_bazel_lib/1.40.3/MODULE.bazel": "668e6bcb4d957fc0e284316dba546b705c8d43c857f87119619ee83c4555b859",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.11.0/MODULE.bazel": "cb1ba9f9999ed0bc08600c221f532c1ddd8d217686b32ba7d45b0713b5131452",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.16.0/MODULE.bazel": "852f9ebbda017572a7c113a2434592dd3b2f55cd9a0faea3d4be5a09a59e4900",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "253d739ba126f62a5767d832765b12b59e9f8d2bc88cc1572f4a73e46eb298ca",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/source.json": "ffab9254c65ba945f8369297ad97ca0dec213d3adc6e07877e23a48624a8b456",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.4/MODULE.bazel": "a05cbd9bc16712a58dc27ffe0dceaefd0da59a9bd87a227379b2a934b26a39ab",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.4/source.json": "9780bc57f521968ee82b7c3e85b7d0c71518fb7ce83ed7a9e5077ce20923207b",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.2/MODULE.bazel": "780d1a6522b28f5edb7ea09630748720721dfe27690d65a2d33aa7509de77e07",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "491f8681205e31bb57892d67442ce448cda4f472a8e6b3dc062865e29a64f89c",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838",
@@ -57,12 +57,15 @@
"https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
"https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
"https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9",
"https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
"https://bcr.bazel.build/modules/bazel_features/1.34.0/MODULE.bazel": "e8475ad7c8965542e0c7aac8af68eb48c4af904be3d614b6aa6274c092c2ea1e",
"https://bcr.bazel.build/modules/bazel_features/1.38.0/MODULE.bazel": "f9b8a9c890ebd216b4049fd12a31d3c2602e3403c7af636b04fbbd7453edc9c9",
"https://bcr.bazel.build/modules/bazel_features/1.38.0/source.json": "31ba776c122b54a2885e23651642e32f087a87bf025465f8040751894b571277",
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
"https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b",
"https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
"https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d",
"https://bcr.bazel.build/modules/bazel_lib/3.0.0/source.json": "895f21909c6fba01d7c17914bb6c8e135982275a1b18cdaa4e62272217ef1751",
"https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
"https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
"https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
@@ -77,7 +80,8 @@
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb",
"https://bcr.bazel.build/modules/bazel_skylib/1.9.0/MODULE.bazel": "72997b29dfd95c3fa0d0c48322d05590418edef451f8db8db5509c57875fb4b7",
"https://bcr.bazel.build/modules/bazel_skylib/1.9.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0",
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.6/MODULE.bazel": "fd1f9432ca04c947e91b500df69ce7c5b6dbfe1bc45ab1820338205dae3383a6",
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.6/source.json": "5d68545f224904745a3cabd35aea6bc2b6cc5a78b7f49f3f69660eab2eeeb273",
"https://bcr.bazel.build/modules/boringssl/0.0.0-20211025-d4f1ab9/MODULE.bazel": "6ee6353f8b1a701fe2178e1d925034294971350b6d3ac37e67e5a7d463267834",
@@ -115,8 +119,8 @@
"https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a",
"https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0",
"https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel": "d1327ba0907d0275ed5103bfbbb13518f6c04955b402213319d0d6c0ce9839d4",
"https://bcr.bazel.build/modules/gazelle/0.45.0/MODULE.bazel": "ecd19ebe9f8e024e1ccffb6d997cc893a974bcc581f1ae08f386bdd448b10687",
"https://bcr.bazel.build/modules/gazelle/0.45.0/source.json": "111d182facc5f5e80f0b823d5f077b74128f40c3fd2eccc89a06f34191bd3392",
"https://bcr.bazel.build/modules/gazelle/0.47.0/MODULE.bazel": "b61bb007c4efad134aa30ee7f4a8e2a39b22aa5685f005edaa022fbd1de43ebc",
"https://bcr.bazel.build/modules/gazelle/0.47.0/source.json": "aeb2e5df14b7fb298625d75d08b9c65bdb0b56014c5eb89da9e5dd0572280ae6",
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
"https://bcr.bazel.build/modules/google_benchmark/1.8.4/MODULE.bazel": "c6d54a11dcf64ee63545f42561eda3fd94c1b5f5ebe1357011de63ae33739d5e",
"https://bcr.bazel.build/modules/google_benchmark/1.8.5/MODULE.bazel": "9ba9b31b984022828a950e3300410977eda2e35df35584c6b0b2d0c2e52766b7",
@@ -176,6 +180,7 @@
"https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/MODULE.bazel": "b3925269f63561b8b880ae7cf62ccf81f6ece55b62cd791eda9925147ae116ec",
"https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/source.json": "da1cb1add160f5e5074b7272e9db6fd8f1b3336c15032cd0a653af9d2f484aed",
"https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92",
"https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83",
"https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4",
"https://bcr.bazel.build/modules/package_metadata/0.0.5/source.json": "2326db2f6592578177751c3e1f74786b79382cd6008834c9d01ec865b9126a85",
"https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
@@ -229,9 +234,9 @@
"https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8",
"https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e",
"https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a",
"https://bcr.bazel.build/modules/rules_apple/3.16.1/MODULE.bazel": "8294474defa70af2534a558ab905c083d69203344145e6f7d544d5098611ec7d",
"https://bcr.bazel.build/modules/rules_apple/3.16.1/source.json": "9190fd9d34a5d048bfbba8a530a57f2c2bf3f61e5634a9ab0b6ab005458857f9",
"https://bcr.bazel.build/modules/rules_apple/3.5.1/MODULE.bazel": "3d1bbf65ad3692003d36d8a29eff54d4e5c1c5f4bfb60f79e28646a924d9101c",
"https://bcr.bazel.build/modules/rules_apple/4.3.3/MODULE.bazel": "c5c2c4adeeac5f3f2f9b7f16abfa8be7ffefa596171d0d92bed4cae9ade0a498",
"https://bcr.bazel.build/modules/rules_apple/4.3.3/source.json": "3cb1d69c8243ffcc42ecbf84ae8b9cccd7b1e2f091b0aee5a3e9c9a45267f312",
"https://bcr.bazel.build/modules/rules_buf/0.1.1/MODULE.bazel": "6189aec18a4f7caff599ad41b851ab7645d4f1e114aa6431acf9b0666eb92162",
"https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
"https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002",
@@ -246,6 +251,8 @@
"https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
"https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5",
"https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513",
"https://bcr.bazel.build/modules/rules_cc/0.1.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0",
"https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8",
"https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8",
"https://bcr.bazel.build/modules/rules_cc/0.2.14/source.json": "55d0a4587c5592fad350f6e698530f4faf0e7dd15e69d43f8d87e220c78bea54",
"https://bcr.bazel.build/modules/rules_foreign_cc/0.10.1/MODULE.bazel": "b9527010e5fef060af92b6724edb3691970a5b1f76f74b21d39f7d433641be60",
@@ -263,8 +270,8 @@
"https://bcr.bazel.build/modules/rules_go/0.48.0/MODULE.bazel": "d00ebcae0908ee3f5e6d53f68677a303d6d59a77beef879598700049c3980a03",
"https://bcr.bazel.build/modules/rules_go/0.50.1/MODULE.bazel": "b91a308dc5782bb0a8021ad4330c81fea5bda77f96b9e4c117b9b9c8f6665ee0",
"https://bcr.bazel.build/modules/rules_go/0.53.0/MODULE.bazel": "a4ed760d3ac0dbc0d7b967631a9a3fd9100d28f7d9fcf214b4df87d4bfff5f9a",
"https://bcr.bazel.build/modules/rules_go/0.56.1/MODULE.bazel": "d5b835c548ac917345f1780cd2da52edc1130a908fe091c92096895303ae78a0",
"https://bcr.bazel.build/modules/rules_go/0.56.1/source.json": "0c902f7272e8d4e47e459af97be472bc19dadbbe6023a0719d1adce8483ac75a",
"https://bcr.bazel.build/modules/rules_go/0.59.0/MODULE.bazel": "b7e43e7414a3139a7547d1b4909b29085fbe5182b6c58cbe1ed4c6272815aeae",
"https://bcr.bazel.build/modules/rules_go/0.59.0/source.json": "1df17bb7865cfc029492c30163cee891d0dd8658ea0d5bfdf252c4b6db5c1ef6",
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
"https://bcr.bazel.build/modules/rules_java/5.1.0/MODULE.bazel": "324b6478b0343a3ce7a9add8586ad75d24076d6d43d2f622990b9c1cfd8a1b15",
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
@@ -292,7 +299,8 @@
"https://bcr.bazel.build/modules/rules_jvm_external/6.0/MODULE.bazel": "37c93a5a78d32e895d52f86a8d0416176e915daabd029ccb5594db422e87c495",
"https://bcr.bazel.build/modules/rules_jvm_external/6.1/MODULE.bazel": "75b5fec090dbd46cf9b7d8ea08cf84a0472d92ba3585b476f44c326eda8059c4",
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0",
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/source.json": "6f5f5a5a4419ae4e37c35a5bb0a6ae657ed40b7abc5a5189111b47fcebe43197",
"https://bcr.bazel.build/modules/rules_jvm_external/6.9/MODULE.bazel": "07c5db05527db7744a54fcffd653e1550d40e0540207a7f7e6d0a4de5bef8274",
"https://bcr.bazel.build/modules/rules_jvm_external/6.9/source.json": "b12970214f3cc144b26610caeb101fa622d910f1ab3d98f0bae1058edbd00bd4",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5",
@@ -306,12 +314,12 @@
"https://bcr.bazel.build/modules/rules_nodejs/6.3.0/MODULE.bazel": "45345e4aba35dd6e4701c1eebf5a4e67af4ed708def9ebcdc6027585b34ee52d",
"https://bcr.bazel.build/modules/rules_nodejs/6.3.3/MODULE.bazel": "b66eadebd10f1f1b25f52f95ab5213a57e82c37c3f656fcd9a57ad04d2264ce7",
"https://bcr.bazel.build/modules/rules_nodejs/6.3.3/source.json": "45bd343155bdfed2543f0e39b80ff3f6840efc31975da4b5795797f4c94147ad",
"https://bcr.bazel.build/modules/rules_oci/2.2.6/MODULE.bazel": "2ba6ddd679269e00aeffe9ca04faa2d0ca4129650982c9246d0d459fe2da47d9",
"https://bcr.bazel.build/modules/rules_oci/2.2.6/source.json": "94e7decb8f95d9465b0bbea71c65064cd16083be1350c7468f131818641dc4a5",
"https://bcr.bazel.build/modules/rules_oci/2.2.7/MODULE.bazel": "f6150e4b224d459f7f6523ef65967464ca4efdd266c7fbf2f5a2a51011957e0c",
"https://bcr.bazel.build/modules/rules_oci/2.2.7/source.json": "b099f02af330f47f19dc67fc9300ef6e1937a8c86882690db0e7a2fcea8c7f6b",
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
"https://bcr.bazel.build/modules/rules_pkg/1.1.0/MODULE.bazel": "9db8031e71b6ef32d1846106e10dd0ee2deac042bd9a2de22b4761b0c3036453",
"https://bcr.bazel.build/modules/rules_pkg/1.1.0/source.json": "fef768df13a92ce6067e1cd0cdc47560dace01354f1d921cfb1d632511f7d608",
"https://bcr.bazel.build/modules/rules_pkg/1.2.0/MODULE.bazel": "c7db3c2b407e673c7a39e3625dc05dc9f12d6682cbd82a3a5924a13b491eda7e",
"https://bcr.bazel.build/modules/rules_pkg/1.2.0/source.json": "9062e00845bf91a4247465d371baa837adf9b6ff44c542f73ba084f07667e1dc",
"https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
"https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483",
@@ -334,7 +342,8 @@
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
"https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7",
"https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43",
"https://bcr.bazel.build/modules/rules_python/1.0.0/source.json": "b0162a65c6312e45e7912e39abd1a7f8856c2c7e41ecc9b6dc688a6f6400a917",
"https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13",
"https://bcr.bazel.build/modules/rules_python/1.3.0/source.json": "25932f917cd279c7baefa6cb1d3fa8750a7a29de522024449b19af6eab51f4a0",
"https://bcr.bazel.build/modules/rules_rust/0.45.1/MODULE.bazel": "a69d0db3a958fab2c6520961e1b2287afcc8b36690fd31bbc4f6f7391397150d",
"https://bcr.bazel.build/modules/rules_scala/7.1.1/MODULE.bazel": "b1f80c52ae49b27d41b9291d8b328b69247de2b7596d35d09afe6147b82cf562",
"https://bcr.bazel.build/modules/rules_scala/7.1.1/source.json": "5038cb231d4020c5965c920681cf961a7bf137b40315025e40f3a7b6a0ac1f0f",
@@ -345,8 +354,8 @@
"https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca",
"https://bcr.bazel.build/modules/rules_swift/1.18.0/MODULE.bazel": "a6aba73625d0dc64c7b4a1e831549b6e375fbddb9d2dde9d80c9de6ec45b24c9",
"https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046",
"https://bcr.bazel.build/modules/rules_swift/2.3.1/MODULE.bazel": "0b42093600d9226bcbdb31fb86d25d4204293d716fdbb2e50a1852547032a660",
"https://bcr.bazel.build/modules/rules_swift/2.3.1/source.json": "87d28609c37d2061db2f6fc3aae8ab7fbda9adf556cd88fbd0c7d520b8d81391",
"https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd",
"https://bcr.bazel.build/modules/rules_swift/2.4.0/source.json": "a6577f57f9febbdc015a01f2a8f3487422032f134d6c61d18ed8e8ca3b9acc7c",
"https://bcr.bazel.build/modules/stardoc/0.5.0/MODULE.bazel": "f9f1f46ba8d9c3362648eea571c6f9100680efc44913618811b58cc9c02cd678",
"https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
"https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
@@ -360,6 +369,7 @@
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91",
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb",
"https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468",
"https://bcr.bazel.build/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351",
"https://bcr.bazel.build/modules/tar.bzl/0.6.0/MODULE.bazel": "a3584b4edcfafcabd9b0ef9819808f05b372957bbdff41601429d5fd0aac2e7c",
"https://bcr.bazel.build/modules/tar.bzl/0.6.0/source.json": "4a620381df075a16cb3a7ed57bd1d05f7480222394c64a20fa51bdb636fda658",
"https://bcr.bazel.build/modules/toolchains_llvm/1.6.0/MODULE.bazel": "39603859cafb1c6830160fcd6370552e836790e6abb2bfb8d13bff53c0c10a64",
@@ -386,7 +396,7 @@
"@@apple_support~//crosstool:setup.bzl%apple_cc_configure_extension": {
"general": {
"bzlTransitiveDigest": "Z3yAd66IJL0GAZUTSeMOjoHiE1SZPPwiIs/XQui5BvE=",
"usagesDigest": "TOb4CUri5UsTKxgIDTNzR0ddIc21eYLCRIm+jqQmjlg=",
"usagesDigest": "tl3VVeQX3Hzh7FhM2gjnkCwEJpRMlY5S6a850WY/xvc=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -413,7 +423,7 @@
},
"@@aspect_rules_esbuild~//esbuild:extensions.bzl%esbuild": {
"general": {
"bzlTransitiveDigest": "8jv3p0xDR/oitFeH8y0+Y5xlyrUbfsTRlc9TSwYkwl8=",
"bzlTransitiveDigest": "RzTo7pj0Tdkwa3Ld5BAVhGFSvWroST8GAQLuwwC0jdQ=",
"usagesDigest": "iDVoyPxUeADmfK8ssoyG3Ehq1bj6p7A43LpEiE266os=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
@@ -521,6 +531,11 @@
}
},
"recordedRepoMappingEntries": [
[
"aspect_bazel_lib~",
"bazel_lib",
"bazel_lib~"
],
[
"aspect_bazel_lib~",
"bazel_skylib",
@@ -555,6 +570,11 @@
"aspect_rules_js~",
"bazel_tools",
"bazel_tools"
],
[
"bazel_lib~",
"bazel_tools",
"bazel_tools"
]
]
}
@@ -1292,8 +1312,8 @@
},
"@@rules_oci~//oci:extensions.bzl%oci": {
"general": {
"bzlTransitiveDigest": "FaY+7xb13bB3hmxqwAWaGp3Tf3Q4Nfdlr+F38CP5mcg=",
"usagesDigest": "39yHQmifPoGf+JSYpnQSnJGugxbrFVge/+ENmUiqZ6M=",
"bzlTransitiveDigest": "AOLP47LtVHSKSDiukosQymx543OwcgeoQP666wwuj3o=",
"usagesDigest": "3Xsv1/UEV8MOARW4BZScPw3Gxtx19OQ5EXOqcL1p9bI=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -1580,6 +1600,43 @@
]
}
},
"@@rules_python~//python/uv:uv.bzl%uv": {
"general": {
"bzlTransitiveDigest": "Xpqjnjzy6zZ90Es9Wa888ZLHhn7IsNGbph/e6qoxzw8=",
"usagesDigest": "qI5PVlIum/YAnGJg5oXGHzDkMFWt2aNSUZY4G8PBbic=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"uv": {
"bzlFile": "@@rules_python~//python/uv/private:uv_toolchains_repo.bzl",
"ruleClassName": "uv_toolchains_repo",
"attributes": {
"toolchain_type": "'@@rules_python~//python/uv:uv_toolchain_type'",
"toolchain_names": [
"none"
],
"toolchain_implementations": {
"none": "'@@rules_python~//python:none'"
},
"toolchain_compatible_with": {
"none": [
"@platforms//:incompatible"
]
},
"toolchain_target_settings": {}
}
}
},
"recordedRepoMappingEntries": [
[
"rules_python~",
"platforms",
"platforms"
]
]
}
},
"@@rules_scala~//scala/extensions:config.bzl%scala_config": {
"general": {
"bzlTransitiveDigest": "TdBxhkZTM7VU6teIFS+KoonKU7wmb5BL7leCWWx7yX8=",
@@ -5056,7 +5113,7 @@
"@@rules_swift~//swift:extensions.bzl%non_module_deps": {
"general": {
"bzlTransitiveDigest": "PAIMhc1bVKfcyoHeg0xO8LMS9KN5yzbsMGwa5O2ifJM=",
"usagesDigest": "A3fzk5iHsrLdI3PokT1bHIdeJ2j9tc09H3/3Old6IfU=",
"usagesDigest": "l2vIL7SL4tJqHIVLWd78Y/ym+r0II64lmvoX8o+0Bb0=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
+24
View File
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
set -euxo pipefail
. ./ci/unity_version.sh
WORKSPACE=$(pwd)
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
BUILD_DIR=$1
LOG_PATH=$2
echo "Building Mac in $BUILD_DIR"
echo "Cleaning up $BUILD_DIR"
/bin/rm -rf "$BUILD_DIR"
/bin/mkdir -p "$BUILD_DIR"
${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
-nographics \
-batchmode \
-quit \
-buildOSXUniversalPlayer "$BUILD_DIR/eagle0.app" \
-logFile "$LOG_PATH" \
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
+19
View File
@@ -0,0 +1,19 @@
#!/bin/bash
set -euxo pipefail
COMMIT=$(/usr/bin/git rev-parse --short HEAD)
/bin/echo "build protos"
./scripts/build_protos.sh
/bin/echo "build Mac plugin"
./scripts/build_mac_plugin.sh
git log -3
/bin/echo "build Mac"
LOG_PATH="/tmp/eagle0/editor_mac.log"
BUILD_DIR=$1
./ci/github_actions/build_mac.sh "$BUILD_DIR" "$LOG_PATH"
+16 -2
View File
@@ -1,6 +1,20 @@
#!/bin/bash
set -euxo pipefail
set -uxo pipefail
/bin/echo "persist Library/"
/usr/bin/rsync -rtlDvq src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ /tmp/eagle0/Library/
# rsync may exit with code 23 ("partial transfer due to error") if Unity's
# temporary files vanish during the copy. This is acceptable for a cache.
/usr/bin/rsync -rtlDvq src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ /tmp/eagle0/Library/
rsync_exit=$?
if [ $rsync_exit -eq 0 ]; then
exit 0
elif [ $rsync_exit -eq 23 ]; then
echo "Warning: rsync exited with 23 (some files vanished during copy). This is expected for Unity temp files."
exit 0
else
echo "Error: rsync failed with exit code $rsync_exit"
exit $rsync_exit
fi
+18
View File
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Allow JIT compilation (required for Mono/IL2CPP) -->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<!-- Allow unsigned executable memory (required for Unity) -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<!-- Disable library validation (required for plugins) -->
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
<!-- Allow outgoing network connections -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
+40
View File
@@ -0,0 +1,40 @@
# 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)
EAGLE_IMAGE=registry.digitalocean.com/eagle0/eagle-server:latest
SHARDOK_IMAGE=registry.digitalocean.com/eagle0/shardok-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
SHARDOK_ADDRESS=shardok:40042
SHARDOK_AUTH_TOKEN=
# Monitoring
SENTRY_DSN=
# Email (Fastmail JMAP)
FASTMAIL_API_TOKEN=
FASTMAIL_FROM_EMAIL=
FASTMAIL_FROM_NAME=
+59
View File
@@ -0,0 +1,59 @@
#!/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
if grep -q "^${KEY}=" "$ENV_FILE" 2>/dev/null; then
# Key exists, update it
sed -i "s|^${KEY}=.*|${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"
+64 -8
View File
@@ -8,9 +8,13 @@
# Run: docker compose -f docker-compose.prod.yml up -d
services:
eagle:
# Blue-green deployment: eagle-blue is the primary (production) instance
# eagle-green is the staging instance for zero-downtime deployments
# See scripts/deploy-blue-green.sh for deployment workflow
eagle-blue:
image: ${EAGLE_IMAGE:-registry.digitalocean.com/eagle0/eagle-server:latest}
container_name: eagle-server
container_name: eagle-blue
command:
- "--gpt-model-name"
- "${GPT_MODEL_NAME:-gpt-5.1}"
@@ -39,7 +43,7 @@ services:
volumes:
- ./saves:/app/saves # Game saves and user database
- ./archived:/app/archived # Archived completed games
- ./jfr:/app/jfr # JFR recordings - dump with: docker exec eagle-server jcmd 1 JFR.dump filename=/app/jfr/profile.jfr
- ./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
depends_on:
@@ -58,6 +62,58 @@ services:
retries: 3
start_period: 30s
eagle-green:
image: ${EAGLE_IMAGE_NEW:-registry.digitalocean.com/eagle0/eagle-server:latest}
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}"
- "--auth-service-url"
- "auth:40033"
ports:
- "40034:40032" # Different host port for staging
environment:
OPENAI_API_KEY: "${OPENAI_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:-}"
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
EAGLE_SAVE_DIR: "/app/saves"
EAGLE_ARCHIVE_DIR: "/app/archived"
SENTRY_DSN: "${SENTRY_DSN:-}"
SENTRY_ENVIRONMENT: "production"
volumes:
- ./saves:/app/saves # Same save directory as blue
- ./archived:/app/archived # Same archive directory as blue
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
depends_on:
- shardok
- auth
restart: "no" # Don't auto-restart during deployment
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
healthcheck:
test: ["CMD-SHELL", "nc -z localhost 40032 || exit 1"]
interval: 10s
timeout: 5s
retries: 6
start_period: 60s
# Backward compatibility alias - for scripts that reference 'eagle' service
eagle:
extends:
service: eagle-blue
auth:
image: ${AUTH_IMAGE:-registry.digitalocean.com/eagle0/auth-server:latest}
container_name: auth-server
@@ -140,7 +196,7 @@ services:
- ./certbot/www:/var/www/certbot:ro
- ./auth:/etc/nginx/auth:ro
depends_on:
- eagle
- eagle-blue
- admin
restart: unless-stopped
logging:
@@ -154,7 +210,7 @@ services:
container_name: admin-server
command:
- "--eagle-addr"
- "eagle:40032"
- "${EAGLE_ADDR:-eagle-blue:40032}" # Can be switched for blue-green
- "--auth-addr"
- "auth:40033"
- "--jfr-sidecar-addr"
@@ -163,7 +219,7 @@ services:
- "8080"
# No external port - accessed via nginx at admin.eagle0.net
depends_on:
- eagle
- eagle-blue
- auth
- jfr-sidecar
restart: unless-stopped
@@ -183,11 +239,11 @@ services:
image: ${JFR_SIDECAR_IMAGE:-registry.digitalocean.com/eagle0/jfr-sidecar:latest}
container_name: jfr-sidecar
# Share PID namespace with Eagle to access its JVM via jcmd
pid: "service:eagle"
pid: "service:eagle-blue"
volumes:
- jvm-tmp:/tmp # Shared with Eagle for JVM attach socket files
depends_on:
- eagle
- eagle-blue
restart: unless-stopped
logging:
driver: "json-file"
+18 -8
View File
@@ -24,10 +24,13 @@ http {
# This prevents stale IP caching when containers restart
resolver 127.0.0.11 valid=10s ipv6=off;
# Upstream for Eagle gRPC server
upstream eagle_grpc {
server eagle:40032;
keepalive 100;
# Eagle backend address - configured via variable for blue-green deployments
# Using a variable makes nginx resolve the hostname at request time, not config load time
# This allows nginx to reload even when the backend is temporarily down
# For blue-green deployments, this is switched between eagle-blue:40032 and eagle-green:40032
# by scripts/deploy-blue-green.sh, then nginx is reloaded with 'nginx -s reload'
map $host $eagle_backend {
default "eagle-blue:40032";
}
# HTTP server for Let's Encrypt challenge and redirect
@@ -71,8 +74,8 @@ http {
# Rate limiting
limit_req zone=grpc_limit burst=50 nodelay;
# gRPC proxy
grpc_pass grpc://eagle_grpc;
# gRPC proxy - uses variable for blue-green deployment support
grpc_pass grpc://$eagle_backend;
# Timeouts for long-running streams
grpc_read_timeout 1200s;
@@ -88,8 +91,8 @@ http {
# Rate limiting
limit_req zone=grpc_limit burst=50 nodelay;
# gRPC proxy
grpc_pass grpc://eagle_grpc;
# gRPC proxy - uses variable for blue-green deployment support
grpc_pass grpc://$eagle_backend;
# Timeouts
grpc_read_timeout 30s;
@@ -106,6 +109,13 @@ http {
proxy_set_header X-Real-IP $remote_addr;
}
# Invitation landing page (proxied to Go auth service)
location /invite/ {
proxy_pass http://auth:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Health check endpoint
location /health {
access_log off;
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
#
# Code sign a macOS .app bundle for distribution
# Usage: codesign_mac_app.sh <app_path> [entitlements_path]
#
# Environment variables:
# SIGNING_IDENTITY - The signing identity (default: "Developer ID Application")
# KEYCHAIN_PASSWORD - Password to unlock the build keychain (optional)
set -euxo pipefail
APP_PATH="$1"
ENTITLEMENTS_PATH="${2:-}"
SIGNING_IDENTITY="${SIGNING_IDENTITY:-Developer ID Application}"
if [ ! -d "$APP_PATH" ]; then
echo "ERROR: App not found at $APP_PATH"
exit 1
fi
# Unlock keychain if password provided
if [ -n "${KEYCHAIN_PASSWORD:-}" ]; then
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain || true
fi
echo "=== Signing nested components first ==="
# Sign all dylibs
find "$APP_PATH" -name "*.dylib" -print0 | while IFS= read -r -d '' item; do
echo "Signing dylib: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign all bundles (plugins)
find "$APP_PATH" -name "*.bundle" -print0 | while IFS= read -r -d '' item; do
echo "Signing bundle: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign XPC services (inside Sparkle framework)
find "$APP_PATH" -name "*.xpc" -print0 | while IFS= read -r -d '' item; do
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)
find "$APP_PATH" -path "*/Frameworks/*.app" -print0 | while IFS= read -r -d '' item; do
echo "Signing nested app: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign standalone executables inside frameworks (like Autoupdate)
find "$APP_PATH" -path "*/Frameworks/*/Versions/*/Autoupdate" -type f -print0 | while IFS= read -r -d '' item; do
echo "Signing executable: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign all frameworks (after their contents are signed)
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"
done
echo "=== Signing main app bundle ==="
if [ -n "$ENTITLEMENTS_PATH" ] && [ -f "$ENTITLEMENTS_PATH" ]; then
echo "Using entitlements: $ENTITLEMENTS_PATH"
codesign --force --verify --verbose --timestamp --options runtime \
--entitlements "$ENTITLEMENTS_PATH" \
--sign "$SIGNING_IDENTITY" "$APP_PATH"
else
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$APP_PATH"
fi
echo "=== Verifying signature ==="
codesign --verify --verbose=4 "$APP_PATH"
echo "=== Checking Gatekeeper assessment ==="
spctl --assess --type exec -v "$APP_PATH" || echo "Note: Gatekeeper may reject until notarized"
echo "Code signing complete: $APP_PATH"
+247
View File
@@ -0,0 +1,247 @@
#!/bin/bash
#
# Blue-Green Deployment Script for Eagle Server
#
# This script performs a zero-downtime deployment by:
# 1. Starting the new version on a staging port (green)
# 2. Waiting for it to become healthy
# 3. Running warmup traffic to pre-heat the JIT
# 4. Stopping the old version (blue) - which flushes state to disk
# 5. Telling green to reload games from disk
# 6. Switching nginx to route traffic to green
# 7. Cleaning up
#
# Usage: ./deploy-blue-green.sh [NEW_IMAGE_TAG]
#
# Example:
# ./deploy-blue-green.sh latest
# ./deploy-blue-green.sh sha-abc123
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
APP_DIR="${APP_DIR:-/opt/eagle0}"
NGINX_CONF="${APP_DIR}/nginx/nginx.conf"
COMPOSE_FILE="${APP_DIR}/docker-compose.prod.yml"
WARMUP_SCRIPT="${SCRIPT_DIR}/warmup-eagle.sh"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Determine which instance is currently active
get_active_instance() {
if grep -q "eagle-blue:40032" "${NGINX_CONF}"; then
echo "blue"
elif grep -q "eagle-green:40032" "${NGINX_CONF}"; then
echo "green"
else
log_error "Cannot determine active instance from nginx config"
exit 1
fi
}
# Pull image with retry using crane (handles OCI/Docker digest mismatch)
pull_with_retry() {
local image=$1
local max_attempts=${2:-3}
local attempt=1
# Use crane if available (handles OCI format correctly)
if [ -x "${APP_DIR}/crane" ]; then
while [ $attempt -le $max_attempts ]; do
log_info "Pulling image with crane (attempt ${attempt}/${max_attempts})..."
if "${APP_DIR}/crane" pull "${image}" /tmp/image.tar && docker load -i /tmp/image.tar; then
rm -f /tmp/image.tar
log_info "Image pulled and loaded successfully"
return 0
fi
rm -f /tmp/image.tar
log_warn "Pull failed, retrying in 5 seconds..."
sleep 5
attempt=$((attempt + 1))
done
else
# Fallback to docker pull if crane not available
log_warn "crane not found at ${APP_DIR}/crane, falling back to docker pull"
while [ $attempt -le $max_attempts ]; do
log_info "Pulling image (attempt ${attempt}/${max_attempts})..."
if docker pull "${image}"; then
log_info "Image pulled successfully"
return 0
fi
log_warn "Pull failed, retrying in 5 seconds..."
sleep 5
attempt=$((attempt + 1))
done
fi
log_error "Failed to pull image after ${max_attempts} attempts"
return 1
}
# Wait for a container to be healthy
wait_for_healthy() {
local container=$1
local max_attempts=${2:-60}
local attempt=1
log_info "Waiting for ${container} to become healthy..."
while [ $attempt -le $max_attempts ]; do
health=$(docker inspect --format='{{.State.Health.Status}}' "${container}" 2>/dev/null || echo "unknown")
if [ "$health" = "healthy" ]; then
log_info "${container} is healthy"
return 0
fi
echo -n "."
sleep 2
attempt=$((attempt + 1))
done
echo ""
log_error "${container} did not become healthy after $((max_attempts * 2)) seconds"
return 1
}
# Main deployment logic
main() {
local new_tag="${1:-latest}"
local registry="registry.digitalocean.com/eagle0/eagle-server"
local new_image="${registry}:${new_tag}"
log_info "Starting blue-green deployment"
log_info "New image: ${new_image}"
cd "${APP_DIR}"
# Determine current active instance
local active=$(get_active_instance)
local staging
if [ "$active" = "blue" ]; then
staging="green"
else
staging="blue"
fi
log_info "Active instance: eagle-${active}"
log_info "Staging instance: eagle-${staging}"
# Pull the new image (with retry for intermittent registry issues)
if ! pull_with_retry "${new_image}" 3; then
log_error "Failed to pull new image, aborting deployment"
exit 1
fi
# Start staging instance with new image
log_info "Starting eagle-${staging} with new image..."
if [ "$staging" = "green" ]; then
EAGLE_IMAGE_NEW="${new_image}" docker compose -f "${COMPOSE_FILE}" --profile blue-green up -d eagle-green
else
EAGLE_IMAGE="${new_image}" docker compose -f "${COMPOSE_FILE}" up -d eagle-blue
fi
# Wait for staging to be healthy
if ! wait_for_healthy "eagle-${staging}" 90; then
log_error "Staging instance failed health check, aborting deployment"
docker compose -f "${COMPOSE_FILE}" stop "eagle-${staging}"
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${staging}"
exit 1
fi
# Run warmup/smoke test
local staging_port
if [ "$staging" = "green" ]; then
staging_port=40034
else
staging_port=40032
fi
log_info "Running warmup against eagle-${staging}..."
if [ -x "${WARMUP_SCRIPT}" ]; then
if ! "${WARMUP_SCRIPT}" "localhost:${staging_port}"; then
log_error "Warmup/smoke test failed, aborting deployment"
docker compose -f "${COMPOSE_FILE}" stop "eagle-${staging}"
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${staging}"
exit 1
fi
else
log_warn "Warmup script not found at ${WARMUP_SCRIPT}, skipping warmup"
log_warn "JIT will be cold on first requests"
fi
# Stop the active instance (this flushes state to disk)
log_info "Stopping eagle-${active} (flushing state to disk)..."
docker compose -f "${COMPOSE_FILE}" stop "eagle-${active}"
# Tell staging to reload games from disk
log_info "Telling eagle-${staging} to reload games from disk..."
if command -v grpcurl &> /dev/null; then
grpcurl -plaintext -d '{}' "localhost:${staging_port}" net.eagle0.eagle.api.Eagle/ReloadGames || true
else
log_warn "grpcurl not installed, skipping game reload"
log_warn "New instance will use games loaded at startup"
fi
# Switch nginx upstream
log_info "Switching nginx upstream to eagle-${staging}..."
if [ "$staging" = "green" ]; then
sed -i.bak 's/eagle-blue:40032/eagle-green:40032/g' "${NGINX_CONF}"
else
sed -i.bak 's/eagle-green:40032/eagle-blue:40032/g' "${NGINX_CONF}"
fi
# Reload nginx
log_info "Reloading nginx..."
docker compose -f "${COMPOSE_FILE}" exec nginx nginx -s reload
# Clean up old instance
log_info "Removing old eagle-${active} container..."
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${active}"
# Update the staging instance's restart policy and image var
# For blue, we need to update EAGLE_IMAGE; for green, update EAGLE_IMAGE_NEW
if [ "$staging" = "blue" ]; then
log_info "Updating EAGLE_IMAGE to ${new_image} for future restarts"
# User should update their .env file
else
log_info "Green is now active. Consider switching to blue on next deployment."
fi
log_info "Deployment complete!"
log_info "Active instance: eagle-${staging}"
log_info ""
log_info "Note: Update your .env file with EAGLE_IMAGE=${new_image}"
log_info " if you want future 'docker compose up' to use this version."
}
# Check for required tools
check_requirements() {
if ! command -v docker &> /dev/null; then
log_error "docker is required but not installed"
exit 1
fi
if ! command -v sed &> /dev/null; then
log_error "sed is required but not installed"
exit 1
fi
if [ ! -f "${NGINX_CONF}" ]; then
log_error "nginx config not found at ${NGINX_CONF}"
exit 1
fi
if [ ! -f "${COMPOSE_FILE}" ]; then
log_error "docker-compose file not found at ${COMPOSE_FILE}"
exit 1
fi
}
# Run
check_requirements
main "$@"
+41
View File
@@ -0,0 +1,41 @@
// +build ignore
// Script to generate Ed25519 key pair for manifest signing.
// Run with: go run scripts/generate_manifest_keys.go
//
// This will output:
// - Private key (base64): Store as MANIFEST_SIGNING_KEY GitHub secret
// - Public key (base64): Embed in EagleInstaller for verification
package main
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"fmt"
"log"
)
func main() {
// Generate Ed25519 key pair
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
log.Fatalf("Failed to generate key pair: %v", err)
}
// Encode keys as base64
privateKeyB64 := base64.StdEncoding.EncodeToString(privateKey)
publicKeyB64 := base64.StdEncoding.EncodeToString(publicKey)
fmt.Println("=== Ed25519 Key Pair for Manifest Signing ===")
fmt.Println()
fmt.Println("PRIVATE KEY (store as GitHub secret MANIFEST_SIGNING_KEY):")
fmt.Println(privateKeyB64)
fmt.Println()
fmt.Println("PUBLIC KEY (embed in EagleInstaller.cs for verification):")
fmt.Println(publicKeyB64)
fmt.Println()
fmt.Printf("Private key size: %d bytes\n", len(privateKey))
fmt.Printf("Public key size: %d bytes\n", len(publicKey))
}
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env bash
#
# Inject Sparkle framework into a macOS .app bundle for auto-updates
# Usage: inject_sparkle.sh <app_path>
#
# Environment variables (required):
# SPARKLE_EDDSA_PUBLIC_KEY - EdDSA public key for verifying updates
#
# Optional environment variables:
# SPARKLE_FEED_URL - Appcast URL (default: https://assets.eagle0.net/mac/appcast.xml)
# SPARKLE_VERSION - Sparkle version to use (default: 2.6.4)
set -euxo pipefail
APP_PATH="$1"
SPARKLE_VERSION="${SPARKLE_VERSION:-2.6.4}"
SPARKLE_FEED_URL="${SPARKLE_FEED_URL:-https://assets.eagle0.net/mac/appcast.xml}"
SPARKLE_CACHE_DIR="/tmp/sparkle-cache"
if [ ! -d "$APP_PATH" ]; then
echo "ERROR: App not found at $APP_PATH"
exit 1
fi
if [ -z "${SPARKLE_EDDSA_PUBLIC_KEY:-}" ]; then
echo "ERROR: SPARKLE_EDDSA_PUBLIC_KEY environment variable not set"
exit 1
fi
# Download Sparkle if not cached
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
fi
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/"
# Also copy the XPC services if present
if [ -d "$SPARKLE_DIR/Sparkle.framework/Versions/B/XPCServices" ]; then
echo "Sparkle XPC services present"
fi
echo "=== Updating Info.plist ==="
PLIST_PATH="$APP_PATH/Contents/Info.plist"
# Add Sparkle configuration to Info.plist
/usr/libexec/PlistBuddy -c "Delete :SUFeedURL" "$PLIST_PATH" 2>/dev/null || true
/usr/libexec/PlistBuddy -c "Add :SUFeedURL string '$SPARKLE_FEED_URL'" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Delete :SUPublicEDKey" "$PLIST_PATH" 2>/dev/null || true
/usr/libexec/PlistBuddy -c "Add :SUPublicEDKey string '$SPARKLE_EDDSA_PUBLIC_KEY'" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Delete :SUEnableAutomaticChecks" "$PLIST_PATH" 2>/dev/null || true
/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")
BUILD_NUMBER=$(git rev-list --count HEAD 2>/dev/null || echo "1")
echo "Setting version: $VERSION (build $BUILD_NUMBER)"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$PLIST_PATH" 2>/dev/null || \
/usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string '$VERSION'" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" "$PLIST_PATH" 2>/dev/null || \
/usr/libexec/PlistBuddy -c "Add :CFBundleVersion string '$BUILD_NUMBER'" "$PLIST_PATH"
# Add URL scheme for invitation codes (eagle0://invite?code=XXXX)
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:CFBundleURLSchemes array" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string 'eagle0'" "$PLIST_PATH"
echo "=== Sparkle injection complete ==="
echo "App: $APP_PATH"
echo "Feed URL: $SPARKLE_FEED_URL"
echo "Version: $VERSION (build $BUILD_NUMBER)"
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
#
# Notarize a macOS .app bundle with Apple
# Usage: notarize_mac_app.sh <app_path>
#
# Environment variables (required):
# APPLE_ID - Apple Developer account email
# APP_SPECIFIC_PASSWORD - App-specific password for notarytool
# TEAM_ID - Apple Developer Team ID
set -euxo pipefail
APP_PATH="$1"
if [ ! -d "$APP_PATH" ]; then
echo "ERROR: App not found at $APP_PATH"
exit 1
fi
if [ -z "${APPLE_ID:-}" ] || [ -z "${APP_SPECIFIC_PASSWORD:-}" ] || [ -z "${TEAM_ID:-}" ]; then
echo "ERROR: Required environment variables not set"
echo " APPLE_ID: ${APPLE_ID:-<not set>}"
echo " APP_SPECIFIC_PASSWORD: ${APP_SPECIFIC_PASSWORD:+<set>}"
echo " TEAM_ID: ${TEAM_ID:-<not set>}"
exit 1
fi
# Create ZIP for notarization submission
ZIP_PATH="${APP_PATH%.app}.zip"
echo "=== Creating ZIP for notarization: $ZIP_PATH ==="
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
echo "=== Submitting to Apple for notarization ==="
SUBMIT_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \
--apple-id "$APPLE_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--team-id "$TEAM_ID" \
--wait 2>&1) || true
echo "$SUBMIT_OUTPUT"
# Extract submission ID and status (look for " status:" to avoid matching "Current status:")
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep "id:" | head -1 | awk '{print $2}')
STATUS=$(echo "$SUBMIT_OUTPUT" | grep "^ status:" | awk '{print $2}')
echo "Submission ID: $SUBMISSION_ID"
echo "Status: $STATUS"
# Clean up the zip
rm "$ZIP_PATH"
if [ "$STATUS" != "Accepted" ]; then
echo "=== Notarization failed! Fetching log for details ==="
xcrun notarytool log "$SUBMISSION_ID" \
--apple-id "$APPLE_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--team-id "$TEAM_ID"
exit 1
fi
echo "=== Stapling notarization ticket to app ==="
# Retry stapling - Apple's CloudKit can have a brief delay after notarization completes
MAX_STAPLE_ATTEMPTS=5
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
echo "Stapling successful"
break
fi
if [ $STAPLE_ATTEMPT -eq $MAX_STAPLE_ATTEMPTS ]; then
echo "ERROR: Stapling failed after $MAX_STAPLE_ATTEMPTS attempts"
exit 1
fi
echo "Stapling failed, waiting 10 seconds before retry..."
sleep 10
STAPLE_ATTEMPT=$((STAPLE_ATTEMPT + 1))
done
echo "=== Verifying notarization ==="
xcrun stapler validate "$APP_PATH"
spctl --assess --type exec -v "$APP_PATH"
echo "Notarization complete: $APP_PATH"
+142
View File
@@ -0,0 +1,142 @@
#!/bin/bash
#
# Warmup Script for Eagle Server
#
# This script warms up the JIT compiler before switching traffic to a new instance.
# It uses the Go warmup tool which:
# 1. Creates a test game via bidirectional streaming
# 2. Posts an Improve command
# 3. Verifies action results and new commands
# 4. Cleans up the test game
#
# Usage: ./warmup-eagle.sh HOST:PORT
#
# Example:
# ./warmup-eagle.sh localhost:40032
# ./warmup-eagle.sh localhost:40034
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
HOST="${1:-localhost:40032}"
log_info "Warming up Eagle server at ${HOST}..."
# Try to find the Go warmup tool
WARMUP_TOOL=""
# Check if we're in the project directory with bazel
if [ -f "${PROJECT_ROOT}/WORKSPACE" ] || [ -f "${PROJECT_ROOT}/WORKSPACE.bazel" ]; then
# Try to find the pre-built binary
BAZEL_BIN="${PROJECT_ROOT}/bazel-bin/src/main/go/net/eagle0/warmup/warmup_/warmup"
if [ -x "${BAZEL_BIN}" ]; then
WARMUP_TOOL="${BAZEL_BIN}"
fi
fi
# Check for the warmup tool in common locations (for deployed environments)
if [ -z "${WARMUP_TOOL}" ]; then
for path in \
"${SCRIPT_DIR}/bin/warmup" \
"/opt/eagle0/scripts/bin/warmup" \
"/opt/eagle0/bin/warmup" \
"/usr/local/bin/eagle-warmup" \
"${SCRIPT_DIR}/warmup"; do
if [ -x "${path}" ]; then
WARMUP_TOOL="${path}"
break
fi
done
fi
# If we found the Go tool, use it
if [ -n "${WARMUP_TOOL}" ]; then
log_info "Using Go warmup tool: ${WARMUP_TOOL}"
if "${WARMUP_TOOL}" --address="${HOST}" --timeout=60s; then
log_info "Warmup complete!"
exit 0
else
log_error "Go warmup tool failed"
exit 1
fi
fi
# Fallback to grpcurl-based warmup
log_warn "Go warmup tool not found, falling back to grpcurl"
# Check for grpcurl
if ! command -v grpcurl &> /dev/null; then
log_error "Neither Go warmup tool nor grpcurl is available"
log_error "Build the warmup tool with: bazel build //src/main/go/net/eagle0/warmup"
log_error "Or install grpcurl: brew install grpcurl (macOS)"
exit 1
fi
# Warmup iterations
WARMUP_ITERATIONS=3
# 1. Call GetRunningGames multiple times - this exercises the gRPC layer and basic game access
log_info "Warming up GetRunningGames..."
for i in $(seq 1 ${WARMUP_ITERATIONS}); do
RESULT=$(grpcurl -plaintext -d '{}' "${HOST}" net.eagle0.eagle.api.Eagle/GetRunningGames 2>&1) || true
if echo "$RESULT" | grep -q "games\|{}"; then
echo -n "."
else
log_error "GetRunningGames failed on iteration $i"
exit 1
fi
done
echo " done"
# 2. Call GetSettings - exercises settings loading
log_info "Warming up GetSettings..."
for i in $(seq 1 ${WARMUP_ITERATIONS}); do
if grpcurl -plaintext -d '{}' "${HOST}" net.eagle0.eagle.api.Eagle/GetSettings > /dev/null 2>&1; then
echo -n "."
else
log_warn "GetSettings failed on iteration $i (non-fatal)"
fi
done
echo " done"
# 3. Call AddSettings with empty list - exercises settings path
log_info "Warming up AddSettings..."
for i in $(seq 1 ${WARMUP_ITERATIONS}); do
if grpcurl -plaintext -d '{"settings": []}' "${HOST}" net.eagle0.eagle.api.Eagle/AddSettings > /dev/null 2>&1; then
echo -n "."
else
log_warn "AddSettings failed on iteration $i (non-fatal)"
fi
done
echo " done"
# Final health check
log_info "Verifying server health..."
if grpcurl -plaintext -d '{}' "${HOST}" net.eagle0.eagle.api.Eagle/GetRunningGames > /dev/null 2>&1; then
log_info "Health check passed"
else
log_error "Health check failed"
exit 1
fi
log_info ""
log_info "Warmup complete (basic mode - bidirectional streaming warmup not available)!"
log_info "The JIT should be warmed for:"
log_info " - gRPC layer and protobuf parsing"
log_info " - Settings loading and management"
log_info ""
log_warn "Note: For full warmup including game creation and command processing,"
log_warn " build and use the Go warmup tool: bazel build //src/main/go/net/eagle0/warmup"
@@ -107,6 +107,23 @@ auto AIAttackerStrategySelector::BestAttackerStrategy(
alCache,
battalionTypeGetter,
braveWaterCost));
} else if (!defenderPositions.empty()) {
// Defenders exist but none are on castles - they're scattering/fleeing.
// Chase them down rather than holding empty castles, since eliminating
// all defenders also wins the battle via LAST_PLAYER_STANDING.
chosenStrategy = AttackUnitsStrategy(GenerateTargetPriorities(
Occupants(
*gameState->units(),
gameState->hex_map()->row_count(),
gameState->hex_map()->column_count()),
gameState->hex_map(),
defenderPositions,
attackerPid,
attackerUnits,
apdCache,
alCache,
battalionTypeGetter,
braveWaterCost));
} else {
chosenStrategy = HoldCastlesStrategy;
}
@@ -126,6 +126,7 @@
<Compile Include="Assets/Modern UI Pack/Scripts/Notification/NotificationManagerEditor.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicEditor.cs" />
<Compile Include="Assets/ConnectionHandler/RunningGameItem.cs" />
<Compile Include="Assets/Tutorial/UI/TutorialCanvasBuilder.cs" />
<Compile Include="Assets/Shardok/Table Rows/ArmyRowController.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/PrisonerExchangeDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/Notifications/Notification.cs" />
@@ -372,6 +373,7 @@
<Compile Include="Assets/Eagle/CommandSelectors/SendSuppliesCommandSelector.cs" />
<Compile Include="Assets/Tutorial/UI/TutorialUIManager.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" />
@@ -384,6 +386,7 @@
<Compile Include="Assets/Eagle/Table Rows/IncomingArmyTableRow.cs" />
<Compile Include="Assets/ConnectionHandler/CreateGameItem.cs" />
<Compile Include="Assets/Eagle/EagleGameController.cs" />
<Compile Include="Assets/Tutorial/TutorialTestSetup.cs" />
<Compile Include="Assets/Eagle/Notifications/FailedSwearBrotherhoodNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/FireEffectAnimator.cs" />
<Compile Include="Assets/common/GUIUtils/EventBasedTable.cs" />
@@ -122,8 +122,13 @@ namespace Auth {
string localAppData =
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
return Path.Combine(localAppData, "eagle0");
#elif UNITY_STANDALONE_OSX || UNITY_EDITOR_OSX
// Mac: ~/Library/Application Support/eagle0
string appSupport =
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
return Path.Combine(appSupport, "eagle0");
#else
// Other platforms don't use the Windows installer
// Other platforms not yet supported
return null;
#endif
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: a2a19f11d1f82412e8e2811b366113ac
@@ -2992,6 +2992,90 @@ MonoBehaviour:
m_PersistentCalls:
m_Calls: []
m_IsOn: 1
--- !u!1 &16227905
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 16227909}
- component: {fileID: 16227908}
- component: {fileID: 16227907}
- component: {fileID: 16227906}
m_Layer: 0
m_Name: TutorialManager
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!114 &16227906
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 16227905}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 370b3269832442e196d1b432bdae2f1e, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Eagle0.Tutorial.TutorialTestSetup
EnableTestTutorials: 1
ResetProgressOnStart: 0
--- !u!114 &16227907
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 16227905}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 52abae862492497cb7a3f8c72f837b37, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Eagle0.Tutorial.TutorialUIManager
ModalPanel: {fileID: 0}
OverlayController: {fileID: 0}
HintIndicatorPrefab: {fileID: 0}
TutorialCanvas: {fileID: 0}
CanvasFont: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
UseFallbackUI: 1
FallbackFont: {fileID: 12800000, guid: f0f6814ff8ef048bcbfd5002f77188ea, type: 3}
AutoBuildCanvasUI: 1
--- !u!114 &16227908
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 16227905}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: c529affa73b04d129ec5826ed0ac75e7, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::Eagle0.Tutorial.TutorialManager
TutorialsEnabled: 1
OnboardingSequence: {fileID: 0}
UIManager: {fileID: 16227907}
DebugLogging: 0
--- !u!4 &16227909
Transform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 16227905}
serializedVersion: 2
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 2560, y: 1440, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
--- !u!1 &17277548
GameObject:
m_ObjectHideFlags: 0
@@ -8532,10 +8616,10 @@ RectTransform:
- {fileID: 1396438488}
m_Father: {fileID: 2130578398}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 333.5, y: -11.255}
m_SizeDelta: {x: 200, y: 20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &75920627
MonoBehaviour:
@@ -10927,10 +11011,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2130578398}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 218.5, y: -11.255}
m_SizeDelta: {x: 20, y: 20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &103511974
MonoBehaviour:
@@ -12298,10 +12382,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 683322975}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 306.5, y: -40}
m_SizeDelta: {x: 51, y: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &114306270
MonoBehaviour:
@@ -15614,6 +15698,10 @@ MonoBehaviour:
displayNameField: {fileID: 1956057658}
setDisplayNameButton: {fileID: 700483942}
displayNameErrorText: {fileID: 77360322}
invitationCodePanel: {fileID: 0}
invitationCodeField: {fileID: 0}
submitInvitationCodeButton: {fileID: 0}
invitationCodeErrorText: {fileID: 0}
connectionStatusText: {fileID: 77360322}
connectionPanel: {fileID: 596196018}
gameSelectionPanel: {fileID: 1102342355}
@@ -20624,10 +20712,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -32.504997}
m_SizeDelta: {x: 492, y: 45.01}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &171635196
MonoBehaviour:
@@ -29397,7 +29485,7 @@ RectTransform:
m_Father: {fileID: 208702977}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 10, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@@ -31829,7 +31917,7 @@ RectTransform:
m_Father: {fileID: 1396438488}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@@ -33967,6 +34055,7 @@ MonoBehaviour:
eagleGameController: {fileID: 1965467940}
ShardokCanvas: {fileID: 2088078267}
connectionHandler: {fileID: 135486720}
resetTutorialsButton: {fileID: 0}
--- !u!1 &305665272
GameObject:
m_ObjectHideFlags: 0
@@ -36778,10 +36867,10 @@ RectTransform:
- {fileID: 1666727525}
m_Father: {fileID: 2039520752}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 321, y: -11.255}
m_SizeDelta: {x: 200, y: 20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &329919723
MonoBehaviour:
@@ -47131,8 +47220,8 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1666727525}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.1, y: 0}
m_AnchorMax: {x: 0.1, y: 1}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@@ -64015,10 +64104,10 @@ RectTransform:
- {fileID: 1375685348}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -215.03}
m_SizeDelta: {x: 492, y: 30}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &548637657
MonoBehaviour:
@@ -71487,10 +71576,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 10, y: -120.03}
m_SizeDelta: {x: 0, y: 10}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &590819392
MonoBehaviour:
@@ -74595,6 +74684,146 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 624827069}
m_CullTransparentMesh: 0
--- !u!1 &625062879
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 625062880}
- component: {fileID: 625062882}
- component: {fileID: 625062881}
m_Layer: 0
m_Name: Open Resources
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &625062880
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 625062879}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 673182551}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &625062881
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 625062879}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_text: 'Reset Tutorials
'
m_isRightToLeft: 0
m_fontAsset: {fileID: 11400000, guid: ec2736df0379a45bfa7349b652fd07d6, type: 2}
m_sharedMaterial: {fileID: 1026247389042360723, guid: ec2736df0379a45bfa7349b652fd07d6,
type: 2}
m_fontSharedMaterials: []
m_fontMaterial: {fileID: 0}
m_fontMaterials: []
m_fontColor32:
serializedVersion: 2
rgba: 4281479730
m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1}
m_enableVertexGradient: 0
m_colorMode: 3
m_fontColorGradient:
topLeft: {r: 1, g: 1, b: 1, a: 1}
topRight: {r: 1, g: 1, b: 1, a: 1}
bottomLeft: {r: 1, g: 1, b: 1, a: 1}
bottomRight: {r: 1, g: 1, b: 1, a: 1}
m_fontColorGradientPreset: {fileID: 0}
m_spriteAsset: {fileID: 0}
m_tintAllSprites: 0
m_StyleSheet: {fileID: 0}
m_TextStyleHashCode: -1183493901
m_overrideHtmlColors: 0
m_faceColor:
serializedVersion: 2
rgba: 4294967295
m_fontSize: 18
m_fontSizeBase: 18
m_fontWeight: 400
m_enableAutoSizing: 0
m_fontSizeMin: 18
m_fontSizeMax: 72
m_fontStyle: 0
m_HorizontalAlignment: 2
m_VerticalAlignment: 512
m_textAlignment: 65535
m_characterSpacing: 0
m_characterHorizontalScale: 1
m_wordSpacing: 0
m_lineSpacing: 0
m_lineSpacingMax: 0
m_paragraphSpacing: 0
m_charWidthMaxAdj: 0
m_TextWrappingMode: 1
m_wordWrappingRatios: 0.4
m_overflowMode: 0
m_linkedTextComponent: {fileID: 0}
parentLinkedComponent: {fileID: 0}
m_enableKerning: 0
m_ActiveFontFeatures: 6e72656b
m_enableExtraPadding: 0
checkPaddingRequired: 0
m_isRichText: 1
m_EmojiFallbackSupport: 1
m_parseCtrlCharacters: 1
m_isOrthographic: 1
m_isCullingEnabled: 0
m_horizontalMapping: 0
m_verticalMapping: 0
m_uvLineOffset: 0
m_geometrySortingOrder: 0
m_IsTextObjectScaleStatic: 0
m_VertexBufferAutoSizeReduction: 0
m_useMaxVisibleDescender: 1
m_pageToDisplay: 1
m_margin: {x: 0, y: 0, z: 0, w: 0}
m_isUsingLegacyAnimationComponent: 0
m_isVolumetricText: 0
m_hasFontAssetChanged: 0
m_baseMaterial: {fileID: 0}
m_maskOffset: {x: 0, y: 0, z: 0, w: 0}
--- !u!222 &625062882
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 625062879}
m_CullTransparentMesh: 1
--- !u!1 &625252446
GameObject:
m_ObjectHideFlags: 0
@@ -82452,6 +82681,160 @@ CanvasRenderer:
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 673008387}
m_CullTransparentMesh: 0
--- !u!1 &673182550
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 673182551}
- component: {fileID: 673182555}
- component: {fileID: 673182554}
- component: {fileID: 673182553}
- component: {fileID: 673182552}
m_Layer: 0
m_Name: Reset Tutorials Button
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &673182551
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 673182550}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 625062880}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &673182552
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 673182550}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 306cc8c2b49d7114eaa3623786fc2126, type: 3}
m_Name:
m_EditorClassIdentifier:
m_IgnoreLayout: 0
m_MinWidth: -1
m_MinHeight: 30
m_PreferredWidth: -1
m_PreferredHeight: 30
m_FlexibleWidth: 1
m_FlexibleHeight: -1
m_LayoutPriority: 1
--- !u!114 &673182553
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 673182550}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 673182554}
m_OnClick:
m_PersistentCalls:
m_Calls:
- m_Target: {fileID: 300036928}
m_TargetAssemblyTypeName: SettingsPanelController, Assembly-CSharp
m_MethodName: OnResetTutorialsClick
m_Mode: 1
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
--- !u!114 &673182554
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 673182550}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0}
m_Type: 1
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!222 &673182555
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 673182550}
m_CullTransparentMesh: 1
--- !u!1 &673307124
GameObject:
m_ObjectHideFlags: 0
@@ -83590,10 +83973,10 @@ RectTransform:
- {fileID: 1119022577}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -354}
m_SizeDelta: {x: 492, y: 40}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &683322976
MonoBehaviour:
@@ -93480,7 +93863,7 @@ GameObject:
- component: {fileID: 749366808}
- component: {fileID: 749366807}
m_Layer: 0
m_Name: Button
m_Name: Open Resources Button
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
@@ -93501,10 +93884,10 @@ RectTransform:
- {fileID: 392813076}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -250.03}
m_SizeDelta: {x: 492, y: 30}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &749366807
MonoBehaviour:
@@ -95360,10 +95743,10 @@ RectTransform:
- {fileID: 699949417}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -180.03}
m_SizeDelta: {x: 492, y: 30}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &770361456
MonoBehaviour:
@@ -96170,10 +96553,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2039520752}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 208.5, y: -11.255}
m_SizeDelta: {x: 15, y: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &779503534
MonoBehaviour:
@@ -99264,10 +99647,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2039520752}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 446, y: -11.255}
m_SizeDelta: {x: 40, y: 15.01}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &806086202
MonoBehaviour:
@@ -99806,10 +100189,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 10, y: -313.27002}
m_SizeDelta: {x: 0, y: 31.459991}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &808870206
MonoBehaviour:
@@ -105263,10 +105646,10 @@ RectTransform:
- {fileID: 113544276}
m_Father: {fileID: 1590704177}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 333.5, y: -11.255}
m_SizeDelta: {x: 200, y: 20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &858025936
MonoBehaviour:
@@ -121385,10 +121768,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1590704177}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 196, y: -11.255}
m_SizeDelta: {x: 15, y: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &972401127
MonoBehaviour:
@@ -122506,7 +122889,7 @@ RectTransform:
m_Father: {fileID: 113544276}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 20, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@@ -123520,7 +123903,7 @@ RectTransform:
m_Father: {fileID: 1425392451}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0.1, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 10, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@@ -140643,10 +141026,10 @@ RectTransform:
- {fileID: 717768291}
m_Father: {fileID: 683322975}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 417, y: -20}
m_SizeDelta: {x: 150, y: 40}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1119022578
MonoBehaviour:
@@ -147583,10 +147966,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 683322975}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 135.5, y: -40}
m_SizeDelta: {x: 51, y: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1184808158
MonoBehaviour:
@@ -153109,10 +153492,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2130578398}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 108.5, y: -11.255}
m_SizeDelta: {x: 150, y: 22.51}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1219993393
MonoBehaviour:
@@ -174192,10 +174575,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2130578398}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 196, y: -11.255}
m_SizeDelta: {x: 15, y: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1390131026
MonoBehaviour:
@@ -178285,10 +178668,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2039520752}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 111, y: -11.255}
m_SizeDelta: {x: 170, y: 22.51}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1415802613
MonoBehaviour:
@@ -178444,10 +178827,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 2130578398}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 448.5, y: -11.255}
m_SizeDelta: {x: 20, y: 20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1415887397
MonoBehaviour:
@@ -181904,10 +182287,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1590704177}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 108.5, y: -11.255}
m_SizeDelta: {x: 150, y: 22.51}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1448655991
MonoBehaviour:
@@ -196899,10 +197282,10 @@ RectTransform:
- {fileID: 1122913432}
m_Father: {fileID: 683322975}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 50, y: -20}
m_SizeDelta: {x: 100, y: 40}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1557328375
MonoBehaviour:
@@ -198649,7 +199032,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 2147483647
m_IsActive: 0
m_IsActive: 1
--- !u!224 &1566150938
RectTransform:
m_ObjectHideFlags: 0
@@ -198670,6 +199053,7 @@ RectTransform:
- {fileID: 770361455}
- {fileID: 548637656}
- {fileID: 749366806}
- {fileID: 673182551}
- {fileID: 2039520752}
- {fileID: 808870205}
- {fileID: 683322975}
@@ -200241,10 +200625,10 @@ RectTransform:
- {fileID: 2123746368}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -98.774994}
m_SizeDelta: {x: 492, y: 22.51}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1590704178
MonoBehaviour:
@@ -219035,10 +219419,10 @@ RectTransform:
- {fileID: 543540660}
m_Father: {fileID: 683322975}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 221, y: -20}
m_SizeDelta: {x: 100, y: 40}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &1775111780
MonoBehaviour:
@@ -221290,7 +221674,7 @@ RectTransform:
m_Father: {fileID: 1110049512}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 10, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
@@ -247406,10 +247790,10 @@ RectTransform:
- {fileID: 806086201}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -281.285}
m_SizeDelta: {x: 492, y: 22.51}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &2039520753
MonoBehaviour:
@@ -252578,10 +252962,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1590704177}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 218.5, y: -11.255}
m_SizeDelta: {x: 20, y: 20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &2096030393
MonoBehaviour:
@@ -255937,7 +256321,7 @@ GameObject:
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
m_IsActive: 0
--- !u!224 &2109805906
RectTransform:
m_ObjectHideFlags: 0
@@ -255957,7 +256341,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -145.03}
m_AnchoredPosition: {x: 256, y: -142.85143}
m_SizeDelta: {x: 492, y: 30}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &2109805907
@@ -257369,10 +257753,10 @@ RectTransform:
m_Children: []
m_Father: {fileID: 1590704177}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 448.5, y: -11.255}
m_SizeDelta: {x: 20, y: 20}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &2123746369
MonoBehaviour:
@@ -258672,10 +259056,10 @@ RectTransform:
- {fileID: 1415887396}
m_Father: {fileID: 1566150938}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 256, y: -71.265}
m_SizeDelta: {x: 492, y: 22.51}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 0, y: 0}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 0, y: 0}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!114 &2130578399
MonoBehaviour:
@@ -268781,6 +269165,7 @@ SceneRoots:
m_Roots:
- {fileID: 279614944}
- {fileID: 815907755}
- {fileID: 16227909}
- {fileID: 122332320}
- {fileID: 682560790}
- {fileID: 1342510189}
@@ -3,6 +3,7 @@ using System.Diagnostics;
using System.IO;
using common;
using eagle;
using Eagle0.Tutorial;
using Net.Eagle0.Shardok.Api;
using Shardok;
using TMPro;
@@ -49,6 +50,8 @@ public class SettingsPanelController : MonoBehaviour {
public GameObject ShardokCanvas;
public ConnectionHandler connectionHandler;
public Button resetTutorialsButton;
void Start() {
_active = false;
panel.SetActive(false);
@@ -148,6 +151,11 @@ public class SettingsPanelController : MonoBehaviour {
Process.Start(Path.Combine(Application.persistentDataPath, "eagle0", "Resources"));
}
public void OnResetTutorialsClick() {
TutorialManager.Instance?.ResetAllProgress();
UnityEngine.Debug.Log("Tutorial progress reset");
}
private void HandleLobby() {
eagleGameController.StopAll();
eagleGameController.gameObject.SetActive(false);
@@ -68,14 +68,10 @@ namespace Shardok {
return;
}
Debug.Log(
$"ArrowVolleyAnimator: Starting volley from cell {sourceCellIndex} to {targetCellIndex}");
StartCoroutine(SpawnArrowVolley(sourcePos.Value, targetPos.Value));
}
private IEnumerator SpawnArrowVolley(Vector2 sourcePosition, Vector2 targetPosition) {
Debug.Log(
$"ArrowVolleyAnimator: Spawning {arrowCount} arrows from {sourcePosition} to {targetPosition}");
var arrows = new List<GameObject>();
for (int i = 0; i < arrowCount; i++) {
@@ -59,7 +59,7 @@ namespace Shardok {
return;
}
Vector2? sourcePos = __hexGrid.GetCellLocalPosition(sourceCellIndex);
Vector2? sourcePos = __hexGrid.GetCellCenterPosition(sourceCellIndex);
Vector2? targetPos = __hexGrid.GetCellCenterPosition(targetCellIndex);
if (sourcePos == null || targetPos == null) {
@@ -135,8 +135,8 @@ namespace Shardok {
return;
}
Vector2? attackerPos = __hexGrid.GetCellLocalPosition(attackerCellIndex);
Vector2? defenderPos = __hexGrid.GetCellLocalPosition(defenderCellIndex);
Vector2? attackerPos = __hexGrid.GetCellCenterPosition(attackerCellIndex);
Vector2? defenderPos = __hexGrid.GetCellCenterPosition(defenderCellIndex);
if (attackerPos == null || defenderPos == null) {
Debug.LogWarning(
@@ -149,8 +149,6 @@ namespace Shardok {
CleanupWeapons();
}
Debug.Log(
$"MeleeAnimator: Starting melee from cell {attackerCellIndex} ({attackerType}) to {defenderCellIndex} ({defenderType})");
_activeAnimation = StartCoroutine(
AnimateClash(attackerPos.Value, defenderPos.Value, attackerType, defenderType));
}
@@ -65,8 +65,8 @@ namespace Shardok {
return;
}
Vector2? sourcePos = _hexGrid.GetCellLocalPosition(sourceCellIndex);
Vector2? targetPos = _hexGrid.GetCellLocalPosition(targetCellIndex);
Vector2? sourcePos = _hexGrid.GetCellCenterPosition(sourceCellIndex);
Vector2? targetPos = _hexGrid.GetCellCenterPosition(targetCellIndex);
if (sourcePos == null || targetPos == null) {
Debug.LogWarning(
@@ -579,7 +579,12 @@ namespace Shardok {
if (Model.UnitsById.TryGetValue(
historyEntry.Actor.Value,
out var actorUnit)) {
int sourceIndex = MapCoordsToGridIndex(actorUnit.Location);
// For Move actions, use stored source coords (unit's pre-move location)
// For other actions, use current unit location
int sourceIndex =
Model.MoveSourceCoords.TryGetValue(i, out var sourceCoords)
? MapCoordsToGridIndex(sourceCoords)
: MapCoordsToGridIndex(actorUnit.Location);
// Get target - either coords (archery) or unit location (melee)
int targetIndex = -1;
@@ -75,6 +75,13 @@ public class ShardokGameModel {
ReserveUnitsById.Values.FirstOrDefault(u => u.Location.Equals(location));
public List<ActionResultView> History { get; private set; }
/// <summary>
/// Tracks the source coordinates for Move actions, keyed by history index.
/// Used for animations since the model is fully updated before animations play.
/// </summary>
public Dictionary<int, Coords> MoveSourceCoords { get; } = new();
public PlayerId PlayerId { get; private set; }
public GameStatus GameStatus { get; private set; }
public RoundId CurrentRound { get; private set; }
@@ -402,6 +409,14 @@ public class ShardokGameModel {
}
private void HandleNewHistoryEntry(ActionResultView entry) {
// For Move actions, record the source coordinates BEFORE applying the diff
// This is needed for animations since all diffs are applied before animations play
if (entry.Type == ActionType.Move && entry.Actor != null) {
if (UnitsById.TryGetValue(entry.Actor.Value, out var actorUnit)) {
MoveSourceCoords[History.Count] = actorUnit.Location;
}
}
History.Add(entry);
var gsvDiff = entry.GameStateViewDiff;
if (gsvDiff == null) {
@@ -0,0 +1,182 @@
# Tutorial System for Eagle0
## Overview
A modular tutorial system for the Eagle0 Unity client supporting:
- **Onboarding**: Full guided playthrough of first several turns
- **Contextual tutorials**: First-encounter and first-attempt triggers with subtle hints
- **Mixed UI**: Modals for concepts, overlays for UI guidance, hint indicators
- **Local persistence**: PlayerPrefs-based state tracking
---
## Current Status
### Completed
- [x] **Phase 1: Foundation** - TutorialState, TutorialManager, TutorialStep/Sequence
- [x] **Phase 2: UI** - Canvas-based modal with runtime construction
- TutorialCanvasBuilder creates full Canvas UI at runtime (no prefab needed)
- Fantasy RPG color scheme (dark purple panel, gold accents)
- Title, description, icon, progress bar, buttons
- IMGUI fallback still available if needed
- [x] **Phase 3: Triggers** - Event-based trigger system with game controller hooks
- [x] **Test setup** - TutorialTestSetup with welcome intro, battle, command tutorials
- [x] **Settings integration** - Reset Tutorials button in Settings panel
- [x] **Game entry trigger** - Welcome tutorial shows when entering a game (not lobby)
- [x] **Font assignment** - Stoke-Regular-SDF assigned to TutorialUIManager.CanvasFont
### Future Work
- [ ] **Lobby tutorial helper** - Separate tutorial for lobby/game selection
- [ ] **Overlay system** - TutorialOverlayController for highlighting UI elements
- [ ] **Hint indicators** - TutorialHintIndicator for subtle pulsing dots
- [ ] **Real tutorial content** - Replace test tutorials with actual onboarding sequence
- [ ] **More contextual triggers** - Diplomacy, hero recruitment, spells, terrain, etc.
---
## Architecture
```
TutorialManager (Singleton, DontDestroyOnLoad)
├── TutorialState (PlayerPrefs persistence)
├── TutorialTriggerRegistry
│ ├── OnboardingTriggers
│ └── ContextualTriggers
└── TutorialUIManager
├── TutorialCanvasBuilder (runtime Canvas construction)
├── TutorialModalPanel (wired up by builder)
├── TutorialOverlayController (stub)
├── TutorialHintIndicator (stub)
└── IMGUI Fallback (working)
```
---
## File Structure
```
Assets/Tutorial/
├── TutorialManager.cs ✓ Singleton, coordinates everything
├── TutorialState.cs ✓ PlayerPrefs persistence
├── TutorialTestSetup.cs ✓ Test tutorials (welcome, battle, command)
├── TUTORIAL_PLAN.md ✓ This document
├── Content/
│ ├── TutorialStep.cs ✓ Step data structure
│ └── TutorialSequence.cs ✓ Sequence ScriptableObject
├── Triggers/
│ └── TutorialTriggerRegistry.cs ✓ Event routing
└── UI/
├── TutorialUIManager.cs ✓ UI coordination + auto-build Canvas
├── TutorialCanvasBuilder.cs ✓ Runtime Canvas UI construction
├── TutorialModalPanel.cs ✓ Canvas-based modal panel controller
├── TutorialOverlayController.cs Stub (future)
└── TutorialHintIndicator.cs Stub (future)
```
---
## Unity Setup
### Required Inspector Assignments
1. **TutorialUIManager.CanvasFont**`Stoke-Regular-SDF` (TextMeshPro font)
2. **TutorialUIManager.FallbackFont**`Stoke-Regular` (for IMGUI backup)
### No Prefab Required
The `TutorialCanvasBuilder` creates the entire Canvas UI hierarchy at runtime:
- Canvas with ScreenSpaceOverlay, sorting order 1000
- Modal blocker (dark semi-transparent background)
- Panel with vertical layout (title, description, progress, buttons)
- All buttons wired to TutorialModalPanel handlers
---
## Integration Points
### EagleGameController.cs
- `SetUpGame()`: Calls `TutorialManager.Instance.Initialize(this, null)` → triggers "game_started" event
- `SwapModel()`: Calls `OnModelUpdated()`
- `ProvinceWasSelected()`: Calls `OnProvinceSelected()`
- `PostCommittedCommand()`: Calls `OnCommandIssued()`
### ShardokGameController.cs
- `SetUpGame()`: Calls `TutorialManager.Instance.Initialize(null, this)`
- `ModelUpdated()`: Calls `OnBattleAction()`
- `OnTurnEnded()`: Calls `OnTurnEnded()`
- Unit selection: Calls `OnUnitSelected()`
### SettingsPanelController.cs
- Reset Tutorials button: Calls `TutorialManager.Instance.ResetAllProgress()`
---
## Test Tutorials (Current)
| ID | Trigger Event | Title | When |
|----|---------------|-------|------|
| `test_intro` | `game_started` | Welcome to Eagle0! | Entering a game |
| `test_first_battle` | `battle_entered` | Battle Begins! | First tactical battle |
| `test_first_command` | `command_issued` | Command Issued! | First strategic command |
---
## Onboarding Flow (Planned)
| Step | Type | Content | Completion |
|------|------|---------|------------|
| 1 | Modal | Welcome to Eagle0 | Button click |
| 2 | Overlay | Map overview - "Select a province" | Province selected |
| 3 | Overlay | Province info panel explanation | Button click |
| 4 | Overlay | Command buttons - "Try March" | March issued |
| 5 | Modal | Turn cycle explanation | Button click |
| 6 | Hidden | Wait for battle available | Model update |
| 7 | Modal | Battle introduction | Button click |
| 8 | Overlay | "Battle!" button highlight | Battle entered |
| 9 | Modal | Tactical combat overview | Button click |
| 10 | Overlay | Select and move a unit | Move issued |
| 11 | Overlay | Attack an enemy | Attack issued |
| 12 | Overlay | End Turn button | Turn ended |
| 13 | Modal | Onboarding complete! | Button click |
---
## Contextual Tutorials (Planned)
### Strategic (First Encounter)
| ID | Trigger | Display |
|----|---------|---------|
| `diplomacy_offer` | First diplomacy command available | Modal |
| `hero_recruitment` | First recruitment available | Modal |
| `province_riot` | First riot in owned province | Modal |
| `weather_control` | First weather command available | Overlay |
| `prisoner_capture` | First prisoner captured | Modal |
### Tactical (First Encounter/Attempt)
| ID | Trigger | Display |
|----|---------|---------|
| `spell_lightning` | Lightning spell available | Tooltip |
| `spell_meteor` | Meteor spell available | Modal |
| `spell_holywave` | Holy Wave available | Tooltip |
| `spell_raisedead` | Raise Dead available | Modal |
| `terrain_fire` | Fire hex encountered | Tooltip |
| `terrain_water` | Water adjacent to unit | Tooltip |
| `ability_charge` | Charge command available | Overlay |
| `flanking` | Flanking opportunity | Hint |
### Lobby (Future)
| ID | Trigger | Display |
|----|---------|---------|
| `lobby_welcome` | First time in lobby | Modal |
| `lobby_join_game` | Viewing game list | Overlay |
| `lobby_create_game` | Create game button | Tooltip |
---
## Testing
1. Ensure TutorialManager GameObject exists with:
- TutorialManager component
- TutorialUIManager component (assign fonts!)
- TutorialTestSetup component
2. Enable `Debug Logging` on TutorialManager for console output
3. Play game → enter a game → see "Welcome to Eagle0!" modal
4. Use Settings → Reset Tutorials to test again
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 69183de61f7d440a29b4a4270395a180
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -83,6 +83,9 @@ namespace Eagle0.Tutorial {
public void Initialize(EagleGameController eagle, ShardokGameController shardok) {
_triggerRegistry?.Initialize(eagle, shardok);
Log($"TutorialManager initialized with controllers - Eagle: {eagle != null}, Shardok: {shardok != null}");
// Trigger game_started event when entering a game (not lobby)
if (eagle != null) { _triggerRegistry?.OnGameEvent("game_started"); }
}
/// <summary>
@@ -100,7 +103,7 @@ namespace Eagle0.Tutorial {
}
if (OnboardingSequence == null) {
Debug.LogWarning("TutorialManager: No onboarding sequence assigned");
Log("StartOnboarding: No onboarding sequence assigned, skipping");
return;
}
@@ -0,0 +1,103 @@
using UnityEngine;
namespace Eagle0.Tutorial {
/// <summary>
/// Test component that sets up a simple tutorial for end-to-end testing.
/// Attach to TutorialManager GameObject to register test tutorials on startup.
/// Can be removed once actual tutorial content is created in the Editor.
/// </summary>
public class TutorialTestSetup : MonoBehaviour {
[Header("Test Configuration")]
[Tooltip("Enable test tutorials")]
public bool EnableTestTutorials = true;
[Tooltip("Reset tutorial progress on startup (for testing)")]
public bool ResetProgressOnStart = false;
private void Start() {
if (!EnableTestTutorials) return;
var manager = TutorialManager.Instance;
if (manager == null) {
Debug.LogWarning("TutorialTestSetup: TutorialManager not found");
return;
}
if (ResetProgressOnStart) { manager.ResetAllProgress(); }
RegisterTestTutorials(manager);
Debug.Log("TutorialTestSetup: Test tutorials registered");
}
private void RegisterTestTutorials(TutorialManager manager) {
// Create welcome/intro tutorial that shows on game start
var introTutorial = CreateIntroTutorial();
manager.TriggerRegistry.RegisterTutorial(introTutorial, "game_started");
// Create a simple "first battle" tutorial
var firstBattleTutorial = CreateFirstBattleTutorial();
manager.TriggerRegistry.RegisterTutorial(firstBattleTutorial, "battle_entered");
// Create a simple "first command" tutorial
var firstCommandTutorial = CreateFirstCommandTutorial();
manager.TriggerRegistry.RegisterTutorial(firstCommandTutorial, "command_issued");
// Note: "game_started" event is triggered by EagleGameController.SetUpGame()
// via TutorialManager.Initialize(), not here
}
private TutorialSequence CreateIntroTutorial() {
var sequence = ScriptableObject.CreateInstance<TutorialSequence>();
sequence.SequenceId = "test_intro";
sequence.DisplayName = "Welcome";
sequence.Steps.Add(new TutorialStep {
StepId = "welcome",
Title = "Welcome to Eagle0!",
Description =
"Eagle0 is a strategy game where you command armies, manage provinces, and engage in tactical combat.\n\nClick on provinces to view their details and issue commands. When battles occur, you'll fight them out on a hex-based tactical map.\n\nGood luck, Commander!",
DisplayMode = TutorialDisplayMode.Modal,
CompletionType = TutorialCompletionType.ButtonClick,
AllowSkip = false
});
return sequence;
}
private TutorialSequence CreateFirstBattleTutorial() {
var sequence = ScriptableObject.CreateInstance<TutorialSequence>();
sequence.SequenceId = "test_first_battle";
sequence.DisplayName = "First Battle";
sequence.Steps.Add(new TutorialStep {
StepId = "battle_intro",
Title = "Battle Begins!",
Description =
"You've entered tactical combat. Command your units on the hex grid to defeat the enemy.\n\n(This is a test tutorial to verify the tutorial system is working.)",
DisplayMode = TutorialDisplayMode.Modal,
CompletionType = TutorialCompletionType.ButtonClick,
AllowSkip = true
});
return sequence;
}
private TutorialSequence CreateFirstCommandTutorial() {
var sequence = ScriptableObject.CreateInstance<TutorialSequence>();
sequence.SequenceId = "test_first_command";
sequence.DisplayName = "First Command";
sequence.Steps.Add(new TutorialStep {
StepId = "command_issued_intro",
Title = "Command Issued!",
Description =
"You've issued your first command. Commands are processed at the end of each turn.\n\n(This is a test tutorial to verify the tutorial system is working.)",
DisplayMode = TutorialDisplayMode.Modal,
CompletionType = TutorialCompletionType.ButtonClick,
AllowSkip = true
});
return sequence;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 370b3269832442e196d1b432bdae2f1e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,475 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
namespace Eagle0.Tutorial {
/// <summary>
/// Builds the tutorial Canvas UI programmatically at runtime.
/// Creates a complete TutorialModalPanel with all required UI elements.
/// </summary>
public static class TutorialCanvasBuilder {
// Colors matching the game's fantasy RPG style
private static readonly Color PanelBackgroundColor = new Color(0.15f, 0.12f, 0.2f, 0.95f);
private static readonly Color BorderColor = new Color(0.6f, 0.5f, 0.3f, 1f);
private static readonly Color TitleColor = new Color(1f, 0.9f, 0.7f, 1f);
private static readonly Color TextColor = new Color(0.9f, 0.85f, 0.8f, 1f);
private static readonly Color ButtonColor = new Color(0.4f, 0.3f, 0.2f, 1f);
private static readonly Color ButtonHoverColor = new Color(0.5f, 0.4f, 0.25f, 1f);
private static readonly Color ButtonTextColor = new Color(1f, 0.95f, 0.85f, 1f);
private static readonly Color ModalBlockerColor = new Color(0f, 0f, 0f, 0.75f);
private static readonly Color ProgressBarColor = new Color(0.6f, 0.5f, 0.3f, 1f);
private static readonly Color ProgressBackgroundColor = new Color(0.2f, 0.15f, 0.1f, 1f);
/// <summary>
/// Creates a complete tutorial Canvas with modal panel.
/// Returns the Canvas GameObject and sets up the TutorialModalPanel component.
/// </summary>
/// <param name="font">TextMeshPro font to use (assign Stoke-Regular-SDF in
/// inspector)</param>
public static Canvas BuildTutorialCanvas(TMP_FontAsset font) {
// Create Canvas
GameObject canvasObj = new GameObject("TutorialCanvas");
Canvas canvas = canvasObj.AddComponent<Canvas>();
canvas.renderMode = RenderMode.ScreenSpaceOverlay;
canvas.sortingOrder = 1000;
CanvasScaler scaler = canvasObj.AddComponent<CanvasScaler>();
scaler.uiScaleMode = CanvasScaler.ScaleMode.ScaleWithScreenSize;
scaler.referenceResolution = new Vector2(1920, 1080);
scaler.matchWidthOrHeight = 0.5f;
canvasObj.AddComponent<GraphicRaycaster>();
// Create Modal Blocker
GameObject blockerObj = CreateModalBlocker(canvasObj.transform);
// Create Panel Container
GameObject panelContainer = CreatePanelContainer(canvasObj.transform, font);
// Add TutorialModalPanel component and wire it up
TutorialModalPanel modalPanel = canvasObj.AddComponent<TutorialModalPanel>();
WireUpModalPanel(modalPanel, blockerObj, panelContainer, font);
// Set up button listeners (Awake runs before fields are wired, so do it here)
SetupButtonListeners(modalPanel);
// Start with everything hidden
blockerObj.SetActive(false);
panelContainer.SetActive(false);
return canvas;
}
private static GameObject CreateModalBlocker(Transform parent) {
GameObject blocker = new GameObject("ModalBlocker");
blocker.transform.SetParent(parent, false);
RectTransform rect = blocker.AddComponent<RectTransform>();
rect.anchorMin = Vector2.zero;
rect.anchorMax = Vector2.one;
rect.offsetMin = Vector2.zero;
rect.offsetMax = Vector2.zero;
Image image = blocker.AddComponent<Image>();
image.color = ModalBlockerColor;
image.raycastTarget = true;
return blocker;
}
private static GameObject CreatePanelContainer(Transform parent, TMP_FontAsset font) {
// Panel Container (centered)
GameObject panel = new GameObject("PanelContainer");
panel.transform.SetParent(parent, false);
RectTransform panelRect = panel.AddComponent<RectTransform>();
panelRect.anchorMin = new Vector2(0.5f, 0.5f);
panelRect.anchorMax = new Vector2(0.5f, 0.5f);
panelRect.sizeDelta = new Vector2(800, 550);
panelRect.anchoredPosition = Vector2.zero;
// Panel Background
Image panelBg = panel.AddComponent<Image>();
panelBg.color = PanelBackgroundColor;
panelBg.raycastTarget = true;
// Add outline/border effect
Outline outline = panel.AddComponent<Outline>();
outline.effectColor = BorderColor;
outline.effectDistance = new Vector2(3, 3);
// Add vertical layout group
VerticalLayoutGroup layout = panel.AddComponent<VerticalLayoutGroup>();
layout.padding = new RectOffset(40, 40, 30, 30);
layout.spacing = 20;
layout.childAlignment = TextAnchor.UpperCenter;
layout.childControlHeight = false;
layout.childControlWidth = true;
layout.childForceExpandHeight = false;
layout.childForceExpandWidth = true;
// Title
CreateTitle(panel.transform, font);
// Icon (placeholder - starts hidden)
CreateIcon(panel.transform);
// Description
CreateDescription(panel.transform, font);
// Flexible spacer to push buttons to bottom
CreateFlexibleSpacer(panel.transform);
// Progress section
CreateProgressSection(panel.transform, font);
// Button row
CreateButtonRow(panel.transform, font);
return panel;
}
private static void CreateTitle(Transform parent, TMP_FontAsset font) {
GameObject titleObj = new GameObject("TitleText");
titleObj.transform.SetParent(parent, false);
RectTransform rect = titleObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(720, 60);
TextMeshProUGUI text = titleObj.AddComponent<TextMeshProUGUI>();
text.text = "Tutorial";
text.fontSize = 42;
text.fontStyle = FontStyles.Bold;
text.color = TitleColor;
text.alignment = TextAlignmentOptions.Center;
if (font != null) text.font = font;
LayoutElement layoutElement = titleObj.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 60;
}
private static void CreateIcon(Transform parent) {
GameObject iconObj = new GameObject("IconImage");
iconObj.transform.SetParent(parent, false);
RectTransform rect = iconObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(100, 100);
Image image = iconObj.AddComponent<Image>();
image.color = Color.white;
image.preserveAspect = true;
LayoutElement layoutElement = iconObj.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 100;
layoutElement.preferredWidth = 100;
// Start hidden
iconObj.SetActive(false);
}
private static void CreateDescription(Transform parent, TMP_FontAsset font) {
GameObject descObj = new GameObject("DescriptionText");
descObj.transform.SetParent(parent, false);
RectTransform rect = descObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(720, 200);
TextMeshProUGUI text = descObj.AddComponent<TextMeshProUGUI>();
text.text = "";
text.fontSize = 26;
text.color = TextColor;
text.alignment = TextAlignmentOptions.TopLeft;
text.enableWordWrapping = true;
text.overflowMode = TextOverflowModes.Overflow; // Don't truncate
if (font != null) text.font = font;
LayoutElement layoutElement = descObj.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 200;
layoutElement.minHeight = 100;
}
private static void CreateSpacer(Transform parent, float height) {
GameObject spacer = new GameObject("Spacer");
spacer.transform.SetParent(parent, false);
spacer.AddComponent<RectTransform>();
LayoutElement layoutElement = spacer.AddComponent<LayoutElement>();
layoutElement.preferredHeight = height;
}
private static void CreateFlexibleSpacer(Transform parent) {
GameObject spacer = new GameObject("FlexibleSpacer");
spacer.transform.SetParent(parent, false);
spacer.AddComponent<RectTransform>();
LayoutElement layoutElement = spacer.AddComponent<LayoutElement>();
layoutElement.flexibleHeight = 1;
}
private static void CreateProgressSection(Transform parent, TMP_FontAsset font) {
GameObject progressSection = new GameObject("ProgressSection");
progressSection.transform.SetParent(parent, false);
RectTransform rect = progressSection.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(720, 50);
VerticalLayoutGroup layout = progressSection.AddComponent<VerticalLayoutGroup>();
layout.spacing = 8;
layout.childAlignment = TextAnchor.MiddleCenter;
layout.childControlHeight = false;
layout.childControlWidth = true;
LayoutElement layoutElement = progressSection.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 50;
// Progress Text
GameObject progressTextObj = new GameObject("ProgressText");
progressTextObj.transform.SetParent(progressSection.transform, false);
RectTransform textRect = progressTextObj.AddComponent<RectTransform>();
textRect.sizeDelta = new Vector2(720, 24);
TextMeshProUGUI progressText = progressTextObj.AddComponent<TextMeshProUGUI>();
progressText.text = "Step 1 of 1";
progressText.fontSize = 20;
progressText.color = TextColor;
progressText.alignment = TextAlignmentOptions.Center;
if (font != null) progressText.font = font;
LayoutElement textLayout = progressTextObj.AddComponent<LayoutElement>();
textLayout.preferredHeight = 24;
// Progress Slider
CreateProgressSlider(progressSection.transform);
}
private static void CreateProgressSlider(Transform parent) {
GameObject sliderObj = new GameObject("ProgressSlider");
sliderObj.transform.SetParent(parent, false);
RectTransform sliderRect = sliderObj.AddComponent<RectTransform>();
sliderRect.sizeDelta = new Vector2(400, 12);
Slider slider = sliderObj.AddComponent<Slider>();
slider.minValue = 0;
slider.maxValue = 1;
slider.value = 0;
slider.interactable = false;
LayoutElement sliderLayout = sliderObj.AddComponent<LayoutElement>();
sliderLayout.preferredHeight = 12;
sliderLayout.preferredWidth = 400;
// Background
GameObject bgObj = new GameObject("Background");
bgObj.transform.SetParent(sliderObj.transform, false);
RectTransform bgRect = bgObj.AddComponent<RectTransform>();
bgRect.anchorMin = Vector2.zero;
bgRect.anchorMax = Vector2.one;
bgRect.offsetMin = Vector2.zero;
bgRect.offsetMax = Vector2.zero;
Image bgImage = bgObj.AddComponent<Image>();
bgImage.color = ProgressBackgroundColor;
// Fill Area
GameObject fillArea = new GameObject("Fill Area");
fillArea.transform.SetParent(sliderObj.transform, false);
RectTransform fillAreaRect = fillArea.AddComponent<RectTransform>();
fillAreaRect.anchorMin = Vector2.zero;
fillAreaRect.anchorMax = Vector2.one;
fillAreaRect.offsetMin = Vector2.zero;
fillAreaRect.offsetMax = Vector2.zero;
// Fill
GameObject fill = new GameObject("Fill");
fill.transform.SetParent(fillArea.transform, false);
RectTransform fillRect = fill.AddComponent<RectTransform>();
fillRect.anchorMin = Vector2.zero;
fillRect.anchorMax = new Vector2(0, 1);
fillRect.offsetMin = Vector2.zero;
fillRect.offsetMax = Vector2.zero;
Image fillImage = fill.AddComponent<Image>();
fillImage.color = ProgressBarColor;
// Wire up slider
slider.fillRect = fillRect;
}
private static void SetupButtonListeners(TutorialModalPanel panel) {
// Button listeners need to be set up after wiring since Awake runs before fields exist
if (panel.ContinueButton != null) {
panel.ContinueButton.onClick.RemoveAllListeners();
panel.ContinueButton.onClick.AddListener(panel.OnContinueClicked);
}
if (panel.SkipButton != null) {
panel.SkipButton.onClick.RemoveAllListeners();
panel.SkipButton.onClick.AddListener(panel.OnSkipClicked);
}
if (panel.SkipAllButton != null) {
panel.SkipAllButton.onClick.RemoveAllListeners();
panel.SkipAllButton.onClick.AddListener(panel.OnSkipAllClicked);
}
}
private static void CreateButtonRow(Transform parent, TMP_FontAsset font) {
GameObject buttonRow = new GameObject("ButtonRow");
buttonRow.transform.SetParent(parent, false);
RectTransform rect = buttonRow.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(720, 60);
HorizontalLayoutGroup layout = buttonRow.AddComponent<HorizontalLayoutGroup>();
layout.spacing = 20;
layout.childAlignment = TextAnchor.MiddleCenter;
layout.childControlHeight = false;
layout.childControlWidth = false;
layout.childForceExpandHeight = false;
layout.childForceExpandWidth = false;
LayoutElement layoutElement = buttonRow.AddComponent<LayoutElement>();
layoutElement.preferredHeight = 60;
// Skip Button (left)
CreateButton(buttonRow.transform, "SkipButton", "Skip", 150, 50, font);
// Skip All Button
CreateButton(buttonRow.transform, "SkipAllButton", "Skip All", 150, 50, font);
// Spacer
GameObject spacer = new GameObject("ButtonSpacer");
spacer.transform.SetParent(buttonRow.transform, false);
spacer.AddComponent<RectTransform>();
LayoutElement spacerLayout = spacer.AddComponent<LayoutElement>();
spacerLayout.flexibleWidth = 1;
// Continue Button (right)
CreateButton(buttonRow.transform, "ContinueButton", "Continue", 180, 50, font);
}
private static GameObject CreateButton(
Transform parent,
string name,
string text,
float width,
float height,
TMP_FontAsset font) {
GameObject buttonObj = new GameObject(name);
buttonObj.transform.SetParent(parent, false);
RectTransform rect = buttonObj.AddComponent<RectTransform>();
rect.sizeDelta = new Vector2(width, height);
Image image = buttonObj.AddComponent<Image>();
image.color = ButtonColor;
Button button = buttonObj.AddComponent<Button>();
ColorBlock colors = button.colors;
colors.normalColor = ButtonColor;
colors.highlightedColor = ButtonHoverColor;
colors.pressedColor = ButtonHoverColor;
colors.selectedColor = ButtonHoverColor;
button.colors = colors;
// Add outline
Outline outline = buttonObj.AddComponent<Outline>();
outline.effectColor = BorderColor;
outline.effectDistance = new Vector2(2, 2);
LayoutElement layoutElement = buttonObj.AddComponent<LayoutElement>();
layoutElement.preferredWidth = width;
layoutElement.preferredHeight = height;
// Button text
GameObject textObj = new GameObject("Text");
textObj.transform.SetParent(buttonObj.transform, false);
RectTransform textRect = textObj.AddComponent<RectTransform>();
textRect.anchorMin = Vector2.zero;
textRect.anchorMax = Vector2.one;
textRect.offsetMin = Vector2.zero;
textRect.offsetMax = Vector2.zero;
TextMeshProUGUI buttonText = textObj.AddComponent<TextMeshProUGUI>();
buttonText.text = text;
buttonText.fontSize = 24;
buttonText.color = ButtonTextColor;
buttonText.alignment = TextAlignmentOptions.Center;
if (font != null) buttonText.font = font;
return buttonObj;
}
private static void WireUpModalPanel(
TutorialModalPanel panel,
GameObject blocker,
GameObject panelContainer,
TMP_FontAsset font) {
panel.ModalBlocker = blocker;
panel.PanelContainer = panelContainer;
// Find and wire up components
Transform containerTransform = panelContainer.transform;
// Title
Transform titleTransform = containerTransform.Find("TitleText");
if (titleTransform != null) {
panel.TitleText = titleTransform.GetComponent<TextMeshProUGUI>();
}
// Icon
Transform iconTransform = containerTransform.Find("IconImage");
if (iconTransform != null) { panel.IconImage = iconTransform.GetComponent<Image>(); }
// Description
Transform descTransform = containerTransform.Find("DescriptionText");
if (descTransform != null) {
panel.DescriptionText = descTransform.GetComponent<TextMeshProUGUI>();
}
// Progress Section
Transform progressSection = containerTransform.Find("ProgressSection");
if (progressSection != null) {
Transform progressTextTransform = progressSection.Find("ProgressText");
if (progressTextTransform != null) {
panel.ProgressText = progressTextTransform.GetComponent<TextMeshProUGUI>();
}
Transform sliderTransform = progressSection.Find("ProgressSlider");
if (sliderTransform != null) {
panel.ProgressSlider = sliderTransform.GetComponent<Slider>();
}
}
// Button Row
Transform buttonRow = containerTransform.Find("ButtonRow");
if (buttonRow != null) {
Transform skipTransform = buttonRow.Find("SkipButton");
if (skipTransform != null) {
panel.SkipButton = skipTransform.GetComponent<Button>();
}
Transform skipAllTransform = buttonRow.Find("SkipAllButton");
if (skipAllTransform != null) {
panel.SkipAllButton = skipAllTransform.GetComponent<Button>();
}
Transform continueTransform = buttonRow.Find("ContinueButton");
if (continueTransform != null) {
panel.ContinueButton = continueTransform.GetComponent<Button>();
Transform continueTextTransform = continueTransform.Find("Text");
if (continueTextTransform != null) {
panel.ContinueButtonText =
continueTextTransform.GetComponent<TextMeshProUGUI>();
}
}
}
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 98467fdbaf584fbb928a373340b542d7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -135,19 +135,31 @@ namespace Eagle0.Tutorial {
_onSkip = null;
}
private void OnContinueClicked() {
/// <summary>
/// Called when Continue button is clicked.
/// Public to allow external setup of button listeners.
/// </summary>
public void OnContinueClicked() {
var callback = _onContinue;
Hide();
callback?.Invoke();
}
private void OnSkipClicked() {
/// <summary>
/// Called when Skip button is clicked.
/// Public to allow external setup of button listeners.
/// </summary>
public void OnSkipClicked() {
var callback = _onSkip;
Hide();
callback?.Invoke();
}
private void OnSkipAllClicked() {
/// <summary>
/// Called when Skip All button is clicked.
/// Public to allow external setup of button listeners.
/// </summary>
public void OnSkipAllClicked() {
Hide();
TutorialManager.Instance?.SkipAllOnboarding();
}
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
namespace Eagle0.Tutorial {
@@ -22,17 +23,189 @@ namespace Eagle0.Tutorial {
[Tooltip("Canvas for tutorial UI (should be above game UI)")]
public Canvas TutorialCanvas;
[Header("Fonts")]
[Tooltip("TextMeshPro font for Canvas UI (assign Stoke-Regular-SDF)")]
public TMP_FontAsset CanvasFont;
[Header("Fallback UI")]
[Tooltip("Use IMGUI fallback when Canvas UI cannot be built")]
public bool UseFallbackUI = true;
[Tooltip("Font for IMGUI fallback (assign Stoke-Regular)")]
public Font FallbackFont;
[Header("Runtime Options")]
[Tooltip("Auto-build Canvas UI at runtime if ModalPanel is not assigned")]
public bool AutoBuildCanvasUI = true;
// Active hints
private Dictionary<string, TutorialHintIndicator> _activeHints =
new Dictionary<string, TutorialHintIndicator>();
// Fallback modal state
private bool _fallbackModalVisible;
private TutorialStep _fallbackStep;
private Action _fallbackOnContinue;
private Action _fallbackOnSkip;
private int _fallbackCurrentIndex;
private int _fallbackTotalSteps;
private bool _fallbackIsOnboarding;
private void Awake() {
// Auto-build Canvas UI if needed
if (ModalPanel == null && AutoBuildCanvasUI) { BuildCanvasUI(); }
// Ensure canvas is set up correctly
if (TutorialCanvas != null) {
TutorialCanvas.sortingOrder = 1000; // Above most game UI
}
}
/// <summary>
/// Builds the Canvas-based tutorial UI at runtime.
/// Called automatically if AutoBuildCanvasUI is true and ModalPanel is not assigned.
/// </summary>
private void BuildCanvasUI() {
TutorialCanvas = TutorialCanvasBuilder.BuildTutorialCanvas(CanvasFont);
if (TutorialCanvas != null) {
// Make canvas a child of this object so it persists with TutorialManager
TutorialCanvas.transform.SetParent(transform, false);
// Get the modal panel that was created
ModalPanel = TutorialCanvas.GetComponent<TutorialModalPanel>();
Debug.Log("TutorialUIManager: Built Canvas UI at runtime");
} else {
Debug.LogWarning("TutorialUIManager: Failed to build Canvas UI");
}
}
private void OnGUI() {
if (!_fallbackModalVisible || _fallbackStep == null) return;
// Semi-transparent background
GUI.color = new Color(0, 0, 0, 0.7f);
GUI.DrawTexture(new Rect(0, 0, Screen.width, Screen.height), Texture2D.whiteTexture);
GUI.color = Color.white;
// Modal window
float windowWidth = Mathf.Min(1200, Screen.width - 40);
float windowHeight = 600;
float windowX = (Screen.width - windowWidth) / 2;
float windowY = (Screen.height - windowHeight) / 2;
GUIStyle windowStyle = new GUIStyle(GUI.skin.window);
windowStyle.fontSize = 48;
if (FallbackFont != null) windowStyle.font = FallbackFont;
GUI.Window(
12345,
new Rect(windowX, windowY, windowWidth, windowHeight),
DrawFallbackModal,
_fallbackStep.Title ?? "Tutorial",
windowStyle);
}
private void DrawFallbackModal(int windowId) {
GUILayout.Space(60);
// Description
GUIStyle descStyle = new GUIStyle(GUI.skin.label);
descStyle.wordWrap = true;
descStyle.fontSize = 40;
if (FallbackFont != null) descStyle.font = FallbackFont;
GUILayout.Label(
_fallbackStep.Description ?? "",
descStyle,
GUILayout.ExpandHeight(true));
GUILayout.FlexibleSpace();
// Progress
if (_fallbackTotalSteps > 1) {
GUIStyle progressStyle = new GUIStyle(GUI.skin.label);
progressStyle.fontSize = 32;
if (FallbackFont != null) progressStyle.font = FallbackFont;
GUILayout.Label(
$"Step {_fallbackCurrentIndex + 1} of {_fallbackTotalSteps}",
progressStyle,
GUILayout.ExpandWidth(true));
}
GUILayout.Space(30);
// Button style
GUIStyle buttonStyle = new GUIStyle(GUI.skin.button);
buttonStyle.fontSize = 36;
if (FallbackFont != null) buttonStyle.font = FallbackFont;
// Buttons
GUILayout.BeginHorizontal();
if (_fallbackStep.AllowSkip) {
if (GUILayout.Button(
"Skip",
buttonStyle,
GUILayout.Width(200),
GUILayout.Height(80))) {
var callback = _fallbackOnSkip;
HideFallbackModal();
callback?.Invoke();
}
if (_fallbackIsOnboarding) {
if (GUILayout.Button(
"Skip All",
buttonStyle,
GUILayout.Width(200),
GUILayout.Height(80))) {
HideFallbackModal();
TutorialManager.Instance?.SkipAllOnboarding();
}
}
}
GUILayout.FlexibleSpace();
string buttonText =
_fallbackCurrentIndex >= _fallbackTotalSteps - 1 ? "Got it!" : "Continue";
if (GUILayout.Button(
buttonText,
buttonStyle,
GUILayout.Width(240),
GUILayout.Height(80))) {
var callback = _fallbackOnContinue;
HideFallbackModal();
callback?.Invoke();
}
GUILayout.EndHorizontal();
GUILayout.Space(30);
}
private void ShowFallbackModal(
TutorialStep step,
int currentIndex,
int totalSteps,
Action onContinue,
Action onSkip,
bool isOnboarding) {
_fallbackStep = step;
_fallbackCurrentIndex = currentIndex;
_fallbackTotalSteps = totalSteps;
_fallbackOnContinue = onContinue;
_fallbackOnSkip = onSkip;
_fallbackIsOnboarding = isOnboarding;
_fallbackModalVisible = true;
}
private void HideFallbackModal() {
_fallbackModalVisible = false;
_fallbackStep = null;
_fallbackOnContinue = null;
_fallbackOnSkip = null;
}
/// <summary>
/// Shows a modal dialog for a tutorial step.
/// </summary>
@@ -43,13 +216,18 @@ namespace Eagle0.Tutorial {
Action onComplete,
Action onSkip,
bool isOnboarding) {
if (ModalPanel == null) {
Debug.LogWarning("TutorialUIManager: No ModalPanel assigned");
if (ModalPanel != null) {
// Ensure canvas is active
if (TutorialCanvas != null && !TutorialCanvas.gameObject.activeSelf) {
TutorialCanvas.gameObject.SetActive(true);
}
ModalPanel.Show(step, currentIndex, totalSteps, onComplete, onSkip, isOnboarding);
} else if (UseFallbackUI) {
ShowFallbackModal(step, currentIndex, totalSteps, onComplete, onSkip, isOnboarding);
} else {
Debug.LogWarning("TutorialUIManager: No ModalPanel assigned and fallback disabled");
onComplete?.Invoke();
return;
}
ModalPanel.Show(step, currentIndex, totalSteps, onComplete, onSkip, isOnboarding);
}
/// <summary>
@@ -148,6 +326,7 @@ namespace Eagle0.Tutorial {
public void HideAll() {
ModalPanel?.Hide();
OverlayController?.HideOverlay();
HideFallbackModal();
// Note: Don't hide hints automatically - they persist until dismissed
}
@@ -277,6 +277,7 @@ namespace EagleInstaller {
async Task<FetchAttempt>
FetchAndWriteOne(string remotePath, string localDir, string expectedSha) {
string writePath = WritePathFromRemotePath(remotePath, localDir);
string tempPath = writePath + ".downloading";
Directory.CreateDirectory(Path.GetDirectoryName(writePath));
@@ -288,41 +289,67 @@ namespace EagleInstaller {
response.EnsureSuccessStatusCode();
await using Stream responseStream = await response.Content.ReadAsStreamAsync();
await using FileStream fileStream = File.Create(writePath);
using MemoryStream memStream = new MemoryStream();
await responseStream.CopyToAsync(memStream);
memStream.Position = 0;
var sha = System.Security.Cryptography.SHA256.Create();
string computedSha = BitConverter.ToString(sha.ComputeHash(memStream))
.Replace("-", string.Empty)
.ToLower();
// Stream directly to disk while computing SHA256 incrementally
using var sha = System.Security.Cryptography.SHA256.Create();
await using (FileStream fileStream = File.Create(tempPath)) {
var buffer = new byte[81920]; // 80KB buffer
int bytesRead;
while ((bytesRead = await responseStream.ReadAsync(buffer, 0, buffer.Length)) >
0) {
// Write to disk
await fileStream.WriteAsync(buffer, 0, bytesRead);
// Update hash incrementally
sha.TransformBlock(buffer, 0, bytesRead, null, 0);
}
}
// Finalize hash computation
sha.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
string computedSha =
BitConverter.ToString(sha.Hash).Replace("-", string.Empty).ToLower();
if (computedSha.Equals(expectedSha)) {
// SHA matches - rename temp file to final location
if (File.Exists(writePath)) { File.Delete(writePath); }
File.Move(tempPath, writePath);
Logger.WriteLine($"✓ Verified and saved: {remotePath}");
memStream.Position = 0;
await memStream.CopyToAsync(fileStream);
return new FetchAttempt {
Success = true,
Kvp = new KeyValuePair<String, String>(remotePath, computedSha)
};
} else {
// SHA mismatch - delete temp file
try {
File.Delete(tempPath);
} catch {}
Logger.WriteError($"✗ SHA mismatch for {remotePath}");
return new FetchAttempt() { Success = false };
}
} catch (TaskCanceledException e) {
// Clean up temp file on failure
try {
File.Delete(tempPath);
} catch {}
Logger.WriteError($"Download cancelled: {e.Message}");
return new FetchAttempt() { Success = false };
} catch (HttpRequestException e) {
try {
File.Delete(tempPath);
} catch {}
Logger.WriteError($"Network error: {e.Message}");
return new FetchAttempt() { Success = false };
} catch (Exception e) {
try {
File.Delete(tempPath);
} catch {}
Logger.WriteError($"Download failed: {e.Message}");
return new FetchAttempt() { Success = false };
} finally { Pool.Release(); }
}
public async Task<bool> DownloadAndLaunchNewInstaller(string installerUrl) {
public async Task<bool> DownloadAndLaunchNewInstaller(
string installerUrl,
string expectedSha) {
try {
Logger.UpdateStatus("Downloading new installer...");
@@ -372,6 +399,28 @@ namespace EagleInstaller {
}
}
// Verify SHA256 before launching
Logger.UpdateStatus("Verifying installer integrity...");
string computedSha;
using (var sha256 = System.Security.Cryptography.SHA256.Create()) using (
var stream = File.OpenRead(newInstallerPath)) {
var hashBytes = sha256.ComputeHash(stream);
computedSha =
BitConverter.ToString(hashBytes).Replace("-", string.Empty).ToLower();
}
if (!computedSha.Equals(expectedSha, StringComparison.OrdinalIgnoreCase)) {
Logger.WriteError($"SECURITY ERROR: Installer SHA mismatch!");
Logger.WriteError($" Expected: {expectedSha}");
Logger.WriteError($" Got: {computedSha}");
// Delete the corrupted/tampered file
try {
File.Delete(newInstallerPath);
} catch {}
return false;
}
Logger.WriteLine($"✓ Installer verified: SHA256 matches");
// Wait a moment to ensure file handles are released
await Task.Delay(500);
@@ -110,7 +110,8 @@ namespace EagleInstaller {
if (installerUpdateInfo.InstallerNeedsUpdate) {
Logger.UpdateStatus("Installer update required...");
bool downloadSuccess = await updater.DownloadAndLaunchNewInstaller(
installerUpdateInfo.NewInstallerUrl);
installerUpdateInfo.NewInstallerUrl,
installerUpdateInfo.NewInstallerVersion);
if (downloadSuccess) {
Logger.UpdateStatus("New installer launched. Exiting current version.");
@@ -1666,6 +1666,8 @@ func handleUserRoutes(w http.ResponseWriter, r *http.Request) {
handleSetUserDisplayName(w, r, userID)
case "set-admin":
handleSetUserAdmin(w, r, userID)
case "delete":
handleDeleteUser(w, r, userID)
default:
http.NotFound(w, r)
}
@@ -1818,6 +1820,38 @@ func handleSetUserAdmin(w http.ResponseWriter, r *http.Request, userID string) {
fmt.Fprintf(w, `<div class="alert alert-success">Admin status %s</div>`, status)
}
func handleDeleteUser(w http.ResponseWriter, r *http.Request, userID string) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
ctx, cancel := createAdminContext(r)
defer cancel()
resp, err := adminClient.DeleteUser(ctx, &adminpb.DeleteUserRequest{
UserId: userID,
})
if err != nil {
log.Printf("Failed to delete user: %v", err)
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintf(w, `<div class="alert alert-error">Error: %v</div>`, err)
return
}
if !resp.Success {
w.Header().Set("Content-Type", "text/html")
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, `<div class="alert alert-error">%s</div>`, resp.ErrorMessage)
return
}
w.Header().Set("HX-Trigger", "userUpdated")
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `<div class="alert alert-success">User deleted</div>`)
}
// Invitation management handlers
// InvitationInfo represents an invitation for display
@@ -1971,6 +2005,8 @@ func handleInvitationRoutes(w http.ResponseWriter, r *http.Request) {
handleResendInvitation(w, r, code)
case "revoke":
handleRevokeInvitation(w, r, code)
case "delete":
handleDeleteInvitation(w, r, code)
default:
http.NotFound(w, r)
}
@@ -2141,6 +2177,36 @@ func handleRevokeInvitation(w http.ResponseWriter, r *http.Request, code string)
fmt.Fprint(w, `<div class="alert alert-success">Invitation revoked</div>`)
}
func handleDeleteInvitation(w http.ResponseWriter, r *http.Request, code string) {
if r.Method != http.MethodPost {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
ctx, cancel := createAdminContext(r)
defer cancel()
resp, err := adminClient.DeleteInvitation(ctx, &adminpb.DeleteInvitationRequest{
InvitationCode: code,
})
if err != nil {
log.Printf("Failed to delete invitation: %v", err)
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, `<div class="alert alert-error">Error: %v</div>`, err)
return
}
if !resp.Success {
w.Header().Set("Content-Type", "text/html")
fmt.Fprintf(w, `<div class="alert alert-error">%s</div>`, resp.ErrorMessage)
return
}
w.Header().Set("HX-Trigger", "invitationUpdated")
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, `<div class="alert alert-success">Invitation deleted</div>`)
}
// Authentication handlers
const (
@@ -83,7 +83,13 @@
Copy Code
</button>
{{else}}
<span class="no-actions">-</span>
<button class="btn-small btn-danger"
hx-post="/invitations/{{.InvitationCode}}/delete"
hx-target="#feedback"
hx-swap="innerHTML"
hx-confirm="Are you sure you want to permanently delete this invitation?">
Delete
</button>
{{end}}
</td>
</tr>
@@ -152,7 +158,8 @@ document.body.addEventListener('htmx:afterRequest', function(evt) {
}, 1500);
}
} else if (evt.detail.pathInfo.requestPath.includes('/revoke') ||
evt.detail.pathInfo.requestPath.includes('/resend')) {
evt.detail.pathInfo.requestPath.includes('/resend') ||
evt.detail.pathInfo.requestPath.includes('/delete')) {
if (evt.detail.successful) {
htmx.trigger(document.getElementById('invitations-table-body'), 'invitationUpdated');
}
@@ -32,7 +32,13 @@
Copy Code
</button>
{{else}}
<span class="no-actions">-</span>
<button class="btn-small btn-danger"
hx-post="/invitations/{{.InvitationCode}}/delete"
hx-target="#feedback"
hx-swap="innerHTML"
hx-confirm="Are you sure you want to permanently delete this invitation?">
Delete
</button>
{{end}}
</td>
</tr>
@@ -65,6 +65,13 @@
onclick="openEditModal('{{.UserID}}', '{{.DisplayName}}', {{.IsAdmin}})">
Edit
</button>
<button class="btn-small btn-danger"
hx-post="/users/{{.UserID}}/delete"
hx-target="#feedback"
hx-swap="innerHTML"
hx-confirm="Are you sure you want to permanently delete this user? This cannot be undone.">
Delete
</button>
</td>
</tr>
{{end}}
@@ -259,6 +266,22 @@ function saveUserChanges() {
font-size: 0.85rem;
}
.btn-danger {
background-color: #dc3545;
border-color: #dc3545;
color: white;
}
.btn-danger:hover {
background-color: #c82333;
border-color: #bd2130;
}
.user-actions {
display: flex;
gap: 0.25rem;
}
#edit-modal article {
max-width: 500px;
}
@@ -30,6 +30,13 @@
onclick="openEditModal('{{.UserID}}', '{{.DisplayName}}', {{.IsAdmin}})">
Edit
</button>
<button class="btn-small btn-danger"
hx-post="/users/{{.UserID}}/delete"
hx-target="#feedback"
hx-swap="innerHTML"
hx-confirm="Are you sure you want to permanently delete this user? This cannot be undone.">
Delete
</button>
</td>
</tr>
{{end}}
@@ -5,6 +5,7 @@ go_library(
srcs = [
"admin_handlers.go",
"handlers.go",
"invitation_handlers.go",
"invitations.go",
"jwt.go",
"main.go",
@@ -350,3 +350,51 @@ func (h *AdminHandler) internalStatusToAdmin(status userpb.InvitationStatus) adm
return adminpb.InvitationStatus_INVITATION_STATUS_UNSPECIFIED
}
}
// DeleteUser permanently deletes a user
func (h *AdminHandler) DeleteUser(ctx context.Context, req *adminpb.DeleteUserRequest) (*adminpb.DeleteUserResponse, error) {
adminUserID, err := h.requireAdmin(ctx)
if err != nil {
return nil, err
}
log.Printf("[Admin] DeleteUser called by %s for user %s", adminUserID, req.UserId)
// Prevent self-deletion
if req.UserId == adminUserID {
return &adminpb.DeleteUserResponse{
Success: false,
ErrorMessage: "cannot delete your own account",
}, nil
}
if err := h.userService.Delete(req.UserId); err != nil {
return &adminpb.DeleteUserResponse{
Success: false,
ErrorMessage: err.Error(),
}, nil
}
return &adminpb.DeleteUserResponse{
Success: true,
}, nil
}
// DeleteInvitation permanently deletes an invitation (only non-pending)
func (h *AdminHandler) DeleteInvitation(ctx context.Context, req *adminpb.DeleteInvitationRequest) (*adminpb.DeleteInvitationResponse, error) {
adminUserID, err := h.requireAdmin(ctx)
if err != nil {
return nil, err
}
log.Printf("[Admin] DeleteInvitation called by %s for code=%s...", adminUserID, req.InvitationCode[:8])
if err := h.invitationService.Delete(req.InvitationCode); err != nil {
return &adminpb.DeleteInvitationResponse{
Success: false,
ErrorMessage: err.Error(),
}, nil
}
return &adminpb.DeleteInvitationResponse{
Success: true,
}, nil
}
@@ -0,0 +1,481 @@
// Package main provides HTTP handlers for invitation landing pages.
package main
import (
"fmt"
"html/template"
"log"
"net/http"
"os"
"strings"
"time"
userpb "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/internal/user"
)
// InvitationHTTPHandler handles HTTP requests for invitation pages
type InvitationHTTPHandler struct {
invitationService *InvitationService
installerURL string
macInstallerURL string
}
// NewInvitationHTTPHandler creates a new invitation HTTP handler
func NewInvitationHTTPHandler(invitationService *InvitationService) *InvitationHTTPHandler {
installerURL := os.Getenv("INSTALLER_DOWNLOAD_URL")
if installerURL == "" {
installerURL = "https://assets.eagle0.net/installer/EagleInstaller.exe"
}
macInstallerURL := os.Getenv("MAC_INSTALLER_URL")
if macInstallerURL == "" {
macInstallerURL = "https://assets.eagle0.net/mac/builds/eagle0-latest.zip"
}
return &InvitationHTTPHandler{
invitationService: invitationService,
installerURL: installerURL,
macInstallerURL: macInstallerURL,
}
}
// RegisterRoutes registers the invitation HTTP routes
func (h *InvitationHTTPHandler) RegisterRoutes() {
http.HandleFunc("/invite/", h.handleInvite)
}
// handleInvite routes requests to the appropriate handler
func (h *InvitationHTTPHandler) handleInvite(w http.ResponseWriter, r *http.Request) {
// Parse path: /invite/{code} or /invite/{code}/install.bat or /invite/{code}/install.sh
path := strings.TrimPrefix(r.URL.Path, "/invite/")
parts := strings.Split(path, "/")
if len(parts) == 0 || parts[0] == "" {
http.Error(w, "Invalid invitation URL", http.StatusBadRequest)
return
}
code := parts[0]
if len(parts) == 2 && parts[1] == "install.bat" {
h.handleInstallBat(w, r, code)
} else if len(parts) == 2 && parts[1] == "install.sh" {
h.handleInstallSh(w, r, code)
} else if len(parts) == 1 {
h.handleLandingPage(w, r, code)
} else {
http.Error(w, "Not found", http.StatusNotFound)
}
}
// isMacUserAgent returns true if the User-Agent indicates a Mac browser
func isMacUserAgent(userAgent string) bool {
ua := strings.ToLower(userAgent)
return strings.Contains(ua, "macintosh") || strings.Contains(ua, "mac os x")
}
// handleLandingPage serves the invitation landing page
func (h *InvitationHTTPHandler) handleLandingPage(w http.ResponseWriter, r *http.Request, code string) {
invitation := h.invitationService.FindByCode(code)
isMac := isMacUserAgent(r.UserAgent())
data := struct {
Valid bool
Code string
ExpiresAt string
ErrorMessage string
InstallBatURL string
InstallerURL string
IsMac bool
InstallShURL string
MacInstallerURL string
}{
Code: code,
InstallBatURL: fmt.Sprintf("/invite/%s/install.bat", code),
InstallerURL: h.installerURL,
IsMac: isMac,
InstallShURL: fmt.Sprintf("/invite/%s/install.sh", code),
MacInstallerURL: h.macInstallerURL,
}
if invitation == nil {
data.Valid = false
data.ErrorMessage = "This invitation code is invalid or does not exist."
} else {
switch invitation.Status {
case userpb.InvitationStatus_INVITATION_STATUS_PENDING:
// Check if expired by date
if invitation.ExpiresAt != nil && invitation.ExpiresAt.AsTime().Before(time.Now()) {
data.Valid = false
data.ErrorMessage = "This invitation has expired."
} else {
data.Valid = true
if invitation.ExpiresAt != nil {
data.ExpiresAt = invitation.ExpiresAt.AsTime().Format("January 2, 2006")
}
}
case userpb.InvitationStatus_INVITATION_STATUS_REDEEMED:
data.Valid = false
data.ErrorMessage = "This invitation has already been used."
case userpb.InvitationStatus_INVITATION_STATUS_EXPIRED:
data.Valid = false
data.ErrorMessage = "This invitation has expired."
case userpb.InvitationStatus_INVITATION_STATUS_REVOKED:
data.Valid = false
data.ErrorMessage = "This invitation has been revoked."
default:
data.Valid = false
data.ErrorMessage = "This invitation is not valid."
}
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
if err := landingPageTemplate.Execute(w, data); err != nil {
log.Printf("[InviteHTTP] Failed to render landing page: %v", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
}
}
// handleInstallBat serves the Windows batch installer script
func (h *InvitationHTTPHandler) handleInstallBat(w http.ResponseWriter, r *http.Request, code string) {
// Validate the code exists and is valid
if !h.invitationService.IsValidCode(code) {
http.Error(w, "Invalid or expired invitation code", http.StatusNotFound)
return
}
// Generate batch script
script := fmt.Sprintf(`@echo off
echo.
echo ====================================
echo Eagle0 Installer
echo ====================================
echo.
echo Downloading Eagle0 installer...
echo.
curl -L -o "%%TEMP%%\EagleInstaller.exe" "%s"
if %%ERRORLEVEL%% neq 0 (
echo.
echo Failed to download installer. Please check your internet connection.
echo You can also download manually from: %s
echo.
pause
exit /b 1
)
echo.
echo Starting installer...
echo.
start "" "%%TEMP%%\EagleInstaller.exe" --code=%s
echo.
echo The installer should now be running.
echo You can close this window.
echo.
timeout /t 5
`, h.installerURL, h.installerURL, code)
w.Header().Set("Content-Type", "application/x-bat")
w.Header().Set("Content-Disposition", "attachment; filename=eagle0-install.bat")
w.Write([]byte(script))
log.Printf("[InviteHTTP] Served install.bat for code %s...", code[:8])
}
// handleInstallSh serves the Mac shell installer script
func (h *InvitationHTTPHandler) handleInstallSh(w http.ResponseWriter, r *http.Request, code string) {
// Validate the code exists and is valid
if !h.invitationService.IsValidCode(code) {
http.Error(w, "Invalid or expired invitation code", http.StatusNotFound)
return
}
// Generate shell script
script := fmt.Sprintf(`#!/bin/bash
#
# Eagle0 Mac Installer
# This script downloads Eagle0 and sets up your invitation code.
#
set -e
echo ""
echo "===================================="
echo " Eagle0 Mac Installer"
echo "===================================="
echo ""
# Create app support directory
APP_SUPPORT_DIR="$HOME/Library/Application Support/eagle0"
mkdir -p "$APP_SUPPORT_DIR"
# Write invitation code
echo "Setting up invitation code..."
cat > "$APP_SUPPORT_DIR/invitation.json" << 'INVITATION_EOF'
{"invitationCode": "%s"}
INVITATION_EOF
echo "Invitation code saved."
echo ""
# Download location
DOWNLOAD_DIR="$HOME/Downloads"
ZIP_PATH="$DOWNLOAD_DIR/eagle0.zip"
APP_PATH="/Applications/eagle0.app"
echo "Downloading Eagle0..."
curl -L -o "$ZIP_PATH" "%s"
echo ""
echo "Extracting to /Applications..."
# Remove old version if exists
if [ -d "$APP_PATH" ]; then
echo "Removing previous version..."
rm -rf "$APP_PATH"
fi
# Unzip to Applications
unzip -q "$ZIP_PATH" -d /Applications/
# Clean up zip
rm "$ZIP_PATH"
# Remove quarantine attribute (app is notarized but downloaded via curl)
xattr -dr com.apple.quarantine "$APP_PATH" 2>/dev/null || true
echo ""
echo "===================================="
echo " Installation Complete!"
echo "===================================="
echo ""
echo "Eagle0 has been installed to /Applications/eagle0.app"
echo "Your invitation code has been saved."
echo ""
echo "Starting Eagle0..."
open "$APP_PATH"
`, code, h.macInstallerURL)
w.Header().Set("Content-Type", "application/x-sh")
w.Header().Set("Content-Disposition", "attachment; filename=eagle0-install.sh")
w.Write([]byte(script))
log.Printf("[InviteHTTP] Served install.sh for code %s...", code[:8])
}
var landingPageTemplate = template.Must(template.New("landing").Parse(`<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Eagle0 Invitation</title>
<style>
* { box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
background: #f5f5f5;
min-height: 100vh;
}
.container {
background: white;
border-radius: 12px;
padding: 40px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
margin-top: 40px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.logo {
font-size: 48px;
font-weight: bold;
color: #1a5f7a;
margin-bottom: 10px;
}
h1 {
font-size: 24px;
color: #333;
margin: 0;
}
.error-box {
background: #fee;
border: 1px solid #fcc;
border-radius: 8px;
padding: 20px;
text-align: center;
color: #c00;
}
.content {
text-align: center;
}
.button {
display: inline-block;
background: #1a5f7a;
color: white !important;
text-decoration: none;
padding: 16px 32px;
border-radius: 8px;
font-weight: bold;
font-size: 18px;
margin: 20px 0;
transition: background 0.2s;
}
.button:hover {
background: #154d62;
}
.instructions {
background: #f8f9fa;
border-radius: 8px;
padding: 20px;
margin: 20px 0;
text-align: left;
}
.instructions h3 {
margin-top: 0;
color: #1a5f7a;
}
.instructions ol {
margin: 0;
padding-left: 20px;
}
.instructions li {
margin: 10px 0;
}
.code-box {
background: #e9ecef;
border: 1px solid #ced4da;
border-radius: 4px;
padding: 12px;
font-family: monospace;
font-size: 14px;
word-break: break-all;
margin: 15px 0;
}
.manual-section {
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.manual-section h3 {
color: #666;
font-size: 14px;
}
.expires {
color: #856404;
background: #fff3cd;
padding: 10px;
border-radius: 4px;
margin-top: 20px;
font-size: 14px;
}
.footer {
text-align: center;
color: #999;
font-size: 12px;
margin-top: 30px;
}
code {
background: #e9ecef;
padding: 2px 6px;
border-radius: 4px;
font-family: monospace;
font-size: 13px;
}
.other-platform {
margin-top: 20px;
text-align: center;
color: #666;
}
.other-platform a {
color: #1a5f7a;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">Eagle0</div>
{{if .Valid}}
<h1>You've been invited!</h1>
{{else}}
<h1>Invitation Error</h1>
{{end}}
</div>
{{if .Valid}}
<div class="content">
<p>You've been invited to play Eagle0, a strategic turn-based game with tactical hex-based combat.</p>
{{if .IsMac}}
<a href="{{.InstallShURL}}" class="button">Download for Mac</a>
<div class="instructions">
<h3>How to install on Mac:</h3>
<ol>
<li>Click the "Download for Mac" button above</li>
<li>Open Terminal and navigate to your Downloads folder:<br>
<code>cd ~/Downloads</code></li>
<li>Run the installer script:<br>
<code>bash eagle0-install.sh</code></li>
<li>Eagle0 will be installed to /Applications and launched automatically</li>
</ol>
</div>
{{else}}
<a href="{{.InstallBatURL}}" class="button">Download for Windows</a>
<div class="instructions">
<h3>How to install on Windows:</h3>
<ol>
<li>Click the "Download for Windows" button above</li>
<li>Open the downloaded <strong>eagle0-install.bat</strong> file</li>
<li>If Windows shows a security prompt, click "More info" then "Run anyway"</li>
<li>The installer will download and start automatically with your invitation code</li>
</ol>
</div>
{{end}}
{{if .ExpiresAt}}
<div class="expires">
This invitation expires on {{.ExpiresAt}}.
</div>
{{end}}
<div class="manual-section">
{{if .IsMac}}
<h3>Alternative: Manual Installation</h3>
<p>If the script doesn't work, you can <a href="{{.MacInstallerURL}}">download the ZIP manually</a>, extract it to /Applications, and enter this code when prompted:</p>
{{else}}
<h3>Alternative: Manual Installation</h3>
<p>If the automatic installer doesn't work, you can <a href="{{.InstallerURL}}">download the installer manually</a> and enter this code when prompted:</p>
{{end}}
<div class="code-box">{{.Code}}</div>
</div>
<div class="other-platform">
{{if .IsMac}}
<p><small>Looking for Windows? <a href="{{.InstallBatURL}}">Download Windows installer</a></small></p>
{{else}}
<p><small>Looking for Mac? <a href="{{.InstallShURL}}">Download Mac installer</a></small></p>
{{end}}
</div>
</div>
{{else}}
<div class="error-box">
<p>{{.ErrorMessage}}</p>
<p>Please contact the person who invited you for a new invitation.</p>
</div>
{{end}}
<div class="footer">
<p>&copy; 2026 Eagle0</p>
</div>
</div>
</body>
</html>
`))
@@ -362,3 +362,44 @@ func (is *InvitationService) GetStats() (pending, redeemed, expired, revoked int
}
return
}
// Delete permanently removes an invitation from the database.
// Only revoked, expired, or redeemed invitations can be deleted.
func (is *InvitationService) Delete(code string) error {
is.mu.Lock()
defer is.mu.Unlock()
idx, ok := is.database.CodeIndex[code]
if !ok || int(idx) >= len(is.database.Invitations) {
return fmt.Errorf("invitation not found")
}
invitation := is.database.Invitations[idx]
// Only allow deletion of non-pending invitations
if invitation.Status == userpb.InvitationStatus_INVITATION_STATUS_PENDING {
// Check if actually expired
if invitation.ExpiresAt == nil || !invitation.ExpiresAt.AsTime().Before(time.Now()) {
return fmt.Errorf("cannot delete a pending invitation (revoke it first)")
}
}
// Remove from code index
delete(is.database.CodeIndex, code)
// Remove from invitations slice
is.database.Invitations = append(is.database.Invitations[:idx], is.database.Invitations[idx+1:]...)
// Rebuild code index since array indices have shifted
is.database.CodeIndex = make(map[string]int32)
for i, inv := range is.database.Invitations {
is.database.CodeIndex[inv.InvitationCode] = int32(i)
}
if err := is.save(); err != nil {
return fmt.Errorf("failed to save: %w", err)
}
log.Printf("[Invitation] Deleted invitation for %s (code %s...)", invitation.Email, code[:8])
return nil
}
+5 -1
View File
@@ -144,13 +144,17 @@ func main() {
}
}()
// Start HTTP server for OAuth callbacks
// Start HTTP server for OAuth callbacks and invitation pages
http.HandleFunc("/oauth/callback", oauthSvc.HandleCallback)
http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "OK")
})
// Register invitation landing page routes
inviteHandler := NewInvitationHTTPHandler(invitationService)
inviteHandler.RegisterRoutes()
log.Printf("HTTP server listening on port %d", actualHTTPPort)
if err := http.ListenAndServe(fmt.Sprintf(":%d", actualHTTPPort), nil); err != nil {
log.Fatalf("HTTP server failed: %v", err)
+35 -61
View File
@@ -9,7 +9,6 @@ import (
"io"
"log"
"net/http"
"net/url"
"os"
"time"
@@ -23,12 +22,13 @@ const (
// EmailService handles sending emails via Fastmail JMAP API
type EmailService struct {
apiToken string
fromEmail string
fromName string
installerURL string
httpClient *http.Client
enabled bool
apiToken string
fromEmail string
fromName string
installerURL string
serverBaseURL string
httpClient *http.Client
enabled bool
// JMAP session info (populated on first send)
accountID string
@@ -43,6 +43,7 @@ func NewEmailService() *EmailService {
fromEmail := os.Getenv("FASTMAIL_FROM_EMAIL")
fromName := os.Getenv("FASTMAIL_FROM_NAME")
installerURL := os.Getenv("INSTALLER_DOWNLOAD_URL")
serverBaseURL := os.Getenv("SERVER_BASE_URL")
if fromEmail == "" {
fromEmail = "noreply@eagle0.net"
@@ -53,6 +54,9 @@ func NewEmailService() *EmailService {
if installerURL == "" {
installerURL = "https://assets.eagle0.net/installer/EagleInstaller.exe"
}
if serverBaseURL == "" {
serverBaseURL = "https://prod.eagle0.net"
}
enabled := apiToken != ""
if !enabled {
@@ -62,10 +66,11 @@ func NewEmailService() *EmailService {
}
return &EmailService{
apiToken: apiToken,
fromEmail: fromEmail,
fromName: fromName,
installerURL: installerURL,
apiToken: apiToken,
fromEmail: fromEmail,
fromName: fromName,
installerURL: installerURL,
serverBaseURL: serverBaseURL,
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
@@ -92,20 +97,17 @@ func (e *EmailService) SendInvitation(invitation *userpb.Invitation) error {
}
}
// Build download URL with invitation code
downloadURL, err := e.buildDownloadURL(invitation.InvitationCode)
if err != nil {
return fmt.Errorf("failed to build download URL: %w", err)
}
// Build invite page URL
invitePageURL := fmt.Sprintf("%s/invite/%s", e.serverBaseURL, invitation.InvitationCode)
// Build email content
subject := "You've been invited to play Eagle0!"
htmlContent, err := e.buildInvitationHTML(invitation, downloadURL)
htmlContent, err := e.buildInvitationHTML(invitation, invitePageURL)
if err != nil {
return fmt.Errorf("failed to build email content: %w", err)
}
textContent := e.buildInvitationText(invitation, downloadURL)
textContent := e.buildInvitationText(invitation, invitePageURL)
// Send via JMAP
if err := e.sendEmail(invitation.Email, subject, htmlContent, textContent); err != nil {
@@ -243,18 +245,7 @@ func (e *EmailService) fetchIdentityAndDrafts() error {
return nil
}
func (e *EmailService) buildDownloadURL(code string) (string, error) {
u, err := url.Parse(e.installerURL)
if err != nil {
return "", err
}
q := u.Query()
q.Set("code", code)
u.RawQuery = q.Encode()
return u.String(), nil
}
func (e *EmailService) buildInvitationHTML(invitation *userpb.Invitation, downloadURL string) (string, error) {
func (e *EmailService) buildInvitationHTML(invitation *userpb.Invitation, invitePageURL string) (string, error) {
const htmlTemplate = `<!DOCTYPE html>
<html>
<head>
@@ -268,11 +259,8 @@ func (e *EmailService) buildInvitationHTML(invitation *userpb.Invitation, downlo
.content { background: #f8f9fa; border-radius: 8px; padding: 30px; margin: 20px 0; }
.button { display: inline-block; background: #1a5f7a; color: white !important; text-decoration: none; padding: 14px 28px; border-radius: 6px; font-weight: bold; margin: 20px 0; }
.button:hover { background: #154d62; }
.code-box { background: #e9ecef; border: 1px solid #ced4da; border-radius: 4px; padding: 12px; font-family: monospace; font-size: 14px; word-break: break-all; margin: 15px 0; }
.footer { text-align: center; color: #6c757d; font-size: 12px; margin-top: 30px; }
.expires { color: #856404; background: #fff3cd; padding: 10px; border-radius: 4px; margin-top: 15px; }
.section-title { font-weight: bold; margin-top: 20px; margin-bottom: 10px; }
.option { margin-bottom: 20px; }
</style>
</head>
<body>
@@ -283,24 +271,20 @@ func (e *EmailService) buildInvitationHTML(invitation *userpb.Invitation, downlo
<h2>You've been invited!</h2>
<p>Someone has invited you to join Eagle0, a strategic turn-based game with tactical hex-based combat.</p>
<div class="option">
<p class="section-title">Option 1: One-Click Install (Recommended)</p>
<p>Copy and paste this command into PowerShell to download and run the installer with your invitation code:</p>
<div class="code-box">powershell -Command "Invoke-WebRequest '{{.InstallerURL}}' -OutFile EagleInstaller.exe; .\EagleInstaller.exe --code={{.InvitationCode}}"</div>
</div>
<p style="text-align: center;">
<a href="{{.InvitePageURL}}" class="button">Accept Invitation</a>
</p>
<div class="option">
<p class="section-title">Option 2: Manual Download</p>
<p style="text-align: center;">
<a href="{{.DownloadURL}}" class="button">Download Eagle0</a>
</p>
<p>After downloading, enter this invitation code when prompted in the game:</p>
<div class="code-box">{{.InvitationCode}}</div>
</div>
<p style="text-align: center; color: #666;">Click the button above to download and install Eagle0.</p>
<div class="expires">
This invitation expires on {{.ExpiresAt}}.
</div>
<p style="margin-top: 20px; font-size: 12px; color: #999;">
If the button doesn't work, copy this link into your browser:<br>
<a href="{{.InvitePageURL}}" style="color: #1a5f7a;">{{.InvitePageURL}}</a>
</p>
</div>
<div class="footer">
<p>This is an automated message. If you didn't expect this invitation, you can safely ignore it.</p>
@@ -321,10 +305,8 @@ func (e *EmailService) buildInvitationHTML(invitation *userpb.Invitation, downlo
}
data := map[string]string{
"DownloadURL": downloadURL,
"InstallerURL": e.installerURL,
"InvitationCode": invitation.InvitationCode,
"ExpiresAt": expiresAt,
"InvitePageURL": invitePageURL,
"ExpiresAt": expiresAt,
}
var buf bytes.Buffer
@@ -335,7 +317,7 @@ func (e *EmailService) buildInvitationHTML(invitation *userpb.Invitation, downlo
return buf.String(), nil
}
func (e *EmailService) buildInvitationText(invitation *userpb.Invitation, downloadURL string) string {
func (e *EmailService) buildInvitationText(invitation *userpb.Invitation, invitePageURL string) string {
var expiresAt string
if invitation.ExpiresAt != nil {
expiresAt = invitation.ExpiresAt.AsTime().Format("January 2, 2006")
@@ -347,22 +329,14 @@ func (e *EmailService) buildInvitationText(invitation *userpb.Invitation, downlo
Someone has invited you to join Eagle0, a strategic turn-based game with tactical hex-based combat.
OPTION 1: One-Click Install (Recommended)
Copy and paste this command into PowerShell:
powershell -Command "Invoke-WebRequest '%s' -OutFile EagleInstaller.exe; .\EagleInstaller.exe --code=%s"
OPTION 2: Manual Download
Download the game: %s
After downloading, enter this invitation code when prompted in the game:
Click here to accept your invitation and download the game:
%s
This invitation expires on %s.
---
This is an automated message. If you didn't expect this invitation, you can safely ignore it.
`, e.installerURL, invitation.InvitationCode, downloadURL, invitation.InvitationCode, expiresAt)
`, invitePageURL, expiresAt)
}
// sendEmail sends an email via Fastmail JMAP API
@@ -466,3 +466,45 @@ func (us *UserService) SetAdmin(userID string, isAdmin bool) error {
}
return fmt.Errorf("user not found")
}
// Delete permanently removes a user from the database
func (us *UserService) Delete(userID string) error {
us.mu.Lock()
defer us.mu.Unlock()
// Find user index
var userIndex = -1
var user *userpb.User
for i, u := range us.database.Users {
if u.UserId == userID {
userIndex = i
user = u
break
}
}
if userIndex == -1 {
return fmt.Errorf("user not found")
}
// Remove from OAuth index
for _, identity := range user.OauthIdentities {
oauthKey := fmt.Sprintf("%s:%s", identity.Provider, identity.ProviderUserId)
delete(us.database.OauthIndex, oauthKey)
}
// Remove from display name index
if user.DisplayNameLower != "" {
delete(us.database.DisplayNameIndex, user.DisplayNameLower)
}
// Remove from users slice
us.database.Users = append(us.database.Users[:userIndex], us.database.Users[userIndex+1:]...)
if err := us.save(); err != nil {
return fmt.Errorf("failed to save: %w", err)
}
log.Printf("[UserService] Deleted user %s (%s)", userID, user.DisplayName)
return nil
}
@@ -0,0 +1,15 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "mac_build_handler_lib",
srcs = ["mac_build_handler.go"],
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/build/mac_build_handler",
visibility = ["//visibility:private"],
deps = ["//src/main/go/net/eagle0/util/aws"],
)
go_binary(
name = "mac_build_handler",
embed = [":mac_build_handler_lib"],
visibility = ["//visibility:public"],
)
@@ -0,0 +1,351 @@
package main
import (
"archive/zip"
"crypto/sha256"
"encoding/xml"
"fmt"
"io"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/nolen777/eagle0/src/main/go/net/eagle0/util/aws"
)
var bucketName = "eagle0-windows" // Using existing bucket with /mac/ prefix
var appcastPath = "mac/appcast.xml"
var buildsRoot = "mac/builds/"
// Sparkle Appcast XML structures
type Appcast struct {
XMLName xml.Name `xml:"rss"`
Version string `xml:"version,attr"`
Xmlns string `xml:"xmlns:sparkle,attr"`
Channel Channel `xml:"channel"`
}
type Channel struct {
Title string `xml:"title"`
Link string `xml:"link"`
Description string `xml:"description"`
Language string `xml:"language"`
Items []Item `xml:"item"`
}
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"`
}
type Enclosure struct {
URL string `xml:"url,attr"`
Length int64 `xml:"length,attr"`
Type string `xml:"type,attr"`
EdSig string `xml:"sparkle:edSignature,attr"`
}
func zipApp(appPath string, zipPath string) error {
zipFile, err := os.Create(zipPath)
if err != nil {
return fmt.Errorf("failed to create zip file: %w", err)
}
defer zipFile.Close()
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
appBase := filepath.Base(appPath)
// Use WalkDir with Lstat to properly handle symlinks
err = filepath.WalkDir(appPath, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}
// Get the relative path from the app's parent directory
relPath, err := filepath.Rel(filepath.Dir(appPath), path)
if err != nil {
return err
}
// Skip if it's the root
if relPath == appBase && d.IsDir() {
return nil
}
// Use Lstat to get info without following symlinks
info, err := os.Lstat(path)
if err != nil {
return err
}
// Check if it's a symlink
if info.Mode()&os.ModeSymlink != 0 {
// Read the symlink target
linkTarget, err := os.Readlink(path)
if err != nil {
return err
}
// Create symlink entry in zip
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
header.Name = relPath
header.Method = zip.Store
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
_, err = writer.Write([]byte(linkTarget))
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
// Use forward slashes and preserve the .app directory structure
header.Name = relPath
if info.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate
}
// Preserve executable permissions
header.SetMode(info.Mode())
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
if err != nil {
return err
}
}
return nil
})
return err
}
func signWithSparkle(filePath string, privateKeyPath string) (string, error) {
// Use Sparkle's sign_update tool
// First, try to find it in the Sparkle cache
sparkleSignTool := "/tmp/sparkle-cache/Sparkle-2.6.4/bin/sign_update"
// If not found, download Sparkle
if _, err := os.Stat(sparkleSignTool); os.IsNotExist(err) {
log.Println("Sparkle sign_update 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", output, err)
}
}
// Sign the file using -f to specify private key file path
// Note: -s is deprecated and expects key as string argument, not file path
cmd := exec.Command(sparkleSignTool, filePath, "-f", privateKeyPath)
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("failed to sign file: %s: %w", string(output), err)
}
// sign_update outputs: sparkle:edSignature="<signature>" length="<length>"
// We need to extract just the signature
signature := strings.TrimSpace(string(output))
// Parse out the signature from the output
if strings.Contains(signature, "sparkle:edSignature=\"") {
start := strings.Index(signature, "sparkle:edSignature=\"") + len("sparkle:edSignature=\"")
end := strings.Index(signature[start:], "\"")
if end > 0 {
signature = signature[start : start+end]
}
}
return signature, nil
}
func getFileSize(path string) (int64, error) {
info, err := os.Stat(path)
if err != nil {
return 0, err
}
return info.Size(), nil
}
func sha256File(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
return "", err
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
func fetchAppcast(bb aws.BucketBasics) (*Appcast, error) {
content, err := bb.FetchString(bucketName, appcastPath)
if err != nil {
// Return empty appcast if doesn't exist
return &Appcast{
Version: "2.0",
Xmlns: "http://www.andymatuschak.org/xml-namespaces/sparkle",
Channel: Channel{
Title: "Eagle0",
Link: "https://assets.eagle0.net/mac/appcast.xml",
Description: "Eagle0 game updates",
Language: "en",
Items: []Item{},
},
}, nil
}
var appcast Appcast
err = xml.Unmarshal([]byte(content), &appcast)
if err != nil {
return nil, fmt.Errorf("failed to parse appcast: %w", err)
}
return &appcast, nil
}
func uploadAppcast(bb aws.BucketBasics, appcast *Appcast) error {
output, err := xml.MarshalIndent(appcast, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal appcast: %w", err)
}
xmlContent := xml.Header + string(output)
return bb.UploadBytesPublic(bucketName, appcastPath, []byte(xmlContent))
}
func main() {
if len(os.Args) < 5 {
fmt.Println("Usage: mac_build_handler <app_path> <version> <build_number> <sparkle_private_key_path>")
os.Exit(1)
}
appPath := os.Args[1]
version := os.Args[2]
buildNumber := os.Args[3]
privateKeyPath := os.Args[4]
if _, err := os.Stat(appPath); os.IsNotExist(err) {
log.Fatalf("App not found: %s", appPath)
}
bb, err := aws.NewBucketBasics()
if err != nil {
log.Fatal(err)
}
// Create ZIP of the app
zipFileName := fmt.Sprintf("eagle0-%s.zip", version)
zipPath := filepath.Join("/tmp", zipFileName)
log.Printf("Creating ZIP: %s", zipPath)
if err := zipApp(appPath, zipPath); err != nil {
log.Fatalf("Failed to create ZIP: %v", err)
}
// Get file size
fileSize, err := getFileSize(zipPath)
if err != nil {
log.Fatalf("Failed to get file size: %v", err)
}
log.Printf("ZIP size: %d bytes", fileSize)
// Sign the ZIP with Sparkle EdDSA
log.Println("Signing ZIP with Sparkle...")
signature, err := signWithSparkle(zipPath, privateKeyPath)
if err != nil {
log.Fatalf("Failed to sign: %v", err)
}
log.Printf("Signature: %s", signature)
// Upload ZIP to S3
remotePath := buildsRoot + zipFileName
log.Printf("Uploading to S3: %s", remotePath)
if err := bb.UploadFilePublic(bucketName, remotePath, zipPath); err != nil {
log.Fatalf("Failed to upload: %v", err)
}
// Also upload as "latest"
latestPath := buildsRoot + "eagle0-latest.zip"
log.Printf("Copying to: %s", latestPath)
if err := bb.UploadFilePublic(bucketName, latestPath, zipPath); err != nil {
log.Fatalf("Failed to upload latest: %v", err)
}
// Update appcast.xml
log.Println("Updating appcast.xml...")
appcast, err := fetchAppcast(bb)
if err != nil {
log.Fatalf("Failed to fetch appcast: %v", err)
}
// Create new item
downloadURL := fmt.Sprintf("https://assets.eagle0.net/%s%s", buildsRoot, zipFileName)
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,
},
}
// Prepend new item (most recent first)
appcast.Channel.Items = append([]Item{newItem}, appcast.Channel.Items...)
// Keep only last 10 versions
if len(appcast.Channel.Items) > 10 {
appcast.Channel.Items = appcast.Channel.Items[:10]
}
if err := uploadAppcast(bb, appcast); err != nil {
log.Fatalf("Failed to upload appcast: %v", err)
}
// Clean up local ZIP
os.Remove(zipPath)
log.Printf("=== Mac build deployed successfully ===")
log.Printf("Version: %s (build %s)", version, buildNumber)
log.Printf("Download URL: %s", downloadURL)
log.Printf("Appcast URL: https://assets.eagle0.net/%s", appcastPath)
}
@@ -1,12 +1,15 @@
package main
import (
"crypto/ed25519"
"encoding/base64"
"fmt"
"github.com/nolen777/eagle0/src/main/go/net/eagle0/util/aws"
"log"
"os"
"strings"
"time"
"github.com/nolen777/eagle0/src/main/go/net/eagle0/util/aws"
)
const bucketName = "eagle0-windows"
@@ -64,7 +67,7 @@ func (mm *ManifestManager) parseManifest(content string) map[string]string {
return sections
}
func (mm *ManifestManager) buildManifest(sections map[string]string) string {
func (mm *ManifestManager) buildManifest(sections map[string]string, privateKey ed25519.PrivateKey) string {
var manifest strings.Builder
manifest.WriteString(fmt.Sprintf("# Generated: %s\n", time.Now().Format(time.RFC3339)))
@@ -90,10 +93,21 @@ func (mm *ManifestManager) buildManifest(sections map[string]string) string {
}
}
return manifest.String()
// The content to sign is everything we've built so far
contentToSign := manifest.String()
// If we have a private key, sign the manifest
if privateKey != nil {
signature := ed25519.Sign(privateKey, []byte(contentToSign))
signatureB64 := base64.StdEncoding.EncodeToString(signature)
// Prepend signature line at the very beginning
return fmt.Sprintf("# signature=%s\n%s", signatureB64, contentToSign)
}
return contentToSign
}
func (mm *ManifestManager) UpdateSection(sectionName, sectionContent string) error {
func (mm *ManifestManager) UpdateSection(sectionName, sectionContent string, privateKey ed25519.PrivateKey) error {
log.Printf("Updating manifest section: %s", sectionName)
// Fetch existing manifest
@@ -103,14 +117,14 @@ func (mm *ManifestManager) UpdateSection(sectionName, sectionContent string) err
existing = ""
}
// Parse sections
// Parse sections (strips out old signature line if present)
sections := mm.parseManifest(existing)
// Update the specified section
sections[sectionName] = sectionContent
// Build new manifest
newManifest := mm.buildManifest(sections)
// Build new manifest (with signature if key provided)
newManifest := mm.buildManifest(sections, privateKey)
// Upload updated manifest with public ACL
err = mm.bucketBasics.UploadBytesPublic(bucketName, manifestPath, []byte(newManifest))
@@ -118,13 +132,36 @@ func (mm *ManifestManager) UpdateSection(sectionName, sectionContent string) err
return fmt.Errorf("failed to upload manifest: %v", err)
}
log.Printf("Successfully updated manifest section: %s", sectionName)
if privateKey != nil {
log.Printf("Successfully updated and signed manifest section: %s", sectionName)
} else {
log.Printf("Successfully updated manifest section: %s (unsigned)", sectionName)
}
return nil
}
func loadPrivateKey(keyPath string) (ed25519.PrivateKey, error) {
keyData, err := os.ReadFile(keyPath)
if err != nil {
return nil, fmt.Errorf("failed to read private key file: %v", err)
}
// Key should be base64-encoded 64-byte Ed25519 private key
keyBytes, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(keyData)))
if err != nil {
return nil, fmt.Errorf("failed to decode private key: %v", err)
}
if len(keyBytes) != ed25519.PrivateKeySize {
return nil, fmt.Errorf("invalid private key size: expected %d bytes, got %d", ed25519.PrivateKeySize, len(keyBytes))
}
return ed25519.PrivateKey(keyBytes), nil
}
func main() {
if len(os.Args) != 3 {
log.Fatal("Usage: manifest_manager <section_name> <section_content_file>")
if len(os.Args) < 3 || len(os.Args) > 4 {
log.Fatal("Usage: manifest_manager <section_name> <section_content_file> [private_key_file]")
}
sectionName := os.Args[1]
@@ -136,6 +173,16 @@ func main() {
log.Fatalf("Failed to read content file: %v", err)
}
// Load private key if provided
var privateKey ed25519.PrivateKey
if len(os.Args) == 4 {
privateKey, err = loadPrivateKey(os.Args[3])
if err != nil {
log.Fatalf("Failed to load private key: %v", err)
}
log.Println("Ed25519 signing key loaded")
}
// Create manifest manager
mm, err := NewManifestManager()
if err != nil {
@@ -143,7 +190,7 @@ func main() {
}
// Update the section
err = mm.UpdateSection(sectionName, string(content))
err = mm.UpdateSection(sectionName, string(content), privateKey)
if err != nil {
log.Fatalf("Failed to update manifest: %v", err)
}
+32
View File
@@ -0,0 +1,32 @@
load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library")
go_library(
name = "warmup_lib",
srcs = ["main.go"],
importpath = "github.com/nolen777/eagle0/src/main/go/net/eagle0/warmup",
visibility = ["//visibility:private"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:api_go_proto", # keep
"//src/main/protobuf/net/eagle0/eagle/common:common_go_proto",
"@org_golang_google_grpc//:grpc",
"@org_golang_google_grpc//credentials/insecure",
"@org_golang_google_grpc//metadata",
],
)
go_binary(
name = "warmup",
embed = [":warmup_lib"],
pure = "on",
visibility = ["//visibility:public"],
)
# Linux x86_64 target for Docker deployment
go_binary(
name = "warmup_linux_amd64",
embed = [":warmup_lib"],
goarch = "amd64",
goos = "linux",
pure = "on",
visibility = ["//visibility:public"],
)
+470
View File
@@ -0,0 +1,470 @@
// Package main implements a warmup tool for Eagle server.
//
// This tool warms up the JIT by:
// 1. Entering the lobby to get available leaders
// 2. Creating a new game with 2 players (1 human, 1 AI)
// 3. Starting a bidirectional stream for that game
// 4. Posting an Improve command when available
// 5. Verifying that we get ActionResults including NEW_ROUND_ACTION
//
// Usage:
//
// warmup --address=localhost:40032
package main
import (
"context"
"flag"
"fmt"
"io"
"log"
"time"
eagle "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/api/eagle"
eaglecommon "github.com/nolen777/eagle0/src/main/protobuf/net/eagle0/eagle/common"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"google.golang.org/grpc/metadata"
)
var (
address = flag.String("address", "localhost:40032", "Eagle server address (host:port)")
timeout = flag.Duration("timeout", 60*time.Second, "Overall timeout for warmup")
)
func main() {
flag.Parse()
log.SetFlags(log.Ltime | log.Lmicroseconds)
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
if err := runWarmup(ctx); err != nil {
log.Fatalf("Warmup failed: %v", err)
}
log.Println("Warmup completed successfully!")
}
func runWarmup(ctx context.Context) error {
log.Printf("Connecting to Eagle server at %s...", *address)
conn, err := grpc.NewClient(*address, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return fmt.Errorf("failed to connect: %w", err)
}
defer conn.Close()
client := eagle.NewEagleClient(conn)
// Add warmup user header to context
md := metadata.Pairs("x-warmup-user", "warmup-tool")
ctx = metadata.NewOutgoingContext(ctx, md)
// Start the bidirectional stream
log.Println("Starting StreamUpdates...")
stream, err := client.StreamUpdates(ctx)
if err != nil {
return fmt.Errorf("failed to start stream: %w", err)
}
// Step 1: Enter the lobby to get available leaders
log.Println("Step 1: Entering lobby...")
if err := stream.Send(&eagle.UpdateStreamRequest{
RequestDetails: &eagle.UpdateStreamRequest_EnterLobbyRequest{
EnterLobbyRequest: &eagle.EnterLobbyRequest{},
},
}); err != nil {
return fmt.Errorf("failed to send lobby request: %w", err)
}
lobbyResp, err := waitForResponse(stream, func(resp *eagle.UpdateStreamResponse) bool {
return resp.GetLobbyResponse() != nil
})
if err != nil {
return fmt.Errorf("failed to get lobby response: %w", err)
}
lobby := lobbyResp.GetLobbyResponse()
newGameOptions := lobby.GetNewGameOptions()
if newGameOptions == nil || len(newGameOptions.GetAvailableLeaders()) == 0 {
return fmt.Errorf("no available leaders in lobby response")
}
leader := newGameOptions.GetAvailableLeaders()[0]
log.Printf(" Found leader: %s", leader.GetNameTextId())
// Step 2: Create a new game
log.Println("Step 2: Creating game...")
if err := stream.Send(&eagle.UpdateStreamRequest{
RequestDetails: &eagle.UpdateStreamRequest_CreateGameRequest{
CreateGameRequest: &eagle.CreateGameRequest{
DesiredLeaderTextId: leader.GetNameTextId(),
TotalPlayerCount: 2,
HumanPlayerCount: 1,
},
},
}); err != nil {
return fmt.Errorf("failed to send create game request: %w", err)
}
createResp, err := waitForResponse(stream, func(resp *eagle.UpdateStreamResponse) bool {
return resp.GetCreateGameResponse() != nil
})
if err != nil {
return fmt.Errorf("failed to get create game response: %w", err)
}
gameResp := createResp.GetCreateGameResponse()
if gameResp.GetResult() != eagle.JoinGameResult_SUCCESS_JOIN_GAME_RESULT {
return fmt.Errorf("failed to create game: %v", gameResp.GetResult())
}
gameID := gameResp.GetGameId()
log.Printf(" Created game: %d", gameID)
// Step 3: Subscribe to the game stream
log.Println("Step 3: Subscribing to game...")
if err := stream.Send(&eagle.UpdateStreamRequest{
RequestDetails: &eagle.UpdateStreamRequest_StreamGameRequest_{
StreamGameRequest: &eagle.UpdateStreamRequest_StreamGameRequest{
GameId: gameID,
UnfilteredResultCount: 0,
},
},
}); err != nil {
return fmt.Errorf("failed to send stream game request: %w", err)
}
// Wait for initial game state with available commands
// Note: ActionResultResponse arrives BEFORE SubscriptionAck, so we can't wait for ack first
streamingTextCount := 0
gotSubscriptionAck := false
gameUpdateResp, err := waitForResponse(stream, func(resp *eagle.UpdateStreamResponse) bool {
// Track subscription ack but don't require it before ActionResultResponse
if resp.GetSubscriptionAck() != nil {
gotSubscriptionAck = true
log.Println(" Subscription acknowledged")
}
gu := resp.GetGameUpdate()
if gu == nil {
return false
}
// Check what type of update this is
switch {
case gu.GetStreamingTextResponse() != nil:
streamingTextCount++
if streamingTextCount%100 == 0 {
log.Printf(" Received %d streaming text updates so far...", streamingTextCount)
}
case gu.GetShardokActionResultResponse() != nil:
log.Printf(" Got ShardokActionResultResponse")
case gu.GetErrorResponse() != nil:
log.Printf(" Got ErrorResponse: %v", gu.GetErrorResponse())
case gu.GetActionResultResponse() != nil:
log.Printf(" Got ActionResultResponse!")
default:
log.Printf(" Got GameUpdate with no recognized content")
}
ar := gu.GetActionResultResponse()
if ar == nil {
return false
}
if ar.GetAvailableCommands() == nil {
log.Printf(" ActionResultResponse has no AvailableCommands (has %d results)", len(ar.GetActionResultViews()))
return false
}
log.Printf(" Found AvailableCommands with %d provinces", len(ar.GetAvailableCommands().GetCommandsByProvince()))
return true
})
log.Printf(" Total streaming text updates received: %d, got subscription ack: %v", streamingTextCount, gotSubscriptionAck)
if err != nil {
return fmt.Errorf("failed to get initial game state: %w", err)
}
gameUpdate := gameUpdateResp.GetGameUpdate()
actionResult := gameUpdate.GetActionResultResponse()
availableCommands := actionResult.GetAvailableCommands()
// Get the token from available commands
eagleToken := availableCommands.GetToken()
// Get commands by province from map
commandsByProvince := availableCommands.GetCommandsByProvince()
log.Printf(" Got game state with %d provinces having commands", len(commandsByProvince))
// Step 4: Find an Improve command and post it
log.Println("Step 4: Looking for Improve command...")
var improveCmd *eagle.AvailableCommand
var provinceID int32
var actingHeroID int32
for pid, provCmds := range commandsByProvince {
for _, cmd := range provCmds.GetCommands() {
if ic := cmd.GetImproveCommand(); ic != nil {
improveCmd = cmd
provinceID = pid
actingHeroID = ic.GetRecommendedHeroId()
break
}
}
if improveCmd != nil {
break
}
}
if improveCmd == nil {
log.Println(" No Improve command available, looking for any command...")
// Try to find any province with commands
for pid, provCmds := range commandsByProvince {
if len(provCmds.GetCommands()) > 0 {
provinceID = pid
log.Printf(" Found command group for province %d with %d commands", pid, len(provCmds.GetCommands()))
break
}
}
// We'll still try to post an Improve command even if not explicitly available
} else {
log.Printf(" Found Improve command for province %d with recommended hero %d", provinceID, actingHeroID)
}
// Post the Improve command
log.Printf(" Posting Improve command with token=%d province=%d heroId=%d...", eagleToken, provinceID, actingHeroID)
if err := stream.Send(&eagle.UpdateStreamRequest{
RequestDetails: &eagle.UpdateStreamRequest_PostCommandRequest{
PostCommandRequest: &eagle.PostCommandRequest{
GameId: gameID,
Command: &eagle.UniversalCommand{
SealedValue: &eagle.UniversalCommand_EagleCommand{
EagleCommand: &eagle.EagleCommand{
ProvinceId: provinceID,
EagleToken: eagleToken,
Command: &eagle.SelectedCommand{
SealedValue: &eagle.SelectedCommand_ImproveCommand{
ImproveCommand: &eagle.ImproveSelectedCommand{
ImprovementType: eaglecommon.ImprovementType_ECONOMY,
ActingHeroId: actingHeroID,
LockType: false,
},
},
},
},
},
},
},
},
}); err != nil {
return fmt.Errorf("failed to send post command request: %w", err)
}
// Step 5: Verify we get ActionResults
log.Println("Step 5: Waiting for command response and collecting results...")
// Track results while waiting for PostCommandResponse
// ActionResultResponse arrives as GameUpdate BEFORE PostCommandResponse
foundNewRound := false
resultCount := 0
commandCount := 0
postStatus := eagle.PostCommandResponse_UNKNOWN
_, err = waitForResponse(stream, func(resp *eagle.UpdateStreamResponse) bool {
if resp.GetPostCommandResponse() != nil {
log.Printf(" Got PostCommandResponse: %v", resp.GetPostCommandResponse().GetStatus())
postStatus = resp.GetPostCommandResponse().GetStatus()
return true
}
// Process GameUpdates while waiting - they contain our action results!
if gu := resp.GetGameUpdate(); gu != nil {
if ar := gu.GetActionResultResponse(); ar != nil {
for _, arv := range ar.GetActionResultViews() {
resultCount++
if arv.GetType() == eaglecommon.ActionResultType_NEW_ROUND_ACTION {
foundNewRound = true
log.Printf(" Found NEW_ROUND_ACTION (date change)!")
}
}
if ar.GetAvailableCommands() != nil {
commandCount = len(ar.GetAvailableCommands().GetCommandsByProvince())
log.Printf(" Got %d action results, %d provinces with commands available", len(ar.GetActionResultViews()), commandCount)
}
}
} else if resp.GetSubscriptionAck() != nil {
log.Println(" (skipping late SubscriptionAck)")
}
return false
})
if err != nil {
return fmt.Errorf("failed to get post command response: %w", err)
}
if postStatus == eagle.PostCommandResponse_SUCCESS {
log.Println(" Command accepted!")
} else if postStatus == eagle.PostCommandResponse_BAD_TOKEN {
log.Printf(" Command rejected: bad token")
} else {
log.Printf(" Command status: %v (continuing anyway - JIT warming up)", postStatus)
}
// Continue collecting any remaining action results with a timeout
log.Println(" Collecting any remaining action results...")
resultCtx, resultCancel := context.WithTimeout(ctx, 10*time.Second)
defer resultCancel()
for {
select {
case <-resultCtx.Done():
log.Println(" Timeout waiting for more results (this is OK)")
goto done
default:
}
resp, err := receiveWithTimeout(stream, 2*time.Second)
if err != nil {
if err == errTimeout {
log.Println(" No more results received")
goto done
}
return fmt.Errorf("error receiving: %w", err)
}
gu := resp.GetGameUpdate()
if gu == nil {
continue
}
ar := gu.GetActionResultResponse()
if ar == nil {
continue
}
for _, arv := range ar.GetActionResultViews() {
resultCount++
if arv.GetType() == eaglecommon.ActionResultType_NEW_ROUND_ACTION {
foundNewRound = true
log.Printf(" Found NEW_ROUND_ACTION (date change)!")
}
}
if ar.GetAvailableCommands() != nil {
commandCount = len(ar.GetAvailableCommands().GetCommandsByProvince())
log.Printf(" Got %d action results, %d provinces with commands available", len(ar.GetActionResultViews()), commandCount)
}
}
done:
// Verify results
log.Println("Verification:")
log.Printf(" Total action results received: %d", resultCount)
log.Printf(" Found NEW_ROUND_ACTION (date change): %v", foundNewRound)
log.Printf(" New commands available: %d provinces", commandCount)
log.Printf(" Post command status: %v", postStatus)
// Step 6: Drop the game to clean up
log.Println("Step 6: Dropping test game...")
if err := stream.Send(&eagle.UpdateStreamRequest{
RequestDetails: &eagle.UpdateStreamRequest_DropGameRequest{
DropGameRequest: &eagle.DropGameRequest{
GameId: gameID,
},
},
}); err != nil {
log.Printf("Warning: failed to send drop game request: %v", err)
} else {
// Wait briefly for drop response
_, err := waitForResponseWithTimeout(stream, func(resp *eagle.UpdateStreamResponse) bool {
return resp.GetDropGameResponse() != nil
}, 5*time.Second)
if err != nil {
log.Printf("Warning: failed to get drop game response: %v", err)
} else {
log.Println(" Game dropped successfully")
}
}
// Report success based on what we achieved
if resultCount == 0 {
return fmt.Errorf("no action results received")
}
if commandCount == 0 {
return fmt.Errorf("no new commands received after posting command")
}
return nil
}
// waitForResponse waits for a response matching the predicate
func waitForResponse(stream eagle.Eagle_StreamUpdatesClient, matches func(*eagle.UpdateStreamResponse) bool) (*eagle.UpdateStreamResponse, error) {
return waitForResponseWithTimeout(stream, matches, 30*time.Second)
}
func waitForResponseWithTimeout(stream eagle.Eagle_StreamUpdatesClient, matches func(*eagle.UpdateStreamResponse) bool, timeout time.Duration) (*eagle.UpdateStreamResponse, error) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
remaining := time.Until(deadline)
resp, err := receiveWithTimeout(stream, remaining)
if err != nil {
if err == errTimeout {
return nil, fmt.Errorf("timeout waiting for response")
}
log.Printf(" [DEBUG] Error receiving: %v", err)
return nil, err
}
// Log what type of response we got
switch resp.GetResponseDetails().(type) {
case *eagle.UpdateStreamResponse_LobbyResponse:
log.Printf(" [DEBUG] Got LobbyResponse")
case *eagle.UpdateStreamResponse_CreateGameResponse:
log.Printf(" [DEBUG] Got CreateGameResponse")
case *eagle.UpdateStreamResponse_GameUpdate:
log.Printf(" [DEBUG] Got GameUpdate")
case *eagle.UpdateStreamResponse_SubscriptionAck:
log.Printf(" [DEBUG] Got SubscriptionAck")
case *eagle.UpdateStreamResponse_PostCommandResponse:
log.Printf(" [DEBUG] Got PostCommandResponse")
case *eagle.UpdateStreamResponse_DropGameResponse:
log.Printf(" [DEBUG] Got DropGameResponse")
default:
log.Printf(" [DEBUG] Got unknown response type: %T", resp.GetResponseDetails())
}
if matches(resp) {
return resp, nil
}
}
return nil, fmt.Errorf("timeout waiting for response")
}
var errTimeout = fmt.Errorf("receive timeout")
func receiveWithTimeout(stream eagle.Eagle_StreamUpdatesClient, timeout time.Duration) (*eagle.UpdateStreamResponse, error) {
type result struct {
resp *eagle.UpdateStreamResponse
err error
}
ch := make(chan result, 1)
go func() {
resp, err := stream.Recv()
ch <- result{resp, err}
}()
select {
case r := <-ch:
if r.err != nil {
if r.err == io.EOF {
return nil, fmt.Errorf("stream closed by server")
}
return nil, r.err
}
return r.resp, nil
case <-time.After(timeout):
return nil, errTimeout
}
}
@@ -1,4 +1,3 @@
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
load("@grpc//bazel:cc_grpc_library.bzl", "cc_grpc_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
@@ -57,16 +56,6 @@ proto_library(
],
)
swift_proto_library(
name = "hostility_swift_proto",
protos = [":hostility_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/protobuf/net/eagle0/eagle/views:__pkg__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
)
scala_proto_library(
name = "hostility_scala_proto",
visibility = [
@@ -183,15 +172,6 @@ proto_library(
],
)
swift_proto_library(
name = "victory_condition_swift_proto",
protos = [":victory_condition_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
)
scala_proto_library(
name = "victory_condition_scala_proto",
visibility = ["//visibility:public"],
@@ -1,4 +1,3 @@
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
@@ -50,26 +49,6 @@ proto_library(
],
)
swift_proto_library(
name = "available_command_swift_proto",
protos = [":available_command_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api/command/util:armed_battalion_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:attack_decision_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:captured_hero_option_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:control_weather_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:diplomacy_option_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:prisoner_management_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:province_orders_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:improvement_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:tribute_amount_swift_proto",
],
)
scala_proto_library(
name = "available_command_scala_proto",
visibility = [
@@ -108,16 +87,6 @@ proto_library(
],
)
swift_proto_library(
name = "command_swift_proto",
protos = [":command_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":available_command_swift_proto",
":selected_command_swift_proto",
],
)
scala_proto_library(
name = "command_scala_proto",
visibility = [
@@ -153,30 +122,6 @@ proto_library(
visibility = ["//visibility:public"],
)
swift_proto_library(
name = "selected_command_swift_proto",
protos = [":selected_command_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api/command/util:armed_battalion_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:attack_decision_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:captured_hero_option_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:control_weather_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:diplomacy_option_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:prisoner_management_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:province_orders_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_status_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:improvement_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:tribute_amount_swift_proto",
],
)
scala_proto_library(
name = "selected_command_scala_proto",
visibility = [
@@ -39,6 +39,12 @@ service Admin {
// Resend invitation email
rpc ResendInvitation(ResendInvitationRequest) returns (ResendInvitationResponse) {}
// Delete a user permanently
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse) {}
// Delete an invitation (only revoked/expired/redeemed invitations can be deleted)
rpc DeleteInvitation(DeleteInvitationRequest) returns (DeleteInvitationResponse) {}
}
// Request to list users
@@ -173,3 +179,23 @@ message ResendInvitationResponse {
bool success = 1;
string error_message = 2;
}
// Delete user request
message DeleteUserRequest {
string user_id = 1;
}
message DeleteUserResponse {
bool success = 1;
string error_message = 2;
}
// Delete invitation request (only revoked/expired/redeemed can be deleted)
message DeleteInvitationRequest {
string invitation_code = 1;
}
message DeleteInvitationResponse {
bool success = 1;
string error_message = 2;
}
@@ -1,18 +1,7 @@
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
swift_proto_library(
name = "appropriate_battalions_swift_proto",
protos = [":appropriate_battalions_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
scala_proto_library(
name = "appropriate_battalions_scala_proto",
visibility = [
@@ -32,16 +21,6 @@ proto_library(
],
)
swift_proto_library(
name = "armed_battalion_swift_proto",
protos = [":armed_battalion_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
scala_proto_library(
name = "armed_battalion_scala_proto",
visibility = [
@@ -63,16 +42,6 @@ proto_library(
],
)
swift_proto_library(
name = "army_stats_swift_proto",
protos = [":army_stats_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
scala_proto_library(
name = "army_stats_scala_proto",
visibility = [
@@ -93,18 +62,6 @@ proto_library(
deps = ["//src/main/protobuf/net/eagle0/common:hostility_proto"],
)
swift_proto_library(
name = "attack_decision_type_swift_proto",
protos = [":attack_decision_type_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:tribute_amount_swift_proto",
],
)
scala_proto_library(
name = "attack_decision_type_scala_proto",
visibility = [
@@ -127,16 +84,6 @@ proto_library(
],
)
swift_proto_library(
name = "battalion_with_food_cost_swift_proto",
protos = [":battalion_with_food_cost_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
scala_proto_library(
name = "battalion_with_food_cost_scala_proto",
visibility = [
@@ -158,16 +105,6 @@ proto_library(
],
)
swift_proto_library(
name = "captured_hero_option_swift_proto",
protos = [":captured_hero_option_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
scala_proto_library(
name = "captured_hero_option_scala_proto",
visibility = [
@@ -189,16 +126,6 @@ proto_library(
],
)
swift_proto_library(
name = "control_weather_type_swift_proto",
protos = [":control_weather_type_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
scala_proto_library(
name = "control_weather_type_scala_proto",
visibility = [
@@ -220,18 +147,6 @@ proto_library(
],
)
swift_proto_library(
name = "diplomacy_option_swift_proto",
protos = [":diplomacy_option_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_swift_proto",
],
)
scala_proto_library(
name = "diplomacy_option_scala_proto",
visibility = [
@@ -254,19 +169,6 @@ proto_library(
deps = ["//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_proto"],
)
swift_proto_library(
name = "expanded_combat_unit_swift_proto",
protos = [":expanded_combat_unit_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/views:battalion_view_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/views:hero_view_swift_proto",
],
)
scala_proto_library(
name = "expanded_combat_unit_scala_proto",
visibility = [
@@ -292,20 +194,6 @@ proto_library(
],
)
swift_proto_library(
name = "expanded_unaffiliated_hero_swift_proto",
protos = [":expanded_unaffiliated_hero_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:unaffiliated_hero_quest_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:unaffiliated_hero_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/views:hero_view_swift_proto",
],
)
scala_proto_library(
name = "expanded_unaffiliated_hero_scala_proto",
visibility = [
@@ -332,16 +220,6 @@ proto_library(
],
)
swift_proto_library(
name = "prisoner_management_type_swift_proto",
protos = [":prisoner_management_type_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
scala_proto_library(
name = "prisoner_management_type_scala_proto",
visibility = [
@@ -363,18 +241,6 @@ proto_library(
],
)
swift_proto_library(
name = "province_orders_swift_proto",
protos = [":province_orders_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_swift_proto",
],
)
scala_proto_library(
name = "province_orders_scala_proto",
visibility = [
@@ -45,6 +45,9 @@ service Eagle {
rpc DeleteGame(DeleteGameRequest) returns (DeleteGameResponse) {}
rpc CheckGameExists(CheckGameExistsRequest) returns (CheckGameExistsResponse) {}
rpc DownloadGameSave(DownloadGameSaveRequest) returns (stream DownloadGameSaveResponse) {}
// Server lifecycle management (for blue-green deployments)
rpc ReloadGames(ReloadGamesRequest) returns (ReloadGamesResponse) {}
}
message PostCommandRequest {
@@ -605,3 +608,12 @@ message DownloadGameSaveRequest {
message DownloadGameSaveResponse {
bytes chunk = 1; // Chunk of zip file data
}
// Server lifecycle management (for blue-green deployments)
message ReloadGamesRequest {}
message ReloadGamesResponse {
bool success = 1;
string error_message = 2;
int32 games_reloaded = 3; // Number of games that were reloaded from disk
}
@@ -1,4 +1,3 @@
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
@@ -8,19 +7,6 @@ package(default_visibility = [
"//src/test/scala/net/eagle0:__subpackages__",
])
swift_proto_library(
name = "action_result_notification_details_swift_proto",
protos = [":action_result_notification_details_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
":profession_swift_proto",
":unaffiliated_hero_quest_swift_proto",
],
)
scala_proto_library(
name = "action_result_notification_details_scala_proto",
deps = [":action_result_notification_details_proto"],
@@ -41,15 +27,6 @@ proto_library(
],
)
swift_proto_library(
name = "action_result_type_swift_proto",
protos = [":action_result_type_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
)
scala_proto_library(
name = "action_result_type_scala_proto",
deps = [":action_result_type_proto"],
@@ -65,15 +42,6 @@ proto_library(
],
)
swift_proto_library(
name = "battalion_type_swift_proto",
protos = [":battalion_type_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
)
scala_proto_library(
name = "battalion_type_scala_proto",
deps = [":battalion_type_proto"],
@@ -89,15 +57,6 @@ proto_library(
],
)
swift_proto_library(
name = "beast_info_swift_proto",
protos = [":beast_info_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
)
scala_proto_library(
name = "beast_info_scala_proto",
deps = [":beast_info_proto"],
@@ -113,16 +72,6 @@ proto_library(
],
)
swift_proto_library(
name = "chronicle_entry_swift_proto",
protos = [":chronicle_entry_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [":date_swift_proto"],
)
scala_proto_library(
name = "chronicle_entry_scala_proto",
deps = ["chronicle_entry_proto"],
@@ -141,12 +90,18 @@ proto_library(
],
)
swift_proto_library(
name = "combat_unit_swift_proto",
protos = [":combat_unit_proto"],
scala_proto_library(
name = "command_type_scala_proto",
deps = [":command_type_proto"],
)
proto_library(
name = "command_type_proto",
srcs = [
"command_type.proto",
],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
)
@@ -166,15 +121,6 @@ proto_library(
deps = ["@com_google_protobuf//:wrappers_proto"],
)
swift_proto_library(
name = "date_swift_proto",
protos = [":date_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
)
scala_proto_library(
name = "date_scala_proto",
deps = [":date_proto"],
@@ -190,19 +136,6 @@ proto_library(
],
)
swift_proto_library(
name = "diplomacy_offer_swift_proto",
protos = [":diplomacy_offer_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
":date_swift_proto",
":diplomacy_offer_status_swift_proto",
],
)
scala_proto_library(
name = "diplomacy_offer_scala_proto",
visibility = [
@@ -226,15 +159,6 @@ proto_library(
],
)
swift_proto_library(
name = "diplomacy_offer_status_swift_proto",
protos = [":diplomacy_offer_status_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
)
scala_proto_library(
name = "diplomacy_offer_status_scala_proto",
deps = [":diplomacy_offer_status_proto"],
@@ -250,15 +174,6 @@ proto_library(
],
)
swift_proto_library(
name = "gender_swift_proto",
protos = [":gender_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
)
scala_proto_library(
name = "gender_scala_proto",
deps = [":gender_proto"],
@@ -272,18 +187,6 @@ proto_library(
],
)
swift_proto_library(
name = "hero_backstory_version_swift_proto",
protos = [":hero_backstory_version_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
":date_swift_proto",
],
)
scala_proto_library(
name = "hero_backstory_version_scala_proto",
deps = [":hero_backstory_version_proto"],
@@ -300,15 +203,6 @@ proto_library(
],
)
swift_proto_library(
name = "improvement_type_swift_proto",
protos = [":improvement_type_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
)
scala_proto_library(
name = "improvement_type_scala_proto",
visibility = [
@@ -330,15 +224,6 @@ proto_library(
],
)
swift_proto_library(
name = "profession_swift_proto",
protos = [":profession_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
)
scala_proto_library(
name = "profession_scala_proto",
deps = [":profession_proto"],
@@ -354,19 +239,6 @@ proto_library(
],
)
swift_proto_library(
name = "province_event_swift_proto",
protos = [":province_event_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
":beast_info_swift_proto",
":date_swift_proto",
],
)
scala_proto_library(
name = "province_event_scala_proto",
deps = [":province_event_proto"],
@@ -386,15 +258,6 @@ proto_library(
],
)
swift_proto_library(
name = "province_order_type_swift_proto",
protos = [":province_order_type_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
)
scala_proto_library(
name = "province_order_type_scala_proto",
deps = [":province_order_type_proto"],
@@ -410,18 +273,6 @@ proto_library(
],
)
swift_proto_library(
name = "recruitment_info_swift_proto",
protos = [":recruitment_info_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
":unaffiliated_hero_quest_swift_proto",
],
)
scala_proto_library(
name = "recruitment_info_scala_proto",
deps = [":recruitment_info_proto"],
@@ -438,15 +289,6 @@ proto_library(
deps = [":unaffiliated_hero_quest_proto"],
)
swift_proto_library(
name = "round_phase_swift_proto",
protos = [":round_phase_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
)
scala_proto_library(
name = "round_phase_scala_proto",
deps = [":round_phase_proto"],
@@ -462,15 +304,6 @@ proto_library(
],
)
swift_proto_library(
name = "tribute_amount_swift_proto",
protos = [":tribute_amount_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
)
scala_proto_library(
name = "tribute_amount_scala_proto",
visibility = [
@@ -494,16 +327,6 @@ proto_library(
],
)
swift_proto_library(
name = "unaffiliated_hero_quest_swift_proto",
protos = [":unaffiliated_hero_quest_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [":battalion_type_swift_proto"],
)
scala_proto_library(
name = "unaffiliated_hero_quest_scala_proto",
deps = [":unaffiliated_hero_quest_proto"],
@@ -520,16 +343,6 @@ proto_library(
deps = [":battalion_type_proto"],
)
swift_proto_library(
name = "unaffiliated_hero_type_swift_proto",
protos = [":unaffiliated_hero_type_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
scala_proto_library(
name = "unaffiliated_hero_type_scala_proto",
deps = [":unaffiliated_hero_type_proto"],
@@ -555,6 +368,7 @@ go_proto_library(
":beast_info_proto",
":chronicle_entry_proto",
":combat_unit_proto",
":command_type_proto",
":date_proto",
":diplomacy_offer_proto",
":diplomacy_offer_status_proto",
@@ -0,0 +1,52 @@
syntax = "proto3";
package net.eagle0.eagle.common.command_type;
option java_multiple_files = true;
option java_package = "net.eagle0.eagle.common.command_type";
// Enum representing the type of a command, without any command-specific data.
// Used to track which command type was last executed on a province.
enum CommandType {
COMMAND_TYPE_UNKNOWN = 0;
COMMAND_TYPE_ALMS = 1;
COMMAND_TYPE_APPREHEND_OUTLAW = 2;
COMMAND_TYPE_ARM_TROOPS = 3;
COMMAND_TYPE_ATTACK_DECISION = 4;
COMMAND_TYPE_CONTROL_WEATHER = 5;
COMMAND_TYPE_DECLINE_QUEST = 6;
COMMAND_TYPE_DEFEND = 7;
COMMAND_TYPE_DIPLOMACY = 8;
COMMAND_TYPE_DIVINE = 9;
COMMAND_TYPE_EXILE_VASSAL = 10;
COMMAND_TYPE_FEAST = 11;
COMMAND_TYPE_FREE_FOR_ALL_DECISION = 12;
COMMAND_TYPE_HANDLE_CAPTURED_HERO = 13;
COMMAND_TYPE_HANDLE_RIOT_CRACK_DOWN = 14;
COMMAND_TYPE_HANDLE_RIOT_DO_NOTHING = 15;
COMMAND_TYPE_HANDLE_RIOT_GIVE = 16;
COMMAND_TYPE_HERO_GIFT = 17;
COMMAND_TYPE_IMPROVE = 18;
COMMAND_TYPE_ISSUE_ORDERS = 19;
COMMAND_TYPE_MANAGE_PRISONERS = 20;
COMMAND_TYPE_MARCH = 21;
COMMAND_TYPE_ORGANIZE_TROOPS = 22;
COMMAND_TYPE_PLEASE_RECRUIT_ME = 23;
COMMAND_TYPE_RECON = 24;
COMMAND_TYPE_RECRUIT_HEROES = 25;
COMMAND_TYPE_RESOLVE_ALLIANCE_OFFER = 26;
COMMAND_TYPE_RESOLVE_BREAK_ALLIANCE = 27;
COMMAND_TYPE_RESOLVE_INVITATION = 28;
COMMAND_TYPE_RESOLVE_RANSOM_OFFER = 29;
COMMAND_TYPE_RESOLVE_TRUCE_OFFER = 30;
COMMAND_TYPE_RESOLVE_TRIBUTE = 31;
COMMAND_TYPE_REST = 32;
COMMAND_TYPE_RETURN = 33;
COMMAND_TYPE_SEND_SUPPLIES = 34;
COMMAND_TYPE_START_EPIDEMIC = 35;
COMMAND_TYPE_SUPPRESS_BEASTS = 36;
COMMAND_TYPE_SWEAR_BROTHERHOOD = 37;
COMMAND_TYPE_TRADE = 38;
COMMAND_TYPE_TRAIN = 39;
COMMAND_TYPE_TRAVEL = 40;
}
@@ -1,4 +1,3 @@
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
@@ -8,31 +7,6 @@ package(default_visibility = [
"//src/test/scala/net/eagle0:__subpackages__",
])
swift_proto_library(
name = "action_result_swift_proto",
protos = [":action_result_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":battalion_swift_proto",
":changed_faction_swift_proto",
":changed_hero_swift_proto",
":changed_province_swift_proto",
":faction_swift_proto",
":hero_swift_proto",
":llm_request_swift_proto",
":llm_response_swift_proto",
":province_swift_proto",
":shardok_battle_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:action_result_notification_details_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:chronicle_entry_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_swift_proto",
],
)
scala_proto_library(
name = "action_result_scala_proto",
deps = [":action_result_proto"],
@@ -58,24 +32,13 @@ proto_library(
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_proto",
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_proto",
"//src/main/protobuf/net/eagle0/eagle/common:chronicle_entry_proto",
"//src/main/protobuf/net/eagle0/eagle/common:command_type_proto",
"//src/main/protobuf/net/eagle0/eagle/common:date_proto",
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_proto",
"@com_google_protobuf//:wrappers_proto",
],
)
swift_proto_library(
name = "army_swift_proto",
protos = [":army_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":supplies_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:tribute_amount_swift_proto",
],
)
scala_proto_library(
name = "army_scala_proto",
deps = [":army_proto"],
@@ -94,15 +57,6 @@ proto_library(
],
)
swift_proto_library(
name = "battalion_swift_proto",
protos = [":battalion_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
],
)
scala_proto_library(
name = "battalion_scala_proto",
deps = [":battalion_proto"],
@@ -116,12 +70,6 @@ proto_library(
deps = ["//src/main/protobuf/net/eagle0/eagle/common:battalion_type_proto"],
)
swift_proto_library(
name = "battle_revelation_swift_proto",
protos = [":battle_revelation_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
)
scala_proto_library(
name = "battle_revelation_scala_proto",
deps = [":battle_revelation_proto"],
@@ -134,20 +82,6 @@ proto_library(
],
)
swift_proto_library(
name = "changed_faction_swift_proto",
protos = [":changed_faction_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":changed_hero_swift_proto",
":changed_province_swift_proto",
":faction_relationship_swift_proto",
":faction_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/views:province_view_swift_proto",
],
)
scala_proto_library(
name = "changed_faction_scala_proto",
deps = [
@@ -169,16 +103,6 @@ proto_library(
],
)
swift_proto_library(
name = "changed_hero_swift_proto",
protos = [":changed_hero_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":event_for_hero_backstory_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:profession_swift_proto",
],
)
scala_proto_library(
name = "changed_hero_scala_proto",
deps = ["changed_hero_proto"],
@@ -196,23 +120,6 @@ proto_library(
],
)
swift_proto_library(
name = "changed_province_swift_proto",
protos = [":changed_province_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":deferred_change_swift_proto",
":province_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:improvement_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:province_event_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:army_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:battle_revelation_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:supplies_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_swift_proto",
],
)
scala_proto_library(
name = "changed_province_scala_proto",
deps = ["changed_province_proto"],
@@ -237,15 +144,6 @@ proto_library(
],
)
swift_proto_library(
name = "client_text_swift_proto",
protos = [":client_text_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/internal:llm_request_swift_proto",
],
)
scala_proto_library(
name = "client_text_scala_proto",
deps = ["client_text_proto"],
@@ -259,12 +157,6 @@ proto_library(
],
)
swift_proto_library(
name = "deferred_change_swift_proto",
protos = [":deferred_change_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
)
scala_proto_library(
name = "deferred_change_scala_proto",
deps = [":deferred_change_proto"],
@@ -275,15 +167,6 @@ proto_library(
srcs = ["deferred_change.proto"],
)
swift_proto_library(
name = "event_for_chronicle_swift_proto",
protos = [":event_for_chronicle_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
],
)
scala_proto_library(
name = "event_for_chronicle_scala_proto",
deps = [":event_for_chronicle_proto"],
@@ -298,19 +181,6 @@ proto_library(
],
)
swift_proto_library(
name = "event_for_hero_backstory_swift_proto",
protos = [":event_for_hero_backstory_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_status_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:profession_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:unaffiliated_hero_quest_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/views:battalion_view_swift_proto",
],
)
scala_proto_library(
name = "event_for_hero_backstory_scala_proto",
deps = [":event_for_hero_backstory_proto"],
@@ -328,19 +198,6 @@ proto_library(
],
)
swift_proto_library(
name = "faction_swift_proto",
protos = [":faction_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":faction_relationship_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:action_result_notification_details_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/views:province_view_swift_proto",
],
)
scala_proto_library(
name = "faction_scala_proto",
deps = [":faction_proto"],
@@ -359,15 +216,6 @@ proto_library(
],
)
swift_proto_library(
name = "faction_relationship_swift_proto",
protos = [":faction_relationship_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
],
)
scala_proto_library(
name = "faction_relationship_scala_proto",
deps = [":faction_relationship_proto"],
@@ -384,17 +232,6 @@ proto_library(
deps = ["//src/main/protobuf/net/eagle0/eagle/common:date_proto"],
)
swift_proto_library(
name = "game_parameters_swift_proto",
protos = [":game_parameters_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_swift_proto",
],
)
scala_proto_library(
name = "game_parameters_scala_proto",
visibility = ["//visibility:public"],
@@ -413,18 +250,6 @@ proto_library(
],
)
swift_proto_library(
name = "game_swift_proto",
protos = [":game_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":action_result_swift_proto",
":game_state_swift_proto",
":shardok_results_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
],
)
scala_proto_library(
name = "game_scala_proto",
deps = [":game_proto"],
@@ -442,27 +267,6 @@ proto_library(
],
)
swift_proto_library(
name = "game_state_swift_proto",
protos = [":game_state_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":battalion_swift_proto",
":faction_swift_proto",
":hero_swift_proto",
":llm_request_swift_proto",
":llm_response_swift_proto",
":province_swift_proto",
":run_status_swift_proto",
":shardok_battle_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:action_result_notification_details_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:chronicle_entry_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_swift_proto",
],
)
scala_proto_library(
name = "game_state_scala_proto",
deps = [":game_state_proto"],
@@ -489,18 +293,6 @@ proto_library(
],
)
swift_proto_library(
name = "hero_swift_proto",
protos = [":hero_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":event_for_hero_backstory_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:gender_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:hero_backstory_version_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:profession_swift_proto",
],
)
scala_proto_library(
name = "hero_scala_proto",
deps = [":hero_proto"],
@@ -518,13 +310,6 @@ proto_library(
],
)
swift_proto_library(
name = "llm_response_swift_proto",
protos = [":llm_response_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [":llm_request_swift_proto"],
)
scala_proto_library(
name = "llm_response_scala_proto",
deps = [":llm_response_proto"],
@@ -540,21 +325,6 @@ proto_library(
],
)
swift_proto_library(
name = "llm_request_swift_proto",
protos = [":llm_request_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":event_for_chronicle_swift_proto",
":event_for_hero_backstory_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_status_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:unaffiliated_hero_quest_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:hero_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/views:battalion_view_swift_proto",
],
)
scala_proto_library(
name = "llm_request_scala_proto",
deps = [":llm_request_proto"],
@@ -578,27 +348,6 @@ proto_library(
],
)
swift_proto_library(
name = "province_swift_proto",
protos = [":province_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":army_swift_proto",
":battle_revelation_swift_proto",
":deferred_change_swift_proto",
":supplies_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:improvement_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:province_event_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:tribute_amount_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_swift_proto",
],
)
scala_proto_library(
name = "province_scala_proto",
visibility = ["//visibility:public"],
@@ -616,8 +365,8 @@ proto_library(
":deferred_change_proto",
":supplies_proto",
":unaffiliated_hero_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_proto",
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_proto",
"//src/main/protobuf/net/eagle0/eagle/common:command_type_proto",
"//src/main/protobuf/net/eagle0/eagle/common:date_proto",
"//src/main/protobuf/net/eagle0/eagle/common:improvement_type_proto",
"//src/main/protobuf/net/eagle0/eagle/common:province_event_proto",
@@ -626,13 +375,6 @@ proto_library(
],
)
swift_proto_library(
name = "run_status_swift_proto",
protos = [":run_status_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [],
)
scala_proto_library(
name = "run_status_scala_proto",
deps = [":run_status_proto"],
@@ -645,13 +387,6 @@ proto_library(
],
)
swift_proto_library(
name = "running_games_swift_proto",
protos = [":running_games_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [],
)
scala_proto_library(
name = "running_games_scala_proto",
deps = [":running_games_proto"],
@@ -662,16 +397,6 @@ proto_library(
srcs = ["running_games.proto"],
)
swift_proto_library(
name = "shardok_battle_swift_proto",
protos = [":shardok_battle_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":army_swift_proto",
"//src/main/protobuf/net/eagle0/common:victory_condition_swift_proto",
],
)
scala_proto_library(
name = "shardok_battle_scala_proto",
deps = [":shardok_battle_proto"],
@@ -688,17 +413,6 @@ proto_library(
],
)
swift_proto_library(
name = "shardok_results_swift_proto",
protos = [":shardok_results_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
"//src/main/protobuf/net/eagle0/shardok/api:action_result_view_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/api:command_descriptor_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_swift_proto",
],
)
scala_proto_library(
name = "shardok_results_scala_proto",
deps = [":shardok_results_proto"],
@@ -716,14 +430,6 @@ proto_library(
],
)
swift_proto_library(
name = "supplies_swift_proto",
protos = [":supplies_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
],
)
scala_proto_library(
name = "supplies_scala_proto",
deps = [":supplies_proto"],
@@ -737,20 +443,6 @@ proto_library(
deps = ["@com_google_protobuf//:wrappers_proto"],
)
swift_proto_library(
name = "unaffiliated_hero_swift_proto",
protos = [":unaffiliated_hero_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:recruitment_info_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:unaffiliated_hero_type_swift_proto",
],
)
scala_proto_library(
name = "unaffiliated_hero_scala_proto",
deps = [":unaffiliated_hero_proto"],
@@ -12,6 +12,7 @@ import "src/main/protobuf/net/eagle0/eagle/common/action_result_notification_det
import "src/main/protobuf/net/eagle0/eagle/common/action_result_type.proto";
import "src/main/protobuf/net/eagle0/eagle/common/battalion_type.proto";
import "src/main/protobuf/net/eagle0/eagle/common/chronicle_entry.proto";
import "src/main/protobuf/net/eagle0/eagle/common/command_type.proto";
import "src/main/protobuf/net/eagle0/eagle/common/date.proto";
import "src/main/protobuf/net/eagle0/eagle/common/round_phase.proto";
import "src/main/protobuf/net/eagle0/eagle/internal/battalion.proto";
@@ -63,7 +64,9 @@ message ActionResult {
.google.protobuf.Int32Value province_acted = 17;
.net.eagle0.eagle.api.SelectedCommand last_command_type_for_acting_province = 28;
// Deprecated: Use last_command_type_for_acting_province instead. Kept for backwards compatibility with saved games.
.net.eagle0.eagle.api.SelectedCommand deprecated_last_command_type_for_acting_province = 28;
.net.eagle0.eagle.common.command_type.CommandType last_command_type_for_acting_province = 43;
.google.protobuf.Int32Value leader = 18;
@@ -7,8 +7,8 @@ syntax = "proto3";
package net.eagle0.eagle.internal;
import "google/protobuf/wrappers.proto";
import "src/main/protobuf/net/eagle0/eagle/api/selected_command.proto";
import "src/main/protobuf/net/eagle0/eagle/common/battalion_type.proto";
import "src/main/protobuf/net/eagle0/eagle/common/command_type.proto";
import "src/main/protobuf/net/eagle0/eagle/common/date.proto";
import "src/main/protobuf/net/eagle0/eagle/common/improvement_type.proto";
import "src/main/protobuf/net/eagle0/eagle/common/province_event.proto";
@@ -104,7 +104,7 @@ message Province {
.net.eagle0.eagle.common.Date last_beasts_date = 25;
.net.eagle0.eagle.common.Date last_riot_date = 35;
.net.eagle0.eagle.api.SelectedCommand last_command = 26;
.net.eagle0.eagle.common.command_type.CommandType last_command = 26;
int32 castle_count = 33;
int32 hero_cap = 39;
@@ -1,18 +1,7 @@
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
swift_proto_library(
name = "action_result_view_swift_proto",
protos = [":action_result_view_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:action_result_notification_details_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_swift_proto",
],
)
scala_proto_library(
name = "action_result_view_scala_proto",
visibility = [
@@ -36,15 +25,6 @@ proto_library(
],
)
swift_proto_library(
name = "army_view_swift_proto",
protos = [":army_view_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:combat_unit_swift_proto",
],
)
scala_proto_library(
name = "army_view_scala_proto",
visibility = [
@@ -65,19 +45,6 @@ proto_library(
],
)
swift_proto_library(
name = "battalion_view_swift_proto",
protos = [":battalion_view_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/protobuf/net/eagle0/eagle/internal:__pkg__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
],
)
scala_proto_library(
name = "battalion_view_scala_proto",
visibility = [
@@ -102,15 +69,6 @@ proto_library(
],
)
swift_proto_library(
name = "faction_relationship_view_swift_proto",
protos = [":faction_relationship_view_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
],
)
scala_proto_library(
name = "faction_relationship_view_scala_proto",
visibility = [
@@ -131,16 +89,6 @@ proto_library(
],
)
swift_proto_library(
name = "faction_view_swift_proto",
protos = [":faction_view_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":faction_relationship_view_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:diplomacy_offer_swift_proto",
],
)
scala_proto_library(
name = "faction_view_scala_proto",
visibility = [
@@ -163,23 +111,6 @@ proto_library(
],
)
swift_proto_library(
name = "game_state_view_swift_proto",
protos = [":game_state_view_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
":battalion_view_swift_proto",
":faction_view_swift_proto",
":hero_view_swift_proto",
":province_view_swift_proto",
":shardok_battle_view_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:chronicle_entry_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_swift_proto",
],
)
scala_proto_library(
name = "game_state_view_scala_proto",
visibility = [
@@ -208,20 +139,6 @@ proto_library(
],
)
swift_proto_library(
name = "hero_view_swift_proto",
protos = [":hero_view_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/api:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
":stat_with_condition_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:gender_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:profession_swift_proto",
],
)
scala_proto_library(
name = "hero_view_scala_proto",
visibility = [
@@ -247,13 +164,6 @@ proto_library(
],
)
swift_proto_library(
name = "incoming_army_view_swift_proto",
protos = [":incoming_army_view_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [":battalion_view_swift_proto"],
)
scala_proto_library(
name = "incoming_army_view_scala_proto",
visibility = [
@@ -270,27 +180,6 @@ proto_library(
deps = [":battalion_view_proto"],
)
swift_proto_library(
name = "province_view_swift_proto",
protos = [":province_view_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
":army_view_swift_proto",
":battalion_view_swift_proto",
":incoming_army_view_swift_proto",
":stat_with_condition_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:date_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:profession_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:province_event_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/common:recruitment_info_swift_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:unaffiliated_hero_swift_proto",
],
)
scala_proto_library(
name = "province_view_scala_proto",
visibility = [
@@ -323,13 +212,6 @@ proto_library(
],
)
swift_proto_library(
name = "stat_with_condition_swift_proto",
protos = [":stat_with_condition_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [],
)
scala_proto_library(
name = "stat_with_condition_scala_proto",
visibility = [
@@ -349,15 +231,6 @@ proto_library(
],
)
swift_proto_library(
name = "shardok_battle_view_swift_proto",
protos = [":shardok_battle_view_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
deps = [
"//src/main/protobuf/net/eagle0/common:hostility_swift_proto",
],
)
scala_proto_library(
name = "shardok_battle_view_scala_proto",
visibility = [
@@ -1,23 +1,8 @@
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
swift_proto_library(
name = "action_result_view_swift_proto",
protos = [":action_result_view_proto"],
visibility = ["//visibility:public"],
deps = [
":game_state_view_swift_proto",
":odds_view_swift_proto",
":unit_view_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:action_type_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:coords_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:game_status_swift_proto",
],
)
scala_proto_library(
name = "action_result_view_scala_proto",
visibility = ["//visibility:public"],
@@ -42,18 +27,6 @@ proto_library(
],
)
swift_proto_library(
name = "command_descriptor_swift_proto",
protos = [":command_descriptor_proto"],
visibility = ["//visibility:public"],
deps = [
":odds_view_swift_proto",
":roll_request_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:command_type_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:coords_swift_proto",
],
)
scala_proto_library(
name = "command_descriptor_scala_proto",
visibility = ["//visibility:public"],
@@ -82,20 +55,6 @@ proto_library(
],
)
swift_proto_library(
name = "game_state_view_swift_proto",
protos = [":game_state_view_proto"],
visibility = ["//visibility:public"],
deps = [
":unit_view_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:game_status_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:hex_map_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:player_info_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:tile_modifier_with_coords_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:weather_swift_proto",
],
)
cc_proto_library(
name = "game_state_view_cc_proto",
visibility = ["//visibility:public"],
@@ -119,13 +78,6 @@ proto_library(
],
)
swift_proto_library(
name = "odds_view_swift_proto",
protos = [":odds_view_proto"],
visibility = ["//visibility:public"],
deps = [":unit_view_swift_proto"],
)
cc_proto_library(
name = "odds_view_cc_proto",
visibility = ["//visibility:public"],
@@ -153,16 +105,6 @@ proto_library(
],
)
swift_proto_library(
name = "roll_request_swift_proto",
protos = [":roll_request_proto"],
visibility = ["//visibility:public"],
deps = [
":odds_view_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:command_type_swift_proto",
],
)
cc_proto_library(
name = "roll_request_cc_proto",
visibility = ["//visibility:public"],
@@ -179,17 +121,6 @@ proto_library(
],
)
swift_proto_library(
name = "unit_view_swift_proto",
protos = [":unit_view_proto"],
visibility = ["//visibility:public"],
deps = [
"//src/main/protobuf/net/eagle0/shardok/common:command_type_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:coords_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:hostility_swift_proto",
],
)
cc_proto_library(
name = "unit_view_cc_proto",
visibility = ["//visibility:public"],
@@ -1,19 +1,8 @@
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
swift_proto_library(
name = "action_type_swift_proto",
protos = [":action_type_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
cc_proto_library(
name = "action_type_cc_proto",
visibility = ["//visibility:public"],
@@ -26,16 +15,6 @@ proto_library(
visibility = ["//src/main/protobuf/net/eagle0/shardok:__subpackages__"],
)
swift_proto_library(
name = "command_type_swift_proto",
protos = [":command_type_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
cc_proto_library(
name = "command_type_cc_proto",
visibility = ["//visibility:public"],
@@ -48,16 +27,6 @@ proto_library(
visibility = ["//src/main/protobuf/net/eagle0/shardok:__subpackages__"],
)
swift_proto_library(
name = "coords_swift_proto",
protos = [":coords_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
cc_proto_library(
name = "coords_cc_proto",
visibility = ["//visibility:public"],
@@ -70,16 +39,6 @@ proto_library(
visibility = ["//src/main/protobuf/net/eagle0/shardok:__subpackages__"],
)
swift_proto_library(
name = "game_status_swift_proto",
protos = [":game_status_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [":victory_condition_swift_proto"],
)
cc_proto_library(
name = "game_status_cc_proto",
visibility = ["//visibility:public"],
@@ -93,21 +52,6 @@ proto_library(
deps = [":victory_condition_proto"],
)
swift_proto_library(
name = "hex_map_swift_proto",
protos = [":hex_map_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
":coords_swift_proto",
":hex_map_direction_swift_proto",
":terrain_swift_proto",
":tile_modifier_swift_proto",
],
)
scala_proto_library(
name = "hex_map_scala_proto",
visibility = ["//visibility:public"],
@@ -134,16 +78,6 @@ proto_library(
],
)
swift_proto_library(
name = "hex_map_direction_swift_proto",
protos = [":hex_map_direction_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
cc_proto_library(
name = "hex_map_direction_cc_proto",
visibility = ["//visibility:public"],
@@ -156,16 +90,6 @@ proto_library(
visibility = ["//src/main/protobuf/net/eagle0/shardok:__subpackages__"],
)
swift_proto_library(
name = "hostility_swift_proto",
protos = [":hostility_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
cc_proto_library(
name = "hostility_cc_proto",
visibility = ["//visibility:public"],
@@ -178,16 +102,6 @@ proto_library(
visibility = ["//visibility:public"],
)
swift_proto_library(
name = "player_info_swift_proto",
protos = [":player_info_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [":victory_condition_swift_proto"],
)
cc_proto_library(
name = "player_info_cc_proto",
visibility = ["//visibility:public"],
@@ -205,16 +119,6 @@ proto_library(
],
)
swift_proto_library(
name = "terrain_swift_proto",
protos = [":terrain_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [":tile_modifier_swift_proto"],
)
cc_proto_library(
name = "terrain_cc_proto",
visibility = ["//visibility:public"],
@@ -234,16 +138,6 @@ proto_library(
deps = [":tile_modifier_proto"],
)
swift_proto_library(
name = "tile_modifier_swift_proto",
protos = [":tile_modifier_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
cc_proto_library(
name = "tile_modifier_cc_proto",
visibility = ["//visibility:public"],
@@ -265,19 +159,6 @@ proto_library(
],
)
swift_proto_library(
name = "tile_modifier_with_coords_swift_proto",
protos = [":tile_modifier_with_coords_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
":coords_swift_proto",
":tile_modifier_swift_proto",
],
)
cc_proto_library(
name = "tile_modifier_with_coords_cc_proto",
visibility = ["//visibility:public"],
@@ -294,16 +175,6 @@ proto_library(
],
)
swift_proto_library(
name = "victory_condition_swift_proto",
protos = [":victory_condition_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [],
)
cc_proto_library(
name = "victory_condition_cc_proto",
visibility = ["//visibility:public"],
@@ -316,16 +187,6 @@ proto_library(
visibility = ["//src/main/protobuf/net/eagle0/shardok:__subpackages__"],
)
swift_proto_library(
name = "weather_swift_proto",
protos = [":weather_proto"],
visibility = [
"//src/main/protobuf/net/eagle0:__subpackages__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [":hex_map_direction_swift_proto"],
)
cc_proto_library(
name = "weather_cc_proto",
visibility = ["//visibility:public"],
@@ -1,26 +1,8 @@
load("@build_bazel_rules_swift//proto:proto.bzl", "swift_proto_library")
load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library")
load("@rules_cc//cc:defs.bzl", "cc_proto_library")
load("@rules_proto//proto:defs.bzl", "proto_library")
load("@rules_scala//scala_proto:scala_proto.bzl", "scala_proto_library")
swift_proto_library(
name = "action_result_swift_proto",
protos = [":action_result_proto"],
visibility = [
"//src/main/protobuf/net/eagle0/eagle/internal:__pkg__",
"//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__",
],
deps = [
":odds_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:action_type_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:coords_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:game_status_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:tile_modifier_with_coords_swift_proto",
"//src/main/protobuf/net/eagle0/shardok/common:weather_swift_proto",
],
)
scala_proto_library(
name = "action_result_scala_proto",
visibility = ["//visibility:public"],
@@ -67,12 +49,6 @@ proto_library(
],
)
swift_proto_library(
name = "odds_swift_proto",
protos = [":odds_proto"],
visibility = ["//src/main/swift/net/eagle0/EagleGameHistoryViewer:__subpackages__"],
)
cc_proto_library(
name = "odds_cc_proto",
visibility = ["//visibility:public"],
@@ -154,7 +154,6 @@ scala_library(
":ai_client_utils",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util:battalion_utils",
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
@@ -11,6 +11,7 @@ scala_library(
"//src/test/scala/net/eagle0/eagle/service/controller:__pkg__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update",
],
@@ -21,6 +22,7 @@ scala_library(
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/views:action_result_view_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_resolution",
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_update",
@@ -75,6 +77,8 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/available:one_province_available_commands_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
@@ -170,6 +174,7 @@ scala_library(
name = "round_phase_advancer",
srcs = ["RoundPhaseAdvancer.scala"],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:round_phase_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
@@ -2,8 +2,8 @@ package net.eagle0.eagle.library
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
import net.eagle0.eagle.api.command.AvailableCommands
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.shardok_interface.{BattleResolution, BattleUpdate}
import net.eagle0.eagle.views.action_result_view.ActionResultView
@@ -5,7 +5,6 @@ import scala.annotation.tailrec
import net.eagle0.common.SeededRandom
import net.eagle0.eagle.{FactionId, GameId, ProvinceId}
import net.eagle0.eagle.api.command.AvailableCommands
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.internal.action_result.ActionResult
import net.eagle0.eagle.library.actions.applier.{ActionResultApplierImpl, ActionResultWithResultingState}
import net.eagle0.eagle.library.actions.availability.AvailableCommandsFactory
@@ -24,6 +23,8 @@ import net.eagle0.eagle.library.EngineImpl.{appliedResults, appliedResultsScala,
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.proto_converters.command.available.OneProvinceAvailableCommandsConverter
import net.eagle0.eagle.model.proto_converters.game_state.GameStateConverter
import net.eagle0.eagle.model.state.command.available.AvailableCommand as ScalaAvailableCommand
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.shardok_interface.{BattleResolution, BattleUpdate}
import net.eagle0.eagle.views.action_result_view.ActionResultView
@@ -286,34 +287,35 @@ case class EngineImpl(
selectedProvinceId: ProvinceId,
selectedCommand: SelectedCommand
): EngineAndResults = {
val maybeAvailableCommands = getAvailablePlayerCommands(factionId)
// Get Scala commands directly from factory
val scalaCommandsByProvince =
availableCommandsFactory.availablePlayerCommands(currentState, factionId)
commandRequire(
maybeAvailableCommands.nonEmpty,
scalaCommandsByProvince.nonEmpty,
s"Attempted to post a command for faction $factionId, but there are none"
)
val availableCommands = maybeAvailableCommands.get
commandRequire(
availableCommands.commandsByProvince.contains(selectedProvinceId),
s"Attempted to select province $selectedProvinceId out of ${availableCommands.commandsByProvince.keys}"
scalaCommandsByProvince.contains(selectedProvinceId),
s"Attempted to select province $selectedProvinceId out of ${scalaCommandsByProvince.keys}"
)
val availableCommandsForProvince =
availableCommands.commandsByProvince(selectedProvinceId)
val scalaCommandsForProvince = scalaCommandsByProvince(selectedProvinceId)
val availableCommandOpt = AvailableCommandTypeMap.matchingAvailableCommand(
availableCommandsForProvince.commands,
// Match using CommandType
val scalaAvailableCommandOpt = AvailableCommandTypeMap.matchingAvailableCommand(
scalaCommandsForProvince.commands,
selectedCommand
)
commandRequire(
availableCommandOpt.isDefined,
scalaAvailableCommandOpt.isDefined,
s"No matching available command for selected command $selectedCommand"
)
val availableCommand = availableCommandOpt.get
val scalaAvailableCommand = scalaAvailableCommandOpt.get
val sequencer = RandomStateSequencer(
val commandType = selectedCommand.commandType
val sequencer = RandomStateSequencer(
initialState = this.currentState,
actionResultApplier = ActionResultApplierImpl(Some(ScalaRuntimeValidator)),
functionalRandom = SeededRandom(this.currentState.randomSeed)
@@ -322,10 +324,10 @@ case class EngineImpl(
commandFactory.makeTCommand(
actingFactionId = factionId,
gameState = gs,
availableCommand = availableCommand,
availableCommand = scalaAvailableCommand,
selectedCommand = selectedCommand
),
selectedCommand = selectedCommand
commandType = commandType
).withProtolessSequentialResultsAction(gs => HeroBackstoryUpdateActionGenerator(gs))
// Validate that the first result has an acting faction set (required for player commands)
@@ -54,12 +54,12 @@ scala_library(
deps = [
":province_update_helpers",
":province_update_helpers2",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province/concrete",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
@@ -155,13 +155,13 @@ scala_library(
":game_state_hero_extensions",
":game_state_misc_extensions",
":game_state_province_extensions",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_faction_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
deps = [
@@ -171,7 +171,6 @@ scala_library(
":game_state_hero_extensions",
":game_state_misc_extensions",
":game_state_province_extensions",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:extra_xp_for_stat_bump_over100",
"//src/main/scala/net/eagle0/eagle/library/settings:xp_for_stat_bump",
@@ -180,6 +179,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_hero_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:notification_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
@@ -1,11 +1,11 @@
package net.eagle0.eagle.library.actions.applier
import net.eagle0.eagle.*
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.library.EagleInternalException
import net.eagle0.eagle.model.action_result.changed_province.concrete.ChangedProvinceC
import net.eagle0.eagle.model.action_result.changed_province.ChangedProvinceT
import net.eagle0.eagle.model.state.command.CommandType
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.concrete.ProvinceC
import net.eagle0.eagle.model.state.province.ProvinceT
@@ -79,10 +79,10 @@ object GameStateProvinceExtensions {
def applyLastCommand(
provinceId: Option[ProvinceId],
lastCommand: Option[SelectedCommand]
lastCommand: Option[CommandType]
): GameState =
provinceId
.filter(_ => lastCommand.exists(!_.isEmpty))
.filter(_ => lastCommand.isDefined)
.map { pid =>
val province = gameState.provinces(pid) match {
case p: ProvinceC => p
@@ -1,5 +1,6 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.common.hostility.Hostility
import net.eagle0.common.MoreOption
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.library.util.{IncomingArmyUtils, ProvinceDistances}
@@ -62,11 +63,13 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
.takeRight(heroCount)
.sum
province.rulingFactionId.map { _ =>
province.rulingFactionId.map { defenderFid =>
ArmyStats(
factionId = province.rulingFactionId.get,
factionId = defenderFid,
heroCount = heroCount,
troopCount = troopCount
troopCount = troopCount,
hostility = FactionUtils.hostilityStatus(attackerFid, defenderFid, gs.factions.values.toVector),
originProvinceId = pid
)
}
}
@@ -76,22 +79,26 @@ object AvailableAttackDecisionCommandFactory extends ScalaAvailableCommandsFacto
pid: ProvinceId,
gs: GameState
): Vector[ArmyStats] =
relevantArmies(pid, gs).map(ma => stats(ma.army, gs)) ++ defendingArmyStats(
relevantArmies(pid, gs).map(ma => stats(toFid, ma, gs)) ++ defendingArmyStats(
toFid,
pid,
gs
)
private def stats(
army: Army,
toFid: FactionId,
movingArmy: MovingArmy,
gs: GameState
): ArmyStats = {
val army = movingArmy.army
val battalions = army.units.flatMap(_.battalionId).map(gs.battalions)
val troopCount = battalions.map(_.size).sum
ArmyStats(
factionId = army.factionId,
heroCount = army.units.size,
troopCount = troopCount
troopCount = troopCount,
hostility = FactionUtils.hostilityStatus(toFid, army.factionId, gs.factions.values.toVector),
originProvinceId = movingArmy.originProvinceId
)
}
@@ -7,11 +7,10 @@ import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.province.ProvinceUtils
import net.eagle0.eagle.library.util.IncomingArmyUtils
import net.eagle0.eagle.model.proto_converters.command.selected.SelectedCommandConverter
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand.*
import net.eagle0.eagle.model.state.command.available.OneProvinceAvailableCommands
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
import net.eagle0.eagle.model.state.command.CommandType
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.model.state.province.ProvinceT
import net.eagle0.eagle.model.state.RoundPhase
@@ -19,66 +18,25 @@ import net.eagle0.eagle.model.state.RoundPhase.*
object AvailableCommandsFactory {
/** Command types that should not suggest a follow-up of the same type. */
private val noFollowCommands: Set[CommandType] = Set(
CommandType.March,
CommandType.Diplomacy,
CommandType.Rest,
CommandType.Trade,
CommandType.ArmTroops,
CommandType.OrganizeTroops,
CommandType.ExileVassal,
CommandType.SwearBrotherhood
)
/** Returns true if after executing `last`, the UI should suggest `next` as a follow-up. */
def shouldFollow(
last: SelectedCommand,
last: CommandType,
next: AvailableCommand
): Boolean = (last, next) match {
case (_: SelectedCommand.MarchSelected, _) => false
case (_: SelectedCommand.DiplomacySelected, _) => false
case (SelectedCommand.RestSelected, _) => false
case (_: SelectedCommand.TradeSelected, _) => false
case (_: SelectedCommand.ArmTroopsSelected, _) => false
case (_: SelectedCommand.OrganizeTroopsSelected, _) => false
case (_: SelectedCommand.ExileVassalSelected, _) => false
case (_: SelectedCommand.SwearBrotherhoodSelected, _) => false
case (last, next) => commandTypesMatch(last, next)
}
/** Compares ordinal values to check if the selected and available commands are the same type. */
private def commandTypesMatch(selected: SelectedCommand, available: AvailableCommand): Boolean =
(selected, available) match {
case (_: SelectedCommand.AlmsSelected, _: AlmsAvailable) => true
case (_: SelectedCommand.ApprehendOutlawSelected, _: ApprehendOutlawAvailable) => true
case (_: SelectedCommand.ArmTroopsSelected, _: ArmTroopsAvailable) => true
case (_: SelectedCommand.AttackDecisionSelected, _: AttackDecisionAvailable) => true
case (_: SelectedCommand.ControlWeatherSelected, _: ControlWeatherAvailable) => true
case (_: SelectedCommand.DeclineQuestSelected, _: DeclineQuestAvailable) => true
case (_: SelectedCommand.DefendSelected, _: DefendAvailable) => true
case (_: SelectedCommand.DiplomacySelected, _: DiplomacyAvailable) => true
case (_: SelectedCommand.DivineSelected, _: DivineAvailable) => true
case (_: SelectedCommand.ExileVassalSelected, _: ExileVassalAvailable) => true
case (SelectedCommand.FeastSelected, _: FeastAvailable) => true
case (_: SelectedCommand.FreeForAllDecisionSelected, _: FreeForAllDecisionAvailable) => true
case (_: SelectedCommand.HandleCapturedHeroSelected, _: HandleCapturedHeroAvailable) => true
case (_: SelectedCommand.HandleRiotCrackDownSelected, _: HandleRiotCrackDownAvailable) => true
case (SelectedCommand.HandleRiotDoNothingSelected, _: HandleRiotDoNothingAvailable) => true
case (_: SelectedCommand.HandleRiotGiveSelected, _: HandleRiotGiveAvailable) => true
case (_: SelectedCommand.HeroGiftSelected, _: HeroGiftAvailable) => true
case (_: SelectedCommand.ImproveSelected, _: ImproveAvailable) => true
case (_: SelectedCommand.IssueOrdersSelected, _: IssueOrdersAvailable) => true
case (_: SelectedCommand.ManagePrisonersSelected, _: ManagePrisonersAvailable) => true
case (_: SelectedCommand.MarchSelected, _: MarchAvailable) => true
case (_: SelectedCommand.OrganizeTroopsSelected, _: OrganizeTroopsAvailable) => true
case (_: SelectedCommand.PleaseRecruitMeSelected, _: PleaseRecruitMeAvailable) => true
case (_: SelectedCommand.ReconSelected, _: ReconAvailable) => true
case (_: SelectedCommand.RecruitHeroesSelected, _: RecruitHeroesAvailable) => true
case (_: SelectedCommand.ResolveAllianceOfferSelected, _: ResolveAllianceOfferAvailable) => true
case (_: SelectedCommand.ResolveBreakAllianceSelected, _: ResolveBreakAllianceAvailable) => true
case (_: SelectedCommand.ResolveInvitationSelected, _: ResolveInvitationAvailable) => true
case (_: SelectedCommand.ResolveRansomOfferSelected, _: ResolveRansomOfferAvailable) => true
case (_: SelectedCommand.ResolveTruceOfferSelected, _: ResolveTruceOfferAvailable) => true
case (_: SelectedCommand.ResolveTributeSelected, _: ResolveTributeAvailable) => true
case (SelectedCommand.RestSelected, _: RestAvailable) => true
case (SelectedCommand.ReturnSelected, _: ReturnAvailable) => true
case (_: SelectedCommand.SendSuppliesSelected, _: SendSuppliesAvailable) => true
case (_: SelectedCommand.StartEpidemicSelected, _: StartEpidemicAvailable) => true
case (_: SelectedCommand.SuppressBeastsSelected, _: SuppressBeastsAvailable) => true
case (_: SelectedCommand.SwearBrotherhoodSelected, _: SwearBrotherhoodAvailable) => true
case (_: SelectedCommand.TradeSelected, _: TradeAvailable) => true
case (_: SelectedCommand.TrainSelected, _: TrainAvailable) => true
case _ => false
}
): Boolean =
if noFollowCommands.contains(last) then false
else last == next.commandType
def suggestedProvinceId(
fid: FactionId,
@@ -210,10 +168,7 @@ class AvailableCommandsFactory(
): Int =
commands.zipWithIndex.find {
case (ac, _) =>
province.lastCommand.forall { lastProto =>
val last = SelectedCommandConverter.fromProto(lastProto)
AvailableCommandsFactory.shouldFollow(last, ac)
}
province.lastCommand.forall(last => AvailableCommandsFactory.shouldFollow(last, ac))
}
.map(_._2)
.getOrElse(0)
@@ -1,6 +1,8 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.common.hostility.Hostility
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.library.util.faction_utils.FactionUtils
import net.eagle0.eagle.library.util.IncomingArmyUtils
import net.eagle0.eagle.model.state.{Army, HostileArmyGroupStatus, MovingArmy}
import net.eagle0.eagle.model.state.command.available.AvailableCommand
@@ -53,19 +55,22 @@ object AvailableFreeForAllDecisionCommandFactory extends ScalaAvailableCommandsF
pid: ProvinceId,
gs: GameState
): Vector[ArmyStats] =
relevantArmies(pid, gs).map(ma => stats(toFid, ma.army, gs))
relevantArmies(pid, gs).map(ma => stats(toFid, ma, gs))
private def stats(
toFid: FactionId,
army: Army,
movingArmy: MovingArmy,
gs: GameState
): ArmyStats = {
val army = movingArmy.army
val battalions = army.units.flatMap(_.battalionId).map(gs.battalions)
val troopCount = battalions.map(_.size).sum
ArmyStats(
factionId = army.factionId,
heroCount = army.units.size,
troopCount = troopCount
troopCount = troopCount,
hostility = FactionUtils.hostilityStatus(toFid, army.factionId, gs.factions.values.toVector),
originProvinceId = movingArmy.originProvinceId
)
}
@@ -71,10 +71,9 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/selected:selected_command_converter",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/province",
],
@@ -316,6 +315,7 @@ scala_library(
":scala_available_commands_factory",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util:incoming_army_utils",
"//src/main/scala/net/eagle0/eagle/library/util/faction_utils",
"//src/main/scala/net/eagle0/eagle/model/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
@@ -444,6 +444,7 @@ scala_library(
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
@@ -1306,6 +1307,7 @@ scala_library(
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:province_order_type_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/common:more_option",
@@ -1344,6 +1346,7 @@ scala_library(
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
@@ -52,11 +52,12 @@ case class EndHandleRiotsPhaseAction(
)
.continue {
case (Some(cs), nextFr) =>
val scs = cs.toScala(opac.commands)
val cmd = commandFactory.makeTCommand(
actingFactionId = province.rulingFactionId.get,
gameState = gs,
availableCommand = cs.available,
selectedCommand = cs.selected
availableCommand = scs.available,
selectedCommand = scs.selected
)
cmd match {
case TCommand.Simple(action) =>
@@ -50,11 +50,12 @@ case class PerformVassalCommandsPhaseAction(
sequencer.withRandomActionResults { (gs, fr) =>
chooseCommand(gs, province.id, fr).continue {
case (Some(cs), nextFr) =>
val scs = cs.toScala(opac.commands)
val cmd = commandFactory.makeTCommand(
actingFactionId = cs.actingFactionId,
gameState = gs,
availableCommand = cs.available,
selectedCommand = cs.selected
availableCommand = scs.available,
selectedCommand = scs.selected
)
cmd match {
case TCommand.Simple(action) =>
@@ -50,11 +50,12 @@ case class PerformVassalDefenseDecisionsAction(
}
chooseCommand(gs, province.id, protoCommands, fr).continue {
case (Some(cs), nextFr) =>
val scs = cs.toScala(opac.commands)
val cmd = commandFactory.makeTCommand(
actingFactionId = cs.actingFactionId,
gameState = gs,
availableCommand = cs.available,
selectedCommand = cs.selected
availableCommand = scs.available,
selectedCommand = scs.selected
)
cmd match {
case TCommand.Simple(action) =>
@@ -1,17 +1,16 @@
package net.eagle0.eagle.library.actions
package impl.command
import net.eagle0.eagle.api.available_command.*
import net.eagle0.eagle.api.selected_command.*
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
object AvailableCommandTypeMap {
def matchingAvailableCommand(
acs: Iterable[AvailableCommand],
sc: SelectedCommand
availableCommands: Iterable[AvailableCommand],
selectedCommand: SelectedCommand
): Option[AvailableCommand] =
acs.find(ac => matches(ac, sc))
availableCommands.find(_.commandType == selectedCommand.commandType)
// This is pretty hacky but avoids a long match that has to be kept up to date.
def matches(ac: AvailableCommand, sc: SelectedCommand): Boolean =
ac.asMessage.sealedValue.number == sc.asMessage.sealedValue.number
ac.commandType == sc.commandType
}
@@ -9,12 +9,14 @@ scala_library(
],
exports = [
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
],
)
@@ -71,10 +73,6 @@ scala_library(
":trade_command",
":train_command",
":travel_command",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:control_weather_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:province_orders_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library:eagle_client_exception",
"//src/main/scala/net/eagle0/eagle/library:eagle_internal_exception",
@@ -90,25 +88,14 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_battalion_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:changed_hero_concrete",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_id_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:combat_unit_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:improvement_type_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:proto_conversion_exception",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:tribute_amount_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:unaffiliated_hero_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/diplomacy_offer",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/diplomacy_offer/status",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/diplomacy_option",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:order_type",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type",
"//src/main/scala/net/eagle0/eagle/model/state:combat_unit",
"//src/main/scala/net/eagle0/eagle/model/state:improvement_type",
"//src/main/scala/net/eagle0/eagle/model/state:tribute_amount",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/command/common",
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer",
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_option",
@@ -223,8 +210,8 @@ scala_library(
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
],
)
File diff suppressed because it is too large Load Diff
@@ -1,8 +1,8 @@
package net.eagle0.eagle.library.actions.impl.command
import net.eagle0.eagle.api.available_command.AvailableCommand
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.library.actions.impl.common.TCommand
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
import net.eagle0.eagle.model.state.game_state.GameState
import net.eagle0.eagle.FactionId
@@ -1,7 +1,6 @@
package net.eagle0.eagle.library.actions.random_state_sequencer
import net.eagle0.common.{FunctionalRandom, RandomState}
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.library.actions.applier.{ActionResultApplier, ActionResultWithResultingState}
import net.eagle0.eagle.library.actions.impl.common.{
ProtolessRandomSimpleAction,
@@ -10,6 +9,7 @@ import net.eagle0.eagle.library.actions.impl.common.{
TCommand
}
import net.eagle0.eagle.model.action_result.ActionResultT
import net.eagle0.eagle.model.state.command.CommandType
import net.eagle0.eagle.model.state.game_state.GameState
/**
@@ -87,7 +87,7 @@ trait RandomStateSequencer {
*/
def withTCommandAndLastCommand(
commandGen: GameState => TCommand,
selectedCommand: SelectedCommand
commandType: CommandType
): RandomStateSequencer
}
@@ -189,10 +189,10 @@ private case class RandomStateSequencerImpl(
override def withTCommandAndLastCommand(
commandGen: GameState => TCommand,
selectedCommand: SelectedCommand
commandType: CommandType
): RandomStateSequencer = {
def wrapWithLastCommand(result: ActionResultT): ActionResultT =
result.withLastCommandTypeForActingProvince(selectedCommand)
result.withLastCommandTypeForActingProvince(commandType)
commandGen(lastState) match {
case TCommand.Simple(action) =>
@@ -127,6 +127,9 @@ scala_library(
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/selected:selected_command_converter",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
],
)
@@ -1,16 +1,51 @@
package net.eagle0.eagle.library.util
import net.eagle0.eagle.{FactionId, ProvinceId}
import net.eagle0.eagle.api.available_command.AvailableCommand
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.api.available_command.AvailableCommand as ProtoAvailableCommand
import net.eagle0.eagle.api.selected_command.SelectedCommand as ProtoSelectedCommand
import net.eagle0.eagle.model.proto_converters.command.selected.SelectedCommandConverter
import net.eagle0.eagle.model.state.command.available.AvailableCommand
import net.eagle0.eagle.model.state.command.selected.SelectedCommand
// Proto-based command selection (legacy, for gradual migration)
case class CommandSelection(
actingFactionId: FactionId,
actingProvinceId: ProvinceId,
available: ProtoAvailableCommand,
selected: ProtoSelectedCommand,
reason: String
) {
def withReason(reason: String): CommandSelection =
this.copy(reason = reason)
/** Convert to ScalaCommandSelection by finding the matching Scala AvailableCommand */
def toScala(scalaAvailableCommands: Iterable[AvailableCommand]): ScalaCommandSelection = {
val scalaSelectedCommand = SelectedCommandConverter.fromProto(selected)
val scalaAvailableCommand = scalaAvailableCommands
.find(_.commandType == scalaSelectedCommand.commandType)
.getOrElse(
throw new IllegalStateException(
s"No matching Scala available command for selected command: ${scalaSelectedCommand.commandType}"
)
)
ScalaCommandSelection(
actingFactionId = actingFactionId,
actingProvinceId = actingProvinceId,
available = scalaAvailableCommand,
selected = scalaSelectedCommand,
reason = reason
)
}
}
// Scala-based command selection
case class ScalaCommandSelection(
actingFactionId: FactionId,
actingProvinceId: ProvinceId,
available: AvailableCommand,
selected: SelectedCommand,
reason: String
) {
def withReason(reason: String): CommandSelection =
def withReason(reason: String): ScalaCommandSelection =
this.copy(reason = reason)
}
@@ -188,6 +188,7 @@ scala_library(
":ransom_offer_helpers",
"//src/main/protobuf/net/eagle0/common:hostility_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:armed_battalion_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:battalion_with_food_cost_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
@@ -260,7 +261,6 @@ scala_library(
":province_gold_surplus_calculator",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/settings:max_combat_unit_count_per_side",
@@ -395,7 +395,6 @@ scala_library(
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:battalion_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
"//src/main/scala/net/eagle0/eagle/library/util/battalion_type_finder",

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