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
109 changed files with 4240 additions and 2493 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
+153 -431
View File
@@ -26,203 +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
- name: Build all Docker images
id: build-all
run: |
set -ex
bazel build --platforms=//:linux_x86_64 //ci:eagle_server_image
# Also build the warmup tool for Linux
bazel build //src/main/go/net/eagle0/warmup:warmup_linux_amd64
# 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 \
//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
# Copy warmup binary to scripts/ so it gets deployed with other scripts
# 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
# 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
# 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)
- 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/
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
- 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
run: |
set -ex
echo "=== Image paths ==="
echo "Eagle: $EAGLE_PATH"
echo "Shardok: $SHARDOK_PATH"
echo "Admin: $ADMIN_PATH"
echo "JFR Sidecar: $JFR_PATH"
# 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 ==="
bazel build \
--platforms=//:linux_x86_64 \
--extra_toolchains=@llvm_toolchain_linux//:all \
//src/main/cpp/net/eagle0/shardok:shardok-server
# 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 ==="
if [ ! -f "$LINUX_BIN" ]; then
echo "ERROR: Binary not found at $LINUX_BIN"
exit 1
fi
# Debug: show what bazel-bin points to
echo "bazel-bin symlink target: $(readlink bazel-bin || echo 'not a symlink')"
# 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"
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
@@ -233,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 }}
@@ -450,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:
@@ -471,10 +211,19 @@ jobs:
- name: Copy config files to droplet
run: |
scp -i ~/.ssh/deploy_key \
docker-compose.prod.yml nginx/nginx.conf \
scripts/deploy-blue-green.sh scripts/warmup-eagle.sh scripts/bin/warmup \
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: |
@@ -500,130 +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 non-Eagle services (not auth - managed by auth_build.yml)
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate shardok admin jfr-sidecar
# Recreate non-Eagle services
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate shardok admin
# Deploy Eagle with blue-green (zero-downtime) if scripts are available
# 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
# Make warmup binary executable if present
if [ -f "/opt/eagle0/scripts/bin/warmup" ]; then
chmod +x /opt/eagle0/scripts/bin/warmup
fi
[ -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 deployment failed, falling back to direct restart"
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
echo "Blue-green scripts not found, using direct restart..."
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle-blue
fi
# Ensure auth is running (but don't force-recreate it)
# 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
+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"
+16 -8
View File
@@ -24,12 +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
# 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'
upstream eagle_grpc {
server eagle-blue:40032;
keepalive 100;
map $host $eagle_backend {
default "eagle-blue:40032";
}
# HTTP server for Let's Encrypt challenge and redirect
@@ -73,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;
@@ -90,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;
@@ -108,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"
+43 -3
View File
@@ -48,6 +48,44 @@ get_active_instance() {
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
@@ -93,9 +131,11 @@ main() {
log_info "Active instance: eagle-${active}"
log_info "Staging instance: eagle-${staging}"
# Pull the new image
log_info "Pulling new image..."
docker pull "${new_image}"
# 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..."
+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"
@@ -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" />
@@ -385,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
}
@@ -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);
@@ -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.");
@@ -17,6 +17,7 @@ import (
type InvitationHTTPHandler struct {
invitationService *InvitationService
installerURL string
macInstallerURL string
}
// NewInvitationHTTPHandler creates a new invitation HTTP handler
@@ -25,9 +26,14 @@ func NewInvitationHTTPHandler(invitationService *InvitationService) *InvitationH
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,
}
}
@@ -38,7 +44,7 @@ func (h *InvitationHTTPHandler) RegisterRoutes() {
// 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
// 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, "/")
@@ -51,6 +57,8 @@ func (h *InvitationHTTPHandler) handleInvite(w http.ResponseWriter, r *http.Requ
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 {
@@ -58,21 +66,34 @@ func (h *InvitationHTTPHandler) handleInvite(w http.ResponseWriter, r *http.Requ
}
}
// 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
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,
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 {
@@ -160,6 +181,88 @@ timeout /t 5
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>
@@ -277,6 +380,21 @@ var landingPageTemplate = template.Must(template.New("landing").Parse(`<!DOCTYPE
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>
@@ -294,17 +412,33 @@ var landingPageTemplate = template.Must(template.New("landing").Parse(`<!DOCTYPE
<div class="content">
<p>You've been invited to play Eagle0, a strategic turn-based game with tactical hex-based combat.</p>
<a href="{{.InstallBatURL}}" class="button">Download & Install</a>
{{if .IsMac}}
<a href="{{.InstallShURL}}" class="button">Download for Mac</a>
<div class="instructions">
<h3>How to install:</h3>
<h3>How to install on Mac:</h3>
<ol>
<li>Click the "Download & Install" button above</li>
<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">
@@ -313,10 +447,23 @@ var landingPageTemplate = template.Must(template.New("landing").Parse(`<!DOCTYPE
{{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">
@@ -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)
}
+86 -28
View File
@@ -136,24 +136,50 @@ func runWarmup(ctx context.Context) error {
return fmt.Errorf("failed to send stream game request: %w", err)
}
// Wait for subscription ack
_, err = waitForResponse(stream, func(resp *eagle.UpdateStreamResponse) bool {
return resp.GetSubscriptionAck() != nil
})
if err != nil {
return fmt.Errorf("failed to get subscription ack: %w", err)
}
log.Println(" Subscription acknowledged")
// 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()
return ar != nil && ar.GetAvailableCommands() != nil
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)
}
@@ -174,12 +200,14 @@ func runWarmup(ctx context.Context) error {
var improveCmd *eagle.AvailableCommand
var provinceID int32
var actingHeroID int32
for pid, provCmds := range commandsByProvince {
for _, cmd := range provCmds.GetCommands() {
if cmd.GetImproveCommand() != nil {
if ic := cmd.GetImproveCommand(); ic != nil {
improveCmd = cmd
provinceID = pid
actingHeroID = ic.GetRecommendedHeroId()
break
}
}
@@ -200,11 +228,11 @@ func runWarmup(ctx context.Context) error {
}
// We'll still try to post an Improve command even if not explicitly available
} else {
log.Printf(" Found Improve command for province %d", provinceID)
log.Printf(" Found Improve command for province %d with recommended hero %d", provinceID, actingHeroID)
}
// Post the Improve command
log.Println(" Posting 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{
@@ -218,7 +246,7 @@ func runWarmup(ctx context.Context) error {
SealedValue: &eagle.SelectedCommand_ImproveCommand{
ImproveCommand: &eagle.ImproveSelectedCommand{
ImprovementType: eaglecommon.ImprovementType_ECONOMY,
ActingHeroId: 0, // Default acting hero
ActingHeroId: actingHeroID,
LockType: false,
},
},
@@ -233,32 +261,58 @@ func runWarmup(ctx context.Context) error {
}
// Step 5: Verify we get ActionResults
log.Println("Step 5: Waiting for command response...")
log.Println("Step 5: Waiting for command response and collecting results...")
// Wait for post command response
postResp, err := waitForResponse(stream, func(resp *eagle.UpdateStreamResponse) bool {
return resp.GetPostCommandResponse() != nil
// 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 postResp.GetPostCommandResponse().GetStatus() != eagle.PostCommandResponse_SUCCESS {
log.Printf(" Command was rejected: %v", postResp.GetPostCommandResponse().GetStatus())
// Continue anyway - the JIT is still warming up
} else {
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)
}
// Collect action results with a timeout
log.Println(" Collecting action results...")
// 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()
foundNewRound := false
resultCount := 0
commandCount := 0
for {
select {
case <-resultCtx.Done():
@@ -306,6 +360,7 @@ done:
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...")
@@ -333,6 +388,9 @@ done:
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
}
@@ -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 = [
@@ -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 = [
@@ -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
@@ -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)
@@ -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",
],
@@ -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",
@@ -1,12 +1,12 @@
package net.eagle0.eagle.model.action_result
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.model.action_result.changed_province.ChangedProvinceT
import net.eagle0.eagle.model.action_result.generated_text_request.GeneratedTextRequestT
import net.eagle0.eagle.model.action_result.types.ActionResultType
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.chronicle_entry.ChronicleEntry
import net.eagle0.eagle.model.state.command.CommandType
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.hero.HeroT
@@ -29,7 +29,7 @@ trait ActionResultT {
def newRoundId: Option[RoundId]
def newDate: Option[Date]
def lastCommandTypeForActingProvince: Option[SelectedCommand]
def lastCommandTypeForActingProvince: Option[CommandType]
def newBattle: Option[ShardokBattle]
def resolvedBattle: Option[String]
@@ -75,7 +75,7 @@ trait ActionResultT {
newRoundPhase: Option[RoundPhase] = newRoundPhase,
newRoundId: Option[RoundId] = newRoundId,
newDate: Option[Date] = newDate,
lastCommandTypeForActingProvince: Option[SelectedCommand] = lastCommandTypeForActingProvince,
lastCommandTypeForActingProvince: Option[CommandType] = lastCommandTypeForActingProvince,
newBattle: Option[ShardokBattle] = newBattle,
resolvedBattle: Option[String] = resolvedBattle,
// resolvedImminentBattle: Option[Int] = None,
@@ -160,9 +160,9 @@ trait ActionResultT {
copy(newRandomSeed = Some(seed))
def withLastCommandTypeForActingProvince(
selectedCommand: SelectedCommand
commandType: CommandType
): ActionResultT =
copy(lastCommandTypeForActingProvince = Some(selectedCommand))
copy(lastCommandTypeForActingProvince = Some(commandType))
def update(updater: ActionResultT.ActionResultUpdater): ActionResultT =
updater(this)
@@ -32,7 +32,6 @@ scala_library(
":changed_hero_trait",
":client_text_visibility_extension_trait",
":notification_trait",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/scala/net/eagle0/eagle/model/action_result/changed_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
"//src/main/scala/net/eagle0/eagle/model/action_result/types",
@@ -40,6 +39,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/chronicle_entry",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
"//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",
@@ -53,7 +53,6 @@ scala_library(
":changed_hero_trait",
":client_text_visibility_extension_trait",
":notification_trait",
"//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_province",
"//src/main/scala/net/eagle0/eagle/model/action_result/generated_text_request",
@@ -62,6 +61,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/chronicle_entry",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
"//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",
@@ -1,7 +1,6 @@
package net.eagle0.eagle.model.action_result.concrete
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.model.action_result.{
ActionResultT,
ChangedBattalionT,
@@ -15,6 +14,7 @@ import net.eagle0.eagle.model.action_result.generated_text_request.GeneratedText
import net.eagle0.eagle.model.action_result.types.ActionResultType
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.chronicle_entry.ChronicleEntry
import net.eagle0.eagle.model.state.command.CommandType
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.hero.HeroT
@@ -36,7 +36,7 @@ case class ActionResultC(
newRoundPhase: Option[RoundPhase] = None,
newRoundId: Option[RoundId] = None,
newDate: Option[Date] = None,
lastCommandTypeForActingProvince: Option[SelectedCommand] = None,
lastCommandTypeForActingProvince: Option[CommandType] = None,
newBattle: Option[ShardokBattle] = None,
resolvedBattle: Option[String] = None,
newChronicleEntry: Option[ChronicleEntry] = None,
@@ -73,7 +73,7 @@ case class ActionResultC(
newRoundPhase: Option[RoundPhase] = newRoundPhase,
newRoundId: Option[RoundId] = newRoundId,
newDate: Option[Date] = newDate,
lastCommandTypeForActingProvince: Option[SelectedCommand] = lastCommandTypeForActingProvince,
lastCommandTypeForActingProvince: Option[CommandType] = lastCommandTypeForActingProvince,
newBattle: Option[ShardokBattle] = newBattle,
resolvedBattle: Option[String] = resolvedBattle,
newChronicleEntry: Option[ChronicleEntry] = newChronicleEntry,
@@ -16,7 +16,6 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/types",
],
deps = [
"//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:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/action_result:changed_battalion_trait",
@@ -31,6 +30,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/chronicle_entry",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
"//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",
@@ -1,8 +1,8 @@
package net.eagle0.eagle.model.proto_converters
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId, RoundId}
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.common.action_result_type.ActionResultType as ActionResultTypeProto
import net.eagle0.eagle.common.command_type.command_type.CommandType as CommandTypeProto
import net.eagle0.eagle.common.round_phase.NewRoundPhase as NewRoundPhaseProto
import net.eagle0.eagle.internal.action_result.ActionResult as ActionResultProto
import net.eagle0.eagle.internal.generated_text_request.{
@@ -28,6 +28,7 @@ import net.eagle0.eagle.model.action_result.generated_text_request.{
}
import net.eagle0.eagle.model.action_result.types.ActionResultType
import net.eagle0.eagle.model.proto_converters.chronicle_entry.ChronicleEntryConverter
import net.eagle0.eagle.model.proto_converters.command.selected.SelectedCommandConverter
import net.eagle0.eagle.model.proto_converters.date.DateConverter
import net.eagle0.eagle.model.proto_converters.faction.FactionConverter
import net.eagle0.eagle.model.proto_converters.generated_text_request.LlmRequestConverter
@@ -35,6 +36,7 @@ import net.eagle0.eagle.model.proto_converters.hero.{GenderConverter, HeroConver
import net.eagle0.eagle.model.proto_converters.shardok_battle.ShardokBattleConverter
import net.eagle0.eagle.model.state.battalion.BattalionT
import net.eagle0.eagle.model.state.chronicle_entry.ChronicleEntry
import net.eagle0.eagle.model.state.command.CommandType
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.faction.FactionT
import net.eagle0.eagle.model.state.hero.HeroT
@@ -55,7 +57,7 @@ object ActionResultProtoConverter {
newRoundPhase: Option[RoundPhase],
newRoundId: Option[RoundId],
newDate: Option[Date],
lastCommandTypeForActingProvince: Option[SelectedCommand],
lastCommandTypeForActingProvince: Option[CommandType],
newBattle: Option[ShardokBattle],
resolvedBattle: Option[String],
newChronicleEntry: Option[ChronicleEntry],
@@ -91,7 +93,9 @@ object ActionResultProtoConverter {
newRoundPhase = newRoundPhase.map(rp => NewRoundPhaseProto(RoundPhaseConverter.toProto(rp))),
newRoundId = newRoundId,
newDate = newDate.map(DateConverter.toProto),
lastCommandTypeForActingProvince = lastCommandTypeForActingProvince.getOrElse(SelectedCommand.Empty),
lastCommandTypeForActingProvince = lastCommandTypeForActingProvince
.map(CommandTypeConverter.toProto)
.getOrElse(CommandTypeProto.COMMAND_TYPE_UNKNOWN),
newBattle = newBattle.map(ShardokBattleConverter.toProto),
resolvedBattle = resolvedBattle,
newChronicleEntry = newChronicleEntry.map(ChronicleEntryConverter.toProto),
@@ -186,9 +190,18 @@ object ActionResultProtoConverter {
newRoundPhase = proto.newRoundPhase.map(nrp => RoundPhaseConverter.fromProto(nrp.value)),
newRoundId = proto.newRoundId,
newDate = proto.newDate.map(d => DateConverter.fromProto(Some(d))),
lastCommandTypeForActingProvince = Option.when(proto.lastCommandTypeForActingProvince != SelectedCommand.Empty)(
proto.lastCommandTypeForActingProvince
),
lastCommandTypeForActingProvince = {
// Backwards compatibility: first check new CommandType field, fall back to deprecated SelectedCommand field
val fromNewField = CommandTypeConverter.fromProto(proto.lastCommandTypeForActingProvince)
fromNewField.orElse {
// Check if the deprecated SelectedCommand has a command set
import net.eagle0.eagle.api.selected_command.SelectedCommand as SelectedCommandProto
val deprecatedCommand = proto.deprecatedLastCommandTypeForActingProvince
Option.when(deprecatedCommand != SelectedCommandProto.defaultInstance) {
SelectedCommandConverter.fromProto(deprecatedCommand).commandType
}
}
},
newBattle = proto.newBattle.map(ShardokBattleConverter.fromProto),
resolvedBattle = proto.resolvedBattle,
newChronicleEntry = proto.newChronicleEntry.map(ChronicleEntryConverter.fromProto),
@@ -24,10 +24,12 @@ scala_library(
":changed_faction_converter",
":changed_hero_converter",
":changed_province_converter",
":command_type_converter",
":notification_converter",
":round_phase_converter",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:action_result_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:command_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
@@ -39,6 +41,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:client_text_visibility_extension_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/types",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/chronicle_entry:chronicle_entry_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/selected:selected_command_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/date",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/faction",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/hero",
@@ -49,6 +52,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/battalion",
"//src/main/scala/net/eagle0/eagle/model/state/chronicle_entry",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
"//src/main/scala/net/eagle0/eagle/model/state/faction",
"//src/main/scala/net/eagle0/eagle/model/state/hero",
"//src/main/scala/net/eagle0/eagle/model/state/province",
@@ -273,6 +277,22 @@ scala_library(
],
)
scala_library(
name = "command_type_converter",
srcs = ["CommandTypeConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/province:__pkg__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
],
deps = [
":proto_conversion_exception",
"//src/main/protobuf/net/eagle0/eagle/common:command_type_scala_proto",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
],
)
scala_library(
name = "combat_unit_converter",
srcs = ["CombatUnitConverter.scala"],
@@ -0,0 +1,109 @@
package net.eagle0.eagle.model.proto_converters
import net.eagle0.eagle.common.command_type.command_type.CommandType as CommandTypeProto
import net.eagle0.eagle.model.state.command.CommandType
/**
* Converter between Scala CommandType enum and proto CommandType enum.
*
* Both toProto and fromProto use exhaustive pattern matching, so adding a new command type without updating this
* converter will cause a compile error.
*/
object CommandTypeConverter:
/**
* Converts Scala CommandType to proto CommandType. Exhaustive matching ensures compile-time safety when adding new
* command types.
*/
def toProto(commandType: CommandType): CommandTypeProto =
commandType match
case CommandType.Alms => CommandTypeProto.COMMAND_TYPE_ALMS
case CommandType.ApprehendOutlaw => CommandTypeProto.COMMAND_TYPE_APPREHEND_OUTLAW
case CommandType.ArmTroops => CommandTypeProto.COMMAND_TYPE_ARM_TROOPS
case CommandType.AttackDecision => CommandTypeProto.COMMAND_TYPE_ATTACK_DECISION
case CommandType.ControlWeather => CommandTypeProto.COMMAND_TYPE_CONTROL_WEATHER
case CommandType.DeclineQuest => CommandTypeProto.COMMAND_TYPE_DECLINE_QUEST
case CommandType.Defend => CommandTypeProto.COMMAND_TYPE_DEFEND
case CommandType.Diplomacy => CommandTypeProto.COMMAND_TYPE_DIPLOMACY
case CommandType.Divine => CommandTypeProto.COMMAND_TYPE_DIVINE
case CommandType.ExileVassal => CommandTypeProto.COMMAND_TYPE_EXILE_VASSAL
case CommandType.Feast => CommandTypeProto.COMMAND_TYPE_FEAST
case CommandType.FreeForAllDecision => CommandTypeProto.COMMAND_TYPE_FREE_FOR_ALL_DECISION
case CommandType.HandleCapturedHero => CommandTypeProto.COMMAND_TYPE_HANDLE_CAPTURED_HERO
case CommandType.HandleRiotCrackDown => CommandTypeProto.COMMAND_TYPE_HANDLE_RIOT_CRACK_DOWN
case CommandType.HandleRiotDoNothing => CommandTypeProto.COMMAND_TYPE_HANDLE_RIOT_DO_NOTHING
case CommandType.HandleRiotGive => CommandTypeProto.COMMAND_TYPE_HANDLE_RIOT_GIVE
case CommandType.HeroGift => CommandTypeProto.COMMAND_TYPE_HERO_GIFT
case CommandType.Improve => CommandTypeProto.COMMAND_TYPE_IMPROVE
case CommandType.IssueOrders => CommandTypeProto.COMMAND_TYPE_ISSUE_ORDERS
case CommandType.ManagePrisoners => CommandTypeProto.COMMAND_TYPE_MANAGE_PRISONERS
case CommandType.March => CommandTypeProto.COMMAND_TYPE_MARCH
case CommandType.OrganizeTroops => CommandTypeProto.COMMAND_TYPE_ORGANIZE_TROOPS
case CommandType.PleaseRecruitMe => CommandTypeProto.COMMAND_TYPE_PLEASE_RECRUIT_ME
case CommandType.Recon => CommandTypeProto.COMMAND_TYPE_RECON
case CommandType.RecruitHeroes => CommandTypeProto.COMMAND_TYPE_RECRUIT_HEROES
case CommandType.ResolveAllianceOffer => CommandTypeProto.COMMAND_TYPE_RESOLVE_ALLIANCE_OFFER
case CommandType.ResolveBreakAlliance => CommandTypeProto.COMMAND_TYPE_RESOLVE_BREAK_ALLIANCE
case CommandType.ResolveInvitation => CommandTypeProto.COMMAND_TYPE_RESOLVE_INVITATION
case CommandType.ResolveRansomOffer => CommandTypeProto.COMMAND_TYPE_RESOLVE_RANSOM_OFFER
case CommandType.ResolveTruceOffer => CommandTypeProto.COMMAND_TYPE_RESOLVE_TRUCE_OFFER
case CommandType.ResolveTribute => CommandTypeProto.COMMAND_TYPE_RESOLVE_TRIBUTE
case CommandType.Rest => CommandTypeProto.COMMAND_TYPE_REST
case CommandType.Return => CommandTypeProto.COMMAND_TYPE_RETURN
case CommandType.SendSupplies => CommandTypeProto.COMMAND_TYPE_SEND_SUPPLIES
case CommandType.StartEpidemic => CommandTypeProto.COMMAND_TYPE_START_EPIDEMIC
case CommandType.SuppressBeasts => CommandTypeProto.COMMAND_TYPE_SUPPRESS_BEASTS
case CommandType.SwearBrotherhood => CommandTypeProto.COMMAND_TYPE_SWEAR_BROTHERHOOD
case CommandType.Trade => CommandTypeProto.COMMAND_TYPE_TRADE
case CommandType.Train => CommandTypeProto.COMMAND_TYPE_TRAIN
case CommandType.Travel => CommandTypeProto.COMMAND_TYPE_TRAVEL
/**
* Converts proto CommandType to Scala CommandType. Exhaustive matching ensures compile-time safety when adding new
* command types. Returns None for UNKNOWN.
*/
def fromProto(commandTypeProto: CommandTypeProto): Option[CommandType] =
commandTypeProto match
case CommandTypeProto.COMMAND_TYPE_ALMS => Some(CommandType.Alms)
case CommandTypeProto.COMMAND_TYPE_APPREHEND_OUTLAW => Some(CommandType.ApprehendOutlaw)
case CommandTypeProto.COMMAND_TYPE_ARM_TROOPS => Some(CommandType.ArmTroops)
case CommandTypeProto.COMMAND_TYPE_ATTACK_DECISION => Some(CommandType.AttackDecision)
case CommandTypeProto.COMMAND_TYPE_CONTROL_WEATHER => Some(CommandType.ControlWeather)
case CommandTypeProto.COMMAND_TYPE_DECLINE_QUEST => Some(CommandType.DeclineQuest)
case CommandTypeProto.COMMAND_TYPE_DEFEND => Some(CommandType.Defend)
case CommandTypeProto.COMMAND_TYPE_DIPLOMACY => Some(CommandType.Diplomacy)
case CommandTypeProto.COMMAND_TYPE_DIVINE => Some(CommandType.Divine)
case CommandTypeProto.COMMAND_TYPE_EXILE_VASSAL => Some(CommandType.ExileVassal)
case CommandTypeProto.COMMAND_TYPE_FEAST => Some(CommandType.Feast)
case CommandTypeProto.COMMAND_TYPE_FREE_FOR_ALL_DECISION => Some(CommandType.FreeForAllDecision)
case CommandTypeProto.COMMAND_TYPE_HANDLE_CAPTURED_HERO => Some(CommandType.HandleCapturedHero)
case CommandTypeProto.COMMAND_TYPE_HANDLE_RIOT_CRACK_DOWN => Some(CommandType.HandleRiotCrackDown)
case CommandTypeProto.COMMAND_TYPE_HANDLE_RIOT_DO_NOTHING => Some(CommandType.HandleRiotDoNothing)
case CommandTypeProto.COMMAND_TYPE_HANDLE_RIOT_GIVE => Some(CommandType.HandleRiotGive)
case CommandTypeProto.COMMAND_TYPE_HERO_GIFT => Some(CommandType.HeroGift)
case CommandTypeProto.COMMAND_TYPE_IMPROVE => Some(CommandType.Improve)
case CommandTypeProto.COMMAND_TYPE_ISSUE_ORDERS => Some(CommandType.IssueOrders)
case CommandTypeProto.COMMAND_TYPE_MANAGE_PRISONERS => Some(CommandType.ManagePrisoners)
case CommandTypeProto.COMMAND_TYPE_MARCH => Some(CommandType.March)
case CommandTypeProto.COMMAND_TYPE_ORGANIZE_TROOPS => Some(CommandType.OrganizeTroops)
case CommandTypeProto.COMMAND_TYPE_PLEASE_RECRUIT_ME => Some(CommandType.PleaseRecruitMe)
case CommandTypeProto.COMMAND_TYPE_RECON => Some(CommandType.Recon)
case CommandTypeProto.COMMAND_TYPE_RECRUIT_HEROES => Some(CommandType.RecruitHeroes)
case CommandTypeProto.COMMAND_TYPE_RESOLVE_ALLIANCE_OFFER => Some(CommandType.ResolveAllianceOffer)
case CommandTypeProto.COMMAND_TYPE_RESOLVE_BREAK_ALLIANCE => Some(CommandType.ResolveBreakAlliance)
case CommandTypeProto.COMMAND_TYPE_RESOLVE_INVITATION => Some(CommandType.ResolveInvitation)
case CommandTypeProto.COMMAND_TYPE_RESOLVE_RANSOM_OFFER => Some(CommandType.ResolveRansomOffer)
case CommandTypeProto.COMMAND_TYPE_RESOLVE_TRUCE_OFFER => Some(CommandType.ResolveTruceOffer)
case CommandTypeProto.COMMAND_TYPE_RESOLVE_TRIBUTE => Some(CommandType.ResolveTribute)
case CommandTypeProto.COMMAND_TYPE_REST => Some(CommandType.Rest)
case CommandTypeProto.COMMAND_TYPE_RETURN => Some(CommandType.Return)
case CommandTypeProto.COMMAND_TYPE_SEND_SUPPLIES => Some(CommandType.SendSupplies)
case CommandTypeProto.COMMAND_TYPE_START_EPIDEMIC => Some(CommandType.StartEpidemic)
case CommandTypeProto.COMMAND_TYPE_SUPPRESS_BEASTS => Some(CommandType.SuppressBeasts)
case CommandTypeProto.COMMAND_TYPE_SWEAR_BROTHERHOOD => Some(CommandType.SwearBrotherhood)
case CommandTypeProto.COMMAND_TYPE_TRADE => Some(CommandType.Trade)
case CommandTypeProto.COMMAND_TYPE_TRAIN => Some(CommandType.Train)
case CommandTypeProto.COMMAND_TYPE_TRAVEL => Some(CommandType.Travel)
case CommandTypeProto.COMMAND_TYPE_UNKNOWN => None
case CommandTypeProto.Unrecognized(_) =>
throw ProtoConversionException(s"Unrecognized command type: $commandTypeProto")
end CommandTypeConverter
@@ -7,6 +7,9 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
],
exports = [
":available_command_converter",
],
deps = [
":available_command_converter",
"//src/main/protobuf/net/eagle0/eagle/api:command_scala_proto",
@@ -20,7 +23,7 @@ scala_library(
name = "available_command_converter",
srcs = ["AvailableCommandConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/main/scala/net/eagle0/eagle/service:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions/impl/action:__pkg__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
@@ -4,10 +4,16 @@ scala_library(
name = "selected_command_converter",
srcs = ["SelectedCommandConverter.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library:__pkg__",
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/util:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__pkg__",
"//src/main/scala/net/eagle0/eagle/service:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
],
deps = [
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api/command/util:armed_battalion_scala_proto",
@@ -23,12 +23,14 @@ scala_library(
deps = [
":event",
":order_type",
"//src/main/protobuf/net/eagle0/eagle/common:command_type_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:province_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:army_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battalion_type_id_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:battle_revelation_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:captured_hero_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:command_type_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:deferred_change_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:improvement_type_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:incoming_end_turn_action_converter",
@@ -36,6 +38,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/proto_converters:supplies_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/state/command:command_type",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/province:orders",
"//src/main/scala/net/eagle0/eagle/model/state/province/concrete",
@@ -1,9 +1,8 @@
package net.eagle0.eagle.model.proto_converters.province
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.api.selected_command.SelectedCommand as SelectedCommandProto
import net.eagle0.eagle.common.battalion_type.BattalionTypeId as BattalionTypeIdProto
import net.eagle0.eagle.common.command_type.command_type.CommandType as CommandTypeProto
import net.eagle0.eagle.common.date.Date as DateProto
import net.eagle0.eagle.common.improvement_type.ImprovementType as ImprovementTypeProto
import net.eagle0.eagle.common.province_event.ProvinceEvent as ProvinceEventProto
@@ -30,6 +29,7 @@ import net.eagle0.eagle.model.proto_converters.{
BattalionTypeIdConverter,
BattleRevelationConverter,
CapturedHeroConverter,
CommandTypeConverter,
DeferredChangeConverter,
ImprovementTypeConverter,
IncomingEndTurnActionConverter,
@@ -48,6 +48,7 @@ import net.eagle0.eagle.model.state.{
MovingArmy,
MovingSupplies
}
import net.eagle0.eagle.model.state.command.CommandType
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.province.{
DeferredChangeT,
@@ -100,7 +101,7 @@ object ProvinceConverter {
hexMapName: String,
castleCount: Int,
heroCap: Int,
lastCommand: Option[SelectedCommand]
lastCommand: Option[CommandType]
) =>
ProvinceProto(
id = id,
@@ -131,7 +132,9 @@ object ProvinceConverter {
activeEvents = activeEvents.map(ProvinceEventConverter.toProto),
lastBeastsDate = lastBeastsDate.map(DateConverter.toProto),
lastRiotDate = lastRiotDate.map(DateConverter.toProto),
lastCommand = lastCommand.getOrElse(SelectedCommand.Empty),
lastCommand = lastCommand
.map(CommandTypeConverter.toProto)
.getOrElse(CommandTypeProto.COMMAND_TYPE_UNKNOWN),
incomingArmies = incomingArmies.map(ArmyConverter.toProto),
hostileArmies = hostileArmies.map(ArmyConverter.toProto),
defendingArmy = defendingArmy.map(ArmyConverter.toProto),
@@ -182,7 +185,7 @@ object ProvinceConverter {
activeEvents: Seq[ProvinceEventProto],
lastBeastsDate: Option[DateProto],
lastRiotDate: Option[DateProto],
lastCommand: SelectedCommandProto,
lastCommand: CommandTypeProto,
castleCount: Int,
heroCap: Int,
deferredChanges: Seq[DeferredChangeProto],
@@ -220,7 +223,7 @@ object ProvinceConverter {
activeEvents = activeEvents.map(ProvinceEventConverter.fromProto).toVector,
lastBeastsDate = lastBeastsDate.map(date => DateConverter.fromProto(Some(date))),
lastRiotDate = lastRiotDate.map(date => DateConverter.fromProto(Some(date))),
lastCommand = Option.when(!lastCommand.isEmpty)(lastCommand),
lastCommand = CommandTypeConverter.fromProto(lastCommand),
incomingArmies = incomingArmies.map(ArmyConverter.fromProto).toVector,
hostileArmies = hostileArmies.map(ArmyConverter.fromProto).toVector,
defendingArmy = defendingArmy.map(ArmyConverter.fromProto),
@@ -0,0 +1,14 @@
load("@rules_scala//scala:scala.bzl", "scala_library")
scala_library(
name = "command_type",
srcs = ["CommandType.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/action_result:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/state/command:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/state/province:__subpackages__",
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
)
@@ -0,0 +1,54 @@
package net.eagle0.eagle.model.state.command
/**
* Enum representing the type of a command, without any command-specific data.
*
* This is used to track which command type was last executed on a province, without needing to serialize the full
* SelectedCommand with all its parameters.
*
* Both SelectedCommand and AvailableCommand have a `toCommandType` method that converts to this enum using exhaustive
* pattern matching. Adding a new command variant without updating the `toCommandType` method will cause a compile
* error.
*/
enum CommandType:
case Alms
case ApprehendOutlaw
case ArmTroops
case AttackDecision
case ControlWeather
case DeclineQuest
case Defend
case Diplomacy
case Divine
case ExileVassal
case Feast
case FreeForAllDecision
case HandleCapturedHero
case HandleRiotCrackDown
case HandleRiotDoNothing
case HandleRiotGive
case HeroGift
case Improve
case IssueOrders
case ManagePrisoners
case March
case OrganizeTroops
case PleaseRecruitMe
case Recon
case RecruitHeroes
case ResolveAllianceOffer
case ResolveBreakAlliance
case ResolveInvitation
case ResolveRansomOffer
case ResolveTruceOffer
case ResolveTribute
case Rest
case Return
case SendSupplies
case StartEpidemic
case SuppressBeasts
case SwearBrotherhood
case Trade
case Train
case Travel
end CommandType
@@ -3,44 +3,50 @@ package net.eagle0.eagle.model.state.command.available
import net.eagle0.common.hostility.Hostility
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.model.state.command.common.*
import net.eagle0.eagle.model.state.command.CommandType
import net.eagle0.eagle.model.state.diplomacy_offer.status.Status
import AvailableCommand.*
/** Scala 3 enum representing available command options for a player. */
enum AvailableCommand:
/**
* Scala 3 enum representing available command options for a player.
*
* Each case must provide a CommandType via the enum parameter, ensuring compile-time safety: adding a new command
* variant without specifying its CommandType will cause a compile error.
*/
enum AvailableCommand(val commandType: CommandType):
case AlmsAvailable(
foodAvailable: Int,
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
)
) extends AvailableCommand(CommandType.Alms)
case ApprehendOutlawAvailable(
outlaws: Vector[ResidentOutlaw],
availableBattalionIds: Vector[BattalionId],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
)
) extends AvailableCommand(CommandType.ApprehendOutlaw)
case ArmTroopsAvailable(
armamentCosts: Vector[ArmamentCost],
availableBattalions: Vector[BattalionId],
maxArmament: Int,
actingProvinceId: ProvinceId,
availableBattalionTypeIds: Vector[Int]
)
) extends AvailableCommand(CommandType.ArmTroops)
case AttackDecisionAvailable(
armies: Vector[ArmyStats],
actingUnits: Vector[ExpandedCombatUnit],
availableDecisions: Vector[AttackDecisionType],
actingProvinceId: ProvinceId
)
) extends AvailableCommand(CommandType.AttackDecision)
case ControlWeatherAvailable(
options: Vector[TargetProvinceOptions],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
)
) extends AvailableCommand(CommandType.ControlWeather)
case DeclineQuestAvailable(
actingProvinceId: ProvinceId,
declinableHeroes: Vector[ExpandedUnaffiliatedHero]
)
) extends AvailableCommand(CommandType.DeclineQuest)
case DefendAvailable(
suitableBattalionsForHeroes: Map[HeroId, SuitableBattalions],
availableFleeProvinceIds: Vector[ProvinceId],
@@ -51,138 +57,144 @@ enum AvailableCommand:
hostileFactionIds: Vector[FactionId],
hostileHeroCount: Int,
hostileTroopCount: Int
)
) extends AvailableCommand(CommandType.Defend)
case DiplomacyAvailable(
options: Vector[DiplomacyOption],
availableHeroIds: Vector[HeroId],
actingProvinceId: ProvinceId,
recommendedHeroId: HeroId
)
) extends AvailableCommand(CommandType.Diplomacy)
case DivineAvailable(
costPerHero: Int,
divinableHeroes: Vector[ExpandedUnaffiliatedHero],
unavailableHeroes: Vector[ExpandedUnaffiliatedHero],
actingProvinceId: ProvinceId
)
) extends AvailableCommand(CommandType.Divine)
case ExileVassalAvailable(
actingProvinceId: ProvinceId,
exilableHeroIds: Vector[HeroId]
)
case FeastAvailable(goldCost: Int, actingProvinceId: ProvinceId)
) extends AvailableCommand(CommandType.ExileVassal)
case FeastAvailable(goldCost: Int, actingProvinceId: ProvinceId) extends AvailableCommand(CommandType.Feast)
case FreeForAllDecisionAvailable(
provinceId: ProvinceId,
armies: Vector[ArmyStats],
actingUnits: Vector[ExpandedCombatUnit],
availableDecisions: Vector[AttackDecisionType]
)
) extends AvailableCommand(CommandType.FreeForAllDecision)
case HandleCapturedHeroAvailable(
availableHeroes: Vector[ExpandedCapturedHero],
actingProvinceId: ProvinceId
)
) extends AvailableCommand(CommandType.HandleCapturedHero)
case HandleRiotCrackDownAvailable(
availableHeroIds: Vector[HeroId],
battalionIdsAvailable: Vector[BattalionId],
actingProvinceId: ProvinceId
)
) extends AvailableCommand(CommandType.HandleRiotCrackDown)
case HandleRiotDoNothingAvailable(actingProvinceId: ProvinceId)
extends AvailableCommand(CommandType.HandleRiotDoNothing)
case HandleRiotGiveAvailable(
foodAvailable: Int,
goldAvailable: Int,
actingProvinceId: ProvinceId
)
) extends AvailableCommand(CommandType.HandleRiotGive)
case HeroGiftAvailable(
eligibleGifts: Vector[EligibleGift],
actingProvinceId: ProvinceId
)
) extends AvailableCommand(CommandType.HeroGift)
case ImproveAvailable(
availableTypes: Vector[ImprovementType],
availableHeroIds: Vector[HeroId],
recommendedHeroId: HeroId,
actingProvinceId: ProvinceId,
lockedType: Option[ImprovementType]
)
) extends AvailableCommand(CommandType.Improve)
case IssueOrdersAvailable(
currentOrders: Vector[ProvinceOrders],
availableOrders: Vector[ProvinceOrderType],
availableFocusProvinces: Vector[ProvinceId],
actingProvinceId: ProvinceId
)
) extends AvailableCommand(CommandType.IssueOrders)
case ManagePrisonersAvailable(
actingProvinceId: ProvinceId,
prisoners: Vector[PrisonerToManage]
)
) extends AvailableCommand(CommandType.ManagePrisoners)
case MarchAvailable(
oneProvinceCommands: Vector[MarchCommandFromOneProvince],
actingProvinceId: ProvinceId,
availableTypes: Vector[BattalionType]
)
) extends AvailableCommand(CommandType.March)
case OrganizeTroopsAvailable(
troopCosts: Vector[TroopCost],
existingBattalions: Vector[BattalionId],
maxBattalions: Int,
actingProvinceId: ProvinceId,
availableBattalionTypes: Vector[BattalionTypeStatus]
)
) extends AvailableCommand(CommandType.OrganizeTroops)
case PleaseRecruitMeAvailable(
availableProvinces: Vector[OneProvincePleaseRecruitMe],
actingProvinceId: ProvinceId
)
) extends AvailableCommand(CommandType.PleaseRecruitMe)
case ReconAvailable(
availableTargetProvinces: Vector[ProvinceId],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
)
) extends AvailableCommand(CommandType.Recon)
case RecruitHeroesAvailable(
availableHeroes: Vector[ExpandedUnaffiliatedHero],
actingProvinceId: ProvinceId
)
) extends AvailableCommand(CommandType.RecruitHeroes)
case ResolveAllianceOfferAvailable(offers: Vector[DiplomacyOfferInfo])
extends AvailableCommand(CommandType.ResolveAllianceOffer)
case ResolveBreakAllianceAvailable(offers: Vector[DiplomacyOfferInfo])
extends AvailableCommand(CommandType.ResolveBreakAlliance)
case ResolveInvitationAvailable(invitations: Vector[DiplomacyOfferInfo])
extends AvailableCommand(CommandType.ResolveInvitation)
case ResolveRansomOfferAvailable(offers: Vector[DiplomacyOfferInfo])
extends AvailableCommand(CommandType.ResolveRansomOffer)
case ResolveTruceOfferAvailable(offers: Vector[DiplomacyOfferInfo])
extends AvailableCommand(CommandType.ResolveTruceOffer)
case ResolveTributeAvailable(
demands: Vector[TributeAndFaction],
availableGold: Int,
availableFood: Int,
actingProvinceId: ProvinceId
)
case RestAvailable(actingProvinceId: ProvinceId)
case ReturnAvailable(actingProvinceId: ProvinceId)
) extends AvailableCommand(CommandType.ResolveTribute)
case RestAvailable(actingProvinceId: ProvinceId) extends AvailableCommand(CommandType.Rest)
case ReturnAvailable(actingProvinceId: ProvinceId) extends AvailableCommand(CommandType.Return)
case SendSuppliesAvailable(
goldAvailable: Int,
foodAvailable: Int,
availableDestinationProvinceIds: Vector[ProvinceId],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
)
) extends AvailableCommand(CommandType.SendSupplies)
case StartEpidemicAvailable(
options: Vector[StartEpidemicOptions],
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId]
)
) extends AvailableCommand(CommandType.StartEpidemic)
case SuppressBeastsAvailable(
availableHeroIds: Vector[HeroId],
availableBattalionIds: Vector[BattalionId],
actingProvinceId: ProvinceId
)
) extends AvailableCommand(CommandType.SuppressBeasts)
case SwearBrotherhoodAvailable(
actingProvinceId: ProvinceId,
availableHeroes: Vector[HeroAndBackstory]
)
) extends AvailableCommand(CommandType.SwearBrotherhood)
case TradeAvailable(
foodAvailable: Int,
goldAvailable: Int,
foodBuyPrice: Double,
foodSellPrice: Double,
actingProvinceId: ProvinceId
)
) extends AvailableCommand(CommandType.Trade)
case TrainAvailable(
actingProvinceId: ProvinceId,
availableHeroIds: Vector[HeroId],
recommendedHeroId: HeroId
)
case TravelAvailable(actingProvinceId: ProvinceId)
) extends AvailableCommand(CommandType.Train)
case TravelAvailable(actingProvinceId: ProvinceId) extends AvailableCommand(CommandType.Travel)
end AvailableCommand
/** Supporting types for AvailableCommand. */
@@ -9,20 +9,21 @@ scala_library(
visibility = [
"//src/main/scala/net/eagle0/eagle/ai:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/state/command:__pkg__",
"//src/test/scala/net/eagle0/eagle/ai:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library:__subpackages__",
"//src/test/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/proto_converters:__subpackages__",
"//src/test/scala/net/eagle0/eagle/model/state/command:__subpackages__",
],
exports = [
"//src/main/protobuf/net/eagle0/common:hostility_scala_proto",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
],
deps = [
"//src/main/protobuf/net/eagle0/common:hostility_scala_proto",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
"//src/main/scala/net/eagle0/eagle/model/state/command/common",
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer/status",
],
@@ -4,13 +4,18 @@ scala_library(
name = "selected",
srcs = ["SelectedCommand.scala"],
visibility = [
"//src/main/scala/net/eagle0/eagle/library/actions:__subpackages__",
"//src/main/scala/net/eagle0/eagle/library:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command:__subpackages__",
"//src/main/scala/net/eagle0/eagle/model/state/command:__pkg__",
"//src/main/scala/net/eagle0/eagle/model/state/command:__subpackages__",
"//src/test/scala/net/eagle0/eagle:__subpackages__",
],
exports = [
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
],
deps = [
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
"//src/main/scala/net/eagle0/eagle/model/state/command/common",
"//src/main/scala/net/eagle0/eagle/model/state/diplomacy_offer/status",
],
@@ -2,87 +2,106 @@ package net.eagle0.eagle.model.state.command.selected
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.model.state.command.common.*
import net.eagle0.eagle.model.state.command.CommandType
import net.eagle0.eagle.model.state.diplomacy_offer.status.Status
/** Scala 3 enum representing a player's selected command. */
enum SelectedCommand:
case AlmsSelected(amount: Int, actingHeroId: HeroId)
/**
* Scala 3 enum representing a player's selected command.
*
* Each case must provide a CommandType via the enum parameter, ensuring compile-time safety: adding a new command
* variant without specifying its CommandType will cause a compile error.
*/
enum SelectedCommand(val commandType: CommandType):
case AlmsSelected(amount: Int, actingHeroId: HeroId) extends SelectedCommand(CommandType.Alms)
case ApprehendOutlawSelected(
heroIdToApprehend: HeroId,
battalionId: Option[BattalionId],
actingHeroId: HeroId
)
case ArmTroopsSelected(armedBattalions: Vector[ArmedBattalion])
case AttackDecisionSelected(decision: AttackDecisionType)
) extends SelectedCommand(CommandType.ApprehendOutlaw)
case ArmTroopsSelected(armedBattalions: Vector[ArmedBattalion]) extends SelectedCommand(CommandType.ArmTroops)
case AttackDecisionSelected(decision: AttackDecisionType) extends SelectedCommand(CommandType.AttackDecision)
case ControlWeatherSelected(
selectedType: ControlWeatherType,
selectedProvinceId: ProvinceId,
actingHeroId: HeroId
)
case DeclineQuestSelected(heroId: HeroId)
) extends SelectedCommand(CommandType.ControlWeather)
case DeclineQuestSelected(heroId: HeroId) extends SelectedCommand(CommandType.DeclineQuest)
case DefendSelected(
fleeProvinceId: Option[ProvinceId],
defendingUnits: Vector[CombatUnit]
)
) extends SelectedCommand(CommandType.Defend)
case DiplomacySelected(
selectedOption: DiplomacyOptionType,
targetFactionId: FactionId,
sentHeroId: HeroId
)
case DivineSelected(heroIds: Vector[HeroId])
case ExileVassalSelected(exiledHeroId: HeroId)
case FeastSelected
) extends SelectedCommand(CommandType.Diplomacy)
case DivineSelected(heroIds: Vector[HeroId]) extends SelectedCommand(CommandType.Divine)
case ExileVassalSelected(exiledHeroId: HeroId) extends SelectedCommand(CommandType.ExileVassal)
case FeastSelected extends SelectedCommand(CommandType.Feast)
case FreeForAllDecisionSelected(provinceId: ProvinceId, decision: AttackDecisionType)
extends SelectedCommand(CommandType.FreeForAllDecision)
case HandleCapturedHeroSelected(heroId: HeroId, selectedOption: CapturedHeroOption)
extends SelectedCommand(CommandType.HandleCapturedHero)
case HandleRiotCrackDownSelected(
heroId: HeroId,
battalionId: Option[BattalionId],
actingHeroId: HeroId
)
case HandleRiotDoNothingSelected
case HandleRiotGiveSelected(foodAmount: Int, goldAmount: Int)
case HeroGiftSelected(recipientHeroId: HeroId, amount: Int)
) extends SelectedCommand(CommandType.HandleRiotCrackDown)
case HandleRiotDoNothingSelected extends SelectedCommand(CommandType.HandleRiotDoNothing)
case HandleRiotGiveSelected(foodAmount: Int, goldAmount: Int) extends SelectedCommand(CommandType.HandleRiotGive)
case HeroGiftSelected(recipientHeroId: HeroId, amount: Int) extends SelectedCommand(CommandType.HeroGift)
case ImproveSelected(improvementType: ImprovementType, actingHeroId: HeroId, lockType: Boolean)
extends SelectedCommand(CommandType.Improve)
case IssueOrdersSelected(newOrders: Vector[ProvinceOrder], newFocusProvince: Option[ProvinceId])
extends SelectedCommand(CommandType.IssueOrders)
case ManagePrisonersSelected(prisonerHeroId: HeroId, chosenOption: PrisonerManagementOption)
extends SelectedCommand(CommandType.ManagePrisoners)
case MarchSelected(
gold: Int,
food: Int,
originProvince: ProvinceId,
destinationProvinceId: ProvinceId,
marchingUnits: Vector[CombatUnit]
)
) extends SelectedCommand(CommandType.March)
case OrganizeTroopsSelected(
changedBattalions: Vector[ChangedBattalion],
newBattalions: Vector[NewBattalion]
)
) extends SelectedCommand(CommandType.OrganizeTroops)
case PleaseRecruitMeSelected(provinceId: ProvinceId, heroId: HeroId, accept: Boolean)
case ReconSelected(actingHeroId: HeroId, targetProvinceId: ProvinceId)
case RecruitHeroesSelected(heroIds: Vector[HeroId])
extends SelectedCommand(CommandType.PleaseRecruitMe)
case ReconSelected(actingHeroId: HeroId, targetProvinceId: ProvinceId) extends SelectedCommand(CommandType.Recon)
case RecruitHeroesSelected(heroIds: Vector[HeroId]) extends SelectedCommand(CommandType.RecruitHeroes)
case ResolveAllianceOfferSelected(originatingFactionId: FactionId, resolution: Status)
extends SelectedCommand(CommandType.ResolveAllianceOffer)
case ResolveBreakAllianceSelected(originatingFactionId: FactionId, resolution: Status)
extends SelectedCommand(CommandType.ResolveBreakAlliance)
case ResolveInvitationSelected(originatingFactionId: FactionId, resolution: Status)
extends SelectedCommand(CommandType.ResolveInvitation)
case ResolveRansomOfferSelected(
offeringFactionId: FactionId,
prisonerHeroId: HeroId,
resolution: Status
)
) extends SelectedCommand(CommandType.ResolveRansomOffer)
case ResolveTruceOfferSelected(originatingFactionId: FactionId, resolution: Status)
extends SelectedCommand(CommandType.ResolveTruceOffer)
case ResolveTributeSelected(demandingFactionId: FactionId, paid: Boolean)
case RestSelected
case ReturnSelected
extends SelectedCommand(CommandType.ResolveTribute)
case RestSelected extends SelectedCommand(CommandType.Rest)
case ReturnSelected extends SelectedCommand(CommandType.Return)
case SendSuppliesSelected(
gold: Int,
food: Int,
actingHeroId: HeroId,
destinationProvinceId: ProvinceId
)
) extends SelectedCommand(CommandType.SendSupplies)
case StartEpidemicSelected(selectedProvinceId: ProvinceId, actingHeroId: HeroId)
extends SelectedCommand(CommandType.StartEpidemic)
case SuppressBeastsSelected(heroId: HeroId, battalionId: Option[BattalionId])
case SwearBrotherhoodSelected(newBrotherHeroId: HeroId)
case TradeSelected(tradeType: TradeType, amount: Int)
case TrainSelected(actingHeroId: HeroId)
case TravelSelected
extends SelectedCommand(CommandType.SuppressBeasts)
case SwearBrotherhoodSelected(newBrotherHeroId: HeroId) extends SelectedCommand(CommandType.SwearBrotherhood)
case TradeSelected(tradeType: TradeType, amount: Int) extends SelectedCommand(CommandType.Trade)
case TrainSelected(actingHeroId: HeroId) extends SelectedCommand(CommandType.Train)
case TravelSelected extends SelectedCommand(CommandType.Travel)
end SelectedCommand
// Supporting enums specific to SelectedCommand
@@ -65,13 +65,13 @@ scala_library(
":event",
":incoming_end_turn_action",
":orders",
"//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/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:battalion_type_id",
"//src/main/scala/net/eagle0/eagle/model/state:battle_revelation",
"//src/main/scala/net/eagle0/eagle/model/state:captured_hero",
"//src/main/scala/net/eagle0/eagle/model/state:improvement_type",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/quest",
"//src/main/scala/net/eagle0/eagle/model/state/unaffiliated_hero",
@@ -1,7 +1,6 @@
package net.eagle0.eagle.model.state.province
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.model.state.{
Army,
BattalionTypeId,
@@ -12,6 +11,7 @@ import net.eagle0.eagle.model.state.{
MovingArmy,
MovingSupplies
}
import net.eagle0.eagle.model.state.command.CommandType
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.unaffiliated_hero.UnaffiliatedHeroT
@@ -87,7 +87,7 @@ trait ProvinceT {
def castleCount: Int
def heroCap: Int
def lastCommand: Option[SelectedCommand]
def lastCommand: Option[CommandType]
def updateWith(
name: String = name,
@@ -126,7 +126,7 @@ trait ProvinceT {
hexMapName: String = hexMapName,
castleCount: Int = castleCount,
heroCap: Int = heroCap,
lastCommand: Option[SelectedCommand] = lastCommand
lastCommand: Option[CommandType] = lastCommand
): ProvinceT
def withRulingFactionId(rulingFactionId: FactionId): ProvinceT =
@@ -21,12 +21,12 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/state/province:orders",
],
deps = [
"//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/state:army",
"//src/main/scala/net/eagle0/eagle/model/state:battle_revelation",
"//src/main/scala/net/eagle0/eagle/model/state:captured_hero",
"//src/main/scala/net/eagle0/eagle/model/state:improvement_type",
"//src/main/scala/net/eagle0/eagle/model/state/command:command_type",
"//src/main/scala/net/eagle0/eagle/model/state/date",
"//src/main/scala/net/eagle0/eagle/model/state/province",
"//src/main/scala/net/eagle0/eagle/model/state/province:deferred_change_trait",
@@ -1,7 +1,6 @@
package net.eagle0.eagle.model.state.province.concrete
import net.eagle0.eagle.{BattalionId, FactionId, HeroId, ProvinceId}
import net.eagle0.eagle.api.selected_command.SelectedCommand
import net.eagle0.eagle.model.state.{
Army,
BattalionTypeId,
@@ -12,6 +11,7 @@ import net.eagle0.eagle.model.state.{
MovingArmy,
MovingSupplies
}
import net.eagle0.eagle.model.state.command.CommandType
import net.eagle0.eagle.model.state.date.Date
import net.eagle0.eagle.model.state.province.{
DeferredChangeT,
@@ -62,7 +62,7 @@ case class ProvinceC(
hexMapName: String = "",
castleCount: Int = 0,
heroCap: Int = 0,
lastCommand: Option[SelectedCommand] = None
lastCommand: Option[CommandType] = None
) extends ProvinceT {
override def updateWith(
name: String = name,
@@ -101,7 +101,7 @@ case class ProvinceC(
hexMapName: String = hexMapName,
castleCount: Int = castleCount,
heroCap: Int = heroCap,
lastCommand: Option[SelectedCommand] = lastCommand
lastCommand: Option[CommandType] = lastCommand
): ProvinceT =
ProvinceC(
id = this.id,
@@ -51,12 +51,19 @@ class AuthorizationInterceptor(jwtService: Option[JwtService] = None) extends Se
Option(headers.get(IMPERSONATE_USER_KEY))
.filter(_.nonEmpty)
private def isLocalhost[ReqT, RespT](call: ServerCall[ReqT, RespT]): Boolean = {
// Check if the connection is from a trusted local source (localhost or Docker bridge network).
// This is used to allow warmup/testing without JWT authentication.
private def isLocalOrDocker[ReqT, RespT](call: ServerCall[ReqT, RespT]): Boolean = {
val remoteAddr = call.getAttributes.get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR)
remoteAddr match {
case inet: InetSocketAddress =>
val addr = inet.getAddress
addr != null && (addr.isLoopbackAddress || addr.getHostAddress == "127.0.0.1" || addr.getHostAddress == "::1")
addr != null && (
addr.isLoopbackAddress || // 127.0.0.1, ::1
addr.getHostAddress == "127.0.0.1" ||
addr.getHostAddress == "::1" ||
addr.isSiteLocalAddress // 10.x.x.x, 172.16-31.x.x, 192.168.x.x (Docker bridge networks)
)
case _ => false
}
}
@@ -116,10 +123,10 @@ class AuthorizationInterceptor(jwtService: Option[JwtService] = None) extends Se
Contexts.interceptCall(effectiveContext, call, headers, next)
case None =>
// No valid JWT token - check for warmup user header (localhost only)
// No valid JWT token - check for warmup user header (localhost or Docker bridge only)
Option(headers.get(WARMUP_USER_KEY)).filter(_.nonEmpty) match {
case Some(warmupUser) if isLocalhost(call) =>
// Warmup/test user from localhost - set a simple context without JWT
case Some(warmupUser) if isLocalOrDocker(call) =>
// Warmup/test user from localhost or Docker bridge - set a simple context without JWT
val warmupContext = AuthorizationUtils.contextWithJwtClaims(
userId = s"warmup-$warmupUser",
displayName = warmupUser,
@@ -127,10 +134,10 @@ class AuthorizationInterceptor(jwtService: Option[JwtService] = None) extends Se
accessToken = ""
)
Contexts.interceptCall(warmupContext, call, headers, next)
case Some(_) =>
// X-Warmup-User from non-localhost - ignore it, treat as unauthenticated
case Some(_) =>
// X-Warmup-User from non-local network - ignore it, treat as unauthenticated
next.startCall(call, headers)
case None =>
case None =>
// No auth at all - let endpoint handle unauthenticated request
next.startCall(call, headers)
}
@@ -182,7 +182,7 @@ class EagleServiceImpl(
case UniversalCommand.Empty =>
throw new EagleClientException("Must include a command")
}
}.map(_ => PostCommandResponse())
}.map(_ => PostCommandResponse(status = PostCommandResponse.Status.SUCCESS))
private class SyncStreamObserver(
responseObserver: SyncResponseObserver
@@ -626,14 +626,14 @@ class EagleServiceImpl(
val players = gameState.factions.map {
case (factionId, faction) =>
val leader = gameState.heroes(faction.factionHeadId)
val leader = gameState.heroes.get(faction.factionHeadId)
val userName = controller.userNameToFactionId.find { case (_, fid) => fid == factionId }
.map(_._1)
RunningGamePlayerInfo(
factionId = factionId,
factionName = faction.name,
leaderName = leader.nameTextId,
factionName = Option(faction.name).filter(_.nonEmpty).getOrElse("[MISSING FACTION NAME]"),
leaderName = leader.map(_.nameTextId).flatMap(Option(_)).filter(_.nonEmpty).getOrElse("[MISSING LEADER]"),
isHuman = userName.isDefined,
userName = userName.getOrElse("")
)
@@ -36,6 +36,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library:game_history",
"//src/main/scala/net/eagle0/eagle/library/util:command_selection",
"//src/main/scala/net/eagle0/eagle/library/util:eagle_require",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/selected:selected_command_converter",
"//src/main/scala/net/eagle0/eagle/service:post_results",
"//src/main/scala/net/eagle0/eagle/service:sync_response_observer",
"//src/main/scala/net/eagle0/eagle/shardok_interface:battle_resolution",
@@ -21,6 +21,7 @@ import net.eagle0.eagle.internal.llm_response.LLMResponse
import net.eagle0.eagle.library.{EagleInternalException, Engine, EngineAndResults}
import net.eagle0.eagle.library.util.CommandSelection
import net.eagle0.eagle.library.util.EagleRequire.{clientRequire, internalRequire}
import net.eagle0.eagle.model.proto_converters.command.selected.SelectedCommandConverter
import net.eagle0.eagle.service.{LlmRequestAfterHistoryCount, PostResults, SyncResponseObserver}
import net.eagle0.eagle.shardok_interface.{BattleResolution, BattleUpdate}
@@ -287,7 +288,7 @@ final case class GameController(
.postCommand(
factionId = factionId,
selectedProvinceId = selectedProvinceId,
selectedCommand = command
selectedCommand = SelectedCommandConverter.fromProto(command)
)
)
@@ -112,6 +112,7 @@ scala_library(
":loaded_hero_conversion",
":start_date",
":start_game_action_result_utils",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/common:chronicle_entry_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:battalion_scala_proto",

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