mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 09:55:41 +00:00
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de22df3848 | ||
|
|
e281f3b012 | ||
|
|
9695db2536 | ||
|
|
aaba34e7de | ||
|
|
9e37236fe3 |
@@ -5,10 +5,6 @@ common --ui_event_filters=-INFO
|
||||
|
||||
common --enable_bzlmod
|
||||
|
||||
# Use pre-built protoc binary instead of compiling from source
|
||||
common --incompatible_enable_proto_toolchain_resolution
|
||||
common --@com_google_protobuf//bazel/toolchains:prefer_prebuilt_protoc=true
|
||||
|
||||
# Don't use toolchains_llvm for the swift app build
|
||||
common:mactools --ignore_dev_dependency
|
||||
|
||||
@@ -37,21 +33,14 @@ common:macos --host_linkopt=-Wl,-no_warn_duplicate_libraries
|
||||
# See: https://github.com/grpc/grpc/issues/37619
|
||||
common:macos --features=-module_maps
|
||||
|
||||
# Pin DEVELOPER_DIR so the apple_cc_autoconf repo rule doesn't re-evaluate
|
||||
# when Xcode updates in-place. The sync script overrides this for mactools.
|
||||
# Fix Xcode version caching issue - avoids need for `bazel clean --expunge` after Xcode updates
|
||||
common:macos --repo_env=DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
|
||||
|
||||
# Xcode config for mactools builds only. Generated by scripts/sync_bazel_xcode.sh.
|
||||
# Bakes the Xcode build version into action cache keys and sets DEVELOPER_DIR.
|
||||
try-import %workspace%/.bazelrc.xcode
|
||||
|
||||
common --java_language_version=25
|
||||
common --java_runtime_version=remotejdk_25
|
||||
common --tool_java_language_version=25
|
||||
common --tool_java_runtime_version=remotejdk_25
|
||||
|
||||
# Workspace status for build stamping (git commit, timestamp)
|
||||
# Only targets with stamp=1 will use these values; we intentionally
|
||||
# do NOT set "common --stamp" so non-stamped targets can share the
|
||||
# remote cache across CI workflows.
|
||||
common --workspace_status_command=tools/workspace_status.sh
|
||||
common --stamp
|
||||
|
||||
@@ -5,38 +5,9 @@
|
||||
*.tif filter=lfs diff=lfs merge=lfs -text
|
||||
*.bytes filter=lfs diff=lfs merge=lfs -text
|
||||
*.psd filter=lfs diff=lfs merge=lfs -text
|
||||
*.psb filter=lfs diff=lfs merge=lfs -text
|
||||
# PSB meta files contain bone/sprite import data and can be very large
|
||||
*.psb.meta filter=lfs diff=lfs merge=lfs -text
|
||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||
# Exclude pre-existing font files that were committed as blobs (not LFS pointers)
|
||||
src/main/csharp/**/GUI[[:space:]]Pro[[:space:]]Kit*/**/*.ttf !filter !diff !merge
|
||||
src/main/csharp/**/Modern[[:space:]]UI[[:space:]]Pack/**/*.ttf !filter !diff !merge
|
||||
*.herodata filter=lfs diff=lfs merge=lfs -text
|
||||
*.ogg filter=lfs diff=lfs merge=lfs -text
|
||||
*.tga filter=lfs diff=lfs merge=lfs -text
|
||||
*.fbx filter=lfs diff=lfs merge=lfs -text
|
||||
*.FBX filter=lfs diff=lfs merge=lfs -text
|
||||
*.pdf filter=lfs diff=lfs merge=lfs -text
|
||||
# Third-party asset packs: track all files via LFS (never manually edited)
|
||||
src/main/csharp/**/DungeonMonsters2D/** filter=lfs diff=lfs merge=lfs -text
|
||||
src/main/csharp/**/Raccoon/** filter=lfs diff=lfs merge=lfs -text
|
||||
src/main/csharp/**/Polytope[[:space:]]Studio/** filter=lfs diff=lfs merge=lfs -text
|
||||
src/main/csharp/**/RRFreelance-Characters/** filter=lfs diff=lfs merge=lfs -text
|
||||
src/main/csharp/**/Dragon/** filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Unity Smart Merge - use UnityYAMLMerge for Unity files
|
||||
# Requires configuring unityyamlmerge in ~/.gitconfig:
|
||||
# [merge]
|
||||
# tool = unityyamlmerge
|
||||
# [mergetool "unityyamlmerge"]
|
||||
# trustExitCode = false
|
||||
# cmd = '/Applications/Unity/Hub/Editor/*/Unity.app/Contents/Tools/UnityYAMLMerge' merge -p "$BASE" "$REMOTE" "$LOCAL" "$MERGED"
|
||||
*.unity merge=unityyamlmerge
|
||||
*.prefab merge=unityyamlmerge
|
||||
*.asset merge=unityyamlmerge
|
||||
*.mat merge=unityyamlmerge
|
||||
*.anim merge=unityyamlmerge
|
||||
*.controller merge=unityyamlmerge
|
||||
*.physicsMaterial2D merge=unityyamlmerge
|
||||
*.physicMaterial merge=unityyamlmerge
|
||||
|
||||
@@ -41,8 +41,7 @@ jobs:
|
||||
set -ex
|
||||
|
||||
# Build auth server image (Go binary has explicit goos/goarch in BUILD.bazel)
|
||||
# --stamp is needed for authservice x_defs (git commit, build time)
|
||||
bazel build --stamp //ci:auth_server_image
|
||||
bazel build //ci:auth_server_image
|
||||
|
||||
# Save the resolved path before any other bazel command changes bazel-bin symlink
|
||||
IMAGE_PATH=$(readlink -f bazel-bin/ci/auth_server_image)
|
||||
|
||||
@@ -13,8 +13,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Show disk usage before cleanup
|
||||
run: |
|
||||
|
||||
@@ -23,10 +23,6 @@ on:
|
||||
- '!src/main/csharp/**'
|
||||
- '!src/test/csharp/**'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -34,34 +30,16 @@ jobs:
|
||||
lint:
|
||||
runs-on: [self-hosted, bazel]
|
||||
steps:
|
||||
- name: Clean workspace
|
||||
run: |
|
||||
git sparse-checkout disable 2>/dev/null || true
|
||||
git config --local core.sparseCheckout false 2>/dev/null || true
|
||||
git config --local --unset extensions.worktreeConfig 2>/dev/null || true
|
||||
rm -f .git/info/sparse-checkout .git/config.worktree 2>/dev/null || true
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
- name: Check BUILD.bazel dependencies
|
||||
run: ./scripts/check_build_deps.sh --strict
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
- name: Check JavaScript syntax
|
||||
run: node --check src/main/go/net/eagle0/admin_server/static/map_editor.js
|
||||
|
||||
test:
|
||||
runs-on: [self-hosted, bazel]
|
||||
steps:
|
||||
- name: Clean workspace
|
||||
run: |
|
||||
git sparse-checkout disable 2>/dev/null || true
|
||||
git config --local core.sparseCheckout false 2>/dev/null || true
|
||||
git config --local --unset extensions.worktreeConfig 2>/dev/null || true
|
||||
rm -f .git/info/sparse-checkout .git/config.worktree 2>/dev/null || true
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -106,7 +84,7 @@ jobs:
|
||||
with:
|
||||
name: test.json
|
||||
path: test.json
|
||||
retention-days: 3
|
||||
retention-days: 5
|
||||
- name: Archive failed test logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -114,4 +92,4 @@ jobs:
|
||||
name: failed-test-logs
|
||||
path: failed_test_logs/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 3
|
||||
retention-days: 5
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
name: Blob Cleanup
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily at 04:00 UTC
|
||||
- cron: '0 4 * * *'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
clean: false
|
||||
|
||||
- name: Clean up unreferenced blobs
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: |
|
||||
bazel run //src/main/go/net/eagle0/build/blob_cleanup:blob_cleanup -- --min-age=1h
|
||||
@@ -31,16 +31,13 @@ on:
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
# Only allow one deployment at a time; queue new ones (never cancel a running deploy).
|
||||
# The build-all job checks if it's still the latest commit on main and skips if not,
|
||||
# so intermediate commits don't waste time building when a newer one is already queued.
|
||||
# Only allow one deployment at a time to prevent race conditions
|
||||
concurrency:
|
||||
group: docker-build-deploy
|
||||
cancel-in-progress: false
|
||||
cancel-in-progress: false # Don't cancel running deployments, queue new ones
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write # Required to delete artifacts after deploy
|
||||
|
||||
jobs:
|
||||
# Single consolidated build job - builds all images with one bazel invocation
|
||||
@@ -53,43 +50,20 @@ jobs:
|
||||
admin_image_tag: ${{ steps.push-images.outputs.admin_image_tag }}
|
||||
jfr_sidecar_image_tag: ${{ steps.push-images.outputs.jfr_sidecar_image_tag }}
|
||||
steps:
|
||||
- name: Skip if superseded
|
||||
id: check-latest
|
||||
run: |
|
||||
# Check if there's a newer run of this workflow waiting in the queue.
|
||||
# If so, skip this build — the queued run will deploy a newer commit.
|
||||
QUEUED=$(curl -s -H "Authorization: Bearer $GH_TOKEN" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/workflows/docker_build.yml/runs?status=queued" \
|
||||
| python3 -c "import sys,json; runs=json.load(sys.stdin).get('workflow_runs',[]); print(len([r for r in runs if r['run_number'] > ${{ github.run_number }}]))")
|
||||
if [ "$QUEUED" -gt 0 ]; then
|
||||
echo "::notice::Skipping build — $QUEUED newer run(s) queued for this workflow"
|
||||
echo "skip=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Checkout repository
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Fetch LFS files needed for admin server
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
run: git lfs pull --include="src/main/go/net/eagle0/admin_server/static/tiles/*"
|
||||
|
||||
- name: Build all Docker images
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
id: build-all
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Build ALL images in a single bazel command - Bazel parallelizes internally
|
||||
# Note: Shardok is built separately for ARM64 and deployed to Hetzner
|
||||
# --stamp is needed for admin_server x_defs (git commit, build time)
|
||||
echo "=== Building Docker images ==="
|
||||
bazel build \
|
||||
--stamp \
|
||||
--platforms=//:linux_x86_64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux//:all \
|
||||
//ci:eagle_server_image \
|
||||
@@ -115,16 +89,8 @@ jobs:
|
||||
echo "Admin: $ADMIN_PATH"
|
||||
echo "JFR Sidecar: $JFR_PATH"
|
||||
|
||||
- name: Upload warmup binary
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: warmup-binary
|
||||
path: scripts/bin/warmup
|
||||
retention-days: 1
|
||||
|
||||
- name: Login to DigitalOcean Container Registry
|
||||
if: steps.check-latest.outputs.skip != 'true' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true'))
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
env:
|
||||
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
@@ -144,59 +110,40 @@ jobs:
|
||||
|
||||
GIT_SHA=$(git rev-parse --short=8 HEAD)
|
||||
|
||||
# Get crane (cached across runs to avoid rebuilding bazel target)
|
||||
CRANE_VERSION="v0.20.2"
|
||||
CRANE_DIR="${HOME}/.local/bin"
|
||||
CRANE="${CRANE_DIR}/crane"
|
||||
if [ ! -x "$CRANE" ] || ! "$CRANE" version 2>/dev/null | grep -q "0.20.2"; then
|
||||
echo "Installing crane ${CRANE_VERSION}..."
|
||||
mkdir -p "$CRANE_DIR"
|
||||
curl -sL "https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Darwin_arm64.tar.gz" | tar xzf - -C "$CRANE_DIR" crane
|
||||
chmod +x "$CRANE"
|
||||
else
|
||||
echo "Using cached crane at $CRANE"
|
||||
fi
|
||||
# Get crane from push target runfiles
|
||||
bazel build //ci:eagle_server_push
|
||||
RUNFILES="bazel-bin/ci/push_eagle_server_push.sh.runfiles"
|
||||
CRANE=$(find "$RUNFILES" -path "*darwin*" -name crane 2>/dev/null | head -1)
|
||||
|
||||
# Set up image paths and tags
|
||||
EAGLE_IMAGE="${{ steps.build-all.outputs.eagle_path }}"
|
||||
EAGLE_TAG="registry.digitalocean.com/eagle0/eagle-server:${GIT_SHA}"
|
||||
ADMIN_IMAGE="${{ steps.build-all.outputs.admin_path }}"
|
||||
ADMIN_TAG="registry.digitalocean.com/eagle0/admin-server:${GIT_SHA}"
|
||||
JFR_IMAGE="${{ steps.build-all.outputs.jfr_path }}"
|
||||
JFR_TAG="registry.digitalocean.com/eagle0/jfr-sidecar:${GIT_SHA}"
|
||||
|
||||
# Push all three images in parallel
|
||||
echo "Pushing Eagle: $EAGLE_TAG"
|
||||
$CRANE push "$EAGLE_IMAGE" "$EAGLE_TAG" &
|
||||
PID_EAGLE=$!
|
||||
|
||||
echo "Pushing Admin: $ADMIN_TAG"
|
||||
$CRANE push "$ADMIN_IMAGE" "$ADMIN_TAG" &
|
||||
PID_ADMIN=$!
|
||||
|
||||
echo "Pushing JFR Sidecar: $JFR_TAG"
|
||||
$CRANE push "$JFR_IMAGE" "$JFR_TAG" &
|
||||
PID_JFR=$!
|
||||
|
||||
# Wait for all pushes to complete
|
||||
PUSH_FAILED=0
|
||||
wait $PID_EAGLE || PUSH_FAILED=1
|
||||
wait $PID_ADMIN || PUSH_FAILED=1
|
||||
wait $PID_JFR || PUSH_FAILED=1
|
||||
if [ $PUSH_FAILED -ne 0 ]; then
|
||||
echo "ERROR: One or more image pushes failed"
|
||||
if [ -z "$CRANE" ] || [ ! -x "$CRANE" ]; then
|
||||
echo "ERROR: crane not found in runfiles"
|
||||
find "$RUNFILES" -name crane 2>/dev/null || echo "No crane found at all"
|
||||
exit 1
|
||||
fi
|
||||
echo "All images pushed, tagging :latest..."
|
||||
|
||||
# Tag :latest in parallel
|
||||
$CRANE copy "$EAGLE_TAG" "registry.digitalocean.com/eagle0/eagle-server:latest" &
|
||||
$CRANE copy "$ADMIN_TAG" "registry.digitalocean.com/eagle0/admin-server:latest" &
|
||||
$CRANE copy "$JFR_TAG" "registry.digitalocean.com/eagle0/jfr-sidecar:latest" &
|
||||
wait
|
||||
echo "Using crane: $CRANE"
|
||||
|
||||
# Push Eagle image
|
||||
EAGLE_IMAGE="${{ steps.build-all.outputs.eagle_path }}"
|
||||
EAGLE_TAG="registry.digitalocean.com/eagle0/eagle-server:${GIT_SHA}"
|
||||
echo "Pushing Eagle: $EAGLE_TAG"
|
||||
$CRANE push "$EAGLE_IMAGE" "$EAGLE_TAG"
|
||||
$CRANE copy "$EAGLE_TAG" "registry.digitalocean.com/eagle0/eagle-server:latest"
|
||||
echo "eagle_image_tag=$EAGLE_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Push Admin image
|
||||
ADMIN_IMAGE="${{ steps.build-all.outputs.admin_path }}"
|
||||
ADMIN_TAG="registry.digitalocean.com/eagle0/admin-server:${GIT_SHA}"
|
||||
echo "Pushing Admin: $ADMIN_TAG"
|
||||
$CRANE push "$ADMIN_IMAGE" "$ADMIN_TAG"
|
||||
$CRANE copy "$ADMIN_TAG" "registry.digitalocean.com/eagle0/admin-server:latest"
|
||||
echo "admin_image_tag=$ADMIN_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
# Push JFR Sidecar image
|
||||
JFR_IMAGE="${{ steps.build-all.outputs.jfr_path }}"
|
||||
JFR_TAG="registry.digitalocean.com/eagle0/jfr-sidecar:${GIT_SHA}"
|
||||
echo "Pushing JFR Sidecar: $JFR_TAG"
|
||||
$CRANE push "$JFR_IMAGE" "$JFR_TAG"
|
||||
$CRANE copy "$JFR_TAG" "registry.digitalocean.com/eagle0/jfr-sidecar:latest"
|
||||
echo "jfr_sidecar_image_tag=$JFR_TAG" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "=== All images pushed successfully ==="
|
||||
@@ -242,12 +189,9 @@ jobs:
|
||||
DO_DROPLET_IP: ${{ secrets.DO_DROPLET_IP }}
|
||||
DO_REGISTRY_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
GITHUB_TOKEN_FOR_ADMIN: ${{ secrets.ADMIN_GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Setup SSH key
|
||||
run: |
|
||||
@@ -256,11 +200,11 @@ jobs:
|
||||
chmod 600 ~/.ssh/deploy_key
|
||||
ssh-keyscan -H "$DO_DROPLET_IP" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
|
||||
- name: Download warmup binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: warmup-binary
|
||||
path: scripts/bin/
|
||||
- name: Build warmup tool
|
||||
run: |
|
||||
bazel build //ci:warmup_tar
|
||||
mkdir -p scripts/bin
|
||||
tar -xf bazel-bin/ci/warmup_tar.tar -C scripts/bin --strip-components=1
|
||||
|
||||
- name: Copy config files to droplet
|
||||
run: |
|
||||
@@ -367,7 +311,6 @@ jobs:
|
||||
export FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
|
||||
export DO_REGISTRY_TOKEN="${DO_REGISTRY_TOKEN}"
|
||||
export NOTIFY_SECRET="${NOTIFY_SECRET}"
|
||||
export GITHUB_TOKEN="${GITHUB_TOKEN_FOR_ADMIN}"
|
||||
|
||||
# Check Docker has IPv6 support
|
||||
if ! cat /etc/docker/daemon.json 2>/dev/null | grep -q '"ip6tables"'; then
|
||||
@@ -379,34 +322,29 @@ jobs:
|
||||
|
||||
echo "Using images: \$EAGLE_IMAGE, \$ADMIN_IMAGE, \$JFR_SIDECAR_IMAGE"
|
||||
|
||||
# Install crane for pulling OCI images (cached across deploys)
|
||||
CRANE_VERSION="v0.20.2"
|
||||
if [ ! -x crane ] || ! ./crane version 2>/dev/null | grep -q "0.20.2"; then
|
||||
echo "Installing crane \${CRANE_VERSION}..."
|
||||
rm -f crane
|
||||
curl -sL "https://github.com/google/go-containerregistry/releases/download/\${CRANE_VERSION}/go-containerregistry_Linux_x86_64.tar.gz" | tar xzf - crane
|
||||
chmod +x crane
|
||||
else
|
||||
echo "Using cached crane"
|
||||
fi
|
||||
# Install crane for pulling OCI images
|
||||
echo "Installing crane..."
|
||||
rm -f crane
|
||||
curl -sL https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz | tar xzf - crane
|
||||
chmod +x crane
|
||||
|
||||
# Pull all images in parallel, then load sequentially
|
||||
echo "Pulling all images in parallel..."
|
||||
./crane pull "\${EAGLE_IMAGE}" eagle.tar &
|
||||
./crane pull "\${ADMIN_IMAGE}" admin.tar &
|
||||
./crane pull "\${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar &
|
||||
docker pull nginx:alpine &
|
||||
docker pull certbot/certbot &
|
||||
wait
|
||||
# Pull and load all images
|
||||
echo "Pulling Eagle image..."
|
||||
./crane pull "\${EAGLE_IMAGE}" eagle.tar && docker load -i eagle.tar && rm eagle.tar
|
||||
|
||||
echo "Loading images..."
|
||||
docker load -i eagle.tar && rm eagle.tar
|
||||
docker load -i admin.tar && rm admin.tar
|
||||
echo "Pulling Admin image..."
|
||||
./crane pull "\${ADMIN_IMAGE}" admin.tar && docker load -i admin.tar && rm admin.tar
|
||||
# Tag as :latest locally so docker-compose fallback uses correct image
|
||||
docker tag "\${ADMIN_IMAGE}" registry.digitalocean.com/eagle0/admin-server:latest
|
||||
docker load -i jfr-sidecar.tar && rm jfr-sidecar.tar
|
||||
|
||||
echo "All images pulled and loaded successfully"
|
||||
echo "Pulling JFR Sidecar image..."
|
||||
./crane pull "\${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar && docker load -i jfr-sidecar.tar && rm jfr-sidecar.tar
|
||||
|
||||
# Pull other compose images
|
||||
docker pull nginx:alpine || true
|
||||
docker pull certbot/certbot || true
|
||||
|
||||
echo "All images pulled successfully"
|
||||
|
||||
# =================================================================
|
||||
# Verify Shardok connectivity before proceeding with deployment
|
||||
@@ -471,19 +409,3 @@ jobs:
|
||||
# Remove images older than 24h (keeps recent images for rollback)
|
||||
docker image prune -a -f --filter "until=24h" || true
|
||||
DEPLOY_SCRIPT
|
||||
|
||||
cleanup:
|
||||
needs: [build-all, deploy]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete warmup-binary artifact
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
ARTIFACT_ID=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
|
||||
-q '.artifacts[] | select(.name == "warmup-binary") | .id')
|
||||
if [ -n "$ARTIFACT_ID" ]; then
|
||||
echo "Deleting warmup-binary artifact (ID: $ARTIFACT_ID)"
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$ARTIFACT_ID" || true
|
||||
fi
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
name: Eagle Build
|
||||
|
||||
on:
|
||||
# Main pushes are covered by docker_build.yml which builds the same target
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'src/main/scala/**'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/**'
|
||||
- 'src/main/protobuf/net/eagle0/common/**'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/eagle_build.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/scala/**'
|
||||
@@ -13,10 +23,6 @@ on:
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/eagle_build.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
@@ -112,29 +112,14 @@ jobs:
|
||||
- name: Delete all installer artifacts
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Delete ALL eagle-installer artifacts to free up storage
|
||||
echo "Fetching all eagle-installer artifacts..."
|
||||
page=1
|
||||
while true; do
|
||||
response=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts?per_page=100&page=$page")
|
||||
ids=$(echo "$response" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for a in data.get('artifacts', []):
|
||||
if a['name'] == 'eagle-installer':
|
||||
print(a['id'])
|
||||
" 2>/dev/null)
|
||||
if [ -z "$ids" ]; then
|
||||
break
|
||||
fi
|
||||
for id in $ids; do
|
||||
echo "Deleting artifact ID: $id"
|
||||
curl -s -X DELETE -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts/$id" || true
|
||||
done
|
||||
page=$((page + 1))
|
||||
artifact_ids=$(gh api "repos/${{ github.repository }}/actions/artifacts" \
|
||||
--paginate -q '.artifacts[] | select(.name == "eagle-installer") | .id')
|
||||
for id in $artifact_ids; do
|
||||
echo "Deleting artifact ID: $id"
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$id" || true
|
||||
done
|
||||
echo "Cleanup complete"
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
name: iOS Addressables Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- ".github/workflows/ios_addressables_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/AddressableAssetsData/**"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Music/**"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Editor/BuildScript.cs"
|
||||
- "ci/github_actions/build_ios_addressables.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/ios_addressables_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/AddressableAssetsData/**"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Music/**"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Editor/BuildScript.cs"
|
||||
- "ci/github_actions/build_ios_addressables.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
# Runner-specific build directory to allow parallel builds on multiple runners
|
||||
EAGLE0_BUILD_DIR: /tmp/eagle0-${{ github.run_id }}
|
||||
|
||||
jobs:
|
||||
build-ios-addressables:
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
run: |
|
||||
# Self-hosted runners persist .git between runs. When a PR is updated,
|
||||
# old local refs (refs/remotes/pull/*/merge) may point to commits whose
|
||||
# objects were never fetched or have been pruned. Remove these stale refs
|
||||
# before checkout to prevent "missing object" errors.
|
||||
if [ -d ".git" ]; then
|
||||
echo "Pruning stale PR refs..."
|
||||
git for-each-ref --format='%(refname)' refs/remotes/pull/ 2>/dev/null | \
|
||||
xargs -r git update-ref -d 2>/dev/null || true
|
||||
fi
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: true
|
||||
|
||||
- name: Fetch LFS files
|
||||
run: |
|
||||
git lfs install
|
||||
git lfs pull
|
||||
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh ios
|
||||
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: ios
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
|
||||
- name: Build iOS Addressables
|
||||
run: ./ci/github_actions/build_ios_addressables.sh "$EAGLE0_BUILD_DIR/editor_ios_addressables.log"
|
||||
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: ios
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ./ci/github_actions/upload_addressables.sh iOS
|
||||
|
||||
- name: Archive Build Log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: editor_ios_addressables.log
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_ios_addressables.log
|
||||
retention-days: 5
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
@@ -1,8 +1,6 @@
|
||||
name: iOS TestFlight
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *' # 9 PM Pacific (UTC-8) / 10 PM PDT (UTC-7)
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_upload:
|
||||
@@ -13,7 +11,7 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
actions: write
|
||||
|
||||
env:
|
||||
# Runner-specific build directory to allow parallel builds on multiple runners
|
||||
@@ -22,93 +20,10 @@ env:
|
||||
KEYCHAIN_NAME: ios-build-${{ github.run_id }}.keychain
|
||||
|
||||
jobs:
|
||||
check-changes:
|
||||
# Skip nightly builds when nothing has changed since the last successful
|
||||
# scheduled run. Manual workflow_dispatch always builds.
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_build: ${{ steps.check.outputs.should_build }}
|
||||
steps:
|
||||
- name: Check for relevant changes since last successful run
|
||||
id: check
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
core.setOutput('should_build', 'true');
|
||||
console.log('Manual trigger — building');
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: { workflow_runs: runs } } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'ios_testflight.yml',
|
||||
status: 'success',
|
||||
event: 'schedule',
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
if (runs.length === 0) {
|
||||
core.setOutput('should_build', 'true');
|
||||
console.log('No previous successful scheduled run — building');
|
||||
return;
|
||||
}
|
||||
|
||||
const lastSha = runs[0].head_sha;
|
||||
console.log(`Last successful scheduled run: ${lastSha}`);
|
||||
console.log(`Current SHA: ${context.sha}`);
|
||||
|
||||
if (lastSha === context.sha) {
|
||||
core.setOutput('should_build', 'false');
|
||||
console.log('No changes since last successful run — skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Paths that can affect the iOS Unity build.
|
||||
const relevantPrefixes = [
|
||||
'src/main/csharp/', // Unity project
|
||||
'src/main/protobuf/', // Generates C# code
|
||||
'src/main/resources/', // Game data & maps
|
||||
'scripts/', // Build scripts
|
||||
'ci/', // CI scripts & workflows
|
||||
'.github/workflows/ios_testflight.yml',
|
||||
'MODULE.bazel',
|
||||
'.bazelrc',
|
||||
];
|
||||
|
||||
const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
basehead: `${lastSha}...${context.sha}`,
|
||||
per_page: 1, // We only need the file list, not patches
|
||||
});
|
||||
|
||||
// If the diff is too large, build to be safe
|
||||
if (comparison.files === undefined) {
|
||||
core.setOutput('should_build', 'true');
|
||||
console.log('Could not retrieve file list — building to be safe');
|
||||
return;
|
||||
}
|
||||
|
||||
const relevant = comparison.files.filter(f =>
|
||||
relevantPrefixes.some(p => f.filename.startsWith(p))
|
||||
);
|
||||
|
||||
if (relevant.length > 0) {
|
||||
core.setOutput('should_build', 'true');
|
||||
console.log(`${relevant.length} relevant file(s) changed:`);
|
||||
relevant.slice(0, 20).forEach(f => console.log(` ${f.filename}`));
|
||||
if (relevant.length > 20) console.log(` ... and ${relevant.length - 20} more`);
|
||||
} else {
|
||||
core.setOutput('should_build', 'false');
|
||||
console.log(`${comparison.files.length} file(s) changed, none in relevant paths — skipping`);
|
||||
}
|
||||
|
||||
build-unity:
|
||||
needs: check-changes
|
||||
if: needs.check-changes.outputs.should_build == 'true'
|
||||
runs-on: [self-hosted, macOS, testflight]
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
outputs:
|
||||
xcode_project_path: ${{ steps.build.outputs.xcode_project_path }}
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
@@ -125,30 +40,33 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
clean: true
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
rm -rf src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee/
|
||||
|
||||
- name: Fetch LFS files
|
||||
run: |
|
||||
git lfs install
|
||||
echo "LFS objects before pull:"
|
||||
git lfs ls-files | wc -l
|
||||
git lfs pull
|
||||
echo "LFS objects after pull:"
|
||||
git lfs ls-files | wc -l
|
||||
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh ios
|
||||
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: ios
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
|
||||
- name: Build iOS Unity Project
|
||||
id: build
|
||||
run: |
|
||||
./ci/github_actions/build_unity_ios.sh "$EAGLE0_BUILD_DIR/eagle0iOS"
|
||||
echo "xcode_project_path=$EAGLE0_BUILD_DIR/eagle0iOS" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: ios
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
if: success()
|
||||
@@ -157,16 +75,18 @@ jobs:
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ./ci/github_actions/upload_addressables.sh iOS
|
||||
|
||||
- name: Purge CDN cache for iOS addressables
|
||||
if: success()
|
||||
env:
|
||||
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
||||
- name: Zip Xcode project for artifact
|
||||
run: |
|
||||
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
||||
-H "Authorization: Bearer $DO_CDN_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"files": ["addressables/iOS/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
cd $EAGLE0_BUILD_DIR
|
||||
# Use tar for speed - Xcode projects have many small files
|
||||
tar -czf eagle0iOS.tar.gz eagle0iOS
|
||||
|
||||
- name: Upload Xcode project
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: xcode-project-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0iOS.tar.gz
|
||||
retention-days: 1
|
||||
|
||||
- name: Archive Build Log
|
||||
if: success() || failure()
|
||||
@@ -174,13 +94,38 @@ jobs:
|
||||
with:
|
||||
name: editor_ios.log
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_ios.log
|
||||
retention-days: 3
|
||||
retention-days: 5
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
archive-and-upload:
|
||||
needs: build-unity
|
||||
runs-on: [self-hosted, macOS, testflight]
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
sparse-checkout: |
|
||||
ci
|
||||
scripts
|
||||
|
||||
- name: Clean download directory
|
||||
run: rm -rf $EAGLE0_BUILD_DIR/eagle0iOS
|
||||
|
||||
- name: Download Xcode project
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: xcode-project-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}
|
||||
|
||||
- name: Extract Xcode project
|
||||
run: |
|
||||
cd $EAGLE0_BUILD_DIR
|
||||
tar -xzf eagle0iOS.tar.gz
|
||||
rm eagle0iOS.tar.gz
|
||||
ls -la eagle0iOS/
|
||||
|
||||
- name: Install Signing Certificate
|
||||
env:
|
||||
IOS_CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }}
|
||||
@@ -232,20 +177,10 @@ jobs:
|
||||
- name: Archive and Export IPA
|
||||
env:
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
||||
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
|
||||
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
|
||||
run: |
|
||||
# Write API key to file for xcodebuild
|
||||
API_KEY_PATH=$(mktemp)
|
||||
echo "$APP_STORE_CONNECT_API_KEY" > "$API_KEY_PATH"
|
||||
export APP_STORE_CONNECT_API_KEY_PATH="$API_KEY_PATH"
|
||||
|
||||
chmod +x ./ci/github_actions/archive_ios.sh
|
||||
./ci/github_actions/archive_ios.sh "$EAGLE0_BUILD_DIR/eagle0iOS" "$EAGLE0_BUILD_DIR/archive" "$APPLE_TEAM_ID" "$PROFILE_UUID"
|
||||
|
||||
rm -f "$API_KEY_PATH"
|
||||
|
||||
- name: Upload to TestFlight
|
||||
if: ${{ github.event.inputs.skip_upload != 'true' }}
|
||||
env:
|
||||
@@ -283,3 +218,21 @@ jobs:
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
cleanup:
|
||||
needs: [build-unity, archive-and-upload]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Delete intermediate artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
artifact_name="xcode-project-${{ github.run_id }}"
|
||||
echo "Deleting artifact: $artifact_name"
|
||||
artifact_id=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
|
||||
-q ".artifacts[] | select(.name == \"$artifact_name\") | .id")
|
||||
if [ -n "$artifact_id" ]; then
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" || true
|
||||
fi
|
||||
|
||||
@@ -25,20 +25,25 @@ on:
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- "ci/mac/**"
|
||||
pull_request:
|
||||
# On PRs, only build Mac when Mac-specific files change. Shared C#/proto
|
||||
# changes are covered by the Windows build — if it passes, Mac will too.
|
||||
paths:
|
||||
- ".github/workflows/mac_build.yml"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/**"
|
||||
- "src/main/protobuf/net/eagle0/common/**"
|
||||
- "src/main/protobuf/net/eagle0/shardok/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/api/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/common/**"
|
||||
- "src/main/protobuf/net/eagle0/eagle/views/**"
|
||||
- "src/main/go/net/eagle0/build/mac_build_handler/**"
|
||||
- "scripts/build_mac_plugin.sh"
|
||||
- "scripts/build_sparkle_plugin.sh"
|
||||
- "src/main/objc/net/eagle0/clients/unity/sparkle/**"
|
||||
- "scripts/inject_sparkle.sh"
|
||||
- "scripts/codesign_mac_app.sh"
|
||||
- "scripts/notarize_submit.sh"
|
||||
- "scripts/notarize_wait.sh"
|
||||
- "ci/github_actions/build_mac.sh"
|
||||
- "ci/github_actions/build_unity_mac.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- "ci/mac/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -48,10 +53,6 @@ on:
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write # Required to delete artifacts after deploy
|
||||
@@ -84,51 +85,30 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
clean: true # Remove untracked files like old SparklePlugin.bundle
|
||||
fetch-depth: 0 # For version numbering from git history
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
# Only clear Bee/ when C# files were added/deleted/renamed (structural
|
||||
# changes that stale the DAG). Content-only modifications are handled by
|
||||
# Bee's incremental compilation. See persist_library.sh for background.
|
||||
BEE_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee"
|
||||
SHA_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha"
|
||||
if [ -f "$SHA_FILE" ] && [ -d "$BEE_DIR" ]; then
|
||||
LAST_SHA=$(cat "$SHA_FILE")
|
||||
if git diff --diff-filter=ADR --name-only "$LAST_SHA" HEAD -- '*.cs' '*.csproj' '*.asmdef' 2>/dev/null | grep -q .; then
|
||||
echo "C# files added/deleted/renamed since $LAST_SHA — clearing Bee/"
|
||||
rm -rf "$BEE_DIR"
|
||||
else
|
||||
echo "No structural C# changes since $LAST_SHA — keeping Bee/"
|
||||
fi
|
||||
else
|
||||
echo "No previous build SHA or no Bee/ — clearing Bee/ as safe default"
|
||||
rm -rf "$BEE_DIR"
|
||||
fi
|
||||
|
||||
- name: Fetch LFS files
|
||||
run: |
|
||||
git lfs install
|
||||
echo "LFS objects before pull:"
|
||||
git lfs ls-files | wc -l
|
||||
git lfs pull
|
||||
echo "LFS objects after pull:"
|
||||
git lfs ls-files | wc -l
|
||||
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh mac
|
||||
|
||||
- name: Sync Bazel Xcode config
|
||||
run: ./scripts/sync_bazel_xcode.sh
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: mac
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
|
||||
- name: Build Mac Unity
|
||||
run: ./ci/github_actions/build_unity_mac.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC"
|
||||
|
||||
- name: Save build SHA for Bee/ cache invalidation
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
run: git rev-parse HEAD > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: mac
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
@@ -245,7 +225,7 @@ jobs:
|
||||
with:
|
||||
name: editor_mac.log
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_mac.log
|
||||
retention-days: 3
|
||||
retention-days: 5
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
@@ -305,8 +285,6 @@ jobs:
|
||||
needs: [build-and-sign, wait-notarization]
|
||||
if: needs.build-and-sign.outputs.should_deploy == 'true'
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
outputs:
|
||||
deployed_version: ${{ steps.deploy-mac.outputs.deployed_version }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -328,11 +306,7 @@ jobs:
|
||||
ditto -x -k eagle0.app.zip .
|
||||
rm eagle0.app.zip
|
||||
|
||||
- name: Sync Bazel Xcode config
|
||||
run: ./scripts/sync_bazel_xcode.sh
|
||||
|
||||
- name: Deploy Mac Build
|
||||
id: deploy-mac
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
@@ -363,37 +337,44 @@ jobs:
|
||||
|
||||
rm "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
echo "deployed_version=$VERSION" >> $GITHUB_OUTPUT
|
||||
# Export version for notify step
|
||||
echo "DEPLOYED_VERSION=$VERSION" >> $GITHUB_ENV
|
||||
|
||||
# Artifact cleanup is handled by the cleanup job on ubuntu-latest
|
||||
- name: Notify clients of update
|
||||
if: success()
|
||||
env:
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
run: |
|
||||
# Wait for CDN cache to clear
|
||||
sleep 60
|
||||
|
||||
# Notify via admin server (required=false for normal deploys)
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=mac&version=$DEPLOYED_VERSION&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
|
||||
- name: Delete this run's Mac app artifacts
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Delete this run's artifacts (names include run ID to avoid conflicts)
|
||||
for artifact_name in signed-mac-app-${{ github.run_id }} notarized-mac-app-${{ github.run_id }}; do
|
||||
echo "Deleting artifact: $artifact_name"
|
||||
artifact_id=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
|
||||
-q ".artifacts[] | select(.name == \"$artifact_name\") | .id")
|
||||
if [ -n "$artifact_id" ]; then
|
||||
echo "Deleting artifact ID: $artifact_id"
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" || true
|
||||
fi
|
||||
done
|
||||
echo "Cleanup complete"
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
notify-mac:
|
||||
needs: deploy
|
||||
if: needs.deploy.outputs.deployed_version != ''
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Purge CDN cache and notify clients
|
||||
env:
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
||||
run: |
|
||||
# Purge CDN cache so clients get the latest files immediately
|
||||
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
||||
-H "Authorization: Bearer $DO_CDN_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"files": ["mac/*", "addressables/StandaloneOSX/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=mac&version=${{ needs.deploy.outputs.deployed_version }}&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
|
||||
# Cleanup job runs regardless of success/failure to prevent artifact accumulation
|
||||
cleanup:
|
||||
needs: [build-and-sign, wait-notarization, deploy, notify-mac]
|
||||
needs: [build-and-sign, wait-notarization, deploy]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -20,10 +20,6 @@ on:
|
||||
default: 'true'
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: shardok-arm64-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -64,14 +60,12 @@ jobs:
|
||||
if [ "$MAGIC" = "7f454c46" ]; then
|
||||
echo "SUCCESS: Binary is ELF format (Linux)"
|
||||
# Check if it's ARM64 (e_machine = 0xB7 = 183 for aarch64)
|
||||
# od -tx2 reads as a 16-bit value in host byte order (little-endian on macOS),
|
||||
# so the little-endian ELF bytes b7 00 are displayed as 00b7.
|
||||
E_MACHINE=$(od -An -j18 -N2 -tx2 "$LINUX_BIN" | tr -d ' ')
|
||||
echo "ELF e_machine: $E_MACHINE"
|
||||
if [ "$E_MACHINE" = "00b7" ]; then
|
||||
if [ "$E_MACHINE" = "b700" ]; then
|
||||
echo "SUCCESS: Binary is ARM64 (aarch64)"
|
||||
else
|
||||
echo "WARNING: Binary e_machine is $E_MACHINE (expected 00b7 for aarch64)"
|
||||
echo "WARNING: Binary e_machine is $E_MACHINE (expected b700 for aarch64)"
|
||||
fi
|
||||
elif [ "$MAGIC" = "cfaeedfe" ] || [ "$MAGIC" = "cffaedfe" ]; then
|
||||
echo "ERROR: Binary is Mach-O format (macOS) - cross-compilation failed!"
|
||||
@@ -134,19 +128,20 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get crane (cached across runs to avoid rebuilding bazel target,
|
||||
# which would discard the ARM64 analysis cache due to platform change)
|
||||
CRANE_VERSION="v0.20.2"
|
||||
CRANE_DIR="${HOME}/.local/bin"
|
||||
CRANE="${CRANE_DIR}/crane"
|
||||
if [ ! -x "$CRANE" ] || ! "$CRANE" version 2>/dev/null | grep -q "0.20.2"; then
|
||||
echo "Installing crane ${CRANE_VERSION}..."
|
||||
mkdir -p "$CRANE_DIR"
|
||||
curl -sL "https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Darwin_arm64.tar.gz" | tar xzf - -C "$CRANE_DIR" crane
|
||||
chmod +x "$CRANE"
|
||||
else
|
||||
echo "Using cached crane at $CRANE"
|
||||
# Build a push target to get crane in runfiles
|
||||
bazel build //ci:eagle_server_push
|
||||
|
||||
# Find the Darwin crane binary
|
||||
RUNFILES="bazel-bin/ci/push_eagle_server_push.sh.runfiles"
|
||||
CRANE=$(find "$RUNFILES" -path "*darwin*" -name crane 2>/dev/null | head -1)
|
||||
if [ -z "$CRANE" ]; then
|
||||
CRANE=$(find "$RUNFILES" -name crane 2>/dev/null | head -1)
|
||||
fi
|
||||
if [ -z "$CRANE" ] || [ ! -e "$CRANE" ]; then
|
||||
echo "ERROR: crane not found"
|
||||
exit 1
|
||||
fi
|
||||
echo "Using crane: $CRANE"
|
||||
|
||||
# Push with arm64-prefixed SHA tag (same repo as x86, different tag)
|
||||
GIT_SHA=$(git rev-parse --short=8 HEAD)
|
||||
@@ -191,22 +186,8 @@ jobs:
|
||||
|
||||
echo "Deploying Shardok ARM64: ${{ needs.build-shardok-arm64.outputs.image_tag }}"
|
||||
|
||||
# Pre-warm NAT64 path to DigitalOcean registry (IPv6-only Hetzner → IPv4 registry)
|
||||
curl -sf --max-time 10 https://registry.digitalocean.com/v2/ > /dev/null 2>&1 || true
|
||||
|
||||
# Pull the new image (retry up to 3 times for NAT64 connectivity flakiness)
|
||||
for attempt in 1 2 3; do
|
||||
echo "Pull attempt $attempt..."
|
||||
if docker pull "${{ needs.build-shardok-arm64.outputs.image_tag }}"; then
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "ERROR: docker pull failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
echo "Pull failed, retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
# Pull the new image
|
||||
docker pull "${{ needs.build-shardok-arm64.outputs.image_tag }}"
|
||||
|
||||
# Stop and remove any container using port 40042 or named shardok*
|
||||
docker ps -q --filter "publish=40042" | xargs -r docker stop
|
||||
|
||||
@@ -1,7 +1,18 @@
|
||||
name: Shardok Build
|
||||
|
||||
on:
|
||||
# Main pushes are covered by shardok_arm64_build.yml which builds the same target
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- 'src/main/cpp/**'
|
||||
- 'src/main/proto/net/eagle0/shardok/**'
|
||||
- 'src/main/proto/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/build/**'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/shardok_build.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/cpp/**'
|
||||
@@ -14,10 +25,6 @@ on:
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/shardok_build.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ on:
|
||||
- "scripts/build_plugins.sh"
|
||||
- "scripts/build_windows_plugin.sh"
|
||||
- "ci/github_actions/build_unity.sh"
|
||||
- "ci/github_actions/restore_library.sh"
|
||||
- "ci/github_actions/persist_library.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
@@ -34,16 +36,14 @@ on:
|
||||
- "scripts/build_plugins.sh"
|
||||
- "scripts/build_windows_plugin.sh"
|
||||
- "ci/github_actions/build_unity.sh"
|
||||
- "ci/github_actions/restore_library.sh"
|
||||
- "ci/github_actions/persist_library.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- "MODULE.bazel"
|
||||
- "WORKSPACE"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
@@ -54,8 +54,6 @@ env:
|
||||
jobs:
|
||||
windows-unity:
|
||||
runs-on: [self-hosted, macOS, unity-windows]
|
||||
outputs:
|
||||
deployed_version: ${{ steps.get-version.outputs.deployed_version }}
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
@@ -72,46 +70,25 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
# Only clear Bee/ when C# files were added/deleted/renamed (structural
|
||||
# changes that stale the DAG). Content-only modifications are handled by
|
||||
# Bee's incremental compilation. See persist_library.sh for background.
|
||||
BEE_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee"
|
||||
SHA_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha"
|
||||
if [ -f "$SHA_FILE" ] && [ -d "$BEE_DIR" ]; then
|
||||
LAST_SHA=$(cat "$SHA_FILE")
|
||||
if git diff --diff-filter=ADR --name-only "$LAST_SHA" HEAD -- '*.cs' '*.csproj' '*.asmdef' 2>/dev/null | grep -q .; then
|
||||
echo "C# files added/deleted/renamed since $LAST_SHA — clearing Bee/"
|
||||
rm -rf "$BEE_DIR"
|
||||
else
|
||||
echo "No structural C# changes since $LAST_SHA — keeping Bee/"
|
||||
fi
|
||||
else
|
||||
echo "No previous build SHA or no Bee/ — clearing Bee/ as safe default"
|
||||
rm -rf "$BEE_DIR"
|
||||
fi
|
||||
clean: true # Remove untracked files from previous builds
|
||||
|
||||
- name: Fetch LFS files
|
||||
run: |
|
||||
git lfs install
|
||||
echo "LFS objects before pull:"
|
||||
git lfs ls-files | wc -l
|
||||
git lfs pull
|
||||
echo "LFS objects after pull:"
|
||||
git lfs ls-files | wc -l
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh windows
|
||||
- name: Restore Library/
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: windows
|
||||
run: ./ci/github_actions/restore_library.sh
|
||||
- name: Build Windows unity
|
||||
run: ./ci/github_actions/build_unity.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN"
|
||||
|
||||
- name: Save build SHA for Bee/ cache invalidation
|
||||
- name: Persist Library/
|
||||
if: success()
|
||||
run: git rev-parse HEAD > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha
|
||||
|
||||
env:
|
||||
UNITY_CACHE_PLATFORM: windows
|
||||
run: ./ci/github_actions/persist_library.sh
|
||||
- name: Upload Addressables to CDN
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
env:
|
||||
@@ -119,14 +96,14 @@ jobs:
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ./ci/github_actions/upload_addressables.sh StandaloneWindows64
|
||||
- name: Deploy Windows unity
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: bazel run //src/main/go/net/eagle0/build/unity3d_windows_build_handler:unity3d_windows_build_handler -- "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN" "/tmp/unity_manifest.txt"
|
||||
|
||||
- name: Update unified manifest
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
@@ -149,12 +126,21 @@ jobs:
|
||||
# Cleanup
|
||||
rm -f /tmp/manifest_signing_key
|
||||
|
||||
- name: Export deployed version
|
||||
id: get-version
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
- name: Notify clients of update
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
run: |
|
||||
# Wait for CDN cache to clear
|
||||
sleep 60
|
||||
|
||||
# Get version from manifest
|
||||
VERSION=$(grep "^version=" /tmp/unity_manifest.txt | cut -d= -f2 || date +%Y.%m.%d)
|
||||
echo "deployed_version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
# Notify via admin server (required=false for normal deploys)
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=windows&version=$VERSION&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
|
||||
- name: Archive build log
|
||||
if: success() || failure()
|
||||
@@ -162,20 +148,7 @@ jobs:
|
||||
with:
|
||||
name: editor_win.log
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_win.log
|
||||
retention-days: 3
|
||||
retention-days: 5
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
notify-windows:
|
||||
needs: windows-unity
|
||||
if: needs.windows-unity.outputs.deployed_version != ''
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify clients of update
|
||||
env:
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
run: |
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=windows&version=${{ needs.windows-unity.outputs.deployed_version }}&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
@@ -23,7 +23,6 @@ bazel-bin
|
||||
bazel-eagle0*
|
||||
bazel-out
|
||||
bazel-testlogs
|
||||
.bazelrc.xcode
|
||||
.ijwb
|
||||
.clwb
|
||||
buildWin.sh
|
||||
@@ -38,8 +37,5 @@ scripts/refresh_name_layers/refresh_name_layers.zip
|
||||
.metals
|
||||
api_keys.txt
|
||||
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/GeneratedProtos/
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/Eagle0Protos/
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/Packages/com.unity.dedicated-server/
|
||||
node_modules/
|
||||
tools/map_generator/output/
|
||||
|
||||
+1
-11
@@ -5,7 +5,6 @@ repos:
|
||||
rev: v4.3.0
|
||||
hooks:
|
||||
- id: check-added-large-files
|
||||
- id: check-merge-conflict
|
||||
- id: no-commit-to-branch
|
||||
args: [--branch, main]
|
||||
- repo: https://github.com/pocc/pre-commit-hooks
|
||||
@@ -14,16 +13,7 @@ repos:
|
||||
- id: clang-format
|
||||
args: [-i, --no-diff]
|
||||
types_or: ["c++", "c#"]
|
||||
exclude: >-
|
||||
(?x)^src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/(
|
||||
Plugins|
|
||||
DungeonMonsters2D|
|
||||
Dragon|
|
||||
ithappy|
|
||||
Polytope\ Studio|
|
||||
RRFreelance-Characters|
|
||||
Raccoon
|
||||
)/
|
||||
exclude: ^src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins
|
||||
- repo: https://github.com/yoheimuta/protolint
|
||||
rev: v0.42.2
|
||||
hooks:
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## CRITICAL BASH RULES (NEVER VIOLATE)
|
||||
|
||||
**NEVER chain bash commands.** Do not use `&&`, `||`, or `;` to combine commands. Each command must be a separate Bash tool call. Use parallel tool calls when commands are independent.
|
||||
|
||||
## CRITICAL GIT RULES (NEVER VIOLATE)
|
||||
|
||||
**NEVER use `git -C`.** Instead, cd to the repo root and run git commands from there.
|
||||
|
||||
**NEVER push directly to main/master.** No exceptions. Not for "small changes." Not for docs. Not ever.
|
||||
|
||||
**NEVER merge PRs.** You create PRs. The user merges them. No exceptions.
|
||||
@@ -231,9 +225,6 @@ to be used for different players or game situations within the same server proce
|
||||
- Real-time bidirectional streaming with server via `PersistentClientConnection.cs`
|
||||
- Strategic map UI in `Assets/Eagle/`, tactical battle UI in `Assets/Shardok/`
|
||||
- Seamless transition between strategic gameplay and hex-based tactical combat
|
||||
- **NEVER add defensive null checks on Unity Inspector fields** - these hide configuration bugs. If a field isn't
|
||||
linked in the editor, it should throw a NullReferenceException so the problem is immediately obvious. Silently
|
||||
skipping code when a required field is null makes bugs harder to find.
|
||||
|
||||
**Go (Build Tools):**
|
||||
|
||||
@@ -342,8 +333,6 @@ dotty.tools.dotc.core.MissingType: Cannot resolve reference to type net.eagle0.e
|
||||
**Configuration:** Game parameters in `/src/main/resources/net/eagle0/eagle/game_parameters.json`
|
||||
**Data Files:** TSV format for battalions, heroes, and other game data
|
||||
|
||||
**NEVER modify hero names.** Do not change names in `heroes.tsv`, `generated_heroes`, or any other hero data files. The names are carefully chosen and are not to be altered.
|
||||
|
||||
## Deployment
|
||||
|
||||
- Bazel handles multi-language builds and dependencies
|
||||
|
||||
+19
-77
@@ -126,79 +126,42 @@ use_repo(
|
||||
"org_golang_x_sys",
|
||||
)
|
||||
|
||||
#
|
||||
# Language Support - Rust
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_rust", version = "0.68.1")
|
||||
|
||||
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
|
||||
rust.toolchain(edition = "2021")
|
||||
use_repo(rust, "rust_toolchains")
|
||||
|
||||
register_toolchains("@rust_toolchains//:all")
|
||||
|
||||
crate = use_extension("@rules_rust//crate_universe:extension.bzl", "crate")
|
||||
crate.from_cargo(
|
||||
name = "map_generator_crates",
|
||||
cargo_lockfile = "//src/main/rust/net/eagle0/eagle/map_generator:Cargo.lock",
|
||||
manifests = ["//src/main/rust/net/eagle0/eagle/map_generator:Cargo.toml"],
|
||||
)
|
||||
use_repo(crate, "map_generator_crates")
|
||||
|
||||
#
|
||||
# Platform Support - Apple/iOS
|
||||
#
|
||||
# Note: rules_apple is NOT included in the main workspace due to rules_swift
|
||||
# version conflicts with grpc. SparklePlugin (which needs rules_apple) is built
|
||||
# in a separate workspace at sparkle_workspace/ to isolate the conflict.
|
||||
#
|
||||
|
||||
bazel_dep(name = "apple_support", version = "1.24.1", repo_name = "build_bazel_apple_support")
|
||||
bazel_dep(name = "rules_apple", version = "4.3.3", repo_name = "build_bazel_rules_apple")
|
||||
bazel_dep(name = "apple_support", version = "1.23.1", repo_name = "build_bazel_apple_support")
|
||||
|
||||
# rules_swift is a transitive dep of grpc and flatbuffers with conflicting versions:
|
||||
# - grpc 1.76.0.bcr.1 requires rules_swift 3.x (compatibility level 3)
|
||||
# - flatbuffers requires rules_swift 2.x (compatibility level 2)
|
||||
# We don't use Swift directly, but we need to force a single version.
|
||||
# Force 3.x since grpc is more complex and harder to downgrade.
|
||||
single_version_override(
|
||||
module_name = "rules_swift",
|
||||
version = "3.1.2",
|
||||
)
|
||||
|
||||
# Register Apple CC toolchain for Objective-C compilation
|
||||
apple_cc_configure = use_extension(
|
||||
"@build_bazel_apple_support//crosstool:setup.bzl",
|
||||
"apple_cc_configure_extension",
|
||||
)
|
||||
use_repo(apple_cc_configure, "local_config_apple_cc", "local_config_apple_cc_toolchains")
|
||||
use_repo(apple_cc_configure, "local_config_apple_cc")
|
||||
|
||||
#
|
||||
# Protocol Buffers & RPC
|
||||
#
|
||||
|
||||
bazel_dep(name = "protobuf", version = "33.5", repo_name = "com_google_protobuf")
|
||||
|
||||
# Use pre-built protoc binaries instead of compiling from source.
|
||||
# See: https://protobuf.dev/reference/cpp/cpp-generated/#invocation
|
||||
prebuilt_protoc = use_extension("@com_google_protobuf//bazel/private:prebuilt_protoc_extension.bzl", "protoc")
|
||||
use_repo(
|
||||
prebuilt_protoc,
|
||||
"prebuilt_protoc.linux_aarch_64",
|
||||
"prebuilt_protoc.linux_ppcle_64",
|
||||
"prebuilt_protoc.linux_s390_64",
|
||||
"prebuilt_protoc.linux_x86_32",
|
||||
"prebuilt_protoc.linux_x86_64",
|
||||
"prebuilt_protoc.osx_aarch_64",
|
||||
"prebuilt_protoc.osx_x86_64",
|
||||
"prebuilt_protoc.win32",
|
||||
"prebuilt_protoc.win64",
|
||||
)
|
||||
|
||||
bazel_dep(name = "rules_proto", version = "7.1.0")
|
||||
bazel_dep(name = "rules_cc", version = "0.2.14")
|
||||
bazel_dep(name = "grpc", version = "1.78.0")
|
||||
|
||||
# Patch grpc 1.78.0 to fix rules_python incompatibility: grpc requests Python
|
||||
# 3.14.0b2 toolchain but rules_python >= 1.6.0 removed it in favor of 3.14.
|
||||
# Upstream fix: https://github.com/grpc/grpc/commit/459279b
|
||||
# TODO: Remove this override once a fixed grpc version is published to BCR.
|
||||
single_version_override(
|
||||
module_name = "grpc",
|
||||
patch_strip = 1,
|
||||
patches = ["//third_party/patches:grpc_fix_python_version.patch"],
|
||||
version = "1.78.0",
|
||||
)
|
||||
|
||||
bazel_dep(name = "grpc", version = "1.74.0")
|
||||
bazel_dep(name = "grpc-java", version = "1.78.0")
|
||||
bazel_dep(name = "rules_proto_grpc_csharp", version = "5.8.0")
|
||||
bazel_dep(name = "flatbuffers", version = "25.12.19")
|
||||
|
||||
#
|
||||
@@ -217,12 +180,8 @@ bazel_dep(name = "aspect_bazel_lib", version = "2.22.5")
|
||||
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
|
||||
|
||||
# Base image for Eagle (Java 25 JRE - smaller runtime, no dev tools needed)
|
||||
# Digest pinned for Bazel repository cache hit; update with:
|
||||
# TOKEN=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/eclipse-temurin:pull" | python3 -c "import json,sys; print(json.load(sys.stdin)['token'])")
|
||||
# curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json" "https://registry-1.docker.io/v2/library/eclipse-temurin/manifests/25-jre" | python3 -c "import json,sys; m=json.load(sys.stdin); [print(p['digest']) for p in m.get('manifests',[]) if p['platform']['architecture']=='amd64']"
|
||||
oci.pull(
|
||||
name = "eclipse_temurin_25_jre",
|
||||
digest = "sha256:98236ffdfb61cff3fc4f8e40e5b80460a5a4c35a0f56a5b4182a926844a7db49",
|
||||
image = "docker.io/library/eclipse-temurin",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "25-jre",
|
||||
@@ -231,7 +190,6 @@ oci.pull(
|
||||
# Base image for JFR sidecar (Java 25 JDK - includes jcmd for JFR dumps)
|
||||
oci.pull(
|
||||
name = "eclipse_temurin_25_jdk",
|
||||
digest = "sha256:bc2d562355813350f47bc472b9721b0b84761866671ad95d9a9f1151fa69da6e",
|
||||
image = "docker.io/library/eclipse-temurin",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "25-jdk",
|
||||
@@ -325,9 +283,6 @@ maven.install(
|
||||
|
||||
# Error tracking
|
||||
"io.sentry:sentry:8.31.0",
|
||||
|
||||
# SQLite for client text storage
|
||||
"org.xerial:sqlite-jdbc:3.46.1.0",
|
||||
],
|
||||
duplicate_version_warning = "error",
|
||||
fail_if_repin_required = True,
|
||||
@@ -400,16 +355,8 @@ http_archive(
|
||||
],
|
||||
)
|
||||
|
||||
# Sparkle framework for macOS auto-updates
|
||||
SPARKLE_VERSION = "2.6.4"
|
||||
|
||||
http_archive(
|
||||
name = "sparkle",
|
||||
build_file = "//src/main/objc/net/eagle0/clients/unity/sparkle/external:BUILD.sparkle",
|
||||
sha256 = "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b",
|
||||
strip_prefix = "",
|
||||
url = "https://github.com/sparkle-project/Sparkle/releases/download/%s/Sparkle-%s.tar.xz" % (SPARKLE_VERSION, SPARKLE_VERSION),
|
||||
)
|
||||
# Note: Sparkle framework is defined in sparkle_workspace/MODULE.bazel
|
||||
# (not in main workspace due to rules_swift version conflicts)
|
||||
|
||||
# Busybox static binary for Docker health checks (provides nc, wget, etc.)
|
||||
# Primary: DigitalOcean Spaces (public, reliable)
|
||||
@@ -499,11 +446,6 @@ register_toolchains(
|
||||
"@rules_scala//testing:scalatest_toolchain",
|
||||
)
|
||||
|
||||
# Apple CC toolchain for Objective-C compilation (SparklePlugin).
|
||||
# Registered before LLVM so it has higher priority; Apple constraint matching
|
||||
# ensures it's only used for Apple-platform targets (objc_library, macos_bundle).
|
||||
register_toolchains("@local_config_apple_cc_toolchains//:all")
|
||||
|
||||
# Set dev_dependency so we can turn this off for swift MacOS builds
|
||||
register_toolchains(
|
||||
"@llvm_toolchain//:all",
|
||||
|
||||
Generated
+65
-2087
File diff suppressed because one or more lines are too long
@@ -243,13 +243,6 @@ pkg_tar(
|
||||
package_dir = "/app",
|
||||
)
|
||||
|
||||
# Package the .e0mj map files for the map editor
|
||||
pkg_tar(
|
||||
name = "admin_maps_layer",
|
||||
srcs = ["//src/main/resources/net/eagle0/shardok/maps:maps_json"],
|
||||
package_dir = "/app/maps",
|
||||
)
|
||||
|
||||
oci_image(
|
||||
name = "admin_server_image",
|
||||
base = "@alpine_linux_linux_amd64",
|
||||
@@ -258,7 +251,6 @@ oci_image(
|
||||
tars = [
|
||||
":busybox_layer",
|
||||
":admin_binary_layer",
|
||||
":admin_maps_layer",
|
||||
],
|
||||
workdir = "/app",
|
||||
)
|
||||
|
||||
@@ -31,30 +31,6 @@ fi
|
||||
|
||||
echo "Found Xcode project: $XCODEPROJ"
|
||||
|
||||
# Ensure export compliance key is set in Info.plist
|
||||
# This bypasses the "Missing Compliance" prompt in App Store Connect.
|
||||
# Set here rather than relying solely on Unity's DeepLinkPostProcessor,
|
||||
# which depends on UNITY_IOS being defined at script compilation time.
|
||||
INFO_PLIST="$XCODE_PROJECT_PATH/Info.plist"
|
||||
if [ -f "$INFO_PLIST" ]; then
|
||||
/usr/libexec/PlistBuddy -c "Set :ITSAppUsesNonExemptEncryption false" "$INFO_PLIST" 2>/dev/null \
|
||||
|| /usr/libexec/PlistBuddy -c "Add :ITSAppUsesNonExemptEncryption bool false" "$INFO_PLIST"
|
||||
echo "Set ITSAppUsesNonExemptEncryption=false in Info.plist"
|
||||
|
||||
# Remove UIApplicationSceneManifest if present.
|
||||
# Unity 6 generates this key, which opts the app into the iOS scene-based
|
||||
# lifecycle. This breaks Application.deepLinkActivated because iOS routes
|
||||
# deep link URLs through UISceneDelegate instead of UIApplicationDelegate,
|
||||
# and Unity doesn't implement the scene delegate path.
|
||||
# See: https://issuetracker.unity3d.com/issues/in-135632
|
||||
if /usr/libexec/PlistBuddy -c "Print :UIApplicationSceneManifest" "$INFO_PLIST" 2>/dev/null; then
|
||||
echo "Found UIApplicationSceneManifest in Info.plist — removing to fix deep link handling"
|
||||
/usr/libexec/PlistBuddy -c "Delete :UIApplicationSceneManifest" "$INFO_PLIST"
|
||||
else
|
||||
echo "No UIApplicationSceneManifest in Info.plist (deep links should work)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Unity always generates "Unity-iPhone" as the main app scheme
|
||||
SCHEME="Unity-iPhone"
|
||||
echo "Using scheme: $SCHEME"
|
||||
@@ -110,24 +86,11 @@ echo "=== Debug: Export options plist ==="
|
||||
cat "$EXPORT_OPTIONS_PLIST"
|
||||
echo "=== End debug ==="
|
||||
|
||||
# Export IPA with App Store Connect API key authentication.
|
||||
# Without the API key, xcodebuild falls back to the Xcode-Token in the
|
||||
# keychain, which expires periodically and breaks headless CI builds.
|
||||
EXPORT_AUTH_ARGS=()
|
||||
if [ -n "${APP_STORE_CONNECT_API_KEY_PATH:-}" ] && [ -n "${APP_STORE_CONNECT_API_KEY_ID:-}" ] && [ -n "${APP_STORE_CONNECT_API_ISSUER_ID:-}" ]; then
|
||||
echo "Using App Store Connect API key for export authentication"
|
||||
EXPORT_AUTH_ARGS=(
|
||||
-authenticationKeyPath "$APP_STORE_CONNECT_API_KEY_PATH"
|
||||
-authenticationKeyID "$APP_STORE_CONNECT_API_KEY_ID"
|
||||
-authenticationKeyIssuerID "$APP_STORE_CONNECT_API_ISSUER_ID"
|
||||
)
|
||||
fi
|
||||
|
||||
# Export IPA (removed -allowProvisioningUpdates as we're using manual signing)
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$ARCHIVE_PATH" \
|
||||
-exportPath "$EXPORT_PATH" \
|
||||
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST" \
|
||||
"${EXPORT_AUTH_ARGS[@]}"
|
||||
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST"
|
||||
|
||||
# Find and rename the IPA to a consistent name
|
||||
IPA_FILE=$(find "$EXPORT_PATH" -name "*.ipa" | head -1)
|
||||
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Build iOS Addressables only (no player build)
|
||||
# This switches Unity to iOS target and builds addressables for CDN upload
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
# Read Unity version from project file
|
||||
UNITY_VERSION=$(grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //')
|
||||
|
||||
# Use runner-specific build directory if EAGLE0_BUILD_DIR is set, otherwise default
|
||||
BUILD_BASE="${EAGLE0_BUILD_DIR:-/tmp/eagle0}"
|
||||
|
||||
WORKSPACE=$(pwd)
|
||||
|
||||
echo "Building protos"
|
||||
./scripts/build_protos.sh
|
||||
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
|
||||
LOG_PATH=${1:-"${BUILD_BASE}/editor_ios_addressables.log"}
|
||||
|
||||
echo "Building iOS Addressables"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_PATH")"
|
||||
|
||||
# Build Addressables for iOS target
|
||||
# Uses BuildiOSAddressables which explicitly switches build target
|
||||
# Capture exit code to show log on failure
|
||||
set +e
|
||||
${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
|
||||
-nographics \
|
||||
-batchmode \
|
||||
-quit \
|
||||
-executeMethod BuildScript.BuildiOSAddressables \
|
||||
-logFile "$LOG_PATH" \
|
||||
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
UNITY_EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ $UNITY_EXIT_CODE -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Unity build failed with exit code $UNITY_EXIT_CODE"
|
||||
echo "=== Unity Editor Log (last 200 lines) ==="
|
||||
tail -200 "$LOG_PATH" || echo "Could not read log file at $LOG_PATH"
|
||||
echo "=== End of Unity Editor Log ==="
|
||||
exit $UNITY_EXIT_CODE
|
||||
fi
|
||||
|
||||
echo "iOS Addressables build complete"
|
||||
echo "Bundles should be in: $WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0/ServerData/iOS/"
|
||||
Executable
+33
@@ -0,0 +1,33 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Persist Unity Library/ cache to persistent storage
|
||||
#
|
||||
# Environment variables:
|
||||
# UNITY_CACHE_PLATFORM - Platform identifier (e.g., "mac", "windows")
|
||||
# Defaults to "mac" if not set
|
||||
#
|
||||
# Note: Library/Bee/ is excluded because it contains DAG files with hardcoded
|
||||
# file paths that become stale when project files change. This prevents
|
||||
# "Data at the root level is invalid" XML errors from stale references.
|
||||
|
||||
set -uxo pipefail
|
||||
|
||||
PLATFORM="${UNITY_CACHE_PLATFORM:-mac}"
|
||||
CACHE_DIR="/tmp/eagle0/Library-${PLATFORM}"
|
||||
|
||||
/bin/echo "persist Library/ to $CACHE_DIR (excluding Bee/)"
|
||||
|
||||
# rsync may exit with code 23 ("partial transfer due to error") if Unity's
|
||||
# temporary files vanish during the copy. This is acceptable for a cache.
|
||||
/usr/bin/rsync -rtlDvq --exclude='Bee/' src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ "$CACHE_DIR/"
|
||||
rsync_exit=$?
|
||||
|
||||
if [ $rsync_exit -eq 0 ]; then
|
||||
exit 0
|
||||
elif [ $rsync_exit -eq 23 ]; then
|
||||
echo "Warning: rsync exited with 23 (some files vanished during copy). This is expected for Unity temp files."
|
||||
exit 0
|
||||
else
|
||||
echo "Error: rsync failed with exit code $rsync_exit"
|
||||
exit $rsync_exit
|
||||
fi
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Restore Unity Library/ cache from persistent storage
|
||||
#
|
||||
# Environment variables:
|
||||
# UNITY_CACHE_PLATFORM - Platform identifier (e.g., "mac", "windows")
|
||||
# Defaults to "mac" if not set
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
PLATFORM="${UNITY_CACHE_PLATFORM:-mac}"
|
||||
CACHE_DIR="/tmp/eagle0/Library-${PLATFORM}"
|
||||
|
||||
/bin/echo "restore Library/ from $CACHE_DIR"
|
||||
/bin/mkdir -p "$CACHE_DIR"
|
||||
/usr/bin/rsync -rtlDvq "$CACHE_DIR/" src/main/csharp/net/eagle0/clients/unity/eagle0/Library/
|
||||
@@ -208,8 +208,6 @@ services:
|
||||
- "${JFR_SIDECAR_ADDR:-jfr-sidecar:8081}" # Can be switched for blue-green
|
||||
- "--http-port"
|
||||
- "8080"
|
||||
- "--maps-dir"
|
||||
- "/app/maps"
|
||||
environment:
|
||||
# Secret for CI to authenticate client update notifications
|
||||
NOTIFY_SECRET: "${NOTIFY_SECRET:-}"
|
||||
@@ -217,10 +215,6 @@ services:
|
||||
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
|
||||
DO_SPACES_ACCESS_KEY: "${ADMIN_S3_ACCESS_KEY:-}"
|
||||
DO_SPACES_SECRET_KEY: "${ADMIN_S3_SECRET_KEY:-}"
|
||||
# GitHub PAT for map editor PR creation (scoped to contents:write + pull_requests:write)
|
||||
GITHUB_TOKEN: "${GITHUB_TOKEN:-}"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# No external port - accessed via nginx at admin.eagle0.net
|
||||
depends_on:
|
||||
- auth
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
# Allied Victory: Battle Resolution Flow
|
||||
|
||||
How allied victories work end-to-end, from Shardok tactical resolution through Eagle strategic
|
||||
processing to player-facing aftermath decisions.
|
||||
|
||||
## 1. Battle Setup (Eagle -> Shardok)
|
||||
|
||||
When Eagle sends a battle to Shardok via `RequestBattlesAction`, each player gets specific victory
|
||||
conditions:
|
||||
|
||||
| Player | Victory Conditions |
|
||||
|-------------|-----------------------------------------------------------------------|
|
||||
| Attacker(s) | `HoldsCriticalTiles`, `LastPlayerStanding`, `LastAllianceStanding` |
|
||||
| Defender | `LastPlayerStanding`, `WinAfterMaxRounds` |
|
||||
|
||||
This is set in `RequestBattlesAction.shardokPlayers`.
|
||||
|
||||
Alliance information is sent separately in `PlayerSetupInfo.allies`. Eagle reads each player's
|
||||
`factionRelationships` and includes all non-HOSTILE factions as allies
|
||||
(`ShardokInterfaceGrpcClient.scala:75-80`).
|
||||
|
||||
## 2. Victory Condition Evaluation (Shardok)
|
||||
|
||||
Victory conditions are checked in `UpdateGameStatusAction.cpp` in priority order:
|
||||
|
||||
### Check 1: LastPlayerStanding (any time)
|
||||
|
||||
If exactly **one** player has surviving units and has `LastPlayerStanding`:
|
||||
- That player is the sole winner.
|
||||
- This applies to solo victories only (1 survivor).
|
||||
|
||||
### Check 2: LastAllianceStanding (any time)
|
||||
|
||||
If **multiple** players survive, check if they form a winning alliance:
|
||||
- ALL survivors must have `LastAllianceStanding` condition
|
||||
- ALL survivors must be **mutually** allied (every pair checks both directions)
|
||||
- If yes: all survivors are winners
|
||||
|
||||
This triggers for AssaultProvince battles when allied attackers eliminate all defenders. Since the
|
||||
defender does **not** have `LastAllianceStanding`, the check correctly requires that only the
|
||||
allied attackers remain alive.
|
||||
|
||||
### Check 3: WinAfterMaxRounds (end-of-round only)
|
||||
|
||||
If the round counter exceeds `max_rounds`:
|
||||
- Find the player with `WinAfterMaxRounds` (always the defender)
|
||||
- That player wins, even if they have no surviving units
|
||||
- Eagle validates exactly 1 player has this condition (`internalRequire` in `RequestBattlesAction`)
|
||||
|
||||
### Check 4: HoldsCriticalTiles (end-of-round only)
|
||||
|
||||
**Single-player castle control**: If one player with `HoldsCriticalTiles` occupies ALL critical
|
||||
tiles with hero-bearing units, that player wins. Additionally, any other player who:
|
||||
- Has `HoldsCriticalTiles`
|
||||
- Is **mutually** allied with the castle holder
|
||||
|
||||
is also added to `winning_shardok_ids` as a co-winner.
|
||||
|
||||
**Allied castle control**: If multiple players collectively occupy all critical tiles with
|
||||
hero-bearing units, AND:
|
||||
- All occupants have `HoldsCriticalTiles`
|
||||
- All occupants are mutually allied
|
||||
|
||||
Then all occupants are winners together.
|
||||
|
||||
## 3. EndGameCondition Assignment Per Player (Shardok -> Eagle)
|
||||
|
||||
Shardok assigns each player a `Victory` or `Loss`. All winners receive `Victory`; there is no
|
||||
`AllyVictory` distinction. The `EndGameCondition` proto reserves fields 2 (`ally_victory`) and
|
||||
3 (`draw`) for backwards compatibility but Eagle rejects both.
|
||||
|
||||
`GameOverResponsePopulator` validates that any player allied to a winner is also in
|
||||
`winning_shardok_ids`. If an ally is missing from the winners list, it throws an internal error.
|
||||
|
||||
**Known issue**: This validation is too strict for `LastPlayerStanding`. When one allied attacker
|
||||
is the sole survivor, their dead co-attacker is allied to the winner but legitimately not in
|
||||
`winning_shardok_ids` (they're dead). The throw should be changed to `Loss` for dead allies, or
|
||||
the check should only apply when the ally has surviving units.
|
||||
|
||||
| Condition | Criterion |
|
||||
|-----------|-----------|
|
||||
| `Victory(type)` | Player is in `winning_shardok_ids` |
|
||||
| `Loss(type)` | Player is not in `winning_shardok_ids` |
|
||||
|
||||
The `type` is the VictoryCondition that caused the game to end (e.g., `HoldsCriticalTiles`).
|
||||
|
||||
### When do multiple players get Victory?
|
||||
|
||||
In standard AssaultProvince battles:
|
||||
|
||||
1. **Allied attackers jointly hold all castles** -> both get `Victory(HoldsCriticalTiles)`.
|
||||
|
||||
2. **One attacker holds all castles alone, ally has `HoldsCriticalTiles` and is mutually allied**
|
||||
-> both get `Victory(HoldsCriticalTiles)`. The ally is added to `winning_shardok_ids` by
|
||||
`UpdateGameStatusAction`.
|
||||
|
||||
3. **Allied attackers eliminate all defenders** -> both get `Victory(LastAllianceStanding)`.
|
||||
This triggers because both have `LastAllianceStanding` and are mutually allied.
|
||||
|
||||
4. **One attacker holds all castles, co-attacker is NOT allied** -> only the castle holder
|
||||
gets `Victory(HoldsCriticalTiles)`; the non-allied co-attacker gets `Loss`.
|
||||
|
||||
## 4. Eagle Processes Battle Results
|
||||
|
||||
`ResolveBattleAction.scala` receives `BattleResolution` containing each player's
|
||||
`EndGameCondition` and resolved units.
|
||||
|
||||
### Winner/Loser Partition
|
||||
|
||||
Players are split by `isVictory` (line 383-386):
|
||||
- `winningResolvedPlayers`: Victory
|
||||
- `losingResolvedPlayers`: Loss
|
||||
|
||||
### The `unitReturned` Function (line 765-788)
|
||||
|
||||
Determines which units go home vs stay at the battle province:
|
||||
|
||||
| Unit Status | Returned? | Notes |
|
||||
|---------------|-----------|-------|
|
||||
| `Fled` | Always | Sent to their army's flee province |
|
||||
| `NeverEntered`| Conditional | Only if: attacker AND has flee province AND did NOT win (`!isVictory`) |
|
||||
| `Captured` | Never | |
|
||||
| `Normal` | Never | |
|
||||
| `Retreated` | Never | |
|
||||
| `Outlawed` | Never | Becomes unaffiliated hero |
|
||||
|
||||
Key point: `NeverEntered` units from **winning** factions stay at the battle province rather than
|
||||
being sent home. This is important for bounced co-attackers who are allied with the actual winner.
|
||||
|
||||
### Withdrawn/Fled Unit Routing (line 690-706)
|
||||
|
||||
For units that ARE returned, `armyFromResolvedArmy` builds a returning army with
|
||||
`fleeProvinceId = None`. If the army has no `fleeProvinceId`, it becomes "shattered" (units
|
||||
become outlaws in the battle province). If the original army had a `fleeProvinceId` set, returned
|
||||
units are sent there as incoming armies.
|
||||
|
||||
### Battle Resolution Branching (line 397-552)
|
||||
|
||||
Three branches based on who won:
|
||||
|
||||
#### Branch A: Defender Won
|
||||
|
||||
Triggered when: `winningShardokPlayers.exists(_.isDefender)`
|
||||
|
||||
- Executes `ProvinceHeldAction`
|
||||
- All non-defender, non-returned, non-outlawed units are **captured**
|
||||
- Province stays with the defending faction
|
||||
|
||||
#### Branch B: Single Attacker Won
|
||||
|
||||
Triggered when: exactly 1 attacking winner
|
||||
|
||||
- Executes `ProvinceConqueredAction` immediately
|
||||
- Winning attacker's non-fled units occupy the province
|
||||
- Losing defenders' non-fled units are captured
|
||||
- Province transfers to the attacker
|
||||
|
||||
#### Branch C: Multiple Allied Attackers Won
|
||||
|
||||
Triggered when: 2+ attacking winners
|
||||
|
||||
- Executes `MultiVictorBattleSetupAction`
|
||||
- Creates `PendingConquestInfo` with aftermath claimants
|
||||
- Province enters the **Battle Aftermath** phase
|
||||
|
||||
## 5. Battle Aftermath (Multi-Victor Only)
|
||||
|
||||
When multiple allied attackers win, the province enters an aftermath decision phase where players
|
||||
decide who keeps it.
|
||||
|
||||
### Claimant Setup
|
||||
|
||||
Each attacking winner becomes an `AftermathClaimant` with:
|
||||
- `units`: their non-fled, non-outlawed units still at the battle province
|
||||
- `armySize`: total troop count of those units
|
||||
- `broughtGold` / `broughtFood`: supplies their armies carried
|
||||
|
||||
Claimants are **sorted by army size descending** (largest army decides first).
|
||||
|
||||
### Decision Phase
|
||||
|
||||
Claimants are presented with a choice **one at a time**, in order:
|
||||
|
||||
**If no one has chosen "Keep" yet:**
|
||||
- **Keep Province**: claim the province for your faction
|
||||
- **Withdraw To**: pick an adjacent province to march your army to, optionally carrying up to
|
||||
the gold/food you brought
|
||||
|
||||
**If someone already chose "Keep":**
|
||||
- **Withdraw To** is the only option (can't have two keepers)
|
||||
|
||||
**Last undecided claimant:**
|
||||
- If no one has chosen "Keep" yet, the last claimant **automatically keeps** (no command shown,
|
||||
resolved by `AutoResolveBattleAftermathAction`)
|
||||
- This guarantees someone always claims the province
|
||||
|
||||
### Finalization
|
||||
|
||||
Once all claimants have decided (`FinalizeAftermathAction`):
|
||||
|
||||
1. The keeper's units run through `ProvinceConqueredAction` -- province transfers to their faction
|
||||
2. Withdrawing claimants' units become incoming armies to their chosen adjacent province,
|
||||
arriving next round, carrying the specified supplies
|
||||
3. `PendingConquestInfo` is cleared from the province
|
||||
|
||||
## 6. Summary: Who Gets What
|
||||
|
||||
### Two allied attackers jointly hold all castles
|
||||
- Shardok: Both get `Victory(HoldsCriticalTiles)` (both in `winning_shardok_ids`)
|
||||
- Eagle: Both are attacking winners -> MultiVictorBattleSetupAction
|
||||
- Players: Largest army picks first: Keep or Withdraw. Last player auto-keeps if no one chose Keep.
|
||||
|
||||
### One attacker holds all castles, allied attacker doesn't
|
||||
- Shardok: Castle-holder gets `Victory(HoldsCriticalTiles)`. Mutually-allied co-attacker with
|
||||
`HoldsCriticalTiles` is added to `winning_shardok_ids` -> also gets `Victory(HoldsCriticalTiles)`.
|
||||
- Eagle: Both are attacking winners -> MultiVictorBattleSetupAction -> Battle Aftermath.
|
||||
- Players: Largest army picks first: Keep or Withdraw.
|
||||
|
||||
### Allied attackers eliminate all defenders
|
||||
- Shardok: Both attackers have `LastAllianceStanding` and are mutually allied -> both get
|
||||
`Victory(LastAllianceStanding)`.
|
||||
- Eagle: Both are attacking winners -> MultiVictorBattleSetupAction -> Battle Aftermath.
|
||||
- Players: Largest army picks first: Keep or Withdraw.
|
||||
|
||||
### Non-allied co-attackers: one holds all castles
|
||||
- Shardok: Castle-holder gets `Victory(HoldsCriticalTiles)`, non-allied co-attacker gets
|
||||
`Loss(HoldsCriticalTiles)`.
|
||||
- Eagle: Single attacker won -> ProvinceConqueredAction. Non-allied co-attacker's units are
|
||||
captured along with the defender's.
|
||||
|
||||
### One allied attacker is sole survivor (LastPlayerStanding)
|
||||
- Shardok: Sole survivor gets `Victory(LastPlayerStanding)`. Dead ally should get `Loss`.
|
||||
- **BUG**: `GameOverResponsePopulator` currently throws because dead ally is allied to winner
|
||||
but not in `winning_shardok_ids`. Needs fix — see known issue in Section 3.
|
||||
- Eagle (once fixed): Single attacker won -> ProvinceConqueredAction. Dead ally's units were
|
||||
already destroyed in battle.
|
||||
|
||||
### Single attacker wins (any condition, no co-attackers)
|
||||
- No alliance considerations. ProvinceConqueredAction immediately.
|
||||
|
||||
### Defender wins (WinAfterMaxRounds or LastPlayerStanding)
|
||||
- ProvinceHeldAction. All non-returned attacker units are captured.
|
||||
|
||||
## 7. Edge Cases and Known Issues
|
||||
|
||||
### BUG: Dead ally causes crash via LastPlayerStanding
|
||||
|
||||
When one allied attacker is the sole survivor (both the co-attacker and defender are dead),
|
||||
`LastPlayerStanding` fires and only the survivor is in `winning_shardok_ids`. The
|
||||
`GameOverResponsePopulator` then sees the dead co-attacker is allied to the winner but not in
|
||||
`winning_shardok_ids`, and throws `ShardokInternalErrorException`. Fix: dead allies should
|
||||
receive `Loss`, not trigger a validation error.
|
||||
|
||||
### Non-allied co-attackers eliminate all defenders
|
||||
|
||||
Two non-allied attackers who both survive after eliminating the defender cannot win via
|
||||
`LastAllianceStanding` (they aren't mutually allied). The game continues until max rounds,
|
||||
at which point the defender wins via `WinAfterMaxRounds`. They must capture all critical tiles.
|
||||
|
||||
### Non-allied co-attackers: one holds all castles
|
||||
|
||||
Only the castle holder wins. The non-allied co-attacker gets `Loss` and their units are captured
|
||||
along with the defender's. The mutual-alliance check in `UpdateGameStatusAction` prevents
|
||||
non-allied players from being added to `winning_shardok_ids`.
|
||||
|
||||
## Key File References
|
||||
|
||||
| Component | File |
|
||||
|-----------|------|
|
||||
| Victory condition checking | `src/main/cpp/net/eagle0/shardok/library/actions/UpdateGameStatusAction.cpp` |
|
||||
| Per-player EndGameCondition | `src/main/cpp/net/eagle0/shardok/server/GameOverResponsePopulator.cpp` |
|
||||
| Battle setup (victory conditions) | `src/main/scala/.../library/actions/impl/action/RequestBattlesAction.scala` |
|
||||
| Alliance communication | `src/main/scala/.../shardok_interface/ShardokInterfaceGrpcClient.scala:75-80` |
|
||||
| Battle result processing | `src/main/scala/.../library/actions/impl/action/ResolveBattleAction.scala` |
|
||||
| Unit return logic | `ResolveBattleAction.scala:765-788` (`unitReturned`) |
|
||||
| Multi-victor setup | `src/main/scala/.../library/actions/impl/action/MultiVictorBattleSetupAction.scala` |
|
||||
| Aftermath availability | `src/main/scala/.../library/actions/availability/AvailableBattleAftermathDecisionCommandFactory.scala` |
|
||||
| Aftermath command | `src/main/scala/.../library/actions/impl/command/BattleAftermathDecisionCommand.scala` |
|
||||
| Auto-resolve last claimant | `src/main/scala/.../library/actions/impl/action/AutoResolveBattleAftermathAction.scala` |
|
||||
| Finalize aftermath | `src/main/scala/.../library/actions/impl/action/FinalizeAftermathAction.scala` |
|
||||
| EndGameCondition enum | `src/main/scala/.../model/state/shardok_battle/EndGameCondition.scala` |
|
||||
+1
-25
@@ -12,7 +12,7 @@ This document catalogs all media assets in the Unity project for licensing revie
|
||||
|----------|-------|-------|
|
||||
| Images | 10,637 | Mostly PNG icons and UI sprites |
|
||||
| Audio | 1,778 | 26 music tracks + 1,752 sound effects |
|
||||
| 3D Models | 66+ | Bridge pack, Animal pack deluxe, Honey Badger |
|
||||
| 3D Models | 52 | Bridge pack only |
|
||||
| Fonts | 16 | TTF files |
|
||||
|
||||
---
|
||||
@@ -55,30 +55,6 @@ These are commercial Unity Asset Store purchases tied to your account:
|
||||
- **Contents:** Bridge construction pieces
|
||||
- **License:** Unity Asset Store
|
||||
|
||||
### Animal pack deluxe
|
||||
- **Location:** `Assets/Animal pack deluxe/`
|
||||
- **Publisher:** janpec
|
||||
- **Asset Store Link:** https://assetstore.unity.com/packages/3d/characters/animals/animal-pack-deluxe-99702
|
||||
- **Contents:** 26 rigged and animated 3D animal models (bear, boar, crocodile, wolf, frog, crab, rabbit, scorpion, snake, stag, deer, rat, goat, pig, etc.) with idle, walk, run, eat, attack, and die animations
|
||||
- **Used for:** Beast province effects on the strategic map (AnimalEffect)
|
||||
- **License:** Unity Asset Store
|
||||
|
||||
### 2D Monster Pack: Basic Bundle (+PSB)
|
||||
- **Location:** `Assets/DungeonMonsters2D/`
|
||||
- **Publisher:** SP1
|
||||
- **Asset Store Link:** https://assetstore.unity.com/packages/2d/characters/2d-monster-pack-basic-bundle-psb-328637
|
||||
- **Contents:** 2D animated monster sprites with PSB (Photoshop) source files. Characters: SkeletonWarrior, SkeletonArcher, SkeletonMage, Zombie, ZombieWarrior, Demon, Succubus, Imp, Spider, Rat, Vampire, DragonRed, Butcher
|
||||
- **Used for:** Beast province effects (MonsterEffect, DragonEffect) and human-type beast effects on the strategic map
|
||||
- **License:** Unity Asset Store
|
||||
|
||||
### Honey Badger 3D Model
|
||||
- **Location:** `Assets/HoneyBadger/`
|
||||
- **Creator:** WildMesh 3D (@WildMesh_3D)
|
||||
- **Source:** https://sketchfab.com/3d-models/realistic-honey-badger-3d-model-09751ed5383c4afb924cfb6485d313f1
|
||||
- **Contents:** Rigged 3D honey badger model (FBX) with 63 animations and PBR texture
|
||||
- **Used for:** Honey badger beast province effect (AnimalEffect)
|
||||
- **License:** Purchased via Sketchfab/Patreon
|
||||
|
||||
### Fantasy Interface Sounds
|
||||
- **Location:** `Assets/Fantasy Interface Sounds/`
|
||||
- **Count:** 320 WAV files
|
||||
|
||||
@@ -1,162 +0,0 @@
|
||||
# Adding Beast-Type-Specific Effects
|
||||
|
||||
When a province has a `BeastsEvent`, `ProvinceBeastsController` spawns a visual effect at the province centroid. By default this is the generic `BeastsEffect` (circling vultures/crows). Beast-specific effects can override this based on `BeastInfo.SingularName`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
ProvinceBeastsController
|
||||
├── GetBeastName(province) → reads BeastsEvent.BeastInfo.SingularName
|
||||
├── GetEffectPrefab(beastName) → switch on name → returns prefab
|
||||
└── SpawnBeastsEffect(provinceId, beastName) → instantiates at centroid
|
||||
```
|
||||
|
||||
**Key files:**
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Assets/Eagle/ProvinceBeastsController.cs` | Routes beast names to prefabs, positions effects |
|
||||
| `Assets/Eagle/BeastsEffect.cs` | Generic effect (circling birds via particle system) |
|
||||
| `Assets/Eagle/DragonEffect.cs` | Dragon-specific effect (3D model with flying/ground modes) |
|
||||
| `Assets/Eagle/MonsterEffect.cs` | Wandering 2D monster group (DungeonMonsters2D prefabs) |
|
||||
| `Assets/Eagle/AnimalEffect.cs` | Wandering 3D animal group (Animal Pack Deluxe prefabs) |
|
||||
| `Assets/Eagle/Effects/` | All effect prefabs |
|
||||
|
||||
## Adding a new beast type
|
||||
|
||||
### 1. Create the effect MonoBehaviour (or reuse an existing one)
|
||||
|
||||
Three effect scripts exist for different asset types:
|
||||
|
||||
**MonsterEffect** (2D sprites, DungeonMonsters2D): Spawns a group of wandering 2D monsters. Uses PascalCase animator states (`Idle`, `Move`) and triggers (`AttackTrigger`, `SpecialATrigger`). Flips sprite X scale for facing direction. Best for DungeonMonsters2D prefabs.
|
||||
|
||||
**AnimalEffect** (3D models, Animal Pack Deluxe): Spawns a group of wandering 3D animals. Uses lowercase animator states (`idle`, `walk`, `eat`, `attack`) with `Animator.Play()`. Rotates model on Y axis for facing. Sets `sortingOrder=103`, `renderQueue=4000`, and `ZTest=Always` to render above the map. Best for Animal Pack Deluxe prefabs.
|
||||
|
||||
**DragonEffect** (single animated 3D creature): Spawns a 3D dragon that alternates between flying (circling at altitude with fly actions) and grounded (wandering with ground actions) modes. Uses `Animator.Play()` with DragonMapAnims controller. Best for large creatures with flight capabilities.
|
||||
|
||||
**Particle-based** (like `BeastsEffect.cs`): Good for simple effects like swarms or atmospheric particles.
|
||||
|
||||
To create a new effect script, use `[RequireComponent(typeof(RectTransform))]` for UI canvas positioning and accept tuning parameters as public fields.
|
||||
|
||||
### 2. Create the prefab
|
||||
|
||||
1. Create an empty GameObject in `Assets/Eagle/Effects/`
|
||||
2. Add a `RectTransform` component
|
||||
3. Add your effect MonoBehaviour
|
||||
4. Wire up any references (animated prefab, textures, materials)
|
||||
5. Save as prefab
|
||||
|
||||
### 3. Register in ProvinceBeastsController
|
||||
|
||||
Add a prefab field and a case in `GetEffectPrefab()`:
|
||||
|
||||
```csharp
|
||||
// In ProvinceBeastsController.cs
|
||||
|
||||
// Add inspector field alongside existing ones:
|
||||
public GameObject wolfEffectPrefab;
|
||||
|
||||
// Add case in GetEffectPrefab(), keyed by singularName from beasts.tsv:
|
||||
private GameObject GetEffectPrefab(string beastName) {
|
||||
switch (beastName) {
|
||||
case "dragon": return dragonEffectPrefab;
|
||||
case "wolf": return wolfEffectPrefab;
|
||||
default: return beastsEffectPrefab;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Wire up in the scene
|
||||
|
||||
In `Gameplay.unity`, find the `ProvinceBeastsController` component and assign your new prefab to the new field.
|
||||
|
||||
### 5. Test
|
||||
|
||||
Use `[ContextMenu]` methods on `ProvinceBeastsController` to test:
|
||||
- "Test Dragon in Motcia (ID 2)" spawns a dragon effect
|
||||
- Add similar methods for your beast type
|
||||
- "Clear All Effects" removes all active effects
|
||||
|
||||
## Beast name matching
|
||||
|
||||
Beast names come from `BeastInfo.SingularName` in the proto, which uses the canonical `singularName` from `src/main/resources/net/eagle0/eagle/beasts.tsv`. The switch in `GetEffectPrefab` uses exact string matching on this canonical name (e.g. `"dragon"`, `"wolf"`, `"skeleton"`).
|
||||
|
||||
Human-type beasts (pirates, bandits, etc.) are matched via a `HumanBeastNames` HashSet in `ProvinceBeastsController` rather than individual switch cases.
|
||||
|
||||
## Beast animation coverage
|
||||
|
||||
### Custom animations
|
||||
|
||||
| Beast | Effect Type | Asset Pack | Prefabs Used |
|
||||
|---|---|---|---|
|
||||
| dragon | DragonEffect | 3dFoin Dragon | dragon_skin.FBX (w/ DragonMapAnims controller) |
|
||||
| skeleton | MonsterEffect | DungeonMonsters2D | SkeletonWarrior, SkeletonArcher, SkeletonMage |
|
||||
| zombie | MonsterEffect | DungeonMonsters2D | Zombie, ZombieWarrior |
|
||||
| rat | MonsterEffect + AnimalEffect | DungeonMonsters2D + Animal Pack Deluxe | Rat (2D), Rat (3D) |
|
||||
| spider | MonsterEffect | DungeonMonsters2D | Spider |
|
||||
| demon | MonsterEffect | DungeonMonsters2D | Demon, Succubus |
|
||||
| orc | MonsterEffect | DungeonMonsters2D | Imp |
|
||||
| vampire | MonsterEffect | DungeonMonsters2D | Vampire |
|
||||
| knight types (6 names) | AnimalEffect | Polytope Studio | PT_Male_Knight_01/02, PT_Female_Knight_01/02 (w/ HumanMapAnims controller) |
|
||||
| soldier types (8 names) | AnimalEffect | Polytope Studio | PT_Male_Soldier_01/02, PT_Female_Soldier_01/02 (w/ HumanMapAnims controller) |
|
||||
| archer types (3 names) | AnimalEffect | Polytope Studio | PT_Male_Archer_01/02, PT_Female_Archer_01/02 (w/ HumanMapAnims controller) |
|
||||
| militia types (8 names) | AnimalEffect | Polytope Studio | PT_Male_Militia_01/02, PT_Female_Militia_01/02 (w/ HumanMapAnims controller) |
|
||||
| peasant types (9 names) | AnimalEffect | Polytope Studio | PT_Male_Peasant_01, PT_Female_Peasant_01_a (w/ HumanMapAnims controller) |
|
||||
| clown | AnimalEffect | Clown Pack | Clown, Fat Clown (w/ per-prefab controller overrides) |
|
||||
| giant | AnimalEffect | Polytope Studio | PT_Male/Female_Peasant at 3x scale (w/ HumanMapAnims controller) |
|
||||
| ogre, troll | AnimalEffect | Orc-Ogre Pack | OrcOgre_Animated (w/ OgreMapAnims controller) |
|
||||
| bear | AnimalEffect | Animal Pack Deluxe | Brown_bear |
|
||||
| boar | AnimalEffect | Animal Pack Deluxe | Wild_boar |
|
||||
| crocodile | AnimalEffect | Animal Pack Deluxe | Crocodile |
|
||||
| snake | AnimalEffect | Animal Pack Deluxe | Viper |
|
||||
| crab | AnimalEffect | Animal Pack Deluxe | Crab |
|
||||
| frog | AnimalEffect | Animal Pack Deluxe | Common_frog, Common_frog_v2, Common_frog_v3 |
|
||||
| rabbit | AnimalEffect | Animal Pack Deluxe | Wild_rabbit, Wild_rabbit_v2 |
|
||||
| salamander | AnimalEffect | Animal Pack Deluxe | Fire_salamander |
|
||||
| scorpion | AnimalEffect | Animal Pack Deluxe | Scorpion, Yellow_fattail_scorpion |
|
||||
| stag | AnimalEffect | Animal Pack Deluxe | Deer, Deer_male |
|
||||
| wild goat | AnimalEffect | Animal Pack Deluxe | Goat, Ibex |
|
||||
| wild pig | AnimalEffect | Animal Pack Deluxe | Iron_age_pig, Iron_age_pig_v2 |
|
||||
| wolf, wolverine | AnimalEffect | Animal Pack Deluxe | Wolf |
|
||||
| honey badger | AnimalEffect | HoneyBadger (Sketchfab) | HoneyBadger (w/ HoneyBadgerMapAnims controller) |
|
||||
| raccoon | AnimalEffect | Raccoon (Sketchfab) | Raccoon (w/ RaccoonMapAnims controller) |
|
||||
| tiger | AnimalEffect | ithappy Animals FREE | Tiger_001 (w/ TigerMapAnims controller) |
|
||||
| elephant | AnimalEffect | Animal Pack Deluxe v2 | African_elephant, Indian_elephant (w/ ElephantMapAnims controller) |
|
||||
| mammoth | AnimalEffect | Animal Pack Deluxe v2 | African_elephant, Indian_elephant (w/ ElephantMapAnims controller) |
|
||||
|
||||
### Human-type beasts
|
||||
|
||||
34 human-type beast names use 3D Polytope Studio characters grouped into 5 archetypes. Each archetype uses a shared `HumanMapAnims.controller` that retargets Clown animation clips (idle, walk, attack, damage) via Unity's Humanoid system. One additional human type (psychopath) uses the 2D Butcher sprite from DungeonMonsters2D.
|
||||
|
||||
| Archetype | Polytope Type | Beast Names (count) |
|
||||
|---|---|---|
|
||||
| **Knight** (heavy armor) | Knight M/F x2 | cultist, heretic, cannibal, terrorist, cossack, desperado (6) |
|
||||
| **Soldier** (medium armor) | Soldier M/F x2 | bandit, brigand, robber, highwayman, marauder, dacoit, freebooter, mobster (8) |
|
||||
| **Archer** (light armor, ranged) | Archer M/F x2 | thief, pirate, buccaneer (3) |
|
||||
| **Militia** (light armed) | Militia M/F x2 | hooligan, street tough, ruffian, scalawag, ne'er-do-well, crook, miscreant, asshole (8) |
|
||||
| **Peasant** (unarmed) | Peasant M/F | agitator, instigator, rabble-rouser, separatist, particularist, nihilist, proud boy, rebel, traitor (9) |
|
||||
|
||||
Asset packs: Polytope Studio - Lowpoly Medieval Characters (Soldiers, Knights, Militia, Archers, Peasants)
|
||||
|
||||
| **Butcher** (armed loner) | DungeonMonsters2D Butcher | psychopath (1) |
|
||||
|
||||
### No custom animation (falls back to generic vultures)
|
||||
|
||||
Ordered by spawn likelihood (most common first):
|
||||
|
||||
| Beast | Likelihood | Notes |
|
||||
|---|---|---|
|
||||
| hippogryph | 0.04 | Fantasy creature |
|
||||
| hippopotamus | 0.04 | Large animal |
|
||||
| lion | 0.04 | Big cat |
|
||||
| chimpanzee | 0.04 | Primate |
|
||||
| velociraptor | 0.02 | Dinosaur |
|
||||
| unknown | 0.00 | Intentional fallback |
|
||||
|
||||
## Tips
|
||||
|
||||
- Keep effects lightweight; multiple provinces may have beasts simultaneously
|
||||
- Use `ParticleSystemScalingMode.Hierarchy` so effects scale with the map zoom
|
||||
- Set appropriate `sortingOrder` on renderers (existing effects use 102-104)
|
||||
- For animated prefabs, set `AnimatorCullingMode.AlwaysAnimate` since UI elements may be considered offscreen by the culling system
|
||||
- For 3D models (AnimalEffect), set `renderQueue=4000` and `ZTest=Always` on materials to prevent clipping behind the map
|
||||
@@ -1,218 +0,0 @@
|
||||
# Programmatic Map Image Generation
|
||||
|
||||
## Overview
|
||||
|
||||
Create a tool to programmatically regenerate the Eagle strategic map images with more equalized province sizes while preserving neighbor relationships. Also implement dynamic text rendering for province names that scales with zoom.
|
||||
|
||||
## Current Map Assets
|
||||
|
||||
| File | Description | Dimensions |
|
||||
|------|-------------|------------|
|
||||
| `Assets/Eagle/rawGray.gz.bytes` | Province ID lookup map (gzip compressed) | 3786 x 1834 |
|
||||
| `Assets/Eagle/map_bw_labels.png` | B&W map with baked-in province labels | 3786 x 1834, RGBA |
|
||||
| `Assets/Eagle/Materials/1.png` - `43.png` | Individual province mask images | 3786 x 1834, grayscale |
|
||||
|
||||
## Province Data
|
||||
|
||||
- 43 provinces defined in `src/main/resources/net/eagle0/eagle/province_map.tsv`
|
||||
- Each has: id, name, neighbors, neighborPositions, hexMapName
|
||||
- Problem provinces (too small): **Shumal** (id=1), **Kojaria** (id=39), **Usvol** (id=33), **Tumala** (id=35), **Chia** (id=43)
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Equalize province sizes** - Reduce disparity so small provinces are easier to click
|
||||
2. **Preserve topology** - Maintain all neighbor relationships
|
||||
3. **Preserve coastline** - Keep ocean/land boundaries
|
||||
4. **Dynamic text rendering** - Province names scale with zoom level
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Province Size Equalization Tool
|
||||
|
||||
### Algorithm: Constrained Voronoi Relaxation
|
||||
|
||||
1. **Extract current state from rawGray:**
|
||||
- Calculate centroid of each province
|
||||
- Calculate area (pixel count) of each province
|
||||
- Identify ocean pixels (value 0 or 255)
|
||||
|
||||
2. **Iterative relaxation:**
|
||||
```
|
||||
For each iteration:
|
||||
For each province:
|
||||
Calculate target area (average of all provinces)
|
||||
If province is too small:
|
||||
Move centroid away from larger neighbors
|
||||
If province is too large:
|
||||
Move centroid toward smaller neighbors
|
||||
Constraint: Don't break neighbor relationships
|
||||
```
|
||||
|
||||
3. **Generate new boundaries:**
|
||||
- Use weighted Voronoi diagram from relaxed centroids
|
||||
- Weight by target area
|
||||
- Mask with original coastline
|
||||
|
||||
4. **Output new images:**
|
||||
- New rawGray (province ID map)
|
||||
- New province masks (1.png - 43.png)
|
||||
- New B&W base map (derived from boundaries)
|
||||
|
||||
### Implementation Options
|
||||
|
||||
**Option A: Python Script (Recommended)**
|
||||
- Use numpy, scipy, PIL/Pillow
|
||||
- Can run offline, generate assets once
|
||||
- Easier to iterate and debug
|
||||
- Output: New image files to replace existing
|
||||
|
||||
**Option B: Unity Editor Script**
|
||||
- C# with Unity's Texture2D
|
||||
- Integrated into build process
|
||||
- Slower, harder to debug
|
||||
|
||||
### Tool Location
|
||||
`tools/map_generator/` or `scripts/generate_map.py`
|
||||
|
||||
---
|
||||
|
||||
## Part 2: Dynamic Province Name Rendering
|
||||
|
||||
### Current State
|
||||
- Province names are baked into `map_bw_labels.png`
|
||||
- Not visible when zoomed out, too small when zoomed in
|
||||
- `SetUpCenterText()` in MapController shows selected/hovered province name in a fixed UI panel
|
||||
|
||||
### New Approach
|
||||
|
||||
1. **Create province label GameObjects:**
|
||||
- One TextMeshPro label per province
|
||||
- Position at province centroid
|
||||
- Child of map content (moves with zoom/pan)
|
||||
|
||||
2. **Dynamic scaling:**
|
||||
- Scale inversely with zoom level
|
||||
- At zoom 1.0: Show major provinces only (or none)
|
||||
- At zoom 2.0+: Show all province names
|
||||
- Font size adjusts to fit province area
|
||||
|
||||
3. **Implementation:**
|
||||
```csharp
|
||||
public class ProvinceLabelsController : MonoBehaviour {
|
||||
public MapPinchZoomHandler zoomHandler;
|
||||
public TextMeshProUGUI labelPrefab;
|
||||
|
||||
private Dictionary<int, TextMeshProUGUI> _labels;
|
||||
|
||||
void Update() {
|
||||
float zoom = zoomHandler.CurrentZoom;
|
||||
foreach (var label in _labels.Values) {
|
||||
label.transform.localScale = Vector3.one / zoom;
|
||||
label.gameObject.SetActive(zoom >= 1.5f);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
4. **Province centroid data:**
|
||||
- Add `centroid_x`, `centroid_y` columns to `province_map.tsv`
|
||||
- Or calculate at runtime from rawGray
|
||||
|
||||
---
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
### New Files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `tools/map_generator/generate_map.py` | Python script to generate new map images |
|
||||
| `tools/map_generator/requirements.txt` | Python dependencies |
|
||||
| `Assets/Eagle/ProvinceLabelsController.cs` | Dynamic province name rendering |
|
||||
|
||||
### Modified Files
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `Assets/Eagle/rawGray.gz.bytes` | Replaced with equalized version |
|
||||
| `Assets/Eagle/Materials/*.png` | Replaced province masks |
|
||||
| `Assets/Eagle/map_bw_labels.png` | Either: regenerated, or removed if using dynamic labels |
|
||||
| `province_map.tsv` | Add centroid columns |
|
||||
| `Assets/Gameplay.unity` | Add ProvinceLabelsController, label prefab instances |
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
1. **Visual inspection:**
|
||||
- Compare old vs new province sizes
|
||||
- Verify small provinces (Shumal, Kojaria, Usvol) are larger
|
||||
- Verify neighbor relationships preserved
|
||||
|
||||
2. **Functional testing:**
|
||||
- Click on each province at various zoom levels
|
||||
- Verify correct province is selected
|
||||
- Verify province colors/weather still work
|
||||
|
||||
3. **Label testing:**
|
||||
- Zoom in/out, verify labels scale appropriately
|
||||
- Pan around, verify labels move with map
|
||||
- Verify labels don't overlap excessively
|
||||
|
||||
---
|
||||
|
||||
## Design Decisions
|
||||
|
||||
1. **Equalization approach:** Reduce disparity only
|
||||
- Make small provinces (Shumal, Kojaria, Usvol, etc.) 2-3x larger
|
||||
- Minimal changes to large provinces
|
||||
- Not targeting perfectly equal areas
|
||||
|
||||
2. **Coastline:** Allow simplification
|
||||
- Coastline can be smoothed/simplified as part of generation
|
||||
- Voronoi edges are acceptable
|
||||
|
||||
3. **Map style:** Solid fills with borders
|
||||
- Clean, simple look
|
||||
- Easier to generate programmatically
|
||||
- Province boundaries rendered as dark lines
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### Step 1: Python Map Generator Tool
|
||||
|
||||
```
|
||||
tools/map_generator/
|
||||
├── generate_map.py # Main script
|
||||
├── requirements.txt # numpy, scipy, Pillow
|
||||
└── README.md # Usage instructions
|
||||
```
|
||||
|
||||
**Algorithm:**
|
||||
1. Load current rawGray.gz.bytes, decompress
|
||||
2. Calculate each province's centroid and area
|
||||
3. Identify land mask (non-ocean pixels)
|
||||
4. Run constrained Voronoi relaxation:
|
||||
- Small provinces push neighbors away
|
||||
- Large provinces pull neighbors in
|
||||
- Stop when min province is >= 50% of average
|
||||
5. Generate weighted Voronoi from relaxed centroids
|
||||
6. Output:
|
||||
- `rawGray_new.bytes` (province ID map)
|
||||
- `1.png` - `43.png` (province masks)
|
||||
- `map_bw.png` (solid fill + borders)
|
||||
- `centroids.json` (for label positioning)
|
||||
|
||||
### Step 2: Dynamic Province Labels (Unity)
|
||||
|
||||
1. Create `ProvinceLabelsController.cs`
|
||||
2. Load centroids from JSON or calculate from rawGray
|
||||
3. Spawn TextMeshPro labels at each centroid
|
||||
4. Update label scale/visibility based on zoom
|
||||
|
||||
### Step 3: Integration
|
||||
|
||||
1. Replace asset files with generated versions
|
||||
2. Update `MapController` to use new assets
|
||||
3. Add `ProvinceLabelsController` to scene
|
||||
4. Remove baked label image dependency
|
||||
@@ -1,79 +0,0 @@
|
||||
# Renaming a Province
|
||||
|
||||
This document explains the steps required to rename a province in Eagle0.
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### 1. Province Map TSV (Server Source of Truth)
|
||||
|
||||
**File:** `src/main/resources/net/eagle0/eagle/province_map.tsv`
|
||||
|
||||
This is the primary source of province data. Each row contains:
|
||||
- Province ID
|
||||
- Province name
|
||||
- Neighbor IDs
|
||||
- Neighbor directions
|
||||
- Province name (repeated)
|
||||
- Neighbor names (dot-separated)
|
||||
- Starting food
|
||||
|
||||
You need to:
|
||||
1. Change the province name in its own row (columns 2 and 5)
|
||||
2. Update the neighbor name references in all neighboring provinces (column 6)
|
||||
|
||||
Example: To rename "Fluria" to "NewName", you would need to update:
|
||||
- Line 26: The Fluria row itself
|
||||
- Lines 5, 10, 18, 24, 27, 38, 41: All provinces that list Fluria as a neighbor
|
||||
|
||||
### 2. LLM Map Description
|
||||
|
||||
**File:** `src/main/scala/net/eagle0/eagle/library/actions/llm_prompt_generators/MapDescription.scala`
|
||||
|
||||
This file contains a hardcoded geographic description of the map used in LLM prompts for generating narrative text. Update any references to the old province name.
|
||||
|
||||
### 3. Client Centroids JSON
|
||||
|
||||
**File:** `src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Eagle/centroids.json`
|
||||
|
||||
This JSON file contains province metadata used for map rendering (centroid positions, areas, orientations). Each entry has a `name` field that should be updated to match.
|
||||
|
||||
Note: The actual province name displayed to players comes from the server via the `ProvinceView` proto, not from this file. However, this file should be kept in sync for consistency and because it may be used for map label positioning.
|
||||
|
||||
### 4. Test Files
|
||||
|
||||
**File:** `src/test/scala/net/eagle0/eagle/library/util/validations/RuntimeValidatorTest.scala`
|
||||
|
||||
Update any test strings that reference the old province name.
|
||||
|
||||
## How Province Names Flow to the Client
|
||||
|
||||
Province names are sent from the Eagle server to the Unity client via the `ProvinceView` protobuf message:
|
||||
|
||||
```protobuf
|
||||
message ProvinceView {
|
||||
int32 id = 1;
|
||||
string name = 6; // <-- Province name sent from server
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
The server reads province names from `province_map.tsv` at startup. The client receives names through the proto and displays them via `province.Name` in C# code. This means:
|
||||
|
||||
- Changing the TSV automatically updates what clients see
|
||||
- No C# code changes are needed (the code uses `province.Name` generically)
|
||||
- The `centroids.json` name field is for map rendering/positioning, not display
|
||||
|
||||
## Files That Do NOT Need Changes
|
||||
|
||||
- **Hero backstory TSV files** (`heroes.tsv`, `generated_heroes.tsv`) - These don't contain province name references
|
||||
- **C# client code** - Uses `province.Name` from the server proto, not hardcoded names
|
||||
- **Proto definitions** - No province names are hardcoded in protos
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Update `province_map.tsv` - province's own row
|
||||
- [ ] Update `province_map.tsv` - all neighbor references
|
||||
- [ ] Update `MapDescription.scala`
|
||||
- [ ] Update `centroids.json`
|
||||
- [ ] Update any test files with province name references
|
||||
- [ ] Build and run tests: `bazel test //src/test/scala/...`
|
||||
@@ -77,4 +77,3 @@ Larger set of goals in [The Big Eagle TODO](https://docs.google.com/document/d/1
|
||||
## Nice to have
|
||||
|
||||
- [x] "What's new" changelog (fetch JSON, show entries since last launch)
|
||||
- [ ] Windows code signing ([Azure Artifact Signing](https://azure.microsoft.com/en-us/products/artifact-signing/), ~$10/mo) to eliminate SmartScreen "unknown publisher" warning
|
||||
|
||||
@@ -19,7 +19,7 @@ When a new player starts their first game in tutorial mode, they experience:
|
||||
## Battle Configuration
|
||||
|
||||
### Defender (Player - John Ranil)
|
||||
- **Heroes**: John Ranil + 2 random vassals (3 total)
|
||||
- **Heroes**: John Ranil + 2 random sworn brothers (3 total)
|
||||
- **Units**:
|
||||
- 2x Light Infantry (300 troops each, 60 training/armament)
|
||||
- 1x Longbowmen (200 troops, 60 training/armament)
|
||||
@@ -29,7 +29,7 @@ When a new player starts their first game in tutorial mode, they experience:
|
||||
### Attacker (Ikhaan Tarn)
|
||||
- **Heroes**: Ikhaan Tarn + 2 sworn brothers (3 total)
|
||||
- **Units**:
|
||||
- 1x Heavy Cavalry (600 troops, 80 training/armament)
|
||||
- 1x Heavy Cavalry (400 troops, 80 training/armament)
|
||||
- 1x Heavy Infantry (500 troops, 80 training/armament)
|
||||
- 1x Longbowmen (300 troops, 80 training/armament)
|
||||
- **Origin Province**: 32
|
||||
@@ -88,16 +88,19 @@ Screen 3: "Defend Your Home" - Call to action
|
||||
|
||||
**Key Methods:**
|
||||
```cpp
|
||||
// Check all events and execute triggered actions (called at end of each round)
|
||||
std::vector<ActionResult> CheckAndExecuteEvents(
|
||||
const GameStateW& state,
|
||||
const SettingsGetter& settings,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator);
|
||||
// Check if attacker should flee
|
||||
bool ShouldTriggerScriptedFlee(const GameStateW& state);
|
||||
|
||||
// Check if player (defender) can flee
|
||||
bool PlayerCanFlee(PlayerId playerId);
|
||||
|
||||
// Execute the scripted flee (100% success rate)
|
||||
std::vector<ActionResult> ExecuteScriptedFlee(...);
|
||||
```
|
||||
|
||||
**Integration:**
|
||||
- `ShardokEngine.cpp`: Calls `CheckAndExecuteEvents()` at end of each round in `HandlePlayerTurnEnd()`
|
||||
- Events fire in config order, each event fires at most once
|
||||
**Integration Points (TODO):**
|
||||
- `ShardokEngine.cpp`: Check `ShouldTriggerScriptedFlee()` during turn processing
|
||||
- `FleeCommandFactory.cpp`: Disable flee commands for defender in tutorial mode
|
||||
|
||||
### Phase 4: Mid-Battle Reinforcements
|
||||
|
||||
@@ -129,33 +132,6 @@ std::vector<ActionResult> CheckAndExecuteEvents(
|
||||
- Create `TutorialHeroJoined` action results adding reinforcement heroes to player faction
|
||||
- Set appropriate loyalty, vigor, location for joined heroes
|
||||
|
||||
### Phase 7: Post-Battle Strategic Dialogue
|
||||
|
||||
After the battle ends and the player returns to the strategic map, narrative dialogues guide them through the next steps.
|
||||
|
||||
**Dialogue 1: Battle Aftermath** (trigger: `tutorial_battle_ended`)
|
||||
- Old Marek reflects on the close call
|
||||
- Tarn has vanished — rumors of sorcery or escape, no one knows
|
||||
- Captured lieutenants: they followed orders, not to blame for Tarn's madness
|
||||
- Player should try to recruit them or hold them
|
||||
- No instruction text needed — Handle Captured Heroes UI is self-explanatory
|
||||
|
||||
**Dialogue 2: Rebuild Support** (trigger: `tutorial_rebuild_support`)
|
||||
- Fires when available commands no longer include HandleCapturedHeroCommand
|
||||
- Marek explains the province needs support rebuilt after the battle
|
||||
- Engineers (John Ranil) can **Improve** the province — build infrastructure, develop economy
|
||||
- Paladins (Elena Fyar) can **Give Alms** — distribute food to win hearts
|
||||
- Instructions highlight **Improve** and **Give Alms** buttons
|
||||
- Goal: get Support to **40** before January for tax revenue
|
||||
|
||||
**Implementation:**
|
||||
- Dialogues defined in `tutorial_strategic.json`
|
||||
- `tutorial_battle_ended`: fire when tutorial battle is removed from `RunningShardokGameModels`
|
||||
- `tutorial_rebuild_support`: fire when available commands no longer include `HandleCapturedHeroCommand`
|
||||
- Register `ImproveButton` and `AlmsButton` as highlight targets in `TutorialTargetRegistry`
|
||||
|
||||
See `TUTORIAL_CONTENT.md` for full dialogue text.
|
||||
|
||||
---
|
||||
|
||||
## File Summary
|
||||
@@ -183,6 +159,12 @@ See `TUTORIAL_CONTENT.md` for full dialogue text.
|
||||
|
||||
## Remaining Implementation
|
||||
|
||||
### ShardokEngine Integration
|
||||
Integrate `TutorialBattleController` into the Shardok battle loop:
|
||||
1. Initialize controller from battle config when battle starts
|
||||
2. Check `ShouldTriggerScriptedFlee()` at end of each round
|
||||
3. If true, call `ExecuteScriptedFlee()` and add results to action queue
|
||||
|
||||
### Disable Player Flee
|
||||
In `FleeCommandFactory.cpp`:
|
||||
- Check if tutorial mode is enabled
|
||||
@@ -239,29 +221,10 @@ In Eagle's battle resolution:
|
||||
```protobuf
|
||||
message TutorialBattleConfig {
|
||||
bool enabled = 1;
|
||||
repeated TutorialEvent events = 2;
|
||||
repeated UnitInitialSize initial_sizes = 3; // For damage tracking
|
||||
}
|
||||
|
||||
message TutorialEvent {
|
||||
string event_id = 1;
|
||||
TutorialTrigger trigger = 2;
|
||||
TutorialAction action = 3;
|
||||
}
|
||||
|
||||
message TutorialTrigger {
|
||||
oneof trigger_type {
|
||||
RoundTrigger after_round = 1;
|
||||
UnitsLostTrigger units_lost = 2;
|
||||
DamageTakenTrigger damage_taken = 3;
|
||||
UnitKilledTrigger unit_killed = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message TutorialAction {
|
||||
oneof action_type {
|
||||
FleeAction flee = 1;
|
||||
ReinforcementsAction reinforcements = 2; // Uses CommonUnit
|
||||
}
|
||||
int32 flee_after_defender_units_lost = 2;
|
||||
int32 flee_after_rounds = 3;
|
||||
int32 attacker_player_id = 4;
|
||||
bool defender_can_flee = 5;
|
||||
repeated string reinforcement_hero_names = 6;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -149,52 +149,6 @@ Triggered during battles when players encounter spells, terrain, or abilities.
|
||||
|
||||
---
|
||||
|
||||
## Post-Battle Dialogue (Narrative Dialogues)
|
||||
|
||||
These fire after the first tutorial battle ends and the player returns to the strategic map. They use the narrative dialogue system (`DialogueManager` / `tutorial_strategic.json`) rather than the modal tutorial system.
|
||||
|
||||
### Battle Aftermath
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| ID | `post_battle_aftermath` |
|
||||
| Trigger | `tutorial_battle_ended` (battle removed from RunningShardokGameModels) |
|
||||
| Speaker | Old Marek the Learned |
|
||||
| Panel Position | top |
|
||||
|
||||
**Dialogue (Marek):**
|
||||
> That was closer than I'd like to admit. We held — barely. But Tarn... Tarn is gone. Not retreated — *gone*. The men say he vanished from the field. Some claim sorcery. Others say he slipped away in the chaos. Whatever the truth, no one can find him.
|
||||
>
|
||||
> But we captured some of his lieutenants. These are soldiers, Sadar — they followed orders, same as we once did. They're not to blame for Tarn's madness. See if you can bring them to our cause, or at least hold them until they come around.
|
||||
|
||||
**Instructions:** *(none — Handle Captured Heroes UI is self-explanatory)*
|
||||
|
||||
### Rebuild Support
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| ID | `post_battle_rebuild` |
|
||||
| Trigger | `tutorial_rebuild_support` (fires when available commands no longer include HandleCapturedHeroCommand) |
|
||||
| Speaker | Old Marek the Learned |
|
||||
| Panel Position | top |
|
||||
|
||||
**Dialogue (Marek):**
|
||||
> Good. Now we need to rebuild our support here in Onmaa. The people are shaken — a battle on their doorstep will do that. We need them behind us if we're going to hold this province.
|
||||
>
|
||||
> Any of our heroes can develop the land or give alms to the people — nothing wins hearts faster than a full belly. Engineers like John Ranil are especially effective at improving a province, and paladins like Elena Fyar are the best at winning hearts. Either way, we need the people's support before the tax collectors come round in January.
|
||||
|
||||
**Instructions:** Use **Improve** and **Give Alms** to raise Support. You must have at least **40** support in the province by January in order to collect gold and food in taxes.
|
||||
|
||||
**Highlight targets:** `SupportField`
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
- Both dialogues go in `tutorial_strategic.json`
|
||||
- Need new trigger events: `tutorial_battle_ended` and `tutorial_rebuild_support`
|
||||
- `tutorial_battle_ended` should fire when the tutorial battle is removed from `RunningShardokGameModels` in `EagleGameModel.ApplyGameStateViewDiff`
|
||||
- `tutorial_rebuild_support` should fire when available commands no longer include `HandleCapturedHeroCommand` (i.e. the captured heroes phase is done and the player has regular commands available)
|
||||
- The "rebuild" dialogue should highlight the Improve and Give Alms buttons in the command panel (requires registering them as highlight targets)
|
||||
|
||||
---
|
||||
|
||||
## Display Modes
|
||||
|
||||
| Mode | Description | Use For |
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
# Two-Stage Animation Implementation Pattern
|
||||
|
||||
Actions with server-determined success/failure outcomes use a two-stage system:
|
||||
1. **Attempt phase**: Animation + sound plays immediately when the command is issued
|
||||
2. **Result phase**: A distinct success or failure animation + sound plays when the server responds
|
||||
|
||||
## Adding a New Two-Stage Action
|
||||
|
||||
To add distinct success/failure animations for an action (using `ExtinguishFire` as an example):
|
||||
|
||||
1. **AnimationType enum** (`ShardokGameController.cs`): Add `ExtinguishFireFailed`
|
||||
2. **AnimationTypeForAction()**: Map `ActionType.ExtinguishFireFailed` to new type
|
||||
3. **AttemptAnimationType()**: Map `ExtinguishFireFailed` back to `ExtinguishFire`
|
||||
4. **ActionTypeForSound()**: Map new type to `ActionType.ExtinguishFireFailed`
|
||||
5. **PlayAnimation()**: Add dispatch case calling the new animator method
|
||||
6. **Animator**: Add `AnimateExtinguishFailed()` method
|
||||
7. **SoundManager**: Ensure result sound is mapped (usually already done)
|
||||
|
||||
See FearAnimator and FleeAnimator for reference implementations.
|
||||
@@ -1,230 +0,0 @@
|
||||
# Plan: Eliminate Battalion Backstory LLM Updates
|
||||
|
||||
## Goal
|
||||
|
||||
Dramatically reduce LLM request volume by eliminating battalion backstory updates. Battalions will retain:
|
||||
- An **initial backstory** (generated once when created)
|
||||
- A **history of events** (structured data, not LLM-generated prose)
|
||||
|
||||
The history will be fed into other LLM updates (hero backstories, chronicle) but will no longer be regenerated into prose for each battalion.
|
||||
|
||||
## Current System Overview
|
||||
|
||||
### Data Flow
|
||||
```
|
||||
Battalion Created → Initial Backstory LLM Request → Text stored
|
||||
↓
|
||||
Battalion participates in events → Events accumulated → Update LLM Request → New text version
|
||||
↓
|
||||
BattalionView.backstoryTextId → Client displays on hover
|
||||
```
|
||||
|
||||
### Event Types (8 total)
|
||||
Events are structured data already captured in `EventForBattalionBackstory`:
|
||||
- OrganizedTroops, SuppressedBeasts, ApprehendedOutlaws, FoughtInBattle
|
||||
- SuppressedRiot, Trained, Armed, SurvivedStarvation
|
||||
|
||||
Each event has date, province, hero, and event-specific details.
|
||||
|
||||
### Key Files
|
||||
|
||||
| Component | File |
|
||||
|-----------|------|
|
||||
| Unity Display | `Assets/Eagle/HeroesAndBattalionsPanelController.cs` |
|
||||
| View Proto | `views/battalion_view.proto` |
|
||||
| View Filter | `BattalionViewFilter.scala` |
|
||||
| Update Action | `BattalionBackstoryUpdateAction.scala` |
|
||||
| Update Generator | `BattalionBackstoryUpdateActionGenerator.scala` |
|
||||
| Update Prompt | `BattalionBackstoryUpdatePromptGenerator.scala` |
|
||||
| Event-to-Text | `BattalionBackstoryUpdatePromptGenerator.textForEvent()` |
|
||||
| BattalionDescriptions | `BattalionDescriptions.scala` (used in other LLM prompts) |
|
||||
|
||||
---
|
||||
|
||||
## Stage 1: Stop Displaying Battalion Backstories in Client
|
||||
|
||||
**Goal:** Remove the UI that shows battalion backstory text on hover.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`Assets/Eagle/HeroesAndBattalionsPanelController.cs`**
|
||||
- In `BattalionLongHoverRowChanged()` (lines 143-169):
|
||||
- Remove or comment out the backstory text population
|
||||
- Keep the header/name display, remove `battalionPopupPanelBackstory.TextId = ...`
|
||||
|
||||
**Unity Scene/Prefab**
|
||||
- Hide or remove the `battalionPopupPanelBackstory` UI element
|
||||
|
||||
### Verification
|
||||
- [ ] Battalion hover popup no longer shows backstory text
|
||||
- [ ] No errors when hovering over battalions
|
||||
- [ ] Other battalion info (name, type, size) still displays
|
||||
|
||||
---
|
||||
|
||||
## Stage 2: Stop Populating Backstory in BattalionView
|
||||
|
||||
**Goal:** Stop sending backstory text ID to the client.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`BattalionViewFilter.scala`**
|
||||
- In `filteredBattalionViewBelongingToPlayer()`:
|
||||
- Set `backstoryTextId = ""` instead of `battalion.backstoryTextId`
|
||||
- In `limitedBattalionView()`:
|
||||
- Same change
|
||||
|
||||
### Verification
|
||||
- [ ] BattalionView messages have empty backstoryTextId
|
||||
- [ ] Client handles empty backstoryTextId gracefully (Stage 1 should have removed the display)
|
||||
|
||||
---
|
||||
|
||||
## Stage 3: Remove backstory_text_id from BattalionView Proto
|
||||
|
||||
**Goal:** Clean up the proto schema.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`views/battalion_view.proto`**
|
||||
- Remove field: `string backstory_text_id = 7;`
|
||||
- Mark field 7 as reserved to prevent reuse
|
||||
|
||||
**`Assets/Eagle/HeroesAndBattalionsPanelController.cs`**
|
||||
- Remove any remaining references to `BackstoryTextId`
|
||||
|
||||
**`BattalionViewFilter.scala`**
|
||||
- Remove `backstoryTextId` from view construction
|
||||
|
||||
### Verification
|
||||
- [ ] Proto compiles
|
||||
- [ ] C# client compiles without BackstoryTextId references
|
||||
- [ ] Full test suite passes
|
||||
|
||||
---
|
||||
|
||||
## Stage 4: Replace Backstory Usage in BattalionDescriptions
|
||||
|
||||
**Goal:** When other LLM prompts need battalion context, use initial backstory + structured history instead of the latest LLM-generated prose.
|
||||
|
||||
### Current Usage in `BattalionDescriptions.scala`
|
||||
|
||||
```scala
|
||||
def backstoryText(): TextGenerationResult =
|
||||
battalion.backstoryVersions.lastOption
|
||||
.map(v => clientTextStore.getText(v.textId))
|
||||
.getOrElse(TextGenerationSuccess(""))
|
||||
|
||||
def fullDescription(): String =
|
||||
s"${battalion.name} is $description. $backstoryText"
|
||||
```
|
||||
|
||||
### New Approach
|
||||
|
||||
Create `historyDescription()` that:
|
||||
1. Gets initial backstory (first version's text, if available)
|
||||
2. Appends structured event history using the same `textForEvent()` logic from `BattalionBackstoryUpdatePromptGenerator`
|
||||
|
||||
```scala
|
||||
def historyDescription(): String = {
|
||||
val initialBackstory = battalion.backstoryVersions.headOption
|
||||
.map(v => clientTextStore.getText(v.textId))
|
||||
.collect { case TextGenerationSuccess(text) => text }
|
||||
.getOrElse("")
|
||||
|
||||
val eventHistory = battalion.backstoryEvents
|
||||
.map(textForEvent) // Reuse existing event-to-text conversion
|
||||
.mkString(" ")
|
||||
|
||||
if eventHistory.isEmpty then initialBackstory
|
||||
else s"$initialBackstory $eventHistory"
|
||||
}
|
||||
```
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`BattalionDescriptions.scala`**
|
||||
- Add `historyDescription()` method
|
||||
- Update `fullDescription()` to use `historyDescription()` instead of `backstoryText()`
|
||||
- Keep `textForEvent()` logic (move from `BattalionBackstoryUpdatePromptGenerator` if needed)
|
||||
|
||||
**Move/refactor `textForEvent()`**
|
||||
- Currently in `BattalionBackstoryUpdatePromptGenerator.scala` (lines 78-262)
|
||||
- Move to `BattalionDescriptions.scala` or a shared utility
|
||||
- This converts events to human-readable text without LLM
|
||||
|
||||
### Verification
|
||||
- [ ] Other LLM prompts that use `BattalionDescriptions` still get meaningful context
|
||||
- [ ] Chronicle updates include battalion history
|
||||
- [ ] Hero backstory updates include relevant battalion history
|
||||
|
||||
---
|
||||
|
||||
## Stage 5: Stop Generating Battalion Backstory Updates
|
||||
|
||||
**Goal:** Remove the LLM request generation entirely.
|
||||
|
||||
### Files to Modify
|
||||
|
||||
**`BattalionBackstoryUpdateActionGenerator.scala`**
|
||||
- Return empty Vector instead of generating actions
|
||||
- Or delete the file entirely
|
||||
|
||||
**`BattalionBackstoryUpdateAction.scala`**
|
||||
- Delete or mark as deprecated
|
||||
|
||||
**`BattalionBackstoryUpdatePromptGenerator.scala`**
|
||||
- Delete or keep only `textForEvent()` if moved
|
||||
|
||||
**`LlmResolver.scala`**
|
||||
- Remove case for `BattalionBackstoryUpdateRequest`
|
||||
- Optionally throw error if somehow called
|
||||
|
||||
**`EndPlayerCommandsPhaseAction.scala`** (and other callers)
|
||||
- Find where `BattalionBackstoryUpdateActionGenerator` is called
|
||||
- Remove those calls
|
||||
|
||||
### Files to Search for Callers
|
||||
```bash
|
||||
grep -r "BattalionBackstoryUpdateAction" src/
|
||||
```
|
||||
|
||||
### Verification
|
||||
- [ ] No `BattalionBackstoryUpdateRequest` LLM requests generated
|
||||
- [ ] Battalions still accumulate events (for history description)
|
||||
- [ ] Full test suite passes
|
||||
- [ ] Game runs without errors
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### Keep or Remove Initial Backstory Generation?
|
||||
The initial backstory (`BattalionInitialBackstoryRequest`) is generated once per battalion. Options:
|
||||
1. **Keep it** - Low volume, provides flavor text for history
|
||||
2. **Remove it** - Use a template-based approach instead
|
||||
|
||||
Recommendation: Keep for now, evaluate later based on volume.
|
||||
|
||||
### Event History Cleanup
|
||||
Events currently accumulate forever on battalions. Consider:
|
||||
- Capping event history length
|
||||
- Summarizing old events
|
||||
- Only keeping "significant" events
|
||||
|
||||
### Affected Tests
|
||||
Search for tests that verify backstory update behavior:
|
||||
```bash
|
||||
bazel query 'tests(//src/test/scala/...) intersect rdeps(//src/test/scala/..., //src/main/scala/net/eagle0/eagle/library/actions/impl/action:battalion_backstory_update_action)'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary of LLM Request Reduction
|
||||
|
||||
| Request Type | Current Volume | After Change |
|
||||
|--------------|---------------|--------------|
|
||||
| BattalionInitialBackstoryRequest | 1 per battalion | 1 per battalion (unchanged) |
|
||||
| BattalionBackstoryUpdateRequest | ~N per battalion per game | **0** |
|
||||
|
||||
For a typical game with ~50 battalions and ~20 rounds, this could eliminate **hundreds** of LLM requests.
|
||||
@@ -1,93 +0,0 @@
|
||||
# Province Event 3D Effects
|
||||
|
||||
Ideas for replacing 2D shader-based province event effects with 3D particle systems.
|
||||
|
||||
## Current State
|
||||
|
||||
| Event | Effect Type | Notes |
|
||||
|-------|-------------|-------|
|
||||
| Festival | 3D particles | Complete |
|
||||
| Epidemic | 3D particles | Complete (rising skulls + green haze) |
|
||||
| Blizzard | 2D shader | Not visible on unowned (white) provinces |
|
||||
| Drought | 2D shader | |
|
||||
| Flood | 2D shader | |
|
||||
| Beasts | None | Tricky - can be any animal or human |
|
||||
|
||||
## Proposed 3D Effects
|
||||
|
||||
### Blizzard
|
||||
|
||||
- Falling snowflakes with slight blue tint (ensures visibility on white backgrounds)
|
||||
- Diagonal wind-blown particles to convey harsh weather
|
||||
- Swirling vortex effect near ground level
|
||||
- Frosty sparkle/glitter particles for magical winter feel
|
||||
|
||||
### Drought
|
||||
|
||||
- Rising dust/sand particles (tan/brown coloring)
|
||||
- Small dust devils (spinning particle columns)
|
||||
- Shimmering golden particles rising to suggest heat distortion
|
||||
- Dried leaves/debris blowing across the province
|
||||
|
||||
### Flood
|
||||
|
||||
- Rain falling (blue-tinted streaks or droplets)
|
||||
- Water splashes/ripples rising from ground
|
||||
- Mist/spray particles hovering low
|
||||
- Floating debris particles (leaves, sticks)
|
||||
|
||||
### Beasts
|
||||
|
||||
This is challenging since "beasts" can represent any dangerous creature. Universal approaches:
|
||||
|
||||
- **Circling vultures/crows overhead** - Predators attract scavengers; works for any beast type and is visually distinctive
|
||||
- Dust clouds suggesting movement at province edges
|
||||
- Glowing eyes appearing/disappearing in shadows
|
||||
- Stylized claw/scratch mark effects
|
||||
- Paw prints materializing on the ground
|
||||
- Red "danger" aura pulsing from ground level
|
||||
|
||||
The vultures/crows approach is recommended as the most universal solution.
|
||||
|
||||
## Implementation Status
|
||||
|
||||
Controllers have been created for all 4 event types:
|
||||
- `ProvinceBlizzardController.cs` - ready, needs prefab
|
||||
- `ProvinceDroughtController.cs` - ready, needs prefab
|
||||
- `ProvinceFloodController.cs` - ready, needs prefab
|
||||
- `ProvinceBeastsController.cs` - ready, needs prefab
|
||||
|
||||
### Prefabs Needed
|
||||
|
||||
Create these prefabs in `Assets/Eagle/Effects/`:
|
||||
|
||||
1. **BlizzardEffect.prefab**
|
||||
- Particle System with falling snowflakes
|
||||
- Blue-tinted particles for visibility on white
|
||||
- Diagonal velocity for wind effect
|
||||
- RectTransform for UI positioning
|
||||
|
||||
2. **DroughtEffect.prefab**
|
||||
- Particle System with rising dust particles
|
||||
- Tan/brown coloring
|
||||
- Upward velocity
|
||||
- Optional spinning sub-emitters for dust devils
|
||||
|
||||
3. **FloodEffect.prefab**
|
||||
- Particle System with falling rain streaks
|
||||
- Blue tint
|
||||
- Fast downward velocity
|
||||
- Optional splash sub-emitter
|
||||
|
||||
4. **BeastsEffect.prefab**
|
||||
- Particle System with bird silhouettes
|
||||
- Circular orbit motion around center
|
||||
- Black/dark particles
|
||||
- Could use a crow/vulture sprite texture
|
||||
|
||||
### Scene Setup
|
||||
|
||||
Wire up in `Gameplay.unity`:
|
||||
1. Add controller components to the Map GameObject
|
||||
2. Assign `mapContainer`, `centroidsJson` (shared with other controllers)
|
||||
3. Assign the effect prefab for each controller
|
||||
@@ -12,7 +12,7 @@ require (
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
|
||||
golang.org/x/sys v0.30.0
|
||||
golang.org/x/sys v0.28.0
|
||||
google.golang.org/grpc v1.68.0
|
||||
google.golang.org/protobuf v1.36.3
|
||||
)
|
||||
|
||||
@@ -43,8 +43,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 h1:VQpB2SpK88C6B5lPHTuSZKb2Qee1QWwiFlC5CKY4AW0=
|
||||
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
"org.scalamock:scalamock_3": -610483078,
|
||||
"org.slf4j:slf4j-api": 1609745898,
|
||||
"org.slf4j:slf4j-simple": 1319506696,
|
||||
"org.xerial:sqlite-jdbc": -1707012205,
|
||||
"repositories": -1949687017,
|
||||
"software.amazon.awssdk:aws-core": 1764800397,
|
||||
"software.amazon.awssdk:http-client-spi": 1697477859,
|
||||
@@ -174,7 +173,6 @@
|
||||
"org.scalamock:scalamock_3": -1778657085,
|
||||
"org.slf4j:slf4j-api": -771685699,
|
||||
"org.slf4j:slf4j-simple": -776509812,
|
||||
"org.xerial:sqlite-jdbc": 1311600081,
|
||||
"software.amazon.awssdk:annotations": -324257201,
|
||||
"software.amazon.awssdk:apache-client": 2066024214,
|
||||
"software.amazon.awssdk:arns": -1240703529,
|
||||
@@ -818,12 +816,6 @@
|
||||
},
|
||||
"version": "2.0.16"
|
||||
},
|
||||
"org.xerial:sqlite-jdbc": {
|
||||
"shasums": {
|
||||
"jar": "6dc7464e3803648d3ff18a7359bab6adf079fcd8495b18991f6f5edcb8ac6e3b"
|
||||
},
|
||||
"version": "3.46.1.0"
|
||||
},
|
||||
"software.amazon.awssdk:annotations": {
|
||||
"shasums": {
|
||||
"jar": "ed03bb4ff78900307dc96bacdec70ddf80c17df3eb6761219573025649f08ff3"
|
||||
@@ -1360,9 +1352,6 @@
|
||||
"org.slf4j:slf4j-simple": [
|
||||
"org.slf4j:slf4j-api"
|
||||
],
|
||||
"org.xerial:sqlite-jdbc": [
|
||||
"org.slf4j:slf4j-api"
|
||||
],
|
||||
"software.amazon.awssdk:apache-client": [
|
||||
"commons-codec:commons-codec",
|
||||
"org.apache.httpcomponents:httpclient",
|
||||
@@ -2415,16 +2404,6 @@
|
||||
"org.slf4j:slf4j-simple": [
|
||||
"org.slf4j.simple"
|
||||
],
|
||||
"org.xerial:sqlite-jdbc": [
|
||||
"org.sqlite",
|
||||
"org.sqlite.core",
|
||||
"org.sqlite.date",
|
||||
"org.sqlite.javax",
|
||||
"org.sqlite.jdbc3",
|
||||
"org.sqlite.jdbc4",
|
||||
"org.sqlite.nativeimage",
|
||||
"org.sqlite.util"
|
||||
],
|
||||
"software.amazon.awssdk:annotations": [
|
||||
"software.amazon.awssdk.annotations"
|
||||
],
|
||||
@@ -2835,7 +2814,6 @@
|
||||
"org.scalamock:scalamock_3",
|
||||
"org.slf4j:slf4j-api",
|
||||
"org.slf4j:slf4j-simple",
|
||||
"org.xerial:sqlite-jdbc",
|
||||
"software.amazon.awssdk:annotations",
|
||||
"software.amazon.awssdk:apache-client",
|
||||
"software.amazon.awssdk:arns",
|
||||
@@ -2912,11 +2890,6 @@
|
||||
"org.slf4j.simple.SimpleServiceProvider"
|
||||
]
|
||||
},
|
||||
"org.xerial:sqlite-jdbc": {
|
||||
"java.sql.Driver": [
|
||||
"org.sqlite.JDBC"
|
||||
]
|
||||
},
|
||||
"software.amazon.awssdk:apache-client": {
|
||||
"software.amazon.awssdk.http.SdkHttpService": [
|
||||
"software.amazon.awssdk.http.apache.ApacheSdkHttpService"
|
||||
|
||||
+4
-64
@@ -2,67 +2,7 @@
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
UNITY_ROOT="$REPO_ROOT/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
OUTPUT_DIR="$UNITY_ROOT/Assets/GeneratedProtos"
|
||||
|
||||
# All C# protobuf generation targets
|
||||
TARGETS=(
|
||||
"//src/main/protobuf/net/eagle0/common:common_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:storage_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:common_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:api_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:common_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:views_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:api_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:api_csharp_grpc_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:util_csharp_proto_srcs"
|
||||
)
|
||||
|
||||
# Subdirectory for each target (avoids filename collisions across packages)
|
||||
SUBDIRS=(
|
||||
"net/eagle0/common"
|
||||
"net/eagle0/shardok/storage"
|
||||
"net/eagle0/shardok/common"
|
||||
"net/eagle0/shardok/api"
|
||||
"net/eagle0/eagle/common"
|
||||
"net/eagle0/eagle/views"
|
||||
"net/eagle0/eagle/api"
|
||||
"net/eagle0/eagle/api"
|
||||
"net/eagle0/eagle/api/command/util"
|
||||
)
|
||||
|
||||
/bin/echo "Building C# protobuf sources..."
|
||||
bazel build "${TARGETS[@]}"
|
||||
|
||||
/bin/echo "Syncing generated .cs files to $OUTPUT_DIR..."
|
||||
|
||||
# Stage new files in a temp directory, then rsync to preserve timestamps
|
||||
# on unchanged files. This avoids triggering Unity reimport when protos
|
||||
# haven't changed, saving ~7 minutes of script recompilation.
|
||||
STAGING_DIR=$(mktemp -d)
|
||||
trap "rm -rf '$STAGING_DIR'" EXIT
|
||||
|
||||
for i in "${!TARGETS[@]}"; do
|
||||
target="${TARGETS[$i]}"
|
||||
subdir="${SUBDIRS[$i]}"
|
||||
|
||||
# Convert target label to bazel-bin path
|
||||
# //src/main/protobuf/net/eagle0/common:common_csharp_proto_srcs
|
||||
# -> bazel-bin/src/main/protobuf/net/eagle0/common/common_csharp_proto_srcs
|
||||
target_path="${target#//}"
|
||||
pkg="${target_path%%:*}"
|
||||
name="${target_path##*:}"
|
||||
bin_dir="$REPO_ROOT/bazel-bin/$pkg/$name"
|
||||
|
||||
dest_dir="$STAGING_DIR/$subdir"
|
||||
mkdir -p "$dest_dir"
|
||||
find "$bin_dir" -name "*.cs" -exec cp {} "$dest_dir/" \;
|
||||
done
|
||||
|
||||
# Sync only changed files and delete removed ones; --checksum compares
|
||||
# content not timestamps so unchanged files keep their original mtime.
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
rsync -rc --delete "$STAGING_DIR/" "$OUTPUT_DIR/"
|
||||
|
||||
/bin/echo "Done. Generated C# proto sources in $OUTPUT_DIR"
|
||||
/bin/echo "build protos"
|
||||
mkdir -p src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/Eagle0Protos/
|
||||
dotnet build src/main/csharp/net/eagle0/clients/unity/eagle0/protos/protos.csproj \
|
||||
-o src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/Eagle0Protos/
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
#
|
||||
# Build the SparklePlugin native library for Unity using Bazel
|
||||
#
|
||||
# The SparklePlugin is built in a separate Bazel workspace (sparkle_workspace/)
|
||||
# to avoid rules_swift version conflicts between grpc and rules_apple.
|
||||
#
|
||||
# Usage: build_sparkle_plugin.sh [output_dir]
|
||||
|
||||
set -euo pipefail
|
||||
@@ -11,13 +14,14 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
OUTPUT_DIR="${1:-$PROJECT_ROOT/src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/macOS}"
|
||||
|
||||
echo "=== Building SparklePlugin with Bazel ==="
|
||||
echo "=== Building SparklePlugin with Bazel (from sparkle_workspace) ==="
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
bazel build //src/main/objc/net/eagle0/clients/unity/sparkle:SparklePlugin
|
||||
# Build in the separate sparkle_workspace to avoid dependency conflicts
|
||||
cd "$PROJECT_ROOT/sparkle_workspace"
|
||||
bazel build //:SparklePlugin
|
||||
|
||||
# Get the zip path from bazel
|
||||
ZIP_PATH=$(bazel cquery --output=files //src/main/objc/net/eagle0/clients/unity/sparkle:SparklePlugin 2>/dev/null)
|
||||
ZIP_PATH=$(bazel cquery --output=files //:SparklePlugin 2>/dev/null)
|
||||
|
||||
echo "=== Extracting SparklePlugin.bundle ==="
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1DHEsiv4cY4gE6AX3sVH82K__mpBD1aznIYCQwQxA_F0/export?gid=0&format=tsv" | tr -d '\r' > /tmp/names.tsv
|
||||
bazel run //src/main/scala/net/eagle0/util:name_list_checker -- /tmp/names.tsv > /dev/null
|
||||
bazel run //src/main/scala/net/eagle0/util:name_list_checker -- /tmp/names.tsv > src/main/resources/net/eagle0/names.tsv
|
||||
bazel run //src/main/scala/net/eagle0/util:name_list_json_maker -- /tmp/names.tsv > src/main/resources/net/eagle0/names.json
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1NhvG73HKyVE36yGpkV2oJiSIXoNqQOYTr5ArLnucYL0/export?gid=0&format=tsv" | tr -d '\r' > src/main/resources/net/eagle0/shardok/battalionTypes.tsv
|
||||
curl -L "https://docs.google.com/spreadsheets/d/1pNWiyxIks2wJ1v7jRLFD24zrKHG2AfhC-nkWmQKQGN4/export?gid=0&format=tsv" | tr -d '\r' > src/main/resources/net/eagle0/eagle/heroes.tsv
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Restart the active Eagle instance.
|
||||
# Reads the active instance from /opt/eagle0/.active-instance (set by deploy-blue-green.sh).
|
||||
#
|
||||
# This is useful when you need Eagle to reload game state from persistence
|
||||
# (e.g., after a rewind) without doing a full blue-green deployment.
|
||||
#
|
||||
# Usage:
|
||||
# eagle-restart # Restart active instance, wait for healthy
|
||||
# eagle-restart --no-wait # Restart without waiting for health check
|
||||
#
|
||||
# To create an alias, add to ~/.bashrc:
|
||||
# alias eagle-restart='/opt/eagle0/scripts/eagle-restart.sh'
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
APP_DIR="${APP_DIR:-/opt/eagle0}"
|
||||
ACTIVE_FILE="${APP_DIR}/.active-instance"
|
||||
HEALTH_TIMEOUT=120 # seconds
|
||||
|
||||
# Read active instance from file, with fallback
|
||||
if [ -f "$ACTIVE_FILE" ]; then
|
||||
ACTIVE=$(cat "$ACTIVE_FILE")
|
||||
else
|
||||
# Fallback: check which container is actually running
|
||||
if docker inspect --format='{{.State.Running}}' eagle-blue 2>/dev/null | grep -q true; then
|
||||
ACTIVE="eagle-blue"
|
||||
elif docker inspect --format='{{.State.Running}}' eagle-green 2>/dev/null | grep -q true; then
|
||||
ACTIVE="eagle-green"
|
||||
else
|
||||
ACTIVE=""
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$ACTIVE" ]; then
|
||||
echo "Error: No active Eagle instance found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NO_WAIT=false
|
||||
if [ "${1:-}" = "--no-wait" ]; then
|
||||
NO_WAIT=true
|
||||
fi
|
||||
|
||||
echo "Restarting ${ACTIVE}..."
|
||||
docker restart "$ACTIVE"
|
||||
|
||||
if [ "$NO_WAIT" = true ]; then
|
||||
echo "Restart initiated. Not waiting for health check."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Waiting for ${ACTIVE} to become healthy (timeout: ${HEALTH_TIMEOUT}s)..."
|
||||
elapsed=0
|
||||
while [ $elapsed -lt $HEALTH_TIMEOUT ]; do
|
||||
health=$(docker inspect --format='{{.State.Health.Status}}' "$ACTIVE" 2>/dev/null || echo "unknown")
|
||||
if [ "$health" = "healthy" ]; then
|
||||
echo "${ACTIVE} is healthy after ${elapsed}s."
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
# Print progress every 10 seconds
|
||||
if [ $((elapsed % 10)) -eq 0 ]; then
|
||||
echo " ...${elapsed}s (status: ${health})"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Warning: ${ACTIVE} did not become healthy within ${HEALTH_TIMEOUT}s (status: ${health})" >&2
|
||||
echo "Check logs with: eagle-logs" >&2
|
||||
exit 1
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Keeps Bazel mactools builds in sync with the installed Xcode version.
|
||||
#
|
||||
# Only needed on runners that do mactools builds (Mac/Sparkle). Non-Mac builds
|
||||
# don't run this script; their apple_cc_autoconf stays cached because none of
|
||||
# its environ vars change, so they're unaffected by Xcode version changes.
|
||||
#
|
||||
# 1. Writes .bazelrc.xcode with mactools-scoped flags:
|
||||
# - action_env for remote cache key invalidation
|
||||
# - DEVELOPER_DIR pointing to the active Xcode
|
||||
#
|
||||
# 2. Runs `bazel clean --expunge` when the version actually changes, because
|
||||
# local_config_apple_cc (a cached repository rule) bakes in the old version
|
||||
# and can only be refreshed by clearing the output base.
|
||||
#
|
||||
# .bazelrc imports the generated file via: try-import %workspace%/.bazelrc.xcode
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
XCODE_BUILD_VERSION=$(xcodebuild -version 2>/dev/null | awk '/Build version/ {print $3}')
|
||||
|
||||
if [ -z "$XCODE_BUILD_VERSION" ]; then
|
||||
echo "Warning: Could not detect Xcode build version, skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
DEVELOPER_DIR=$(xcode-select -p 2>/dev/null)
|
||||
|
||||
BAZELRC_XCODE=".bazelrc.xcode"
|
||||
EXPECTED_LINE="common:mactools --action_env=XCODE_BUILD_VERSION=${XCODE_BUILD_VERSION}"
|
||||
|
||||
# Check if the file already has the right version (first line is the version marker)
|
||||
if [ -f "$BAZELRC_XCODE" ]; then
|
||||
CURRENT_FIRST_LINE=$(head -1 "$BAZELRC_XCODE")
|
||||
if [ "$CURRENT_FIRST_LINE" = "$EXPECTED_LINE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Xcode version changed (or first run) — expunge local cache to clear
|
||||
# stale local_config_apple_cc, then write the new config
|
||||
OLD_VERSION="unknown"
|
||||
if [ -f "$BAZELRC_XCODE" ]; then
|
||||
OLD_VERSION=$(sed -n 's/.*XCODE_BUILD_VERSION=//p' "$BAZELRC_XCODE" | head -1)
|
||||
fi
|
||||
echo "Xcode build version changed: ${OLD_VERSION} -> ${XCODE_BUILD_VERSION}"
|
||||
echo "Running bazel clean --expunge to clear stale toolchain config..."
|
||||
bazel clean --expunge 2>/dev/null || true
|
||||
|
||||
cat > "$BAZELRC_XCODE" << EOF
|
||||
${EXPECTED_LINE}
|
||||
common:mactools --repo_env=DEVELOPER_DIR=${DEVELOPER_DIR}
|
||||
EOF
|
||||
echo "Updated ${BAZELRC_XCODE} — next mactools build will rebuild with Xcode ${XCODE_BUILD_VERSION}"
|
||||
@@ -0,0 +1 @@
|
||||
7.7.1
|
||||
@@ -0,0 +1,2 @@
|
||||
# Bazel symlinks
|
||||
bazel-*
|
||||
@@ -0,0 +1,28 @@
|
||||
module(name = "sparkle_workspace")
|
||||
|
||||
# Minimal dependencies for building SparklePlugin
|
||||
# This workspace is isolated from the main workspace to avoid
|
||||
# rules_swift version conflicts between grpc and rules_apple
|
||||
|
||||
bazel_dep(name = "apple_support", version = "1.21.1", repo_name = "build_bazel_apple_support")
|
||||
bazel_dep(name = "rules_apple", version = "4.3.3", repo_name = "build_bazel_rules_apple")
|
||||
|
||||
# Register Apple CC toolchain for Objective-C compilation
|
||||
apple_cc_configure = use_extension(
|
||||
"@build_bazel_apple_support//crosstool:setup.bzl",
|
||||
"apple_cc_configure_extension",
|
||||
)
|
||||
use_repo(apple_cc_configure, "local_config_apple_cc")
|
||||
|
||||
# Sparkle framework for macOS auto-updates
|
||||
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
SPARKLE_VERSION = "2.6.4"
|
||||
|
||||
http_archive(
|
||||
name = "sparkle",
|
||||
build_file = "//:external/BUILD.sparkle",
|
||||
sha256 = "50612a06038abc931f16011d7903b8326a362c1074dabccb718404ce8e585f0b",
|
||||
strip_prefix = "",
|
||||
url = "https://github.com/sparkle-project/Sparkle/releases/download/%s/Sparkle-%s.tar.xz" % (SPARKLE_VERSION, SPARKLE_VERSION),
|
||||
)
|
||||
Generated
+451
@@ -0,0 +1,451 @@
|
||||
{
|
||||
"lockFileVersion": 13,
|
||||
"registryFileHashes": {
|
||||
"https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed",
|
||||
"https://bcr.bazel.build/modules/abseil-cpp/20240116.1/source.json": "9be551b8d4e3ef76875c0d744b5d6a504a27e3ae67bc6b28f46415fd2d2957da",
|
||||
"https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896",
|
||||
"https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85",
|
||||
"https://bcr.bazel.build/modules/apple_support/1.21.1/MODULE.bazel": "5809fa3efab15d1f3c3c635af6974044bac8a4919c62238cce06acee8a8c11f1",
|
||||
"https://bcr.bazel.build/modules/apple_support/1.23.1/MODULE.bazel": "53763fed456a968cf919b3240427cf3a9d5481ec5466abc9d5dc51bc70087442",
|
||||
"https://bcr.bazel.build/modules/apple_support/1.23.1/source.json": "d888b44312eb0ad2c21a91d026753f330caa48a25c9b2102fae75eb2b0dcfdd2",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.30.0/source.json": "b07e17f067fe4f69f90b03b36ef1e08fe0d1f3cac254c1241a1818773e3423bc",
|
||||
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
|
||||
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/source.json": "f121b43eeefc7c29efbd51b83d08631e2347297c95aac9764a701f2a6a2bb953",
|
||||
"https://bcr.bazel.build/modules/buildozer/7.1.2/MODULE.bazel": "2e8dd40ede9c454042645fd8d8d0cd1527966aa5c919de86661e62953cd73d84",
|
||||
"https://bcr.bazel.build/modules/buildozer/7.1.2/source.json": "c9028a501d2db85793a6996205c8de120944f50a0d570438fcae0457a5f9d1f8",
|
||||
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
|
||||
"https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4",
|
||||
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6",
|
||||
"https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/source.json": "41e9e129f80d8c8bf103a7acc337b76e54fad1214ac0a7084bf24f4cd924b8b4",
|
||||
"https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f",
|
||||
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075",
|
||||
"https://bcr.bazel.build/modules/jsoncpp/1.9.5/source.json": "4108ee5085dd2885a341c7fab149429db457b3169b86eb081fa245eadf69169d",
|
||||
"https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902",
|
||||
"https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74",
|
||||
"https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.11/source.json": "f7e188b79ebedebfe75e9e1d098b8845226c7992b307e28e1496f23112e8fc29",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d",
|
||||
"https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc",
|
||||
"https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7",
|
||||
"https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92",
|
||||
"https://bcr.bazel.build/modules/protobuf/29.0-rc3/source.json": "c16a6488fb279ef578da7098e605082d72ed85fc8d843eaae81e7d27d0f4625d",
|
||||
"https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0",
|
||||
"https://bcr.bazel.build/modules/protobuf/3.19.6/MODULE.bazel": "9233edc5e1f2ee276a60de3eaa47ac4132302ef9643238f23128fea53ea12858",
|
||||
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e",
|
||||
"https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/source.json": "be4789e951dd5301282729fe3d4938995dc4c1a81c2ff150afc9f1b0504c6022",
|
||||
"https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206",
|
||||
"https://bcr.bazel.build/modules/re2/2023-09-01/source.json": "e044ce89c2883cd957a2969a43e79f7752f9656f6b20050b62f90ede21ec6eb4",
|
||||
"https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8",
|
||||
"https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e",
|
||||
"https://bcr.bazel.build/modules/rules_apple/4.3.3/MODULE.bazel": "c5c2c4adeeac5f3f2f9b7f16abfa8be7ffefa596171d0d92bed4cae9ade0a498",
|
||||
"https://bcr.bazel.build/modules/rules_apple/4.3.3/source.json": "3cb1d69c8243ffcc42ecbf84ae8b9cccd7b1e2f091b0aee5a3e9c9a45267f312",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.11/MODULE.bazel": "9f249c5624a4788067b96b8b896be10c7e8b4375dc46f6d8e1e51100113e0992",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.1.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0",
|
||||
"https://bcr.bazel.build/modules/rules_cc/0.1.2/source.json": "53fcb09b5816c83ca60d9d7493faf3bfaf410dfc2f15deb52d6ddd146b8d43f0",
|
||||
"https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6",
|
||||
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8",
|
||||
"https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/source.json": "c8b1e2c717646f1702290959a3302a178fb639d987ab61d548105019f11e527e",
|
||||
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
|
||||
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
|
||||
"https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe",
|
||||
"https://bcr.bazel.build/modules/rules_java/7.6.5/MODULE.bazel": "481164be5e02e4cab6e77a36927683263be56b7e36fef918b458d7a8a1ebadb1",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939",
|
||||
"https://bcr.bazel.build/modules/rules_java/8.5.1/source.json": "db1a77d81b059e0f84985db67a22f3f579a529a86b7997605be3d214a0abe38e",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0",
|
||||
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/source.json": "6f5f5a5a4419ae4e37c35a5bb0a6ae657ed40b7abc5a5189111b47fcebe43197",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
|
||||
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5",
|
||||
"https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0",
|
||||
"https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d",
|
||||
"https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c",
|
||||
"https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff",
|
||||
"https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a",
|
||||
"https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
|
||||
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
|
||||
"https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73",
|
||||
"https://bcr.bazel.build/modules/rules_proto/6.0.2/source.json": "17a2e195f56cb28d6bbf763e49973d13890487c6945311ed141e196fb660426d",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.22.1/MODULE.bazel": "26114f0c0b5e93018c0c066d6673f1a2c3737c7e90af95eff30cfee38d0bbac7",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58",
|
||||
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13",
|
||||
"https://bcr.bazel.build/modules/rules_python/1.3.0/source.json": "25932f917cd279c7baefa6cb1d3fa8750a7a29de522024449b19af6eab51f4a0",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b",
|
||||
"https://bcr.bazel.build/modules/rules_shell/0.3.0/source.json": "c55ed591aa5009401ddf80ded9762ac32c358d2517ee7820be981e2de9756cf3",
|
||||
"https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca",
|
||||
"https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd",
|
||||
"https://bcr.bazel.build/modules/rules_swift/2.4.0/source.json": "a6577f57f9febbdc015a01f2a8f3487422032f134d6c61d18ed8e8ca3b9acc7c",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5",
|
||||
"https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216",
|
||||
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91",
|
||||
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb",
|
||||
"https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43",
|
||||
"https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0",
|
||||
"https://bcr.bazel.build/modules/zlib/1.2.12/MODULE.bazel": "3b1a8834ada2a883674be8cbd36ede1b6ec481477ada359cd2d3ddc562340b27",
|
||||
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/MODULE.bazel": "af322bc08976524477c79d1e45e241b6efbeb918c497e8840b8ab116802dda79",
|
||||
"https://bcr.bazel.build/modules/zlib/1.3.1.bcr.3/source.json": "2be409ac3c7601245958cd4fcdff4288be79ed23bd690b4b951f500d54ee6e7d",
|
||||
"https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198"
|
||||
},
|
||||
"selectedYankedVersions": {},
|
||||
"moduleExtensions": {
|
||||
"@@rules_java~//java:rules_java_deps.bzl%compatibility_proxy": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "C4xqrMy1wN4iuTN6Z2eCm94S5XingHhD6uwrIXvCxVI=",
|
||||
"usagesDigest": "pwHZ+26iLgQdwvdZeA5wnAjKnNI3y6XO2VbhOTeo5h8=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
"generatedRepoSpecs": {
|
||||
"compatibility_proxy": {
|
||||
"bzlFile": "@@rules_java~//java:rules_java_deps.bzl",
|
||||
"ruleClassName": "_compatibility_proxy_repo_rule",
|
||||
"attributes": {}
|
||||
}
|
||||
},
|
||||
"recordedRepoMappingEntries": [
|
||||
[
|
||||
"rules_java~",
|
||||
"bazel_tools",
|
||||
"bazel_tools"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"@@rules_kotlin~//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "eecmTsmdIQveoA97hPtH3/Ej/kugbdCI24bhXIXaly8=",
|
||||
"usagesDigest": "aJF6fLy82rR95Ff5CZPAqxNoFgOMLMN5ImfBS0nhnkg=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
"generatedRepoSpecs": {
|
||||
"com_github_jetbrains_kotlin_git": {
|
||||
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:compiler.bzl",
|
||||
"ruleClassName": "kotlin_compiler_git_repository",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip"
|
||||
],
|
||||
"sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88"
|
||||
}
|
||||
},
|
||||
"com_github_jetbrains_kotlin": {
|
||||
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:compiler.bzl",
|
||||
"ruleClassName": "kotlin_capabilities_repository",
|
||||
"attributes": {
|
||||
"git_repository_name": "com_github_jetbrains_kotlin_git",
|
||||
"compiler_version": "1.9.23"
|
||||
}
|
||||
},
|
||||
"com_github_google_ksp": {
|
||||
"bzlFile": "@@rules_kotlin~//src/main/starlark/core/repositories:ksp.bzl",
|
||||
"ruleClassName": "ksp_compiler_plugin_repository",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip"
|
||||
],
|
||||
"sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d",
|
||||
"strip_version": "1.9.23-1.0.20"
|
||||
}
|
||||
},
|
||||
"com_github_pinterest_ktlint": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_file",
|
||||
"attributes": {
|
||||
"sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985",
|
||||
"urls": [
|
||||
"https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint"
|
||||
],
|
||||
"executable": true
|
||||
}
|
||||
},
|
||||
"rules_android": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806",
|
||||
"strip_prefix": "rules_android-0.1.1",
|
||||
"urls": [
|
||||
"https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"recordedRepoMappingEntries": [
|
||||
[
|
||||
"rules_kotlin~",
|
||||
"bazel_tools",
|
||||
"bazel_tools"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"@@rules_python~//python/uv:uv.bzl%uv": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "Xpqjnjzy6zZ90Es9Wa888ZLHhn7IsNGbph/e6qoxzw8=",
|
||||
"usagesDigest": "qI5PVlIum/YAnGJg5oXGHzDkMFWt2aNSUZY4G8PBbic=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
"generatedRepoSpecs": {
|
||||
"uv": {
|
||||
"bzlFile": "@@rules_python~//python/uv/private:uv_toolchains_repo.bzl",
|
||||
"ruleClassName": "uv_toolchains_repo",
|
||||
"attributes": {
|
||||
"toolchain_type": "'@@rules_python~//python/uv:uv_toolchain_type'",
|
||||
"toolchain_names": [
|
||||
"none"
|
||||
],
|
||||
"toolchain_implementations": {
|
||||
"none": "'@@rules_python~//python:none'"
|
||||
},
|
||||
"toolchain_compatible_with": {
|
||||
"none": [
|
||||
"@platforms//:incompatible"
|
||||
]
|
||||
},
|
||||
"toolchain_target_settings": {}
|
||||
}
|
||||
}
|
||||
},
|
||||
"recordedRepoMappingEntries": [
|
||||
[
|
||||
"rules_python~",
|
||||
"platforms",
|
||||
"platforms"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"@@rules_swift~//swift:extensions.bzl%non_module_deps": {
|
||||
"general": {
|
||||
"bzlTransitiveDigest": "l+Zu+SMObRQy3DG2LEw0eGVPkYRnyVj+M1QyR5AAFmM=",
|
||||
"usagesDigest": "l2vIL7SL4tJqHIVLWd78Y/ym+r0II64lmvoX8o+0Bb0=",
|
||||
"recordedFileInputs": {},
|
||||
"recordedDirentsInputs": {},
|
||||
"envVariables": {},
|
||||
"generatedRepoSpecs": {
|
||||
"com_github_apple_swift_protobuf": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/apple/swift-protobuf/archive/1.20.2.tar.gz"
|
||||
],
|
||||
"sha256": "3fb50bd4d293337f202d917b6ada22f9548a0a0aed9d9a4d791e6fbd8a246ebb",
|
||||
"strip_prefix": "swift-protobuf-1.20.2/",
|
||||
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_protobuf/BUILD.overlay"
|
||||
}
|
||||
},
|
||||
"com_github_grpc_grpc_swift": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/grpc/grpc-swift/archive/1.16.0.tar.gz"
|
||||
],
|
||||
"sha256": "58b60431d0064969f9679411264b82e40a217ae6bd34e17096d92cc4e47556a5",
|
||||
"strip_prefix": "grpc-swift-1.16.0/",
|
||||
"build_file": "@@rules_swift~//third_party:com_github_grpc_grpc_swift/BUILD.overlay"
|
||||
}
|
||||
},
|
||||
"com_github_apple_swift_docc_symbolkit": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/apple/swift-docc-symbolkit/archive/refs/tags/swift-5.10-RELEASE.tar.gz"
|
||||
],
|
||||
"sha256": "de1d4b6940468ddb53b89df7aa1a81323b9712775b0e33e8254fa0f6f7469a97",
|
||||
"strip_prefix": "swift-docc-symbolkit-swift-5.10-RELEASE",
|
||||
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_docc_symbolkit/BUILD.overlay"
|
||||
}
|
||||
},
|
||||
"com_github_apple_swift_nio": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/apple/swift-nio/archive/2.42.0.tar.gz"
|
||||
],
|
||||
"sha256": "e3304bc3fb53aea74a3e54bd005ede11f6dc357117d9b1db642d03aea87194a0",
|
||||
"strip_prefix": "swift-nio-2.42.0/",
|
||||
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio/BUILD.overlay"
|
||||
}
|
||||
},
|
||||
"com_github_apple_swift_nio_http2": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/apple/swift-nio-http2/archive/1.26.0.tar.gz"
|
||||
],
|
||||
"sha256": "f0edfc9d6a7be1d587e5b403f2d04264bdfae59aac1d74f7d974a9022c6d2b25",
|
||||
"strip_prefix": "swift-nio-http2-1.26.0/",
|
||||
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio_http2/BUILD.overlay"
|
||||
}
|
||||
},
|
||||
"com_github_apple_swift_nio_transport_services": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/apple/swift-nio-transport-services/archive/1.15.0.tar.gz"
|
||||
],
|
||||
"sha256": "f3498dafa633751a52b9b7f741f7ac30c42bcbeb3b9edca6d447e0da8e693262",
|
||||
"strip_prefix": "swift-nio-transport-services-1.15.0/",
|
||||
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio_transport_services/BUILD.overlay"
|
||||
}
|
||||
},
|
||||
"com_github_apple_swift_nio_extras": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/apple/swift-nio-extras/archive/1.4.0.tar.gz"
|
||||
],
|
||||
"sha256": "4684b52951d9d9937bb3e8ccd6b5daedd777021ef2519ea2f18c4c922843b52b",
|
||||
"strip_prefix": "swift-nio-extras-1.4.0/",
|
||||
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio_extras/BUILD.overlay"
|
||||
}
|
||||
},
|
||||
"com_github_apple_swift_log": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/apple/swift-log/archive/1.4.4.tar.gz"
|
||||
],
|
||||
"sha256": "48fe66426c784c0c20031f15dc17faf9f4c9037c192bfac2f643f65cb2321ba0",
|
||||
"strip_prefix": "swift-log-1.4.4/",
|
||||
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_log/BUILD.overlay"
|
||||
}
|
||||
},
|
||||
"com_github_apple_swift_nio_ssl": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/apple/swift-nio-ssl/archive/2.23.0.tar.gz"
|
||||
],
|
||||
"sha256": "4787c63f61dd04d99e498adc3d1a628193387e41efddf8de19b8db04544d016d",
|
||||
"strip_prefix": "swift-nio-ssl-2.23.0/",
|
||||
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_nio_ssl/BUILD.overlay"
|
||||
}
|
||||
},
|
||||
"com_github_apple_swift_collections": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/apple/swift-collections/archive/1.0.4.tar.gz"
|
||||
],
|
||||
"sha256": "d9e4c8a91c60fb9c92a04caccbb10ded42f4cb47b26a212bc6b39cc390a4b096",
|
||||
"strip_prefix": "swift-collections-1.0.4/",
|
||||
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_collections/BUILD.overlay"
|
||||
}
|
||||
},
|
||||
"com_github_apple_swift_atomics": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"urls": [
|
||||
"https://github.com/apple/swift-atomics/archive/1.1.0.tar.gz"
|
||||
],
|
||||
"sha256": "1bee7f469f7e8dc49f11cfa4da07182fbc79eab000ec2c17bfdce468c5d276fb",
|
||||
"strip_prefix": "swift-atomics-1.1.0/",
|
||||
"build_file": "@@rules_swift~//third_party:com_github_apple_swift_atomics/BUILD.overlay"
|
||||
}
|
||||
},
|
||||
"build_bazel_rules_swift_index_import": {
|
||||
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
|
||||
"ruleClassName": "http_archive",
|
||||
"attributes": {
|
||||
"build_file": "@@rules_swift~//third_party:build_bazel_rules_swift_index_import/BUILD.overlay",
|
||||
"canonical_id": "index-import-5.8",
|
||||
"urls": [
|
||||
"https://github.com/MobileNativeFoundation/index-import/releases/download/5.8.0.1/index-import.tar.gz"
|
||||
],
|
||||
"sha256": "28c1ffa39d99e74ed70623899b207b41f79214c498c603915aef55972a851a15"
|
||||
}
|
||||
},
|
||||
"build_bazel_rules_swift_local_config": {
|
||||
"bzlFile": "@@rules_swift~//swift/internal:swift_autoconfiguration.bzl",
|
||||
"ruleClassName": "swift_autoconfiguration",
|
||||
"attributes": {}
|
||||
}
|
||||
},
|
||||
"recordedRepoMappingEntries": [
|
||||
[
|
||||
"rules_swift~",
|
||||
"bazel_tools",
|
||||
"bazel_tools"
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
# This file marks sparkle_workspace as a separate Bazel workspace.
|
||||
# It prevents Bazel from looking up to the parent directory's workspace.
|
||||
# The actual dependencies are defined in MODULE.bazel (bzlmod).
|
||||
@@ -95,16 +95,9 @@ auto ConvertUnit(
|
||||
shardokUnit.mutate_stun_rounds_remaining(0);
|
||||
|
||||
for (const PlayerId pid : allPlayerIds) {
|
||||
int8_t initialKnowledge = 0;
|
||||
for (const auto visibleToPid : unit.visible_to_player_ids()) {
|
||||
if (visibleToPid == pid) {
|
||||
initialKnowledge = 100;
|
||||
break;
|
||||
}
|
||||
}
|
||||
shardokUnit.mutable_opponent_knowledge()->Mutate(
|
||||
static_cast<flatbuffers::uoffset_t>(pid),
|
||||
initialKnowledge);
|
||||
0);
|
||||
}
|
||||
|
||||
shardokUnit.mutate_has_moved_in_zoc(false);
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
#include <cstring>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -148,9 +147,6 @@ public:
|
||||
|
||||
static auto FromPath(const string& path) -> byte_vector {
|
||||
std::ifstream inputFileStream(path, std::ios::binary | std::ios::ate);
|
||||
if (!inputFileStream.is_open()) {
|
||||
throw std::runtime_error("Failed to open file: " + path);
|
||||
}
|
||||
const std::streamsize size = inputFileStream.tellg();
|
||||
inputFileStream.seekg(0, std::ios::beg);
|
||||
byte_vector serializedGame((size_t(size)));
|
||||
|
||||
@@ -32,32 +32,6 @@ CoordsSet AICommandFilter::BuildEnemyLocations(const GameStateW& gameState, Play
|
||||
return enemyLocations;
|
||||
}
|
||||
|
||||
std::vector<size_t> AICommandFilter::FilterLoopingCommands(
|
||||
const CommandListSPtr& commands,
|
||||
const GameStateW& gameState) {
|
||||
std::vector<size_t> filteredIndices;
|
||||
filteredIndices.reserve(commands->size());
|
||||
|
||||
for (size_t i = 0; i < commands->size(); ++i) {
|
||||
const auto& cmd = (*commands)[i];
|
||||
|
||||
bool shouldFilter = false;
|
||||
|
||||
if (cmd->GetCommandType() == CommandType::METEOR_TARGET_COMMAND ||
|
||||
cmd->GetCommandType() == CommandType::METEOR_CANCEL_COMMAND) {
|
||||
const int unitId = cmd->GetActorUnitId();
|
||||
const Unit* unit = gameState->units()->Get(unitId);
|
||||
if (unit->attached_hero().profession_info().cast_target().row() > -1) {
|
||||
shouldFilter = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldFilter) { filteredIndices.push_back(i); }
|
||||
}
|
||||
|
||||
return filteredIndices;
|
||||
}
|
||||
|
||||
std::vector<size_t> AICommandFilter::FilterCommands(
|
||||
const CommandListSPtr& commands,
|
||||
PlayerId pid,
|
||||
@@ -164,17 +138,6 @@ bool AICommandFilter::IsWastefulAction(
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandType::METEOR_TARGET_COMMAND:
|
||||
case CommandType::METEOR_CANCEL_COMMAND: {
|
||||
// Filter out re-target/cancel when the mage already has a target.
|
||||
// Re-targeting exists for human misclick correction; the AI commits
|
||||
// to its first target choice and doesn't need to reconsider.
|
||||
const int unitId = cmd.GetActorUnitId();
|
||||
const Unit* unit = gameState->units()->Get(unitId);
|
||||
if (unit->attached_hero().profession_info().cast_target().row() > -1) { return true; }
|
||||
break;
|
||||
}
|
||||
|
||||
case CommandType::START_FIRE_COMMAND: {
|
||||
// Fire spell filtering - be very restrictive for attackers
|
||||
// Fire only affects adjacent tiles and lasts multiple rounds
|
||||
|
||||
@@ -44,18 +44,6 @@ public:
|
||||
const APDCache& apdCache,
|
||||
const BattalionTypeGetter& battalionTypeLookup);
|
||||
|
||||
/**
|
||||
* Lightweight filter for the root of the search tree.
|
||||
* Only removes commands that are genuinely never useful (e.g. meteor
|
||||
* re-targeting when a target is already set), as opposed to the full
|
||||
* heuristic filter which aggressively prunes for lookahead performance.
|
||||
* The full filter could miss good moves at the root that only look
|
||||
* bad locally but prove worthwhile with deeper search.
|
||||
*/
|
||||
static std::vector<size_t> FilterLoopingCommands(
|
||||
const CommandListSPtr& commands,
|
||||
const GameStateW& gameState);
|
||||
|
||||
private:
|
||||
// Helper to build enemy locations once for efficiency
|
||||
static CoordsSet BuildEnemyLocations(const GameStateW& gameState, PlayerId pid);
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
#include "AIAttackerStrategySelector.hpp"
|
||||
#include "AICommandEvaluator.hpp"
|
||||
#include "AICommandFilter.hpp"
|
||||
#include "TranspositionTable.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/score/AIScoreCalculator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
@@ -60,12 +59,6 @@ auto IterativeDeepeningAI::IterativeSearch(
|
||||
return result;
|
||||
}
|
||||
|
||||
// Lightweight root filter: only remove genuinely never-useful commands
|
||||
// (e.g. meteor re-targeting). The full aggressive filter is reserved for
|
||||
// lookahead where pruning is a performance optimization, not a decision.
|
||||
const std::vector<size_t> rootFilteredIndices =
|
||||
AICommandFilter::FilterLoopingCommands(commands, state);
|
||||
|
||||
// Check if we're in SET_UP phase and enforce maximum depth limit
|
||||
bool isSetupPhase =
|
||||
(state->status()->state() ==
|
||||
@@ -97,8 +90,7 @@ auto IterativeDeepeningAI::IterativeSearch(
|
||||
std::vector<size_t> sortedIndices = GetCommandsSortedByPreviousDepth(
|
||||
currentDepth,
|
||||
scoresByDepth,
|
||||
highestDepthCompleted,
|
||||
rootFilteredIndices);
|
||||
highestDepthCompleted);
|
||||
|
||||
size_t evaluatedCount = 0;
|
||||
bool allEvaluated = true;
|
||||
@@ -352,14 +344,14 @@ auto IterativeDeepeningAI::SearchCommandAtDepthWithEngine(
|
||||
auto IterativeDeepeningAI::GetCommandsSortedByPreviousDepth(
|
||||
const size_t currentDepth,
|
||||
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
|
||||
const std::vector<size_t>& highestDepthCompleted,
|
||||
const std::vector<size_t>& filteredIndices) -> std::vector<size_t> {
|
||||
if (currentDepth == 1) {
|
||||
// For depth 1, return filtered indices in natural order
|
||||
return filteredIndices;
|
||||
}
|
||||
const std::vector<size_t>& highestDepthCompleted) -> std::vector<size_t> {
|
||||
std::vector<size_t> indices(scoresByDepth.size());
|
||||
std::iota(indices.begin(), indices.end(), 0);
|
||||
|
||||
std::vector<size_t> indices = filteredIndices;
|
||||
if (currentDepth == 1) {
|
||||
// For depth 1, return natural order
|
||||
return indices;
|
||||
}
|
||||
|
||||
// Sort by score at previous depth
|
||||
const size_t prevDepth = currentDepth - 1;
|
||||
|
||||
@@ -101,8 +101,7 @@ private:
|
||||
[[nodiscard]] static std::vector<size_t> GetCommandsSortedByPreviousDepth(
|
||||
size_t currentDepth,
|
||||
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
|
||||
const std::vector<size_t>& highestDepthCompleted,
|
||||
const std::vector<size_t>& filteredIndices);
|
||||
const std::vector<size_t>& highestDepthCompleted);
|
||||
|
||||
[[nodiscard]] static SearchResult SelectBestResult(
|
||||
const std::vector<std::vector<ScoreValue>>& scoresByDepth,
|
||||
|
||||
@@ -20,9 +20,6 @@
|
||||
#include <sstream>
|
||||
#endif
|
||||
|
||||
#include <set>
|
||||
#include <unordered_map>
|
||||
|
||||
#include "AIAttackerStrategySelector.hpp"
|
||||
#include "AIConfig.hpp"
|
||||
#include "AIDefenderStrategySelector.hpp"
|
||||
@@ -174,117 +171,8 @@ auto ShardokAIClient::StandardChooseCommandIndex(
|
||||
// }
|
||||
// }
|
||||
|
||||
assert(commandCount == realAvailableCommands->size());
|
||||
// Verify that the AI's guessed state produces the same available commands as reality
|
||||
if (commandCount != realAvailableCommands->size()) {
|
||||
fprintf(stderr,
|
||||
"AI command count mismatch: guessed=%zu real=%zu player=%d round=%d\n",
|
||||
commandCount,
|
||||
realAvailableCommands->size(),
|
||||
playerId,
|
||||
guessedState->current_round());
|
||||
|
||||
const auto maxDump = std::max(commandCount, realAvailableCommands->size());
|
||||
for (size_t i = 0; i < maxDump; i++) {
|
||||
const auto *realName = i < realAvailableCommands->size()
|
||||
? net::eagle0::shardok::common::CommandType_Name(
|
||||
(*realAvailableCommands)[i]->GetCommandType())
|
||||
.c_str()
|
||||
: "(none)";
|
||||
const auto *guessedName = i < commandCount
|
||||
? net::eagle0::shardok::common::CommandType_Name(
|
||||
(*guessedCommands)[i]->GetCommandType())
|
||||
.c_str()
|
||||
: "(none)";
|
||||
const int realActor = i < realAvailableCommands->size()
|
||||
? (*realAvailableCommands)[i]->GetActorUnitId()
|
||||
: -1;
|
||||
const int guessedActor =
|
||||
i < commandCount ? (*guessedCommands)[i]->GetActorUnitId() : -1;
|
||||
const int realRow = i < realAvailableCommands->size()
|
||||
? (*realAvailableCommands)[i]->GetTargetRow()
|
||||
: -1;
|
||||
const int realCol = i < realAvailableCommands->size()
|
||||
? (*realAvailableCommands)[i]->GetTargetColumn()
|
||||
: -1;
|
||||
const int guessedRow = i < commandCount ? (*guessedCommands)[i]->GetTargetRow() : -1;
|
||||
const int guessedCol = i < commandCount ? (*guessedCommands)[i]->GetTargetColumn() : -1;
|
||||
fprintf(stderr,
|
||||
" [%zu] real: %s unit=%d (%d,%d) guessed: %s unit=%d (%d,%d)\n",
|
||||
i,
|
||||
realName,
|
||||
realActor,
|
||||
realRow,
|
||||
realCol,
|
||||
guessedName,
|
||||
guessedActor,
|
||||
guessedRow,
|
||||
guessedCol);
|
||||
}
|
||||
|
||||
// Find positions in real commands that are missing from guessed commands
|
||||
// Group by unit to find the missing position per unit
|
||||
std::unordered_map<int, std::set<std::pair<int, int>>> realPositions;
|
||||
std::unordered_map<int, std::set<std::pair<int, int>>> guessedPositions;
|
||||
for (size_t i = 0; i < realAvailableCommands->size(); i++) {
|
||||
const auto &cmd = (*realAvailableCommands)[i];
|
||||
realPositions[cmd->GetActorUnitId()].emplace(
|
||||
cmd->GetTargetRow(),
|
||||
cmd->GetTargetColumn());
|
||||
}
|
||||
for (size_t i = 0; i < commandCount; i++) {
|
||||
const auto &cmd = (*guessedCommands)[i];
|
||||
guessedPositions[cmd->GetActorUnitId()].emplace(
|
||||
cmd->GetTargetRow(),
|
||||
cmd->GetTargetColumn());
|
||||
}
|
||||
for (const auto &[unitId, positions] : realPositions) {
|
||||
for (const auto &[row, col] : positions) {
|
||||
if (!guessedPositions[unitId].contains({row, col})) {
|
||||
fprintf(stderr,
|
||||
" Missing from guessed: unit=%d pos=(%d,%d)",
|
||||
unitId,
|
||||
row,
|
||||
col);
|
||||
const Coords missingCoords(row, col);
|
||||
const auto *occupant = guessedState.GetOccupant(missingCoords);
|
||||
if (occupant) {
|
||||
fprintf(stderr,
|
||||
" -> guessed occupant: unit_id=%d player=%d status=%d "
|
||||
"loc=(%d,%d) hidden=%d battalion_size=%d\n",
|
||||
occupant->unit_id(),
|
||||
occupant->player_id(),
|
||||
static_cast<int>(occupant->status()),
|
||||
occupant->location().row(),
|
||||
occupant->location().column(),
|
||||
occupant->hidden(),
|
||||
occupant->battalion().size());
|
||||
} else {
|
||||
fprintf(stderr, " -> no occupant in guessed state\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dump all guessed state units for full picture
|
||||
fprintf(stderr, " Guessed state units (%u total):\n", guessedState->units()->size());
|
||||
for (size_t i = 0; i < guessedState->units()->size(); i++) {
|
||||
const auto *u = guessedState->units()->Get(static_cast<unsigned int>(i));
|
||||
fprintf(stderr,
|
||||
" unit_id=%d player=%d status=%d loc=(%d,%d) hidden=%d "
|
||||
"starting_pos_idx=%d\n",
|
||||
u->unit_id(),
|
||||
u->player_id(),
|
||||
static_cast<int>(u->status()),
|
||||
u->location().row(),
|
||||
u->location().column(),
|
||||
u->hidden(),
|
||||
u->starting_position_index());
|
||||
}
|
||||
|
||||
throw ShardokInternalErrorException(
|
||||
"AI command count mismatch: guessed=" + std::to_string(commandCount) +
|
||||
" real=" + std::to_string(realAvailableCommands->size()));
|
||||
}
|
||||
for (size_t i = 0; i < commandCount; i++) {
|
||||
CheckCommand((*realAvailableCommands)[i], (*guessedCommands)[i]);
|
||||
}
|
||||
|
||||
@@ -93,8 +93,7 @@ auto AbstractAIScoreCalculator::CalculateUnitsScoreComponents(
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT: break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT: break;
|
||||
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT:
|
||||
throw ShardokInternalErrorException("Unknown unit status");
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
#include "ShardokGameController.hpp"
|
||||
|
||||
#include <execinfo.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <ranges>
|
||||
@@ -155,43 +153,28 @@ void ShardokGameController::DoAIThread() {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
// Phase 2: AI thinks (NO LOCK - this is the slow part)
|
||||
const auto results = aiClient->ChooseCommandIndex(settings, gsv, availableCommands);
|
||||
// Phase 2: AI thinks (NO LOCK - this is the slow part)
|
||||
const auto results = aiClient->ChooseCommandIndex(settings, gsv, availableCommands);
|
||||
|
||||
// Phase 3: Post the command (brief lock)
|
||||
{
|
||||
unique_lock lk(masterLock);
|
||||
// Phase 3: Post the command (brief lock)
|
||||
{
|
||||
unique_lock lk(masterLock);
|
||||
|
||||
// Verify state hasn't changed while we were thinking
|
||||
if (engine->GetUnfilteredHistoryCount() != expectedHistoryCount) {
|
||||
// State changed (e.g., human posted command) - re-evaluate
|
||||
printf("AI: State changed while thinking, re-evaluating\n");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (engine->GameIsOver()) {
|
||||
aiThreadKeepGoing = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
engine->PostCommand(playerId, results.chosenIndex);
|
||||
LockedNotifyClients();
|
||||
aiThreadKeepGoing = !engine->GameIsOver();
|
||||
// Verify state hasn't changed while we were thinking
|
||||
if (engine->GetUnfilteredHistoryCount() != expectedHistoryCount) {
|
||||
// State changed (e.g., human posted command) - re-evaluate
|
||||
printf("AI: State changed while thinking, re-evaluating\n");
|
||||
continue;
|
||||
}
|
||||
} catch (const std::exception &e) {
|
||||
fprintf(stderr,
|
||||
"AI thread exception in game %s, player %d, round %d: %s\n",
|
||||
cachedGameId.c_str(),
|
||||
playerId,
|
||||
gsv.current_round(),
|
||||
e.what());
|
||||
|
||||
void *backtraceArray[20];
|
||||
const int backtraceSize = backtrace(backtraceArray, 20);
|
||||
backtrace_symbols_fd(backtraceArray, backtraceSize, STDERR_FILENO);
|
||||
if (engine->GameIsOver()) {
|
||||
aiThreadKeepGoing = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw;
|
||||
engine->PostCommand(playerId, results.chosenIndex);
|
||||
LockedNotifyClients();
|
||||
aiThreadKeepGoing = !engine->GameIsOver();
|
||||
}
|
||||
}
|
||||
printf("Exiting AI thread.\n");
|
||||
|
||||
@@ -29,17 +29,6 @@ class AvailableCommandsFactoryImpl : public AvailableCommandsFactory {
|
||||
private:
|
||||
const SettingsGetter settings;
|
||||
|
||||
static auto CannotBecomeOutlaw(const GameStateW &gameState, PlayerId pid) -> bool {
|
||||
if (pid == UNCONTROLLED_PLAYER_ID) return false;
|
||||
|
||||
const auto &player = std::find_if(
|
||||
begin(*gameState->player_infos()),
|
||||
end(*gameState->player_infos()),
|
||||
[pid](const PlayerInfoFb *pi) { return pi->player_id() == pid; });
|
||||
|
||||
return (player != end(*gameState->player_infos()) && player->cannot_become_outlaw());
|
||||
}
|
||||
|
||||
static auto IsAttacker(const GameStateW &gameState, PlayerId pid) -> bool {
|
||||
if (pid == UNCONTROLLED_PLAYER_ID) return false;
|
||||
|
||||
@@ -67,18 +56,13 @@ public:
|
||||
commandFactories(CommandFactoriesList(settings).GetFactories()),
|
||||
playerSetupCommandFactory(settings) {}
|
||||
|
||||
[[nodiscard]] auto GetPlayerSetupCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId playerId,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const
|
||||
[[nodiscard]] auto GetPlayerSetupCommands(const GameStateW &gameState, PlayerId playerId) const
|
||||
-> CommandListSPtr override;
|
||||
|
||||
[[nodiscard]] auto GetAvailableCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId player,
|
||||
bool includeFollowUps,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const
|
||||
-> CommandListSPtr override;
|
||||
bool includeFollowUps) const -> CommandListSPtr override;
|
||||
};
|
||||
|
||||
auto AvailableCommandsFactory::MakeAvailableCommandsFactory(const SettingsGetter &settings)
|
||||
@@ -88,14 +72,9 @@ auto AvailableCommandsFactory::MakeAvailableCommandsFactory(const SettingsGetter
|
||||
|
||||
auto AvailableCommandsFactoryImpl::GetPlayerSetupCommands(
|
||||
const GameStateW &gameState,
|
||||
const PlayerId playerId,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const -> CommandListSPtr {
|
||||
const PlayerId playerId) const -> CommandListSPtr {
|
||||
CommandList previewCommands{};
|
||||
playerSetupCommandFactory.AddAvailablePlayerSetupCommands(
|
||||
previewCommands,
|
||||
playerId,
|
||||
gameState,
|
||||
reinforcementPositions);
|
||||
playerSetupCommandFactory.AddAvailablePlayerSetupCommands(previewCommands, playerId, gameState);
|
||||
|
||||
return make_shared<CommandList>(previewCommands);
|
||||
}
|
||||
@@ -109,7 +88,6 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
|
||||
const bool hasHero = unit->has_attached_hero();
|
||||
const auto allyPids = AlliedPids(gameState, unit->player_id());
|
||||
const bool isAttacker = IsAttacker(gameState, unit->player_id());
|
||||
const bool cannotBecomeOutlaw = CannotBecomeOutlaw(gameState, unit->player_id());
|
||||
|
||||
const bool unitMovedIntoZoc = unit->has_moved_in_zoc();
|
||||
const ActionPoints remainingActionPoints = unit->remaining_action_points();
|
||||
@@ -146,7 +124,6 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
|
||||
.units = gameState->units(),
|
||||
.allyPids = allyPids,
|
||||
.isAttacker = isAttacker,
|
||||
.cannotBecomeOutlaw = cannotBecomeOutlaw,
|
||||
.unitMovedIntoZoc = unitMovedIntoZoc};
|
||||
|
||||
for (const auto &commandFactory : commandFactories) {
|
||||
@@ -191,14 +168,12 @@ void AvailableCommandsFactoryImpl::AddAvailableCommandsForOneUnit(
|
||||
auto AvailableCommandsFactoryImpl::GetAvailableCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId playerId,
|
||||
bool includeFollowUps,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const -> CommandListSPtr {
|
||||
bool includeFollowUps) const -> CommandListSPtr {
|
||||
CommandList commands{};
|
||||
|
||||
vector<AdjacentTile> adjacentTiles;
|
||||
|
||||
playerSetupCommandFactory
|
||||
.AddAvailablePlayerSetupCommands(commands, playerId, gameState, reinforcementPositions);
|
||||
playerSetupCommandFactory.AddAvailablePlayerSetupCommands(commands, playerId, gameState);
|
||||
if (!commands.empty()) return make_shared<CommandList>(commands);
|
||||
|
||||
for (const Unit *unit : *gameState->units()) {
|
||||
|
||||
@@ -9,12 +9,8 @@
|
||||
#ifndef AvailableCommandsFactory_hpp
|
||||
#define AvailableCommandsFactory_hpp
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
|
||||
|
||||
@@ -33,16 +29,12 @@ public:
|
||||
|
||||
[[nodiscard]] virtual auto GetPlayerSetupCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId playerId,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions = std::nullopt) const
|
||||
-> CommandListSPtr = 0;
|
||||
PlayerId playerId) const -> CommandListSPtr = 0;
|
||||
|
||||
[[nodiscard]] virtual auto GetAvailableCommands(
|
||||
const GameStateW &gameState,
|
||||
PlayerId player,
|
||||
bool includeFollowUps,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions = std::nullopt) const
|
||||
-> CommandListSPtr = 0;
|
||||
bool includeFollowUps) const -> CommandListSPtr = 0;
|
||||
};
|
||||
} // namespace shardok
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library/actions:update_game_status_action",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/actions:update_opponent_knowledge_action",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/fb_helpers:game_state_helpers",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/tutorial:tutorial_battle_controller",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:game_state_validator",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/view_filters:action_result_filter",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/view_filters:game_state_filter",
|
||||
@@ -58,7 +57,6 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library/actions:new_round_action",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/command_factories:command_factories_list",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/commands:end_turn_command",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:coordinates",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/unit",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
|
||||
@@ -286,49 +286,21 @@ void ShardokEngine::HandlePlayerTurnEnd(const std::shared_ptr<RandomGenerator> &
|
||||
true /* atEndOfRound */)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
// Check for tutorial events at end of round
|
||||
if (!GameIsOver() && tutorialController_.IsEnabled()) {
|
||||
auto tutorialEventResults = tutorialController_.CheckAndExecuteEvents(
|
||||
gameState,
|
||||
settingsGetter,
|
||||
randomGenerator);
|
||||
ApplyAndAddActionResults(
|
||||
UpdateOpponentKnowledgeAction(settingsGetter).Execute(gameState, randomGenerator));
|
||||
|
||||
if (!tutorialEventResults.actionResults.empty()) {
|
||||
ApplyAndAddActionResults(tutorialEventResults.actionResults);
|
||||
ApplyAndAddActionResults(
|
||||
NewRoundAction(gameState, settingsGetter).Execute(gameState, randomGenerator));
|
||||
|
||||
// Update game status after tutorial events (e.g., attackers fled, defender wins)
|
||||
ApplyAndAddActionResults(UpdateGameStatusAction(
|
||||
GetCurrentGameState(),
|
||||
criticalTileCoords,
|
||||
settingsGetter,
|
||||
true /* atEndOfRound */)
|
||||
.Execute(gameState, randomGenerator));
|
||||
ApplyAndAddActionResults(
|
||||
StartPlayerTurnAction(gameState, UNCONTROLLED_PLAYER_ID, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
// If tutorial events ended the game, don't start new round
|
||||
if (GameIsOver()) { return; }
|
||||
ApplyAndAddActionResults(PerformUndeadCommandsAction(gameState, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
// If reinforcement placement is needed, pause the turn flow
|
||||
if (tutorialEventResults.reinforcementPlacement.has_value()) {
|
||||
pendingReinforcementPlacement_ = tutorialEventResults.reinforcementPlacement;
|
||||
|
||||
// Set state to REINFORCEMENT_PLACEMENT
|
||||
ActionResultProto placementStateAction{};
|
||||
placementStateAction.set_type(
|
||||
net::eagle0::shardok::common::TUTORIAL_REINFORCEMENTS_ARRIVED);
|
||||
placementStateAction.mutable_game_status()->set_state(
|
||||
GameStatusProto::State::GameStatus_State_REINFORCEMENT_PLACEMENT);
|
||||
*placementStateAction.mutable_game_status()->mutable_description() =
|
||||
"Place your reinforcements!";
|
||||
// Set current player to the reinforcement recipient
|
||||
placementStateAction.mutable_next_player()->set_value(
|
||||
pendingReinforcementPlacement_->playerId);
|
||||
ApplyAndAddActionResult(placementStateAction);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ResumeAfterReinforcementPlacement(randomGenerator);
|
||||
ApplyAndAddActionResults(StartPlayerTurnAction(gameState, 0, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
} else {
|
||||
const auto updateAction = UpdateGameStatusAction(
|
||||
@@ -344,27 +316,6 @@ void ShardokEngine::HandlePlayerTurnEnd(const std::shared_ptr<RandomGenerator> &
|
||||
}
|
||||
}
|
||||
|
||||
void ShardokEngine::ResumeAfterReinforcementPlacement(
|
||||
const std::shared_ptr<RandomGenerator> &randomGenerator) {
|
||||
pendingReinforcementPlacement_ = std::nullopt;
|
||||
|
||||
ApplyAndAddActionResults(
|
||||
UpdateOpponentKnowledgeAction(settingsGetter).Execute(gameState, randomGenerator));
|
||||
|
||||
ApplyAndAddActionResults(
|
||||
NewRoundAction(gameState, settingsGetter).Execute(gameState, randomGenerator));
|
||||
|
||||
ApplyAndAddActionResults(
|
||||
StartPlayerTurnAction(gameState, UNCONTROLLED_PLAYER_ID, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
ApplyAndAddActionResults(PerformUndeadCommandsAction(gameState, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
|
||||
ApplyAndAddActionResults(StartPlayerTurnAction(gameState, 0, settingsGetter)
|
||||
.Execute(gameState, randomGenerator));
|
||||
}
|
||||
|
||||
void ShardokEngine::PostPlacementCommands(
|
||||
const PlayerId player,
|
||||
const vector<UnitPlacementInfo> &placementInfos,
|
||||
@@ -373,15 +324,8 @@ void ShardokEngine::PostPlacementCommands(
|
||||
throw ShardokClientErrorException("Posting for a player who's not the current player ID!");
|
||||
}
|
||||
|
||||
const auto reinforcementPositions =
|
||||
pendingReinforcementPlacement_.has_value()
|
||||
? std::optional<std::vector<Coords>>(
|
||||
pendingReinforcementPlacement_->startingPositions)
|
||||
: std::nullopt;
|
||||
const auto placementCommands = availableCommandsFactory->GetPlayerSetupCommands(
|
||||
gameState,
|
||||
player,
|
||||
reinforcementPositions);
|
||||
const auto placementCommands =
|
||||
availableCommandsFactory->GetPlayerSetupCommands(gameState, player);
|
||||
|
||||
// first make sure they're all valid and there are no duplicates
|
||||
for (size_t i = 0; i < placementInfos.size(); i++) {
|
||||
@@ -421,19 +365,6 @@ void ShardokEngine::PostPlacementCommands(
|
||||
}
|
||||
}
|
||||
|
||||
// If we were in reinforcement placement, check if all units are now placed
|
||||
if (pendingReinforcementPlacement_.has_value()) {
|
||||
const auto reserveUnits =
|
||||
ReserveUnitsForPlayer(gameState->units(), pendingReinforcementPlacement_->playerId);
|
||||
if (reserveUnits.empty()) {
|
||||
// All reinforcements placed — resume the turn flow
|
||||
ResumeAfterReinforcementPlacement(randomGenerator);
|
||||
return;
|
||||
}
|
||||
// Still have units to place — stay in REINFORCEMENT_PLACEMENT state
|
||||
return;
|
||||
}
|
||||
|
||||
const auto updateAction = UpdateGameStatusAction(
|
||||
GetCurrentGameState(),
|
||||
criticalTileCoords,
|
||||
@@ -632,16 +563,10 @@ auto ShardokEngine::GetAvailableCommandProtos(const PlayerId playerId, const boo
|
||||
auto ShardokEngine::UncachedGetAvailableCommands(
|
||||
const PlayerId playerId,
|
||||
const bool includeFollowUps) const -> CommandListSPtr {
|
||||
const auto reinforcementPositions =
|
||||
pendingReinforcementPlacement_.has_value()
|
||||
? std::optional<std::vector<Coords>>(
|
||||
pendingReinforcementPlacement_->startingPositions)
|
||||
: std::nullopt;
|
||||
return availableCommandsFactory->GetAvailableCommands(
|
||||
gameState,
|
||||
playerId,
|
||||
/* includeFollowUps=*/includeFollowUps,
|
||||
reinforcementPositions);
|
||||
/* includeFollowUps=*/includeFollowUps);
|
||||
}
|
||||
|
||||
void AddUnits(vector<net::eagle0::shardok::storage::ResolvedUnit> &to, const Units &from) {
|
||||
@@ -673,7 +598,6 @@ void AddUnits(vector<net::eagle0::shardok::storage::ResolvedUnit> &to, const Uni
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT:
|
||||
ru.set_status(
|
||||
net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT);
|
||||
break;
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/fb_helpers/GameStateHelpers.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/CoordsSet.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/tutorial/TutorialBattleController.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/action_result_view.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/game_state_view.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/api/unit_view.pb.h"
|
||||
@@ -37,7 +36,6 @@ using net::eagle0::shardok::api::UnitView;
|
||||
using PlayerInfoProto = net::eagle0::shardok::common::PlayerInfo;
|
||||
using net::eagle0::shardok::storage::ShardokActionWithResultingState;
|
||||
using HexMapProto = net::eagle0::shardok::common::HexMap;
|
||||
using TutorialBattleConfigProto = net::eagle0::common::TutorialBattleConfig;
|
||||
|
||||
class ShardokEngine {
|
||||
private:
|
||||
@@ -54,9 +52,6 @@ private:
|
||||
|
||||
const CoordsSet criticalTileCoords;
|
||||
|
||||
TutorialBattleController tutorialController_;
|
||||
std::optional<ReinforcementPlacementInfo> pendingReinforcementPlacement_;
|
||||
|
||||
mutable CommandListSPtr cachedAvailableCommands{};
|
||||
|
||||
void ApplyAndAddActionResult(const ActionResult &result);
|
||||
@@ -77,7 +72,6 @@ private:
|
||||
}
|
||||
|
||||
void HandlePlayerTurnEnd(const std::shared_ptr<RandomGenerator> &randomGenerator);
|
||||
void ResumeAfterReinforcementPlacement(const std::shared_ptr<RandomGenerator> &randomGenerator);
|
||||
[[nodiscard]] auto UncachedGetAvailableCommands(PlayerId playerId, bool includeFollowUps) const
|
||||
-> CommandListSPtr;
|
||||
|
||||
@@ -203,17 +197,6 @@ public:
|
||||
|
||||
[[nodiscard]] auto GetGameSettings() const -> GameSettingsSPtr { return gameSettings; }
|
||||
|
||||
/// Configure the tutorial battle controller (call after construction if needed)
|
||||
void SetTutorialBattleConfig(const TutorialBattleConfigProto &config) {
|
||||
tutorialController_ = TutorialBattleController(config);
|
||||
}
|
||||
|
||||
/// Returns the pending reinforcement placement info, if any.
|
||||
[[nodiscard]] auto GetPendingReinforcementPlacement() const
|
||||
-> const std::optional<ReinforcementPlacementInfo> & {
|
||||
return pendingReinforcementPlacement_;
|
||||
}
|
||||
|
||||
static inline auto GameIsOver(const fb::GameStatus *status) -> bool {
|
||||
return (status->state() == net::eagle0::shardok::storage::fb::GameStatus_::State_VICTORY);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,6 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library/actions:end_player_setup_command",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/actions:place_hidden_unit_command",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/actions:place_unit_command",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:coordinates",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:hex_map_utils",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:player_utils",
|
||||
|
||||
+3
-22
@@ -66,30 +66,11 @@ auto PlayerSetupCommandFactory::AddAvailablePlaceAndHideUnitCommandsForOneUnit(
|
||||
auto PlayerSetupCommandFactory::AddAvailablePlayerSetupCommands(
|
||||
CommandList &existingCommands,
|
||||
PlayerId playerId,
|
||||
const GameStateW &gameState,
|
||||
const std::optional<std::vector<Coords>> &reinforcementPositions) const -> void {
|
||||
const GameStateW &gameState) const -> void {
|
||||
if (playerId < 0) return;
|
||||
|
||||
const auto state = gameState->status()->state();
|
||||
|
||||
// Handle reinforcement placement phase
|
||||
if (state == net::eagle0::shardok::storage::fb::GameStatus_::State_REINFORCEMENT_PLACEMENT) {
|
||||
if (!reinforcementPositions.has_value()) return;
|
||||
|
||||
const auto unplacedUnits = ReserveUnitsForPlayer(gameState->units(), playerId);
|
||||
if (unplacedUnits.empty()) return;
|
||||
|
||||
for (const auto &[unitId, unit] : unplacedUnits) {
|
||||
for (const Coords &position : *reinforcementPositions) {
|
||||
if (!gameState.GetOccupant(position)) {
|
||||
existingCommands.push_back(std::make_shared<PlaceUnitCommand>(unit, position));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (gameState->status()->state() !=
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP)
|
||||
return;
|
||||
}
|
||||
|
||||
if (state != net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP) return;
|
||||
|
||||
const bool isDefender = PlayerIsDefender(gameState, playerId);
|
||||
|
||||
|
||||
+1
-7
@@ -5,12 +5,8 @@
|
||||
#ifndef EAGLE0_PLAYERSETUPCOMMANDFACTORY_HPP
|
||||
#define EAGLE0_PLAYERSETUPCOMMANDFACTORY_HPP
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCommand.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
|
||||
namespace shardok {
|
||||
@@ -33,9 +29,7 @@ public:
|
||||
auto AddAvailablePlayerSetupCommands(
|
||||
CommandList& existingCommands,
|
||||
PlayerId playerId,
|
||||
const GameStateW& gameState,
|
||||
const std::optional<std::vector<Coords>>& reinforcementPositions = std::nullopt) const
|
||||
-> void;
|
||||
const GameStateW& gameState) const -> void;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
+19
-1
@@ -29,6 +29,7 @@ using Unit = net::eagle0::shardok::storage::fb::Unit;
|
||||
inline auto GetMutableUnit(GameState *state, UnitId unitId) -> Unit * {
|
||||
return const_cast<Unit *>(state->mutable_units()->GetMutableObject(unitId));
|
||||
}
|
||||
using net::eagle0::shardok::storage::fb::DrawType;
|
||||
using net::eagle0::shardok::storage::fb::VictoryCondition;
|
||||
using net::eagle0::shardok::storage::fb::VictoryType;
|
||||
|
||||
@@ -258,6 +259,23 @@ void MutatingApplyResult(
|
||||
->mutable_end_game_condition()
|
||||
->mutate_victory_type(VictoryType::VictoryType_UNKNOWN_VICTORY_TYPE);
|
||||
break;
|
||||
case net::eagle0::shardok::common::EndGameCondition::kAllyVictory:
|
||||
mutatingGameState->mutable_status()
|
||||
->mutable_end_game_condition()
|
||||
->mutate_victory_type(VictoryType::VictoryType_ALLY_VICTORY);
|
||||
mutatingGameState->mutable_status()
|
||||
->mutable_end_game_condition()
|
||||
->mutate_victory_details(static_cast<VictoryCondition>(
|
||||
result.game_status().end_game_condition().ally_victory()));
|
||||
break;
|
||||
case net::eagle0::shardok::common::EndGameCondition::kDraw:
|
||||
mutatingGameState->mutable_status()
|
||||
->mutable_end_game_condition()
|
||||
->mutate_victory_type(VictoryType::VictoryType_DRAW);
|
||||
mutatingGameState->mutable_status()
|
||||
->mutable_end_game_condition()
|
||||
->mutate_draw_details(static_cast<DrawType>(
|
||||
result.game_status().end_game_condition().draw()));
|
||||
case net::eagle0::shardok::common::EndGameCondition::kLoss:
|
||||
mutatingGameState->mutable_status()
|
||||
->mutable_end_game_condition()
|
||||
@@ -313,7 +331,7 @@ void MutatingApplyResult(
|
||||
.size()
|
||||
: changedUnit->battalion().size();
|
||||
|
||||
auto status = changedUnit->status();
|
||||
auto status = net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT;
|
||||
if (IsDestroyed(*changedUnit)) {
|
||||
status =
|
||||
changedUnit->has_attached_hero()
|
||||
|
||||
@@ -34,7 +34,6 @@ auto IsResolved(const net::eagle0::shardok::storage::fb::UnitStatus status) -> b
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT: return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ using net::eagle0::shardok::common::GameStatus;
|
||||
auto actorAfter = *currentState->units()->Get(actorId);
|
||||
actorAfter.mutable_location() = target;
|
||||
actorAfter.mutate_hidden(true);
|
||||
actorAfter.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
|
||||
MutatingBumpAllKnowledgeToMinimum(&actorAfter, minimumKnowledge);
|
||||
|
||||
ActionResult placeResult{};
|
||||
|
||||
@@ -16,7 +16,6 @@ auto PlaceUnitCommand::InternalExecute(
|
||||
const std::shared_ptr<RandomGenerator>& /*generator*/) const -> vector<ActionResult> {
|
||||
auto actorAfter = *actor;
|
||||
actorAfter.mutable_location() = target;
|
||||
actorAfter.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
|
||||
|
||||
ActionResult placeResult{};
|
||||
placeResult.set_type(ActionType::PLACE_UNIT);
|
||||
|
||||
@@ -89,9 +89,7 @@ auto UpdateGameStatusAction::InternalExecute(
|
||||
// If the game has already ended, just return that state
|
||||
if (GameIsOver(gameState->status())) { return results; }
|
||||
if (gameState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP ||
|
||||
gameState->status()->state() ==
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_REINFORCEMENT_PLACEMENT) {
|
||||
net::eagle0::shardok::storage::fb::GameStatus_::State_SET_UP) {
|
||||
return results;
|
||||
}
|
||||
|
||||
@@ -211,13 +209,21 @@ auto UpdateGameStatusAction::InternalExecute(
|
||||
}
|
||||
}
|
||||
|
||||
throw ShardokInternalErrorException(
|
||||
"No player has WIN_AFTER_MAX_ROUNDS but max rounds exceeded");
|
||||
results.emplace_back();
|
||||
results[0].set_type(net::eagle0::shardok::common::GAME_OVER);
|
||||
results[0].mutable_game_status()->set_state(GameStatusProto::DRAW);
|
||||
results[0].mutable_game_status()->clear_winning_shardok_ids();
|
||||
results[0].mutable_game_status()->mutable_end_game_condition()->set_draw(
|
||||
net::eagle0::shardok::common::DRAW_AFTER_MAX_ROUNDS);
|
||||
results[0].mutable_game_status()->set_description(
|
||||
"Draw! No victory after " + std::to_string(settingsGetter.Backing().max_rounds()) +
|
||||
" rounds");
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// check for castle occupation
|
||||
if (!criticalTileLocations.empty()) {
|
||||
// First check: single player holding all critical tiles
|
||||
for (const auto* pi : *gameState->player_infos()) {
|
||||
if (HasVictoryCondition(
|
||||
pi,
|
||||
@@ -253,30 +259,6 @@ auto UpdateGameStatusAction::InternalExecute(
|
||||
results[0].mutable_game_status()->set_state(
|
||||
GameStatusProto::State::GameStatus_State_VICTORY);
|
||||
results[0].mutable_game_status()->add_winning_shardok_ids(pi->player_id());
|
||||
|
||||
// Also add mutually-allied players with HoldsCriticalTiles
|
||||
for (const auto* otherPi : *gameState->player_infos()) {
|
||||
if (otherPi->player_id() == pi->player_id()) continue;
|
||||
if (!HasVictoryCondition(
|
||||
otherPi,
|
||||
net::eagle0::shardok::storage::fb::
|
||||
VictoryCondition_VICTORY_CONDITION_HOLDS_CRITICAL_TILES))
|
||||
continue;
|
||||
bool mutuallyAllied =
|
||||
std::ranges::any_of(
|
||||
*pi->allies(),
|
||||
[otherPi](const auto* ap) {
|
||||
return ap->player_id() == otherPi->player_id();
|
||||
}) &&
|
||||
std::ranges::any_of(*otherPi->allies(), [pi](const auto* ap) {
|
||||
return ap->player_id() == pi->player_id();
|
||||
});
|
||||
if (mutuallyAllied) {
|
||||
results[0].mutable_game_status()->add_winning_shardok_ids(
|
||||
otherPi->player_id());
|
||||
}
|
||||
}
|
||||
|
||||
results[0].mutable_game_status()->mutable_end_game_condition()->set_victory(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
|
||||
results[0].mutable_game_status()->set_description(
|
||||
@@ -286,71 +268,6 @@ auto UpdateGameStatusAction::InternalExecute(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Second check: allied players collectively holding all critical tiles
|
||||
unordered_set<PlayerId> occupantPlayerIds{};
|
||||
bool allTilesOccupiedWithHeroes = true;
|
||||
for (const auto& criticalTile : criticalTileLocations) {
|
||||
const auto* possibleOccupant = currentState.GetOccupant(criticalTile);
|
||||
if (!possibleOccupant || !possibleOccupant->has_attached_hero()) {
|
||||
allTilesOccupiedWithHeroes = false;
|
||||
break;
|
||||
}
|
||||
occupantPlayerIds.insert(possibleOccupant->player_id());
|
||||
}
|
||||
|
||||
if (allTilesOccupiedWithHeroes && occupantPlayerIds.size() > 1) {
|
||||
// All occupants must have the HOLDS_CRITICAL_TILES victory condition
|
||||
bool allHaveCondition = true;
|
||||
for (const PlayerId pid : occupantPlayerIds) {
|
||||
const auto* pi = GetPlayerInfo(pid);
|
||||
if (!HasVictoryCondition(
|
||||
pi,
|
||||
net::eagle0::shardok::storage::fb::
|
||||
VictoryCondition_VICTORY_CONDITION_HOLDS_CRITICAL_TILES)) {
|
||||
allHaveCondition = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allHaveCondition) {
|
||||
// Check mutual alliance among all occupants
|
||||
bool allAllied = true;
|
||||
for (const PlayerId pid1 : occupantPlayerIds) {
|
||||
const auto* p1Info = GetPlayerInfo(pid1);
|
||||
for (const PlayerId pid2 : occupantPlayerIds) {
|
||||
if (pid1 == pid2) continue;
|
||||
if (!std::ranges::any_of(
|
||||
*p1Info->allies(),
|
||||
[pid2](const net::eagle0::shardok::storage::fb::AlliedPlayer*
|
||||
alliedPlayer) {
|
||||
return alliedPlayer->player_id() == pid2;
|
||||
})) {
|
||||
allAllied = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!allAllied) break;
|
||||
}
|
||||
|
||||
if (allAllied) {
|
||||
results.emplace_back();
|
||||
results[0].set_type(net::eagle0::shardok::common::GAME_OVER);
|
||||
results[0].mutable_game_status()->set_state(
|
||||
GameStatusProto::State::GameStatus_State_VICTORY);
|
||||
*results[0].mutable_game_status()->mutable_winning_shardok_ids() = {
|
||||
std::begin(occupantPlayerIds),
|
||||
std::end(occupantPlayerIds)};
|
||||
results[0].mutable_game_status()->mutable_end_game_condition()->set_victory(
|
||||
net::eagle0::shardok::common::VICTORY_CONDITION_HOLDS_CRITICAL_TILES);
|
||||
results[0].mutable_game_status()->set_description(
|
||||
"Alliance victory! Allied attackers collectively control all "
|
||||
"critical tiles");
|
||||
ResolveHiddenLosers(results[0]);
|
||||
return results;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we got all the way through, keep the status as it is
|
||||
|
||||
@@ -35,7 +35,6 @@ public:
|
||||
const Units* units;
|
||||
const vector<PlayerId>& allyPids;
|
||||
const bool isAttacker;
|
||||
const bool cannotBecomeOutlaw;
|
||||
const bool unitMovedIntoZoc;
|
||||
};
|
||||
|
||||
|
||||
@@ -25,8 +25,7 @@ void FleeCommandFactory::AddAvailableFleeCommands(
|
||||
const Units *allUnits,
|
||||
const HexMap *map,
|
||||
const vector<PlayerId> &allyPids,
|
||||
const ActionPoints remainingActionPoints,
|
||||
const bool cannotBecomeOutlaw) const {
|
||||
const ActionPoints remainingActionPoints) const {
|
||||
if (!settings.ActionCostFor(settings.Backing().flee_action_point_cost())
|
||||
.IsPossible(remainingActionPoints))
|
||||
return;
|
||||
@@ -84,7 +83,7 @@ void FleeCommandFactory::AddAvailableFleeCommands(
|
||||
unit->player_id(),
|
||||
unit->unit_id(),
|
||||
odds));
|
||||
} else if (!cannotBecomeOutlaw) {
|
||||
} else {
|
||||
commands.push_back(std::make_shared<BecomeOutlawCommand>(
|
||||
settings.ActionCostFor(settings.Backing().flee_action_point_cost()),
|
||||
unit->player_id(),
|
||||
@@ -101,8 +100,7 @@ void FleeCommandFactory::AddAvailableCommands(
|
||||
params.units,
|
||||
params.hexMap,
|
||||
params.allyPids,
|
||||
params.remainingActionPoints,
|
||||
params.cannotBecomeOutlaw);
|
||||
params.remainingActionPoints);
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
@@ -27,8 +27,7 @@ public:
|
||||
const Units *allUnits,
|
||||
const HexMap *map,
|
||||
const vector<PlayerId> &allyPids,
|
||||
ActionPoints remainingActionPoints,
|
||||
bool cannotBecomeOutlaw) const;
|
||||
ActionPoints remainingActionPoints) const;
|
||||
|
||||
void AddAvailableCommands(CommandList &commands, const CommandParams ¶ms) const override;
|
||||
|
||||
|
||||
+3
-9
@@ -21,18 +21,13 @@ auto MeteorTargetCommandFactory::AddAvailableMeteorTargetCommands(
|
||||
if (unit->attached_hero().profession_info().meteor_cast_state() !=
|
||||
net::eagle0::shardok::storage::fb::MultiroundMagicState_TARGET)
|
||||
return;
|
||||
|
||||
// If the mage already has a target, these commands are optional (re-targeting).
|
||||
// If no target yet, the player must choose before ending their turn.
|
||||
const bool isRetarget = unit->attached_hero().profession_info().cast_target().row() > -1;
|
||||
const bool requiredToEndTurn = !isRetarget;
|
||||
if (unit->attached_hero().profession_info().cast_target().row() > -1) return;
|
||||
|
||||
existingCommands.emplace_back(std::make_shared<MeteorCancelCommand>(
|
||||
settings.ActionCostFor(settings.Backing().meteor_cancel_action_point_cost()),
|
||||
settings,
|
||||
unit->player_id(),
|
||||
unit->unit_id(),
|
||||
requiredToEndTurn));
|
||||
unit->unit_id()));
|
||||
|
||||
const Coords &unitLocation = unit->location();
|
||||
for (const Coords &meteorCoords : CoordsInMeteorRange(map, unitLocation, settings)) {
|
||||
@@ -42,8 +37,7 @@ auto MeteorTargetCommandFactory::AddAvailableMeteorTargetCommands(
|
||||
unit->player_id(),
|
||||
unit->unit_id(),
|
||||
meteorCoords,
|
||||
settings.Backing().minimum_knowledge_for_profession(),
|
||||
requiredToEndTurn));
|
||||
settings.Backing().minimum_knowledge_for_profession()));
|
||||
}
|
||||
}
|
||||
void MeteorTargetCommandFactory::AddAvailableCommands(
|
||||
|
||||
-19
@@ -84,25 +84,6 @@ void ArcheryCommandFactory::AddAvailableArcheryCommands(
|
||||
targetCoords += HexMapUtils::GetAdjacentCoords(hexMap, position);
|
||||
}
|
||||
|
||||
// Wind-assisted range 3 for longbowmen
|
||||
if (battalionType->alwaysArcheryCapable &&
|
||||
weather->wind().speed_in_mph() >=
|
||||
settings.Backing().longbow_wind_bonus_range_min_speed()) {
|
||||
const int windDir = (int)weather->wind().direction();
|
||||
for (const Coords &farCoords : TilesWithExactDistance(hexMap, position, 3)) {
|
||||
auto dirTo = DirectionsTo(hexMap, position, farCoords);
|
||||
int relMain = ((int)dirTo.main - windDir + 6) % 6;
|
||||
bool inCone = (relMain == 0 || relMain == 1 || relMain == 5);
|
||||
if (!inCone && dirTo.usesSecondary) {
|
||||
int relSec = ((int)dirTo.secondary - windDir + 6) % 6;
|
||||
inCone = (relSec == 0 || relSec == 1 || relSec == 5);
|
||||
}
|
||||
if (inCone && !HexMapUtils::LineIsBlockedByMountains(hexMap, position, farCoords)) {
|
||||
targetCoords.Add(farCoords);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const Coords &archeryCoords : targetCoords) {
|
||||
const auto *const enemyOccupant =
|
||||
KnownEnemyOccupant(unit->player_id(), units, allyPids, archeryCoords);
|
||||
|
||||
@@ -16,13 +16,11 @@ MeteorCancelCommand::MeteorCancelCommand(
|
||||
const ActionCost& cost,
|
||||
const SettingsGetter& settingsGetter,
|
||||
const PlayerId playerId,
|
||||
const UnitId casterId,
|
||||
const bool requiredToEndTurn)
|
||||
const UnitId casterId)
|
||||
: ShardokCommand(playerId),
|
||||
cost(cost),
|
||||
settings(settingsGetter),
|
||||
casterId(casterId),
|
||||
requiredToEndTurn(requiredToEndTurn) {}
|
||||
casterId(casterId) {}
|
||||
|
||||
auto MeteorCancelCommand::InternalExecute(
|
||||
const GameStateW& currentState,
|
||||
|
||||
@@ -21,15 +21,13 @@ protected:
|
||||
const ActionCost cost;
|
||||
const SettingsGetter settings;
|
||||
const UnitId casterId;
|
||||
const bool requiredToEndTurn;
|
||||
|
||||
public:
|
||||
MeteorCancelCommand(
|
||||
const ActionCost &cost,
|
||||
const SettingsGetter &settingsGetter,
|
||||
PlayerId playerId,
|
||||
UnitId casterId,
|
||||
bool requiredToEndTurn = true);
|
||||
UnitId casterId);
|
||||
|
||||
~MeteorCancelCommand() override = default;
|
||||
|
||||
@@ -38,7 +36,7 @@ public:
|
||||
}
|
||||
|
||||
auto GetCommandProto() const -> CommandProto override;
|
||||
[[nodiscard]] auto IsRequiredToEndTurn() const -> bool override { return requiredToEndTurn; }
|
||||
[[nodiscard]] auto IsRequiredToEndTurn() const -> bool override { return true; }
|
||||
[[nodiscard]] auto CanDoWithLowMorale() const -> bool override { return true; }
|
||||
[[nodiscard]] auto CanDoWithLowVigor() const -> bool override { return true; }
|
||||
|
||||
|
||||
@@ -41,15 +41,13 @@ MeteorTargetCommand::MeteorTargetCommand(
|
||||
const PlayerId playerId,
|
||||
const UnitId casterId,
|
||||
Coords target,
|
||||
const int minimumKnowledge,
|
||||
const bool requiredToEndTurn)
|
||||
const int minimumKnowledge)
|
||||
: ShardokCommand(playerId),
|
||||
cost(cost),
|
||||
settings(settingsGetter),
|
||||
casterId(casterId),
|
||||
target(std::move(target)),
|
||||
minimumKnowledge(minimumKnowledge),
|
||||
requiredToEndTurn(requiredToEndTurn) {}
|
||||
minimumKnowledge(minimumKnowledge) {}
|
||||
|
||||
auto MeteorTargetCommand::GetCommandProto() const -> CommandProto {
|
||||
CommandProto proto{};
|
||||
|
||||
@@ -24,7 +24,6 @@ protected:
|
||||
const UnitId casterId;
|
||||
const Coords target;
|
||||
const int minimumKnowledge;
|
||||
const bool requiredToEndTurn;
|
||||
|
||||
public:
|
||||
MeteorTargetCommand(
|
||||
@@ -33,8 +32,7 @@ public:
|
||||
PlayerId playerId,
|
||||
UnitId casterId,
|
||||
Coords target,
|
||||
int minimumKnowledge,
|
||||
bool requiredToEndTurn = true);
|
||||
int minimumKnowledge);
|
||||
|
||||
~MeteorTargetCommand() override = default;
|
||||
|
||||
@@ -43,7 +41,7 @@ public:
|
||||
}
|
||||
|
||||
auto GetCommandProto() const -> CommandProto override;
|
||||
[[nodiscard]] auto IsRequiredToEndTurn() const -> bool override { return requiredToEndTurn; }
|
||||
[[nodiscard]] auto IsRequiredToEndTurn() const -> bool override { return true; }
|
||||
[[nodiscard]] auto CanDoWithLowMorale() const -> bool override { return true; }
|
||||
[[nodiscard]] auto CanDoWithLowVigor() const -> bool override { return true; }
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ auto ReinforceCommand::InternalExecute(
|
||||
const Unit* joiningUnit = currentState->units()->Get(joiningUnitId);
|
||||
auto joinerAfter = *joiningUnit;
|
||||
joinerAfter.mutable_location() = location;
|
||||
joinerAfter.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT);
|
||||
|
||||
ActionResult placeResult{};
|
||||
placeResult.set_type(ActionType::REINFORCED);
|
||||
|
||||
@@ -62,7 +62,6 @@ auto FromPlayerInfoProto(flatbuffers::FlatBufferBuilder& fbb, const PlayerInfoPr
|
||||
pib.add_allies(alliesOffset);
|
||||
pib.add_victory_conditions(vcOffset);
|
||||
pib.add_starting_food(piProto.starting_food());
|
||||
pib.add_cannot_become_outlaw(piProto.cannot_become_outlaw());
|
||||
|
||||
return pib.Finish();
|
||||
}
|
||||
@@ -177,10 +176,7 @@ auto SetupInitialGameState(
|
||||
modifiedUnit.mutable_battalion().mutate_base_morale(baseMorale);
|
||||
modifiedUnit.mutable_battalion().mutate_morale(baseMorale);
|
||||
|
||||
if (modifiedUnit.status() !=
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT) {
|
||||
modifiedUnit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT);
|
||||
}
|
||||
modifiedUnit.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT);
|
||||
|
||||
unitsVec.push_back(modifiedUnit);
|
||||
}
|
||||
@@ -240,12 +236,14 @@ auto ToProto(const GameStatus* fbStatus) -> GameStatusProto {
|
||||
fbStatus->end_game_condition()->victory_details()));
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::VictoryType_ALLY_VICTORY:
|
||||
protoStatus.mutable_end_game_condition()->set_victory(
|
||||
protoStatus.mutable_end_game_condition()->set_ally_victory(
|
||||
static_cast<net::eagle0::shardok::common::VictoryCondition>(
|
||||
fbStatus->end_game_condition()->victory_details()));
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::VictoryType_DRAW:
|
||||
throw InvalidProtoException("Draw victory type is no longer supported");
|
||||
protoStatus.mutable_end_game_condition()->set_draw(
|
||||
static_cast<net::eagle0::shardok::common::DrawType>(
|
||||
fbStatus->end_game_condition()->draw_details()));
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::VictoryType_LOSS:
|
||||
protoStatus.mutable_end_game_condition()->set_loss(
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
load("//tools:copts.bzl", "COPTS")
|
||||
|
||||
cc_library(
|
||||
name = "tutorial_battle_controller",
|
||||
srcs = ["TutorialBattleController.cpp"],
|
||||
hdrs = ["TutorialBattleController.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/common:random_generator",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:game_state_w",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/map:coordinates",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings:game_settings",
|
||||
"//src/main/cpp/net/eagle0/shardok/library/util:action_result_flatbuffer_helpers",
|
||||
"//src/main/protobuf/net/eagle0/common:tutorial_battle_config_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:action_type_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
|
||||
],
|
||||
)
|
||||
@@ -1,293 +0,0 @@
|
||||
//
|
||||
// TutorialBattleController.cpp
|
||||
// eagle0
|
||||
//
|
||||
// Created for tutorial battle system.
|
||||
// Copyright 2025 none. All rights reserved.
|
||||
//
|
||||
|
||||
#include "TutorialBattleController.hpp"
|
||||
|
||||
#include <ranges>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/util/ActionResultFlatbufferHelpers.hpp"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using UnitStatusFB = net::eagle0::shardok::storage::fb::UnitStatus;
|
||||
using ActionType = net::eagle0::shardok::common::ActionType;
|
||||
using FleeActionProto = net::eagle0::common::FleeAction;
|
||||
using ReinforcementsActionProto = net::eagle0::common::ReinforcementsAction;
|
||||
using UnitKilledTriggerProto = net::eagle0::common::UnitKilledTrigger;
|
||||
|
||||
TutorialBattleController::TutorialBattleController()
|
||||
: enabled_(false),
|
||||
config_(),
|
||||
firedEventIds_() {}
|
||||
|
||||
TutorialBattleController::TutorialBattleController(const TutorialBattleConfigProto& protoConfig)
|
||||
: enabled_(protoConfig.enabled()),
|
||||
config_(protoConfig),
|
||||
firedEventIds_() {}
|
||||
|
||||
auto TutorialBattleController::CheckAndExecuteEvents(
|
||||
const GameStateW& state,
|
||||
const SettingsGetter& settings,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator) -> TutorialEventResults {
|
||||
if (!enabled_) { return {}; }
|
||||
|
||||
TutorialEventResults combined;
|
||||
|
||||
// Check events in config order
|
||||
for (const auto& event : config_.events()) {
|
||||
// Skip already-fired events
|
||||
if (firedEventIds_.contains(event.event_id())) { continue; }
|
||||
|
||||
// Check if trigger condition is met
|
||||
if (EvaluateTrigger(event.trigger(), state)) {
|
||||
// Mark as fired before executing (to prevent re-triggering)
|
||||
firedEventIds_.insert(event.event_id());
|
||||
|
||||
// Execute the action
|
||||
auto eventResults = ExecuteAction(event.action(), state, settings, randomGenerator);
|
||||
combined.actionResults.insert(
|
||||
combined.actionResults.end(),
|
||||
eventResults.actionResults.begin(),
|
||||
eventResults.actionResults.end());
|
||||
|
||||
// Only one reinforcement placement can be pending at a time
|
||||
if (eventResults.reinforcementPlacement.has_value()) {
|
||||
combined.reinforcementPlacement = eventResults.reinforcementPlacement;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
auto TutorialBattleController::EvaluateTrigger(
|
||||
const TutorialTriggerProto& trigger,
|
||||
const GameStateW& state) const -> bool {
|
||||
switch (trigger.trigger_type_case()) {
|
||||
case TutorialTriggerProto::kAfterRound: {
|
||||
const int currentRound = state->current_round();
|
||||
return currentRound >= trigger.after_round().round_number();
|
||||
}
|
||||
|
||||
case TutorialTriggerProto::kUnitsLost: {
|
||||
const auto& unitsLost = trigger.units_lost();
|
||||
const int lostCount = CountLostUnits(state, unitsLost.player_id());
|
||||
return lostCount >= unitsLost.threshold();
|
||||
}
|
||||
|
||||
case TutorialTriggerProto::kDamageTaken: {
|
||||
const auto& damageTaken = trigger.damage_taken();
|
||||
const int totalDamage = GetTotalDamageTaken(state, damageTaken.player_id());
|
||||
return totalDamage >= damageTaken.threshold();
|
||||
}
|
||||
|
||||
case TutorialTriggerProto::kUnitKilled: {
|
||||
return IsUnitKilled(state, trigger.unit_killed());
|
||||
}
|
||||
|
||||
case TutorialTriggerProto::TRIGGER_TYPE_NOT_SET:
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
auto TutorialBattleController::ExecuteAction(
|
||||
const TutorialActionProto& action,
|
||||
const GameStateW& state,
|
||||
[[maybe_unused]] const SettingsGetter& settings,
|
||||
[[maybe_unused]] const std::shared_ptr<RandomGenerator>& randomGenerator) const
|
||||
-> TutorialEventResults {
|
||||
switch (action.action_type_case()) {
|
||||
case TutorialActionProto::kFlee:
|
||||
return {.actionResults = ExecuteFleeAction(action.flee(), state),
|
||||
.reinforcementPlacement = std::nullopt};
|
||||
|
||||
case TutorialActionProto::kReinforcements:
|
||||
return ExecuteReinforcementsAction(action.reinforcements(), state);
|
||||
|
||||
case TutorialActionProto::ACTION_TYPE_NOT_SET:
|
||||
default: return {};
|
||||
}
|
||||
}
|
||||
|
||||
auto TutorialBattleController::CountLostUnits(const GameStateW& state, const PlayerId playerId)
|
||||
const -> int {
|
||||
int count = 0;
|
||||
for (const auto* unit : *state->units()) {
|
||||
if (unit->player_id() != playerId) { continue; }
|
||||
|
||||
// Count units that are no longer in battle (destroyed, captured, fled, retreated)
|
||||
const auto status = unit->status();
|
||||
if (status != UnitStatusFB::UnitStatus_NORMAL_UNIT &&
|
||||
status != UnitStatusFB::UnitStatus_RESERVE_UNIT &&
|
||||
status != UnitStatusFB::UnitStatus_NEVER_ENTERED_UNIT &&
|
||||
status != UnitStatusFB::UnitStatus_RESERVED_SLOT &&
|
||||
status != UnitStatusFB::UnitStatus_PENDING_REINFORCEMENT) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
auto TutorialBattleController::GetTotalDamageTaken(const GameStateW& state, const PlayerId playerId)
|
||||
const -> int {
|
||||
int totalDamage = 0;
|
||||
for (const auto* unit : *state->units()) {
|
||||
if (unit->player_id() != playerId) { continue; }
|
||||
|
||||
// Calculate damage as difference between initial and current size
|
||||
// Initial sizes are stored in config since flatbuffers don't track starting sizes
|
||||
int initialSize = 0;
|
||||
for (const auto& initial : config_.initial_sizes()) {
|
||||
if (initial.unit_id() == unit->unit_id()) {
|
||||
initialSize = initial.size();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (initialSize == 0) { continue; }
|
||||
|
||||
const int currentSize = unit->battalion().size();
|
||||
if (initialSize > currentSize) { totalDamage += (initialSize - currentSize); }
|
||||
}
|
||||
return totalDamage;
|
||||
}
|
||||
|
||||
auto TutorialBattleController::IsUnitKilled(
|
||||
const GameStateW& state,
|
||||
const UnitKilledTriggerProto& trigger) const -> bool {
|
||||
for (const auto* unit : *state->units()) {
|
||||
// Check if this unit matches the identifier
|
||||
bool matches = false;
|
||||
|
||||
switch (trigger.unit_identifier_case()) {
|
||||
case UnitKilledTriggerProto::kUnitId:
|
||||
matches = (unit->unit_id() == trigger.unit_id());
|
||||
break;
|
||||
|
||||
case UnitKilledTriggerProto::kEagleHeroId:
|
||||
if (unit->has_attached_hero()) {
|
||||
matches = (unit->attached_hero().eagle_hero_id() == trigger.eagle_hero_id());
|
||||
}
|
||||
break;
|
||||
|
||||
case UnitKilledTriggerProto::UNIT_IDENTIFIER_NOT_SET:
|
||||
default: continue;
|
||||
}
|
||||
|
||||
if (!matches) { continue; }
|
||||
|
||||
// Check if the unit is "killed" (destroyed/captured status)
|
||||
const auto status = unit->status();
|
||||
if (status == UnitStatusFB::UnitStatus_DESTROYED_SUMMONED_UNIT ||
|
||||
status == UnitStatusFB::UnitStatus_CAPTURED_UNIT) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto TutorialBattleController::ExecuteFleeAction(
|
||||
const FleeActionProto& fleeAction,
|
||||
const GameStateW& state) const -> std::vector<ActionResult> {
|
||||
std::vector<ActionResult> results;
|
||||
const PlayerId fleeingPlayerId = fleeAction.player_id();
|
||||
|
||||
// First, create a "tutorial enemy fled" notification
|
||||
ActionResult fleeNotification{};
|
||||
fleeNotification.set_type(ActionType::TUTORIAL_ENEMY_FLED);
|
||||
results.push_back(fleeNotification);
|
||||
|
||||
// Determine which units should flee
|
||||
std::set<int32_t> specificUnitIds(fleeAction.unit_ids().begin(), fleeAction.unit_ids().end());
|
||||
std::set<int32_t> specificHeroIds(
|
||||
fleeAction.eagle_hero_ids().begin(),
|
||||
fleeAction.eagle_hero_ids().end());
|
||||
const bool fleeAllUnits = specificUnitIds.empty() && specificHeroIds.empty();
|
||||
|
||||
// Find all matching units that are still active and make them flee
|
||||
for (const auto* unit : *state->units()) {
|
||||
if (unit->player_id() != fleeingPlayerId) { continue; }
|
||||
if (unit->status() != UnitStatusFB::UnitStatus_NORMAL_UNIT) { continue; }
|
||||
|
||||
// Check if this unit should flee
|
||||
if (!fleeAllUnits) {
|
||||
const bool matchesUnitId = specificUnitIds.contains(unit->unit_id());
|
||||
const bool matchesHeroId =
|
||||
unit->has_attached_hero() &&
|
||||
specificHeroIds.contains(unit->attached_hero().eagle_hero_id());
|
||||
if (!matchesUnitId && !matchesHeroId) { continue; }
|
||||
}
|
||||
|
||||
// Create flee result for this unit (100% success - guaranteed flee)
|
||||
ActionResult fleeResult{};
|
||||
fleeResult.set_type(ActionType::FLED);
|
||||
fleeResult.mutable_player()->set_value(fleeingPlayerId);
|
||||
fleeResult.mutable_actor()->set_value(unit->unit_id());
|
||||
|
||||
auto* resolvedUnit = fleeResult.add_resolved_units();
|
||||
AddUnitToResolved(*resolvedUnit, *unit);
|
||||
resolvedUnit->set_status(net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT);
|
||||
|
||||
results.push_back(fleeResult);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
auto TutorialBattleController::ExecuteReinforcementsAction(
|
||||
const ReinforcementsActionProto& reinforcementsAction,
|
||||
const GameStateW& state) const -> TutorialEventResults {
|
||||
TutorialEventResults eventResults;
|
||||
|
||||
if (reinforcementsAction.units().empty()) { return eventResults; }
|
||||
|
||||
const PlayerId playerId = reinforcementsAction.player_id();
|
||||
|
||||
// Look up starting positions from the hex map's attacker starting positions
|
||||
using Coords = net::eagle0::shardok::storage::fb::Coords;
|
||||
std::vector<Coords> availablePositions;
|
||||
const int startingPosGroupIndex = reinforcementsAction.attacker_starting_position_index();
|
||||
const auto* attackerPositions = state->hex_map()->attacker_starting_positions();
|
||||
if (startingPosGroupIndex >= 0 &&
|
||||
startingPosGroupIndex < static_cast<int>(attackerPositions->size())) {
|
||||
for (const auto* pos : *attackerPositions->Get(startingPosGroupIndex)->positions()) {
|
||||
if (!state.GetOccupant(*pos)) { availablePositions.push_back(*pos); }
|
||||
}
|
||||
}
|
||||
|
||||
// Build set of eagle hero IDs for this specific reinforcement event
|
||||
std::set<int32_t> expectedHeroIds;
|
||||
for (const auto& commonUnit : reinforcementsAction.units()) {
|
||||
if (commonUnit.has_hero()) { expectedHeroIds.insert(commonUnit.hero().eagle_hero_id()); }
|
||||
}
|
||||
|
||||
// Place only the PENDING_REINFORCEMENT units that belong to this event
|
||||
ActionResult reinforcementsResult{};
|
||||
reinforcementsResult.set_type(ActionType::TUTORIAL_REINFORCEMENTS_ARRIVED);
|
||||
reinforcementsResult.mutable_player()->set_value(playerId);
|
||||
|
||||
size_t posIndex = 0;
|
||||
for (const auto* unit : *state->units()) {
|
||||
if (unit->status() != UnitStatusFB::UnitStatus_PENDING_REINFORCEMENT) { continue; }
|
||||
if (!unit->has_attached_hero() ||
|
||||
!expectedHeroIds.contains(unit->attached_hero().eagle_hero_id())) {
|
||||
continue;
|
||||
}
|
||||
if (posIndex >= availablePositions.size()) { break; }
|
||||
|
||||
net::eagle0::shardok::storage::fb::Unit changedUnit = *unit;
|
||||
changedUnit.mutate_status(UnitStatusFB::UnitStatus_NORMAL_UNIT);
|
||||
changedUnit.mutable_location() = availablePositions[posIndex++];
|
||||
AddChangedUnit(reinforcementsResult, changedUnit);
|
||||
}
|
||||
|
||||
eventResults.actionResults.push_back(reinforcementsResult);
|
||||
return eventResults;
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -1,116 +0,0 @@
|
||||
//
|
||||
// TutorialBattleController.hpp
|
||||
// eagle0
|
||||
//
|
||||
// Created for tutorial battle system.
|
||||
// Copyright 2025 none. All rights reserved.
|
||||
//
|
||||
|
||||
#ifndef TutorialBattleController_hpp
|
||||
#define TutorialBattleController_hpp
|
||||
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/common/RandomGenerator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/GameStateW.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/map/Coordinates.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/common/tutorial_battle_config.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/storage/action_result.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using ActionResult = net::eagle0::shardok::storage::ActionResult;
|
||||
using TutorialBattleConfigProto = net::eagle0::common::TutorialBattleConfig;
|
||||
using TutorialEventProto = net::eagle0::common::TutorialEvent;
|
||||
using TutorialTriggerProto = net::eagle0::common::TutorialTrigger;
|
||||
using TutorialActionProto = net::eagle0::common::TutorialAction;
|
||||
|
||||
/// Info about a pending reinforcement placement phase.
|
||||
struct ReinforcementPlacementInfo {
|
||||
PlayerId playerId;
|
||||
std::vector<Coords> startingPositions;
|
||||
};
|
||||
|
||||
/// Results from tutorial event processing, including action results and any pending
|
||||
/// reinforcement placement.
|
||||
struct TutorialEventResults {
|
||||
std::vector<ActionResult> actionResults;
|
||||
std::optional<ReinforcementPlacementInfo> reinforcementPlacement;
|
||||
};
|
||||
|
||||
/// Controller for scripted tutorial battles with event-driven triggers and actions.
|
||||
/// Events are checked at the end of each round and fire in config order.
|
||||
/// Each event fires at most once (tracked by event_id).
|
||||
class TutorialBattleController {
|
||||
public:
|
||||
/// Creates a disabled controller.
|
||||
TutorialBattleController();
|
||||
|
||||
/// Creates a controller from proto configuration.
|
||||
explicit TutorialBattleController(const TutorialBattleConfigProto& protoConfig);
|
||||
|
||||
/// Returns true if the controller is enabled for this battle.
|
||||
[[nodiscard]] auto IsEnabled() const -> bool { return enabled_; }
|
||||
|
||||
/// Check all events at end of round, return actions for triggered events.
|
||||
/// Events fire in config order - first triggered event executes, then next.
|
||||
/// Each event fires at most once.
|
||||
[[nodiscard]] auto CheckAndExecuteEvents(
|
||||
const GameStateW& state,
|
||||
const SettingsGetter& settings,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator) -> TutorialEventResults;
|
||||
|
||||
/// Gets the set of fired event IDs (for testing/debugging).
|
||||
[[nodiscard]] auto GetFiredEventIds() const -> const std::set<std::string>& {
|
||||
return firedEventIds_;
|
||||
}
|
||||
|
||||
private:
|
||||
bool enabled_ = false;
|
||||
TutorialBattleConfigProto config_;
|
||||
std::set<std::string> firedEventIds_; // Track which events have fired
|
||||
|
||||
/// Evaluates whether a trigger condition is met.
|
||||
[[nodiscard]] auto EvaluateTrigger(const TutorialTriggerProto& trigger, const GameStateW& state)
|
||||
const -> bool;
|
||||
|
||||
/// Executes an action and returns the resulting action results.
|
||||
[[nodiscard]] auto ExecuteAction(
|
||||
const TutorialActionProto& action,
|
||||
const GameStateW& state,
|
||||
const SettingsGetter& settings,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator) const -> TutorialEventResults;
|
||||
|
||||
// Helper methods for trigger evaluation
|
||||
|
||||
/// Counts units lost (destroyed, captured, fled, retreated) for a player.
|
||||
[[nodiscard]] auto CountLostUnits(const GameStateW& state, PlayerId playerId) const -> int;
|
||||
|
||||
/// Gets total damage taken (cumulative size reduction) for a player.
|
||||
[[nodiscard]] auto GetTotalDamageTaken(const GameStateW& state, PlayerId playerId) const -> int;
|
||||
|
||||
/// Checks if a specific unit has been killed.
|
||||
[[nodiscard]] auto IsUnitKilled(
|
||||
const GameStateW& state,
|
||||
const net::eagle0::common::UnitKilledTrigger& trigger) const -> bool;
|
||||
|
||||
// Helper methods for action execution
|
||||
|
||||
/// Executes a flee action for specified units.
|
||||
[[nodiscard]] auto ExecuteFleeAction(
|
||||
const net::eagle0::common::FleeAction& fleeAction,
|
||||
const GameStateW& state) const -> std::vector<ActionResult>;
|
||||
|
||||
/// Executes a reinforcements action, placing units directly at available starting positions.
|
||||
[[nodiscard]] auto ExecuteReinforcementsAction(
|
||||
const net::eagle0::common::ReinforcementsAction& reinforcementsAction,
|
||||
const GameStateW& state) const -> TutorialEventResults;
|
||||
};
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif /* TutorialBattleController_hpp */
|
||||
@@ -331,8 +331,7 @@ auto PlacedUnitsForPlayer(const Units *units, const PlayerId pid)
|
||||
std::map<UnitId, const Unit *> placedUnits{};
|
||||
for (const Unit *unit : *units) {
|
||||
if (unit->player_id() == pid &&
|
||||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT &&
|
||||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT)
|
||||
unit->status() != net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT)
|
||||
placedUnits[unit->unit_id()] = unit;
|
||||
}
|
||||
|
||||
|
||||
@@ -73,9 +73,6 @@ void DumpGameStateToFile(const GameStateW& gameState, const std::string& gameId)
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
|
||||
out << "RETREATED_UNIT\n";
|
||||
break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT:
|
||||
out << "PENDING_REINFORCEMENT\n";
|
||||
break;
|
||||
default: out << "OTHER (" << (int)unit->status() << ")\n";
|
||||
}
|
||||
out << " Remaining Action Points: " << (int)unit->remaining_action_points() << "\n";
|
||||
|
||||
@@ -49,8 +49,7 @@ auto GameStateFilteredForPlayer(
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_FLED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT: break;
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT: break;
|
||||
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT:
|
||||
throw ShardokInternalErrorException("Unknown unit status");
|
||||
@@ -101,7 +100,6 @@ auto GameStateFilteredForPlayer(
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RETREATED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_OUTLAWED_UNIT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT:
|
||||
case net::eagle0::shardok::storage::fb::UnitStatus_UNKNOWN_UNIT: break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,6 +188,22 @@ auto GameStateGuesser::GuessedState(
|
||||
gameStateView.status().end_game_condition().victory()));
|
||||
break;
|
||||
|
||||
case net::eagle0::shardok::common::EndGameCondition::ConditionCase::kAllyVictory:
|
||||
endGameCondition.mutate_victory_type(
|
||||
net::eagle0::shardok::storage::fb::VictoryType_ALLY_VICTORY);
|
||||
endGameCondition.mutate_victory_details(
|
||||
static_cast<net::eagle0::shardok::storage::fb::VictoryCondition>(
|
||||
gameStateView.status().end_game_condition().ally_victory()));
|
||||
break;
|
||||
|
||||
case net::eagle0::shardok::common::EndGameCondition::ConditionCase::kDraw:
|
||||
endGameCondition.mutate_victory_type(
|
||||
net::eagle0::shardok::storage::fb::VictoryType_DRAW);
|
||||
endGameCondition.mutate_draw_details(
|
||||
static_cast<net::eagle0::shardok::storage::fb::DrawType>(
|
||||
gameStateView.status().end_game_condition().draw()));
|
||||
break;
|
||||
|
||||
case net::eagle0::shardok::common::EndGameCondition::ConditionCase::kLoss:
|
||||
endGameCondition.mutate_victory_type(
|
||||
net::eagle0::shardok::storage::fb::VictoryType_LOSS);
|
||||
|
||||
@@ -12,7 +12,6 @@ cc_library(
|
||||
deps = [
|
||||
":server_configuration",
|
||||
"//src/main/cpp/net/eagle0/common:tsv_parser",
|
||||
"//src/main/cpp/net/eagle0/common:unit_conversions",
|
||||
"//src/main/cpp/net/eagle0/shardok/ai:shardok_ai_client",
|
||||
"//src/main/cpp/net/eagle0/shardok/controller",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:engine",
|
||||
@@ -20,7 +19,6 @@ cc_library(
|
||||
"//src/main/cpp/net/eagle0/shardok/library/settings_loader",
|
||||
"//src/main/cpp/net/eagle0/shardok/util:battalion_type_registrar",
|
||||
"//src/main/cpp/net/eagle0/shardok/util:map_loader",
|
||||
"//src/main/protobuf/net/eagle0/common:tutorial_battle_config_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/common:victory_condition_cc_proto",
|
||||
],
|
||||
)
|
||||
@@ -47,25 +45,6 @@ cc_library(
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "game_over_response_populator",
|
||||
srcs = ["GameOverResponsePopulator.cpp"],
|
||||
hdrs = ["GameOverResponsePopulator.hpp"],
|
||||
copts = COPTS,
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_c_types",
|
||||
"//src/main/cpp/net/eagle0/shardok/library:shardok_exception",
|
||||
"//src/main/flatbuffer/net/eagle0/shardok/storage:unit_cc_fbs",
|
||||
"//src/main/protobuf/net/eagle0/common:common_unit_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/common:shardok_internal_interface_cc_grpc",
|
||||
"//src/main/protobuf/net/eagle0/common:victory_condition_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:game_status_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:player_info_cc_proto",
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:action_result_cc_proto",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "eagle_interface_grpc_server",
|
||||
srcs = ["EagleInterfaceGrpcServer.cpp"],
|
||||
@@ -73,7 +52,6 @@ cc_library(
|
||||
copts = COPTS,
|
||||
visibility = ["//src/main/cpp/net/eagle0/shardok:__subpackages__"],
|
||||
deps = [
|
||||
":game_over_response_populator",
|
||||
":games_manager",
|
||||
":token_auth",
|
||||
"//src/main/cpp/net/eagle0/common:unit_conversions",
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include "src/main/cpp/net/eagle0/common/UnitConversions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/controller/ShardokGameController.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokEngine.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/server/GameOverResponsePopulator.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/util/MapLoader.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/common/victory_condition.pb.h"
|
||||
|
||||
@@ -46,16 +45,6 @@ public:
|
||||
auto EagleInterfaceImpl::ControllerForGame(const GameId &gameId, const GameSetupInfo &setupInfo)
|
||||
-> std::shared_ptr<ShardokGameController> {
|
||||
auto controller = gamesManager->GetController(gameId);
|
||||
|
||||
// If game exists but Eagle is requesting a fresh start with no prior history
|
||||
// (happens after admin rewind), replace the old game
|
||||
if (controller != nullptr && setupInfo.has_new_game_request() &&
|
||||
setupInfo.known_result_count() == 0) {
|
||||
printf("Replacing existing game %s with fresh game (rewind detected)\n", gameId.c_str());
|
||||
gamesManager->RemoveController(gameId);
|
||||
controller = nullptr;
|
||||
}
|
||||
|
||||
if (controller == nullptr) {
|
||||
if (setupInfo.current_game_state().empty()) {
|
||||
StartGame(setupInfo.new_game_request());
|
||||
@@ -74,6 +63,22 @@ auto EagleInterfaceImpl::ControllerForGame(const GameId &gameId, const GameSetup
|
||||
return controller;
|
||||
}
|
||||
|
||||
static auto PopulateGameOverResponse(
|
||||
const string &gameId,
|
||||
const GameStatus &gameStatus,
|
||||
const vector<net::eagle0::shardok::common::PlayerInfo> &resolvedPlayerInfos,
|
||||
const vector<net::eagle0::shardok::storage::ResolvedUnit> &resolvedUnits,
|
||||
net::eagle0::common::GameOverResponse *gameOverResponse) -> bool;
|
||||
|
||||
static auto IncludeUnitProtoInReturn(
|
||||
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus &internalStatus) -> bool;
|
||||
|
||||
static auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit;
|
||||
|
||||
static auto FromInternalStatus(
|
||||
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus internalStatus)
|
||||
-> net::eagle0::common::UnitStatus;
|
||||
|
||||
const std::string &kShardokGameRequestExtension = *(new string(".e0gr"));
|
||||
|
||||
EagleInterfaceImpl::EagleInterfaceImpl(
|
||||
@@ -138,8 +143,7 @@ auto ConvertPlayerInfo(
|
||||
pi.food(),
|
||||
victoryConditions,
|
||||
alliedPids,
|
||||
shardokUnits,
|
||||
pi.cannot_become_outlaw());
|
||||
shardokUnits);
|
||||
}
|
||||
|
||||
void EagleInterfaceImpl::StartGame(const NewGameRequest &request) {
|
||||
@@ -171,21 +175,7 @@ void EagleInterfaceImpl::StartGame(const NewGameRequest &request) {
|
||||
playersInfo.push_back(ConvertPlayerInfo(request.user_infos(), (PlayerId)i));
|
||||
}
|
||||
|
||||
// Validate that exactly one player has WIN_AFTER_MAX_ROUNDS
|
||||
int winAfterMaxRoundsCount = 0;
|
||||
for (const auto &psi : playersInfo) {
|
||||
for (const auto &vc : psi.playerInfo.victory_conditions()) {
|
||||
if (vc == net::eagle0::shardok::common::VICTORY_CONDITION_WIN_AFTER_MAX_ROUNDS) {
|
||||
winAfterMaxRoundsCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (winAfterMaxRoundsCount != 1) {
|
||||
throw NewGameException(
|
||||
StatusCode::INVALID_ARGUMENT,
|
||||
"Exactly one player must have WIN_AFTER_MAX_ROUNDS victory condition, found " +
|
||||
std::to_string(winAfterMaxRoundsCount));
|
||||
}
|
||||
// FIXME: validate sanity of the victory conditions
|
||||
|
||||
// Grab the nonempty gameId
|
||||
const string &gameId = request.game_id();
|
||||
@@ -203,7 +193,6 @@ void EagleInterfaceImpl::StartGame(const NewGameRequest &request) {
|
||||
mapName,
|
||||
customMapBytes,
|
||||
month,
|
||||
request.tutorial_battle_config(),
|
||||
request.SerializeAsString());
|
||||
}
|
||||
|
||||
@@ -232,14 +221,20 @@ auto EagleInterfaceImpl::PostCommand(
|
||||
request->game_setup_info().known_result_count(),
|
||||
response);
|
||||
} catch (ShardokClientErrorException &e) {
|
||||
std::cerr << "Bad request from client: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INVALID_ARGUMENT, e.what());
|
||||
std::cerr << "Bad request from client!" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
} catch (ShardokInternalErrorException &e) {
|
||||
std::cerr << "Internal error: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INTERNAL, e.what());
|
||||
std::cerr << "ShardokInternalErrorException thrown!" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
} catch (std::exception &e) {
|
||||
std::cerr << "Unknown exception: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INTERNAL, e.what());
|
||||
std::cerr << "Unknown exception in client->PostCommand" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
return Status::OK;
|
||||
@@ -278,14 +273,20 @@ auto EagleInterfaceImpl::PostPlacementCommands(
|
||||
request->game_setup_info().known_result_count(),
|
||||
response);
|
||||
} catch (ShardokClientErrorException &e) {
|
||||
std::cerr << "Bad request from client: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INVALID_ARGUMENT, e.what());
|
||||
std::cerr << "Bad request from client!" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
} catch (ShardokInternalErrorException &e) {
|
||||
std::cerr << "Internal error: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INTERNAL, e.what());
|
||||
std::cerr << "ShardokInternalErrorException thrown!" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
} catch (std::exception &e) {
|
||||
std::cerr << "Unknown exception: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INTERNAL, e.what());
|
||||
std::cerr << "Unknown exception in client->PostCommand" << std::endl;
|
||||
std::cerr << "Exception: " << e.what();
|
||||
|
||||
throw e;
|
||||
}
|
||||
|
||||
return Status::OK;
|
||||
@@ -361,6 +362,177 @@ auto EagleInterfaceImpl::GetHexMapNames(
|
||||
return Status::OK;
|
||||
}
|
||||
|
||||
auto PopulateGameOverResponse(
|
||||
const string &gameId,
|
||||
const GameStatus &gameStatus,
|
||||
const vector<net::eagle0::shardok::common::PlayerInfo> &resolvedPlayerInfos,
|
||||
const vector<net::eagle0::shardok::storage::ResolvedUnit> &resolvedUnits,
|
||||
net::eagle0::common::GameOverResponse *gameOverResponse) -> bool {
|
||||
const auto &endCondition = gameStatus.end_game_condition();
|
||||
|
||||
int32_t winningFactionId = -1;
|
||||
|
||||
for (const auto &rpi : resolvedPlayerInfos) {
|
||||
// for (int shardokPid = 0; shardokPid < request.user_infos_size(); shardokPid++) {
|
||||
auto shardokPid = rpi.player_id();
|
||||
const int32_t eagleFactionId = rpi.eagle_faction_id();
|
||||
|
||||
// const net::eagle0::common::PlayerSetupInfo &playerSetupInfo =
|
||||
// request.user_infos(shardokPid);
|
||||
auto *resolutionInfo = gameOverResponse->add_user_infos();
|
||||
|
||||
// Set the endgame condition (win/lose/draw) appropriately
|
||||
resolutionInfo->set_eagle_faction_id(eagleFactionId);
|
||||
if (endCondition.condition_case() == EndGameCondition::kDraw) {
|
||||
// Everyone gets a draw
|
||||
resolutionInfo->mutable_end_game_condition()->set_draw(
|
||||
net::eagle0::common::DrawType(endCondition.draw()));
|
||||
} else if (std::ranges::contains(gameStatus.winning_shardok_ids(), shardokPid)) {
|
||||
// The winning player gets the victory set
|
||||
resolutionInfo->mutable_end_game_condition()->set_victory(
|
||||
net::eagle0::common::VictoryCondition(endCondition.victory()));
|
||||
winningFactionId = eagleFactionId;
|
||||
} else if (std::ranges::any_of(
|
||||
gameStatus.winning_shardok_ids(),
|
||||
[&rpi, shardokPid](const PlayerId /*pid*/) {
|
||||
return std::ranges::any_of(
|
||||
rpi.allies(),
|
||||
[shardokPid](const net::eagle0::shardok::common::AlliedPlayer
|
||||
&ap) {
|
||||
return ap.player_id() == shardokPid;
|
||||
});
|
||||
})) {
|
||||
// Allies of the winning player get an allied victory set
|
||||
resolutionInfo->mutable_end_game_condition()->set_ally_victory(
|
||||
net::eagle0::common::VictoryCondition(endCondition.victory()));
|
||||
} else {
|
||||
resolutionInfo->mutable_end_game_condition()->set_loss(
|
||||
net::eagle0::common::VictoryCondition(endCondition.victory()));
|
||||
}
|
||||
|
||||
// copy in all the resolvedUnits that correspond to this player
|
||||
for (const auto &internalRu : resolvedUnits) {
|
||||
const auto unit = (Unit *)internalRu.unit_bytes().data();
|
||||
if (!unit->has_attached_hero()) continue;
|
||||
if (!IncludeUnitProtoInReturn(internalRu.status())) continue;
|
||||
if (unit->player_id() != shardokPid) continue;
|
||||
|
||||
auto *ru = resolutionInfo->add_units();
|
||||
*ru->mutable_unit() = FromUnitFb(*unit);
|
||||
ru->set_status(FromInternalStatus(internalRu.status()));
|
||||
}
|
||||
}
|
||||
|
||||
printf("Finishing game %s: ", gameId.c_str());
|
||||
if (endCondition.condition_case() == EndGameCondition::kDraw) {
|
||||
printf("It's a draw!\n");
|
||||
} else {
|
||||
printf("%d wins\n", winningFactionId);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto IncludeUnitProtoInReturn(
|
||||
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus &internalStatus) -> bool {
|
||||
switch (internalStatus) {
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_CAPTURED_UNIT:
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NORMAL_UNIT:
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT:
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_RETREATED_UNIT:
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT:
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT: return true;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_DESTROYED_SUMMONED_UNIT:
|
||||
return false;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_UNKNOWN_UNIT:
|
||||
case net::eagle0::shardok::storage::
|
||||
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MIN_SENTINEL_DO_NOT_USE_:
|
||||
case net::eagle0::shardok::storage::
|
||||
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MAX_SENTINEL_DO_NOT_USE_:
|
||||
throw ShardokInternalErrorException("Bad unit status on resolved unit");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit {
|
||||
net::eagle0::common::CommonUnit commonUnit{};
|
||||
commonUnit.set_eagle_player_id(unit.eagle_player_id());
|
||||
commonUnit.mutable_hero()->set_eagle_hero_id(unit.attached_hero().eagle_hero_id());
|
||||
commonUnit.mutable_hero()->set_profession(
|
||||
(net::eagle0::common::CommonProfession)unit.attached_hero()
|
||||
.profession_info()
|
||||
.profession());
|
||||
|
||||
commonUnit.mutable_hero()->set_strength(unit.attached_hero().strength());
|
||||
commonUnit.mutable_hero()->set_strength_xp(unit.attached_hero().strength_xp());
|
||||
|
||||
commonUnit.mutable_hero()->set_agility(unit.attached_hero().agility());
|
||||
commonUnit.mutable_hero()->set_agility_xp(unit.attached_hero().agility_xp());
|
||||
|
||||
commonUnit.mutable_hero()->set_constitution(unit.attached_hero().constitution());
|
||||
commonUnit.mutable_hero()->set_constitution_xp(unit.attached_hero().constitution_xp());
|
||||
|
||||
commonUnit.mutable_hero()->set_charisma(unit.attached_hero().charisma());
|
||||
commonUnit.mutable_hero()->set_charisma_xp(unit.attached_hero().charisma_xp());
|
||||
|
||||
commonUnit.mutable_hero()->set_wisdom(unit.attached_hero().wisdom());
|
||||
commonUnit.mutable_hero()->set_wisdom_xp(unit.attached_hero().wisdom_xp());
|
||||
|
||||
commonUnit.mutable_hero()->set_integrity(unit.attached_hero().integrity());
|
||||
commonUnit.mutable_hero()->set_ambition(unit.attached_hero().ambition());
|
||||
commonUnit.mutable_hero()->set_gregariousness(unit.attached_hero().gregariousness());
|
||||
commonUnit.mutable_hero()->set_bravery(unit.attached_hero().bravery());
|
||||
|
||||
commonUnit.mutable_hero()->set_vigor(unit.attached_hero().vigor());
|
||||
commonUnit.mutable_hero()->set_spent_vigor(unit.attached_hero().spent_vigor());
|
||||
|
||||
commonUnit.mutable_battalion()->set_eagle_battalion_id(unit.battalion().eagle_battalion_id());
|
||||
commonUnit.mutable_battalion()->set_size(unit.battalion().size());
|
||||
commonUnit.mutable_battalion()->set_type(
|
||||
static_cast<net::eagle0::common::CommonBattalionTypeId>(unit.battalion().type()));
|
||||
commonUnit.mutable_battalion()->set_armament(unit.battalion().armament());
|
||||
commonUnit.mutable_battalion()->set_training(unit.battalion().training());
|
||||
|
||||
return commonUnit;
|
||||
}
|
||||
|
||||
auto FromInternalStatus(
|
||||
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus internalStatus)
|
||||
-> net::eagle0::common::UnitStatus {
|
||||
switch (internalStatus) {
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_CAPTURED_UNIT:
|
||||
return net::eagle0::common::UnitStatus::CAPTURED_UNIT;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NORMAL_UNIT:
|
||||
return net::eagle0::common::UnitStatus::NORMAL_UNIT;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT:
|
||||
return net::eagle0::common::UnitStatus::FLED_UNIT;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_RETREATED_UNIT:
|
||||
return net::eagle0::common::UnitStatus::RETREATED_UNIT;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT:
|
||||
return net::eagle0::common::UnitStatus::NEVER_ENTERED_UNIT;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT:
|
||||
return net::eagle0::common::UnitStatus::OUTLAWED_UNIT;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_DESTROYED_SUMMONED_UNIT:
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_UNKNOWN_UNIT:
|
||||
case net::eagle0::shardok::storage::
|
||||
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MIN_SENTINEL_DO_NOT_USE_:
|
||||
case net::eagle0::shardok::storage::
|
||||
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MAX_SENTINEL_DO_NOT_USE_:
|
||||
throw ShardokInternalErrorException("Bad unit status on resolved unit");
|
||||
}
|
||||
|
||||
throw ShardokInternalErrorException("Bad unit status on resolved unit");
|
||||
}
|
||||
|
||||
auto EagleInterfaceImpl::SubscribeToGame(
|
||||
ServerContext *context,
|
||||
const GameSubscriptionRequest *request,
|
||||
@@ -371,16 +543,7 @@ auto EagleInterfaceImpl::SubscribeToGame(
|
||||
shared_ptr<ShardokGameController> controller;
|
||||
try {
|
||||
controller = ControllerForGame(request->game_id(), request->game_setup_info());
|
||||
} catch (NewGameException &e) { return e.GetStatus(); } catch (ShardokClientErrorException &e) {
|
||||
std::cerr << "Bad request from client: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INVALID_ARGUMENT, e.what());
|
||||
} catch (ShardokInternalErrorException &e) {
|
||||
std::cerr << "Internal error: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INTERNAL, e.what());
|
||||
} catch (std::exception &e) {
|
||||
std::cerr << "Unknown exception in SubscribeToGame: " << e.what() << std::endl;
|
||||
return Status(StatusCode::INTERNAL, e.what());
|
||||
}
|
||||
} catch (NewGameException &e) { return e.GetStatus(); }
|
||||
|
||||
if (!controller) { return Status(StatusCode::NOT_FOUND, "Game not found"); }
|
||||
|
||||
|
||||
@@ -1,176 +0,0 @@
|
||||
#include "GameOverResponsePopulator.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <ranges>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/ShardokException.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/common/victory_condition.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using net::eagle0::common::VictoryCondition;
|
||||
using std::string;
|
||||
using std::vector;
|
||||
|
||||
auto PopulateGameOverResponse(
|
||||
const string &gameId,
|
||||
const net::eagle0::shardok::common::GameStatus &gameStatus,
|
||||
const vector<net::eagle0::shardok::common::PlayerInfo> &resolvedPlayerInfos,
|
||||
const vector<net::eagle0::shardok::storage::ResolvedUnit> &resolvedUnits,
|
||||
net::eagle0::common::GameOverResponse *gameOverResponse) -> bool {
|
||||
const auto &endCondition = gameStatus.end_game_condition();
|
||||
|
||||
int32_t winningFactionId = -1;
|
||||
|
||||
for (const auto &rpi : resolvedPlayerInfos) {
|
||||
auto shardokPid = rpi.player_id();
|
||||
const int32_t eagleFactionId = rpi.eagle_faction_id();
|
||||
|
||||
auto *resolutionInfo = gameOverResponse->add_user_infos();
|
||||
|
||||
// Set the endgame condition (win/lose) appropriately
|
||||
resolutionInfo->set_eagle_faction_id(eagleFactionId);
|
||||
if (std::ranges::contains(gameStatus.winning_shardok_ids(), shardokPid)) {
|
||||
// The winning player gets the victory set
|
||||
resolutionInfo->mutable_end_game_condition()->set_victory(
|
||||
net::eagle0::common::VictoryCondition(endCondition.victory()));
|
||||
winningFactionId = eagleFactionId;
|
||||
} else if (std::ranges::any_of(
|
||||
gameStatus.winning_shardok_ids(),
|
||||
[&rpi](const PlayerId pid) {
|
||||
return std::ranges::any_of(
|
||||
rpi.allies(),
|
||||
[pid](const net::eagle0::shardok::common::AlliedPlayer &ap) {
|
||||
return ap.player_id() == pid;
|
||||
});
|
||||
})) {
|
||||
// Allied to a winner — their side won, so they get victory too
|
||||
resolutionInfo->mutable_end_game_condition()->set_victory(
|
||||
net::eagle0::common::VictoryCondition(endCondition.victory()));
|
||||
} else {
|
||||
resolutionInfo->mutable_end_game_condition()->set_loss(
|
||||
net::eagle0::common::VictoryCondition(endCondition.victory()));
|
||||
}
|
||||
|
||||
// copy in all the resolvedUnits that correspond to this player
|
||||
for (const auto &internalRu : resolvedUnits) {
|
||||
const auto unit = (Unit *)internalRu.unit_bytes().data();
|
||||
if (!unit->has_attached_hero()) continue;
|
||||
if (!IncludeUnitProtoInReturn(internalRu.status())) continue;
|
||||
if (unit->player_id() != shardokPid) continue;
|
||||
|
||||
auto *ru = resolutionInfo->add_units();
|
||||
*ru->mutable_unit() = FromUnitFb(*unit);
|
||||
ru->set_status(FromInternalStatus(internalRu.status()));
|
||||
}
|
||||
}
|
||||
|
||||
printf("Finishing game %s: %d wins\n", gameId.c_str(), winningFactionId);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto IncludeUnitProtoInReturn(
|
||||
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus &internalStatus) -> bool {
|
||||
switch (internalStatus) {
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_CAPTURED_UNIT:
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NORMAL_UNIT:
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT:
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_RETREATED_UNIT:
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT:
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT: return true;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_DESTROYED_SUMMONED_UNIT:
|
||||
return false;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_UNKNOWN_UNIT:
|
||||
case net::eagle0::shardok::storage::
|
||||
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MIN_SENTINEL_DO_NOT_USE_:
|
||||
case net::eagle0::shardok::storage::
|
||||
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MAX_SENTINEL_DO_NOT_USE_:
|
||||
throw ShardokInternalErrorException("Bad unit status on resolved unit");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit {
|
||||
net::eagle0::common::CommonUnit commonUnit{};
|
||||
commonUnit.set_eagle_player_id(unit.eagle_player_id());
|
||||
commonUnit.mutable_hero()->set_eagle_hero_id(unit.attached_hero().eagle_hero_id());
|
||||
commonUnit.mutable_hero()->set_profession(
|
||||
(net::eagle0::common::CommonProfession)unit.attached_hero()
|
||||
.profession_info()
|
||||
.profession());
|
||||
|
||||
commonUnit.mutable_hero()->set_strength(unit.attached_hero().strength());
|
||||
commonUnit.mutable_hero()->set_strength_xp(unit.attached_hero().strength_xp());
|
||||
|
||||
commonUnit.mutable_hero()->set_agility(unit.attached_hero().agility());
|
||||
commonUnit.mutable_hero()->set_agility_xp(unit.attached_hero().agility_xp());
|
||||
|
||||
commonUnit.mutable_hero()->set_constitution(unit.attached_hero().constitution());
|
||||
commonUnit.mutable_hero()->set_constitution_xp(unit.attached_hero().constitution_xp());
|
||||
|
||||
commonUnit.mutable_hero()->set_charisma(unit.attached_hero().charisma());
|
||||
commonUnit.mutable_hero()->set_charisma_xp(unit.attached_hero().charisma_xp());
|
||||
|
||||
commonUnit.mutable_hero()->set_wisdom(unit.attached_hero().wisdom());
|
||||
commonUnit.mutable_hero()->set_wisdom_xp(unit.attached_hero().wisdom_xp());
|
||||
|
||||
commonUnit.mutable_hero()->set_integrity(unit.attached_hero().integrity());
|
||||
commonUnit.mutable_hero()->set_ambition(unit.attached_hero().ambition());
|
||||
commonUnit.mutable_hero()->set_gregariousness(unit.attached_hero().gregariousness());
|
||||
commonUnit.mutable_hero()->set_bravery(unit.attached_hero().bravery());
|
||||
|
||||
commonUnit.mutable_hero()->set_vigor(unit.attached_hero().vigor());
|
||||
commonUnit.mutable_hero()->set_spent_vigor(unit.attached_hero().spent_vigor());
|
||||
|
||||
commonUnit.mutable_battalion()->set_eagle_battalion_id(unit.battalion().eagle_battalion_id());
|
||||
commonUnit.mutable_battalion()->set_size(unit.battalion().size());
|
||||
commonUnit.mutable_battalion()->set_type(
|
||||
static_cast<net::eagle0::common::CommonBattalionTypeId>(unit.battalion().type()));
|
||||
commonUnit.mutable_battalion()->set_armament(unit.battalion().armament());
|
||||
commonUnit.mutable_battalion()->set_training(unit.battalion().training());
|
||||
|
||||
return commonUnit;
|
||||
}
|
||||
|
||||
auto FromInternalStatus(
|
||||
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus internalStatus)
|
||||
-> net::eagle0::common::UnitStatus {
|
||||
switch (internalStatus) {
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_CAPTURED_UNIT:
|
||||
return net::eagle0::common::UnitStatus::CAPTURED_UNIT;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NORMAL_UNIT:
|
||||
return net::eagle0::common::UnitStatus::NORMAL_UNIT;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_FLED_UNIT:
|
||||
return net::eagle0::common::UnitStatus::FLED_UNIT;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_RETREATED_UNIT:
|
||||
return net::eagle0::common::UnitStatus::RETREATED_UNIT;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_NEVER_ENTERED_UNIT:
|
||||
return net::eagle0::common::UnitStatus::NEVER_ENTERED_UNIT;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_OUTLAWED_UNIT:
|
||||
return net::eagle0::common::UnitStatus::OUTLAWED_UNIT;
|
||||
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_DESTROYED_SUMMONED_UNIT:
|
||||
case net::eagle0::shardok::storage::ResolvedUnit_UnitStatus_UNKNOWN_UNIT:
|
||||
case net::eagle0::shardok::storage::
|
||||
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MIN_SENTINEL_DO_NOT_USE_:
|
||||
case net::eagle0::shardok::storage::
|
||||
ResolvedUnit_UnitStatus_ResolvedUnit_UnitStatus_INT_MAX_SENTINEL_DO_NOT_USE_:
|
||||
throw ShardokInternalErrorException("Bad unit status on resolved unit");
|
||||
}
|
||||
|
||||
throw ShardokInternalErrorException("Bad unit status on resolved unit");
|
||||
}
|
||||
|
||||
} // namespace shardok
|
||||
@@ -1,36 +0,0 @@
|
||||
#ifndef GameOverResponsePopulator_hpp
|
||||
#define GameOverResponsePopulator_hpp
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "src/main/flatbuffer/net/eagle0/shardok/storage/unit.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/common/common_unit.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/common/shardok_internal_interface.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/game_status.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/player_info.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/storage/action_result.pb.h"
|
||||
|
||||
namespace shardok {
|
||||
|
||||
using net::eagle0::shardok::storage::fb::Unit;
|
||||
|
||||
auto PopulateGameOverResponse(
|
||||
const std::string &gameId,
|
||||
const net::eagle0::shardok::common::GameStatus &gameStatus,
|
||||
const std::vector<net::eagle0::shardok::common::PlayerInfo> &resolvedPlayerInfos,
|
||||
const std::vector<net::eagle0::shardok::storage::ResolvedUnit> &resolvedUnits,
|
||||
net::eagle0::common::GameOverResponse *gameOverResponse) -> bool;
|
||||
|
||||
auto IncludeUnitProtoInReturn(
|
||||
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus &internalStatus) -> bool;
|
||||
|
||||
auto FromUnitFb(const Unit &unit) -> net::eagle0::common::CommonUnit;
|
||||
|
||||
auto FromInternalStatus(
|
||||
const net::eagle0::shardok::storage::ResolvedUnit::UnitStatus internalStatus)
|
||||
-> net::eagle0::common::UnitStatus;
|
||||
|
||||
} // namespace shardok
|
||||
|
||||
#endif /* GameOverResponsePopulator_hpp */
|
||||
@@ -13,7 +13,6 @@
|
||||
#include <utility>
|
||||
|
||||
#include "ServerConfiguration.hpp"
|
||||
#include "src/main/cpp/net/eagle0/common/UnitConversions.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/controller/ShardokGameController.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings_loader/SettingsLoader.hpp"
|
||||
@@ -31,7 +30,6 @@ auto SetUpController(
|
||||
int month,
|
||||
const vector<Unit> &units,
|
||||
const vector<PlayerInfoProto> &playerSetupInfos,
|
||||
const TutorialBattleConfigProto &tutorialConfig,
|
||||
const string &serializedRequest) -> shared_ptr<ShardokGameController>;
|
||||
|
||||
ShardokGamesManager::ShardokGamesManager(const std::vector<std::string> &extraSettings) {
|
||||
@@ -57,11 +55,6 @@ auto ShardokGamesManager::GetController(const GameId &gameId)
|
||||
return existing->second;
|
||||
}
|
||||
|
||||
void ShardokGamesManager::RemoveController(const GameId &gameId) {
|
||||
std::shared_lock<std::shared_mutex> lk(runningControllersLock);
|
||||
runningControllers.erase(gameId);
|
||||
}
|
||||
|
||||
auto ShardokGamesManager::UnlockedCreateFromGameStateBytes(
|
||||
const byte_vector &gameStateBytes,
|
||||
int32_t startingHistoryCount,
|
||||
@@ -84,7 +77,6 @@ auto ShardokGamesManager::UnlockedCreateSpecifiedGame(
|
||||
const string &mapName,
|
||||
const string &customMapBytes,
|
||||
const int month,
|
||||
const TutorialBattleConfigProto &tutorialConfig,
|
||||
const string &serializedRequest) -> GameId {
|
||||
// Check for existing game with this ID and early-exit
|
||||
std::shared_lock<std::shared_mutex> lk(runningControllersLock);
|
||||
@@ -119,7 +111,6 @@ auto ShardokGamesManager::UnlockedCreateSpecifiedGame(
|
||||
month,
|
||||
units,
|
||||
playerInfos,
|
||||
tutorialConfig,
|
||||
serializedRequest);
|
||||
|
||||
return LockedCreateGame(controller, playerSetupInfos, serializedRequest);
|
||||
@@ -146,7 +137,6 @@ auto SetUpController(
|
||||
const int month,
|
||||
const vector<Unit> &units,
|
||||
const vector<PlayerInfoProto> &playerInfos,
|
||||
const TutorialBattleConfigProto &tutorialConfig,
|
||||
const string &serializedRequest) -> shared_ptr<ShardokGameController> {
|
||||
auto unplacedUnits = std::vector<Unit>();
|
||||
|
||||
@@ -154,72 +144,17 @@ auto SetUpController(
|
||||
for (auto up : units) {
|
||||
up.mutate_unit_id(nextUnitId++);
|
||||
up.mutate_status(net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT);
|
||||
|
||||
// Apply initial visibility settings from tutorial config
|
||||
if (tutorialConfig.enabled() && up.has_attached_hero()) {
|
||||
const int heroId = up.attached_hero().eagle_hero_id();
|
||||
for (const auto &visibility : tutorialConfig.initial_visibilities()) {
|
||||
if (visibility.eagle_hero_id() == heroId) {
|
||||
for (const int playerId : visibility.visible_to_player_ids()) {
|
||||
if (playerId >= 0 && playerId < 10) {
|
||||
up.mutable_opponent_knowledge()->Mutate(
|
||||
static_cast<flatbuffers::uoffset_t>(playerId),
|
||||
100);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
unplacedUnits.push_back(up);
|
||||
}
|
||||
|
||||
// Add reinforcement units from tutorial config as PENDING_REINFORCEMENT.
|
||||
// These units exist in the game state from the start but are invisible and inactive
|
||||
// until the TutorialBattleController activates them at the triggered round.
|
||||
if (tutorialConfig.enabled()) {
|
||||
vector<PlayerId> allPlayerIds;
|
||||
for (size_t i = 0; i < playerInfos.size(); i++) {
|
||||
allPlayerIds.push_back(static_cast<PlayerId>(i));
|
||||
}
|
||||
|
||||
for (const auto &event : tutorialConfig.events()) {
|
||||
if (!event.has_action() ||
|
||||
event.action().action_type_case() !=
|
||||
net::eagle0::common::TutorialAction::kReinforcements) {
|
||||
continue;
|
||||
}
|
||||
const auto &reinforcements = event.action().reinforcements();
|
||||
const PlayerId playerId = reinforcements.player_id();
|
||||
|
||||
for (const auto &commonUnit : reinforcements.units()) {
|
||||
Unit unit = ConvertUnit(commonUnit, playerId, allPlayerIds);
|
||||
unit.mutate_unit_id(nextUnitId++);
|
||||
unit.mutate_status(
|
||||
net::eagle0::shardok::storage::fb::UnitStatus_PENDING_REINFORCEMENT);
|
||||
unplacedUnits.push_back(unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For tutorial battles, override settings so duels are always accepted
|
||||
auto effectiveSettings = gameSettings;
|
||||
if (tutorialConfig.enabled()) {
|
||||
effectiveSettings = std::make_shared<GameSettings>(*gameSettings);
|
||||
effectiveSettings->GetSetter().SetInt(kSettingsKeys.duelBaseAcceptOdds, 200);
|
||||
}
|
||||
|
||||
unique_ptr<ShardokEngine> engine = std::make_unique<ShardokEngine>(
|
||||
effectiveSettings,
|
||||
gameSettings,
|
||||
std::move(unplacedUnits),
|
||||
customMapBytes.empty() ? LoadMap(mapName) : LoadMapFromBytes(customMapBytes),
|
||||
month,
|
||||
gameId,
|
||||
playerInfos);
|
||||
|
||||
engine->SetTutorialBattleConfig(tutorialConfig);
|
||||
|
||||
return std::make_shared<ShardokGameController>(std::move(engine), mapName, serializedRequest);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
#include "src/main/cpp/net/eagle0/shardok/ai/ShardokAIClient.hpp"
|
||||
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
|
||||
#include "src/main/protobuf/net/eagle0/common/tutorial_battle_config.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/game_status.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/player_info.pb.h"
|
||||
#include "src/main/protobuf/net/eagle0/shardok/common/victory_condition.pb.h"
|
||||
@@ -26,7 +25,6 @@ class GameStatePersister;
|
||||
|
||||
using net::eagle0::shardok::common::GameStatus;
|
||||
using PlayerInfoProto = net::eagle0::shardok::common::PlayerInfo;
|
||||
using TutorialBattleConfigProto = net::eagle0::common::TutorialBattleConfig;
|
||||
using std::string;
|
||||
using std::vector;
|
||||
using VictoryConditionProto = net::eagle0::shardok::common::VictoryCondition;
|
||||
@@ -47,8 +45,7 @@ public:
|
||||
const vector<VictoryConditionProto> &victoryConditions,
|
||||
const vector<PlayerId> &alliedPids,
|
||||
const int32_t eagleFactionId,
|
||||
const bool isAi,
|
||||
const bool cannotBecomeOutlaw) -> PlayerInfoProto {
|
||||
const bool isAi) -> PlayerInfoProto {
|
||||
PlayerInfoProto pi;
|
||||
pi.set_player_id(pid);
|
||||
pi.set_is_defender(def);
|
||||
@@ -57,7 +54,6 @@ public:
|
||||
for (const PlayerId alliedPid : alliedPids) { pi.add_allies()->set_player_id(alliedPid); }
|
||||
pi.set_eagle_faction_id(eagleFactionId);
|
||||
pi.set_is_ai(isAi);
|
||||
pi.set_cannot_become_outlaw(cannotBecomeOutlaw);
|
||||
|
||||
return pi;
|
||||
}
|
||||
@@ -70,8 +66,7 @@ public:
|
||||
const int food,
|
||||
const vector<VictoryConditionProto> &victoryConditions,
|
||||
const vector<PlayerId> &alliedPids,
|
||||
vector<Unit> us,
|
||||
const bool cannotBecomeOutlaw)
|
||||
vector<Unit> us)
|
||||
: playerInfo(MakePlayerInfo(
|
||||
pid,
|
||||
def,
|
||||
@@ -79,8 +74,7 @@ public:
|
||||
victoryConditions,
|
||||
alliedPids,
|
||||
eagleFactionId,
|
||||
isAi,
|
||||
cannotBecomeOutlaw)),
|
||||
isAi)),
|
||||
units(std::move(us)) {}
|
||||
};
|
||||
|
||||
@@ -101,15 +95,12 @@ public:
|
||||
|
||||
auto GetController(const GameId &gameId) -> std::shared_ptr<ShardokGameController>;
|
||||
|
||||
void RemoveController(const GameId &gameId);
|
||||
|
||||
auto UnlockedCreateSpecifiedGame(
|
||||
const GameId &gameId,
|
||||
const vector<PlayerInfoWithUnits> &playerSetupInfos,
|
||||
const string &mapName,
|
||||
const string &customMapBytes,
|
||||
int month,
|
||||
const TutorialBattleConfigProto &tutorialConfig,
|
||||
const string &serializedRequest) -> GameId;
|
||||
|
||||
auto UnlockedCreateFromGameStateBytes(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user