Compare commits

..
Author SHA1 Message Date
adminandClaude Opus 4.5 e7b92ae927 Add toScala conversion to CommandSelection
Adds a toScala() method to CommandSelection that converts proto-based
command selections to ScalaCommandSelection with proper Scala types.
This encapsulates the proto→Scala conversion logic that was previously
duplicated in each caller.

Updates the three action classes in library/ that use CommandSelection:
- EndHandleRiotsPhaseAction
- PerformVassalCommandsPhaseAction
- PerformVassalDefenseDecisionsAction

These now use cs.toScala(opac.commands) instead of manually converting
the selected command and finding the matching available command.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:35:31 -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
88 changed files with 3190 additions and 2027 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
+51 -35
View File
@@ -450,6 +450,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:
@@ -472,12 +475,21 @@ jobs:
- name: Copy config files to droplet
run: |
# Create directory structure on remote
ssh -i ~/.ssh/deploy_key deploy@"$DO_DROPLET_IP" "mkdir -p /opt/eagle0/scripts/bin /opt/eagle0/nginx"
# Use -p to create parents, and test write access before copying
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
# Remove existing warmup binary (it may have read-only permissions from bazel)
rm -f /opt/eagle0/scripts/bin/warmup
SETUP_DIRS
# Copy files preserving structure
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: |
@@ -503,6 +515,9 @@ 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
@@ -512,35 +527,29 @@ jobs:
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 using shared script (preserves vars set by other workflows)
chmod +x update-env.sh
./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}"
# Login to registry
echo "\$DO_REGISTRY_TOKEN" | docker login registry.digitalocean.com -u "\$DO_REGISTRY_TOKEN" --password-stdin
@@ -552,8 +561,16 @@ jobs:
# 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
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
if [ ! -x crane ]; then
echo "ERROR: Failed to install crane"
ls -la crane || true
exit 1
fi
echo "Crane installed"
ls -la crane
# crane uses Docker config for auth
echo "Pulling Eagle image with crane..."
@@ -580,7 +597,8 @@ jobs:
docker load -i jfr-sidecar.tar
rm jfr-sidecar.tar
rm ./crane
# Keep crane for blue-green deploys (don't delete it)
echo "Crane kept at ./crane for future blue-green deploys"
# Also pull other compose images
docker pull nginx:alpine || true
@@ -620,9 +638,6 @@ jobs:
# (nginx caches DNS at startup, so it needs restart after eagle/shardok)
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
sleep 10
docker compose -f docker-compose.prod.yml ps
@@ -631,6 +646,7 @@ jobs:
echo "=== Verifying container image tags ==="
docker compose -f docker-compose.prod.yml images
# Cleanup old images
# Cleanup stopped containers and old images
docker container prune -f
docker image prune -f
DEPLOY_SCRIPT
+149
View File
@@ -0,0 +1,149 @@
name: Mac Build
on:
push:
branches: [ "main" ]
paths:
- ".github/workflows/mac_build.yml"
- "src/main/csharp/net/eagle0/clients/unity/**"
- "src/main/proto/**"
- "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/**"
- "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_notarization:
description: 'Skip notarization (for testing)'
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.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
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
# Create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain || true
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.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
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.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.event.inputs.skip_notarization != 'true'
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.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
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
+14
View File
@@ -1,5 +1,19 @@
# CLAUDE.md
## CRITICAL GIT RULES (NEVER VIOLATE)
**NEVER push directly to main/master.** No exceptions. Not for "small changes." Not for docs. Not ever.
**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
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.
---
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
+12 -5
View File
@@ -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
@@ -294,11 +294,15 @@ http_archive(
)
# Busybox static binary for Docker health checks (provides nc, wget, etc.)
# https://busybox.net/downloads/binaries/
# Primary: GitHub release mirror (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://github.com/nolen777/eagle0/releases/download/busybox-1.35.0/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,
)
+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"
+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"
+7
View File
@@ -108,6 +108,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..."
+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)"
+50
View File
@@ -0,0 +1,50 @@
#!/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 ==="
xcrun notarytool submit "$ZIP_PATH" \
--apple-id "$APPLE_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--team-id "$TEAM_ID" \
--wait
# Clean up the zip
rm "$ZIP_PATH"
echo "=== Stapling notarization ticket to app ==="
xcrun stapler staple "$APP_PATH"
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;
}
@@ -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,88 @@ 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}
UseFallbackUI: 1
FallbackFont: {fileID: 12800000, guid: f0f6814ff8ef048bcbfd5002f77188ea, type: 3}
--- !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 +8614,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 +11009,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 +12380,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 +15696,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 +20710,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 +29483,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 +31915,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 +34053,7 @@ MonoBehaviour:
eagleGameController: {fileID: 1965467940}
ShardokCanvas: {fileID: 2088078267}
connectionHandler: {fileID: 135486720}
resetTutorialsButton: {fileID: 0}
--- !u!1 &305665272
GameObject:
m_ObjectHideFlags: 0
@@ -36778,10 +36865,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 +47218,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 +64102,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 +71574,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 +74682,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 +82679,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 +83971,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 +93861,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 +93882,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 +95741,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 +96551,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 +99645,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 +100187,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 +105644,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 +121766,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 +122887,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 +123901,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 +141024,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 +147964,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 +153490,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 +174573,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 +178666,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 +178825,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 +182285,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 +197280,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 +199030,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 +199051,7 @@ RectTransform:
- {fileID: 770361455}
- {fileID: 548637656}
- {fileID: 749366806}
- {fileID: 673182551}
- {fileID: 2039520752}
- {fileID: 808870205}
- {fileID: 683322975}
@@ -200241,10 +200623,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 +219417,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 +221672,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 +247788,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 +252960,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 +256319,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 +256339,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 +257751,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 +259054,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 +269163,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,139 @@
# 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 (partial)** - IMGUI fallback modal with Stoke font
- [x] **Phase 3: Triggers (partial)** - Basic trigger system with hooks in game controllers
- [x] **Test setup** - TutorialTestSetup for province selection, battle entry, command triggers
- [x] **Settings integration** - Reset Tutorials button in Settings panel
### Remaining
- [ ] **Canvas UI prefabs** - Replace IMGUI fallback with styled Canvas-based modal
- [ ] **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
├── TutorialModalPanel (TODO)
├── TutorialOverlayController (TODO)
├── TutorialHintIndicator (TODO)
└── IMGUI Fallback (working)
```
---
## File Structure (Current)
```
Assets/Tutorial/
├── TutorialManager.cs ✓ Singleton, coordinates everything
├── TutorialState.cs ✓ PlayerPrefs persistence
├── TutorialTestSetup.cs ✓ Test component for validation
├── Content/
│ ├── TutorialStep.cs ✓ Step data structure
│ └── TutorialSequence.cs ✓ Sequence ScriptableObject
├── Triggers/
│ └── TutorialTriggerRegistry.cs ✓ Event routing
└── UI/
├── TutorialUIManager.cs ✓ UI coordination + IMGUI fallback
├── TutorialModalPanel.cs ✓ Stub (needs Canvas implementation)
├── TutorialOverlayController.cs ✓ Stub
└── TutorialHintIndicator.cs ✓ Stub
```
---
## Integration Points (Implemented)
### EagleGameController.cs
- `SetUpGame()`: Initializes TutorialManager, starts onboarding
- `SwapModel()`: Calls `OnModelUpdated()`
- `ProvinceWasSelected()`: Calls `OnProvinceSelected()`
- `PostCommittedCommand()`: Calls `OnCommandIssued()`
### ShardokGameController.cs
- `SetUpGame()`: Initializes TutorialManager for battle
- `ModelUpdated()`: Calls `OnBattleAction()`
- `OnTurnEnded()`: Calls `OnTurnEnded()`
- Unit selection: Calls `OnUnitSelected()`
### SettingsPanelController.cs
- Reset Tutorials button: Calls `TutorialManager.Instance.ResetAllProgress()`
---
## 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 |
---
## Testing
1. Add `TutorialTestSetup` component to TutorialManager GameObject
2. Assign `Stoke-Regular.ttf` to `FallbackFont` on TutorialUIManager
3. Enable `Debug Logging` on TutorialManager for console output
4. Play game → select province → see test tutorial modal
5. Use Settings → Reset Tutorials to test again
@@ -100,7 +100,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,100 @@
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 a simple "first province selected" tutorial
var provinceSelectedTutorial = CreateProvinceSelectedTutorial();
manager.TriggerRegistry.RegisterTutorial(provinceSelectedTutorial, "province_selected");
// 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");
}
private TutorialSequence CreateProvinceSelectedTutorial() {
var sequence = ScriptableObject.CreateInstance<TutorialSequence>();
sequence.SequenceId = "test_province_selected";
sequence.DisplayName = "Province Selection";
sequence.Steps.Add(new TutorialStep {
StepId = "province_selected_intro",
Title = "Province Selected!",
Description =
"You've selected a province. Here you can see information about the province and issue commands to your forces.\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 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:
@@ -22,10 +22,26 @@ namespace Eagle0.Tutorial {
[Tooltip("Canvas for tutorial UI (should be above game UI)")]
public Canvas TutorialCanvas;
[Header("Fallback UI")]
[Tooltip("Use IMGUI fallback when no ModalPanel is assigned")]
public bool UseFallbackUI = true;
[Tooltip("Font for IMGUI fallback (assign Stoke-Regular)")]
public Font FallbackFont;
// 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() {
// Ensure canvas is set up correctly
if (TutorialCanvas != null) {
@@ -33,6 +49,132 @@ namespace Eagle0.Tutorial {
}
}
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 +185,14 @@ namespace Eagle0.Tutorial {
Action onComplete,
Action onSkip,
bool isOnboarding) {
if (ModalPanel == null) {
Debug.LogWarning("TutorialUIManager: No ModalPanel assigned");
if (ModalPanel != null) {
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 +291,7 @@ namespace Eagle0.Tutorial {
public void HideAll() {
ModalPanel?.Hide();
OverlayController?.HideOverlay();
HideFallbackModal();
// Note: Don't hide hints automatically - they persist until dismissed
}
@@ -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,350 @@
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
cmd := exec.Command(sparkleSignTool, filePath, "-s", privateKeyPath)
output, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("failed to sign file: %w", 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)
}
+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",
@@ -17,6 +17,7 @@ scala_library(
deps = [
":game_history",
"//src/main/protobuf/net/eagle0/eagle/api:command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/views:action_result_view_scala_proto",
@@ -44,6 +45,7 @@ scala_library(
":round_phase_advancer",
"//src/main/protobuf/net/eagle0/eagle/api:available_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/api:selected_command_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:action_result_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/views:action_result_view_scala_proto",
@@ -73,8 +75,11 @@ scala_library(
"//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/command/available:one_province_available_commands_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/selected:selected_command_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 +175,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",
@@ -23,7 +23,10 @@ import net.eagle0.eagle.library.util.EagleRequire.internalRequire
import net.eagle0.eagle.library.EngineImpl.{appliedResults, appliedResultsScala, withUpdateChecks}
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.command.selected.SelectedCommandConverter
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.CommandType
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 +289,38 @@ 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,
selectedCommand
// Convert proto SelectedCommand to Scala
val scalaSelectedCommand = SelectedCommandConverter.fromProto(selectedCommand)
// Match using CommandType
val scalaAvailableCommandOpt = AvailableCommandTypeMap.matchingAvailableCommand(
scalaCommandsForProvince.commands,
scalaSelectedCommand
)
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 = scalaSelectedCommand.commandType
val sequencer = RandomStateSequencer(
initialState = this.currentState,
actionResultApplier = ActionResultApplierImpl(Some(ScalaRuntimeValidator)),
functionalRandom = SeededRandom(this.currentState.randomSeed)
@@ -322,10 +329,10 @@ case class EngineImpl(
commandFactory.makeTCommand(
actingFactionId = factionId,
gameState = gs,
availableCommand = availableCommand,
selectedCommand = selectedCommand
availableCommand = scalaAvailableCommand,
selectedCommand = scalaSelectedCommand
),
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,9 +444,11 @@ 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",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:available_command_type_map",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:handle_riot_utils",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:t_command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:action_with_resulting_state",
@@ -460,6 +462,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/model/action_result/concrete:action_result_concrete",
"//src/main/scala/net/eagle0/eagle/model/action_result/types",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/available:available_command_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/selected:selected_command_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/state:round_phase",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
@@ -1306,11 +1309,13 @@ 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",
"//src/main/scala/net/eagle0/eagle:eagle_pkg",
"//src/main/scala/net/eagle0/eagle/library/actions/applier:action_result_applier",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:available_command_type_map",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:t_command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
@@ -1321,6 +1326,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/available:available_command_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/selected:selected_command_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
@@ -1344,9 +1350,11 @@ 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",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:available_command_type_map",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/command:t_command_factory",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:protoless_random_sequential_results_action",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/common:t_command",
@@ -1356,6 +1364,7 @@ scala_library(
"//src/main/scala/net/eagle0/eagle/library/util/province",
"//src/main/scala/net/eagle0/eagle/model/action_result:action_result_trait",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/available:available_command_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/command/selected:selected_command_converter",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//src/main/scala/net/eagle0/eagle/model/state/command/available",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
@@ -52,11 +52,12 @@ case class EndHandleRiotsPhaseAction(
)
.continue {
case (Some(cs), nextFr) =>
val cmd = commandFactory.makeTCommand(
actingFactionId = province.rulingFactionId.get,
val scalaCs = cs.toScala(opac.commands)
val cmd = commandFactory.makeTCommand(
actingFactionId = scalaCs.actingFactionId,
gameState = gs,
availableCommand = cs.available,
selectedCommand = cs.selected
availableCommand = scalaCs.available,
selectedCommand = scalaCs.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 cmd = commandFactory.makeTCommand(
actingFactionId = cs.actingFactionId,
val scalaCs = cs.toScala(opac.commands)
val cmd = commandFactory.makeTCommand(
actingFactionId = scalaCs.actingFactionId,
gameState = gs,
availableCommand = cs.available,
selectedCommand = cs.selected
availableCommand = scalaCs.available,
selectedCommand = scalaCs.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 cmd = commandFactory.makeTCommand(
actingFactionId = cs.actingFactionId,
val scalaCs = cs.toScala(opac.commands)
val cmd = commandFactory.makeTCommand(
actingFactionId = scalaCs.actingFactionId,
gameState = gs,
availableCommand = cs.available,
selectedCommand = cs.selected
availableCommand = scalaCs.available,
selectedCommand = scalaCs.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,10 @@ 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/library/actions/impl/command:available_command_type_map",
"//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,58 @@
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.library.actions.impl.command.AvailableCommandTypeMap
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)
/**
* Converts this proto-based CommandSelection to a ScalaCommandSelection.
* @param scalaCommands
* The Scala available commands to match against
* @return
* ScalaCommandSelection with Scala types, or throws if no matching available command found
*/
def toScala(scalaCommands: Vector[AvailableCommand]): ScalaCommandSelection = {
val scalaSelected = SelectedCommandConverter.fromProto(selected)
val scalaAvailable = AvailableCommandTypeMap
.matchingAvailableCommand(scalaCommands, scalaSelected)
.getOrElse(
throw new IllegalStateException(
s"No matching Scala available command for selected command: ${scalaSelected.commandType}"
)
)
ScalaCommandSelection(
actingFactionId = actingFactionId,
actingProvinceId = actingProvinceId,
available = scalaAvailable,
selected = scalaSelected,
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("")
)
@@ -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",
@@ -104,16 +104,42 @@ TEST_F(AIAttackerStrategySelectorTest,
.strategyType);
}
TEST_F(AIAttackerStrategySelectorTest,
CanClaimCastles_castlesAvailable_returnsHoldCastlesStrategy) {
TEST_F(AIAttackerStrategySelectorTest, DefenderNotOnCastle_returnsAttackUnitsStrategy) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddIndex(0);
criticalTileCoords.AddIndex(1);
// 2 critical tiles, 3 attackers
// defender not in critical tile
// defender not in critical tile - they're scattering, so chase them
defender->mutable_location() = Coords(2, 2);
EXPECT_EQ(
AIStrategy::STRATEGY_ATTACK_UNITS,
AIAttackerStrategySelector::BestAttackerStrategy(
0,
gameState,
criticalTileCoords,
GetGameSettings()->GetGetter().Backing().max_rounds(),
apdCache,
alCache,
[](BattalionTypeId typeId) {
return GetGameSettings()->GetGetter().GetBattalionType(typeId);
},
GetGameSettings()->GetGetter().Backing().brave_water_action_point_cost(),
*waterCrossingCommandChooser,
{})
.strategyType);
}
TEST_F(AIAttackerStrategySelectorTest, NoDefendersRemaining_returnsHoldCastlesStrategy) {
CoordsSet criticalTileCoords(6, 6);
criticalTileCoords.AddIndex(0);
criticalTileCoords.AddIndex(1);
// 2 critical tiles, 3 attackers
// defender has fled - no defenders remaining
defender->mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT);
EXPECT_EQ(
AIStrategy::STRATEGY_HOLD_CASTLES,
AIAttackerStrategySelector::BestAttackerStrategy(
@@ -1,7 +1,6 @@
package net.eagle0.eagle.library.actions.availability
import net.eagle0.eagle.api.available_command.*
import net.eagle0.eagle.api.selected_command.{ImproveSelectedCommand, MarchSelectedCommand, TrainSelectedCommand}
import net.eagle0.eagle.common.combat_unit.CombatUnit
import net.eagle0.eagle.common.improvement_type.ImprovementType.AGRICULTURE
import net.eagle0.eagle.common.province_event.ImminentRiotEvent
@@ -18,7 +17,7 @@ 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.available.OneProvinceAvailableCommands
import net.eagle0.eagle.model.state.command.common.ImprovementType
import net.eagle0.eagle.model.state.command.selected.SelectedCommand as ScalaSelectedCommand
import net.eagle0.eagle.model.state.command.CommandType
import net.eagle0.eagle.model.state.game_state.GameState
import org.scalamock.scalatest.MockFactory
import org.scalatest.flatspec.AnyFlatSpec
@@ -706,7 +705,7 @@ class AvailableCommandsFactoryTest extends AnyFlatSpec with Matchers with MockFa
"shouldFollow" should "return true for train after train" in {
AvailableCommandsFactory.shouldFollow(
ScalaSelectedCommand.TrainSelected(actingHeroId = 4),
CommandType.Train,
ScalaAvailableCommand.TrainAvailable(
actingProvinceId = 1,
availableHeroIds = Vector(3, 4, 5),
@@ -717,13 +716,7 @@ class AvailableCommandsFactoryTest extends AnyFlatSpec with Matchers with MockFa
it should "return false for march after march" in {
AvailableCommandsFactory.shouldFollow(
ScalaSelectedCommand.MarchSelected(
gold = 10,
food = 20,
originProvince = 3,
destinationProvinceId = 15,
marchingUnits = Vector.empty
),
CommandType.March,
ScalaAvailableCommand.MarchAvailable(
oneProvinceCommands = Vector.empty,
actingProvinceId = 17,
@@ -734,8 +727,7 @@ class AvailableCommandsFactoryTest extends AnyFlatSpec with Matchers with MockFa
it should "return true for improve after improve" in {
AvailableCommandsFactory.shouldFollow(
ScalaSelectedCommand
.ImproveSelected(improvementType = ImprovementType.Agriculture, actingHeroId = 3, lockType = false),
CommandType.Improve,
ScalaAvailableCommand.ImproveAvailable(
availableTypes = Vector(ImprovementType.Agriculture),
availableHeroIds = Vector.empty,
@@ -748,7 +740,7 @@ class AvailableCommandsFactoryTest extends AnyFlatSpec with Matchers with MockFa
it should "return false for improve after train" in {
AvailableCommandsFactory.shouldFollow(
ScalaSelectedCommand.TrainSelected(actingHeroId = 3),
CommandType.Train,
ScalaAvailableCommand.ImproveAvailable(
availableTypes = Vector(ImprovementType.Agriculture),
availableHeroIds = Vector.empty,
@@ -78,9 +78,9 @@ scala_test(
"//src/main/scala/net/eagle0/eagle/library/settings:riot_max_gold",
"//src/main/scala/net/eagle0/eagle/library/util:idable",
"//src/main/scala/net/eagle0/eagle/model/proto_converters/game_state",
"//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/common",
"//src/main/scala/net/eagle0/eagle/model/state/command/selected",
"//src/main/scala/net/eagle0/eagle/model/state/game_state",
"@maven//:org_scalamock_scalamock_3",
],
@@ -202,6 +202,7 @@ scala_test(
name = "end_handle_riots_phase_action_test",
srcs = ["EndHandleRiotsPhaseActionTest.scala"],
deps = [
"//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/library/actions/applier:action_result_applier_impl",
"//src/main/scala/net/eagle0/eagle/library/actions/impl/action:end_handle_riots_phase_action",
@@ -209,6 +209,7 @@ scala_test(
deps = [
"//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/common:round_phase_scala_proto",
"//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto",
"//src/main/scala/net/eagle0/common:functional_random",