Compare commits

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

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

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 07:35:31 -08:00
3719 changed files with 35943 additions and 47134 deletions
@@ -1,37 +0,0 @@
name: Artifact Storage Check
on:
schedule:
# Run every 6 hours
- cron: '0 */6 * * *'
workflow_dispatch:
jobs:
check-storage:
runs-on: ubuntu-latest
steps:
- name: Check artifact storage size
env:
GH_TOKEN: ${{ github.token }}
run: |
# Calculate total artifact storage
total_bytes=$(gh api "repos/${{ github.repository }}/actions/artifacts" \
--paginate -q '.artifacts[].size_in_bytes' | awk '{sum+=$1} END {print sum}')
total_mb=$((total_bytes / 1024 / 1024))
echo "Total artifact storage: ${total_mb} MB"
# Fail if over 500MB
if [ "$total_mb" -gt 500 ]; then
echo "::error::Artifact storage is ${total_mb} MB, which exceeds the 500 MB threshold!"
echo ""
echo "Largest artifacts:"
gh api "repos/${{ github.repository }}/actions/artifacts" \
--paginate -q '.artifacts[] | "\(.size_in_bytes)\t\(.name)\t\(.created_at)"' | \
sort -rn | head -20 | \
awk -F'\t' '{printf "%d MB\t%s\t%s\n", $1/1024/1024, $2, $3}'
exit 1
fi
echo "Storage is within acceptable limits."
+40 -62
View File
@@ -24,7 +24,7 @@ permissions:
jobs:
build-auth:
runs-on: [self-hosted, bazel]
runs-on: self-hosted
outputs:
image_tag: ${{ steps.push-auth.outputs.image_tag }}
steps:
@@ -104,14 +104,6 @@ jobs:
DISCORD_CLIENT_SECRET: ${{ secrets.DISCORD_CLIENT_SECRET }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
GH_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }}
GH_OAUTH_CLIENT_SECRET: ${{ secrets.GH_OAUTH_CLIENT_SECRET }}
APPLE_SIGNIN_CLIENT_ID: ${{ secrets.APPLE_SIGNIN_CLIENT_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_SIGNIN_KEY_ID: ${{ secrets.APPLE_SIGNIN_KEY_ID }}
APPLE_SIGNIN_PRIVATE_KEY: ${{ secrets.APPLE_SIGNIN_PRIVATE_KEY }}
TWITCH_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }}
TWITCH_CLIENT_SECRET: ${{ secrets.TWITCH_CLIENT_SECRET }}
JWT_PRIVATE_KEY: ${{ secrets.JWT_PRIVATE_KEY }}
FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }}
FASTMAIL_FROM_EMAIL: ${{ secrets.FASTMAIL_FROM_EMAIL }}
@@ -120,6 +112,16 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Copy update-env script to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.DO_DROPLET_IP }}
username: deploy
key: ${{ secrets.DO_SSH_KEY }}
source: "deploy/update-env.sh,deploy/env.template"
target: /opt/eagle0/
strip_components: 1
- name: Deploy auth service to production
uses: appleboy/ssh-action@v1.0.3
with:
@@ -127,75 +129,51 @@ jobs:
username: deploy
key: ${{ secrets.DO_SSH_KEY }}
script_stop: true
envs: AUTH_IMAGE,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GH_OAUTH_CLIENT_ID,GH_OAUTH_CLIENT_SECRET,APPLE_SIGNIN_CLIENT_ID,APPLE_TEAM_ID,APPLE_SIGNIN_KEY_ID,APPLE_SIGNIN_PRIVATE_KEY,TWITCH_CLIENT_ID,TWITCH_CLIENT_SECRET,JWT_PRIVATE_KEY,FASTMAIL_API_TOKEN,FASTMAIL_FROM_EMAIL,FASTMAIL_FROM_NAME
envs: AUTH_IMAGE,DISCORD_CLIENT_ID,DISCORD_CLIENT_SECRET,GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,JWT_PRIVATE_KEY,FASTMAIL_API_TOKEN,FASTMAIL_FROM_EMAIL,FASTMAIL_FROM_NAME
script: |
set -x
cd /opt/eagle0
# Export env vars for docker compose (appleboy/ssh-action sets them but doesn't export)
export AUTH_IMAGE="${AUTH_IMAGE}"
export DISCORD_CLIENT_ID="${DISCORD_CLIENT_ID}"
export DISCORD_CLIENT_SECRET="${DISCORD_CLIENT_SECRET}"
export GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID}"
export GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET}"
export GH_OAUTH_CLIENT_ID="${GH_OAUTH_CLIENT_ID}"
export GH_OAUTH_CLIENT_SECRET="${GH_OAUTH_CLIENT_SECRET}"
export APPLE_SIGNIN_CLIENT_ID="${APPLE_SIGNIN_CLIENT_ID}"
export APPLE_TEAM_ID="${APPLE_TEAM_ID}"
export APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
export APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
export TWITCH_CLIENT_ID="${TWITCH_CLIENT_ID}"
export TWITCH_CLIENT_SECRET="${TWITCH_CLIENT_SECRET}"
export JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
export FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
# Update env vars using shared script (preserves vars set by other workflows)
chmod +x update-env.sh
./update-env.sh \
"AUTH_IMAGE=${AUTH_IMAGE}" \
"DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID}" \
"DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET}" \
"GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}" \
"GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}" \
"JWT_PRIVATE_KEY=${JWT_PRIVATE_KEY}" \
"FASTMAIL_API_TOKEN=${FASTMAIL_API_TOKEN}" \
"FASTMAIL_FROM_EMAIL=${FASTMAIL_FROM_EMAIL}" \
"FASTMAIL_FROM_NAME=${FASTMAIL_FROM_NAME}"
# Login to registry
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
echo "Deploying auth service: $AUTH_IMAGE"
# Pull the image directly (docker is already logged in)
echo "Pulling Auth image..."
docker pull "${AUTH_IMAGE}" || { echo "ERROR: Failed to pull auth image"; exit 1; }
# Use crane to pull image
echo "Installing crane..."
curl -sL https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz | tar xzf - crane
chmod +x crane
# Tag as :latest locally so any fallback uses correct image
docker tag "${AUTH_IMAGE}" registry.digitalocean.com/eagle0/auth-server:latest
echo "Pulling Auth image with crane..."
./crane pull "${AUTH_IMAGE}" auth.tar || { echo "ERROR: Failed to pull auth image"; exit 1; }
echo "Loading Auth image into Docker..."
docker load -i auth.tar
rm auth.tar
rm ./crane
# Debug: check environment and .env file
echo "DEBUG: AUTH_IMAGE=$AUTH_IMAGE"
env | grep AUTH || echo "AUTH_IMAGE not in env output"
if [ -f .env ]; then
echo "DEBUG: .env file contents related to AUTH:"
grep AUTH .env || echo "No AUTH in .env"
fi
# Recreate auth container - pass AUTH_IMAGE explicitly on command line
AUTH_IMAGE="${AUTH_IMAGE}" docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate auth
# Only recreate the auth container (not eagle, shardok, etc.)
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate auth
# Wait for health check
sleep 5
# Verify container is using the correct image
# Note: docker-compose may use :latest tag (which we tagged to the correct image)
echo "=== Verifying auth container image ==="
RUNNING_IMAGE=$(docker inspect auth-server --format '{{.Config.Image}}')
RUNNING_DIGEST=$(docker inspect auth-server --format '{{.Image}}')
EXPECTED_DIGEST=$(docker inspect "${AUTH_IMAGE}" --format '{{.Id}}')
echo "Expected image: ${AUTH_IMAGE}"
echo "Running image: ${RUNNING_IMAGE}"
echo "Expected digest: ${EXPECTED_DIGEST}"
echo "Running digest: ${RUNNING_DIGEST}"
if [ "$RUNNING_DIGEST" != "$EXPECTED_DIGEST" ]; then
echo "ERROR: Container is running wrong image!"
exit 1
fi
echo "Image digests match - correct image is running"
# Show container status
docker compose -f docker-compose.prod.yml ps auth
# Verify container is using correct image
echo "=== Verifying auth container image ==="
docker compose -f docker-compose.prod.yml images auth
# Cleanup old images
docker image prune -f
+6 -5
View File
@@ -26,16 +26,16 @@ permissions:
jobs:
test:
runs-on: [self-hosted, bazel]
runs-on: self-hosted
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
- name: Check BUILD.bazel dependencies
run: ./scripts/check_build_deps.sh --strict
- name: Run tests
id: test
continue-on-error: true
run: bazel test --build_event_json_file=test.json //src/test/... //src/main/go/...
- name: Collect failed test logs
if: always()
@@ -75,7 +75,6 @@ jobs:
with:
name: test.json
path: test.json
retention-days: 5
- name: Archive failed test logs
if: always()
uses: actions/upload-artifact@v4
@@ -83,4 +82,6 @@ jobs:
name: failed-test-logs
path: failed_test_logs/
if-no-files-found: ignore
retention-days: 5
- name: Fail if tests failed
if: steps.test.outcome == 'failure'
run: exit 1
+21
View File
@@ -0,0 +1,21 @@
name: Build Protos
on:
pull_request:
paths:
- "src/main/protobuf/**"
permissions:
contents: read
jobs:
build:
runs-on: self-hosted
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
- name: Run tests
run: ./scripts/build_protos.sh
-2
View File
@@ -37,7 +37,6 @@ jobs:
with:
name: ubuntu-noble-sysroot-amd64
path: tools/sysroot/output/
retention-days: 1
- name: Install AWS CLI
run: |
@@ -99,7 +98,6 @@ jobs:
with:
name: ubuntu-noble-sysroot-arm64
path: tools/sysroot/output/
retention-days: 1
- name: Install AWS CLI
run: |
+35
View File
@@ -0,0 +1,35 @@
name: Client Presigner
on:
push:
branches: [ "main" ]
paths:
- ".github/workflows/client_presigner.yml"
- "src/main/go/net/eagle0/client_download/**"
- "src/main/go/net/eagle0/util/**"
pull_request:
paths:
- ".github/workflows/client_presigner.yml"
- "src/main/go/net/eagle0/client_download/**"
- "src/main/go/net/eagle0/util/**"
permissions:
contents: read
jobs:
client-presigner:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
with:
lfs: false
clean: false
- name: Build Client Presigner
run: bazel build --platforms=@io_bazel_rules_go//go/toolchain:linux_amd64 //src/main/go/net/eagle0/client_download
- name: Archive presigner binary
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: client_download
path: bazel-bin/src/main/go/net/eagle0/client_download/client_download_/client_download
+490 -226
View File
@@ -4,18 +4,10 @@ on:
push:
branches: [ "main" ]
paths:
# Note: C++ changes trigger shardok_arm64_build.yml instead
# Note: Auth changes trigger auth_build.yml instead
# Note: Windows installer changes trigger installer_build.yml instead
- 'src/main/cpp/**'
- 'src/main/go/**'
- '!src/main/go/net/eagle0/authservice/**'
- '!src/main/go/net/eagle0/authcli/**'
- '!src/main/go/net/eagle0/clients/**'
- 'src/main/scala/**'
- 'src/main/protobuf/**'
- '!src/main/protobuf/net/eagle0/eagle/api/auth.proto'
- '!src/main/protobuf/net/eagle0/eagle/api/admin/**'
- '!src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
- 'src/main/resources/**'
- 'ci/BUILD.bazel'
- 'MODULE.bazel'
@@ -30,63 +22,293 @@ on:
default: 'false'
type: boolean
# Only allow one deployment at a time to prevent race conditions
concurrency:
group: docker-build-deploy
cancel-in-progress: false # Don't cancel running deployments, queue new ones
permissions:
contents: read
jobs:
# Single consolidated build job - builds all images with one bazel invocation
# This uses 1 runner slot instead of 4, and Bazel parallelizes internally
# NOTE: Must run on a runner with the bazel label to ensure consistent Bazel cache
build-all:
runs-on: [self-hosted, bazel]
build-eagle:
runs-on: self-hosted
outputs:
eagle_image_tag: ${{ steps.push-images.outputs.eagle_image_tag }}
admin_image_tag: ${{ steps.push-images.outputs.admin_image_tag }}
jfr_sidecar_image_tag: ${{ steps.push-images.outputs.jfr_sidecar_image_tag }}
image_tag: ${{ steps.push-eagle.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
- name: Build all Docker images
id: build-all
- name: Build Eagle Docker image
id: build-eagle
run: |
set -ex
bazel build --platforms=//:linux_x86_64 //ci:eagle_server_image
# Build ALL images in a single bazel command - Bazel parallelizes internally
# Note: Shardok is built separately for ARM64 and deployed to Hetzner
echo "=== Building Docker images ==="
bazel build \
--platforms=//:linux_x86_64 \
--extra_toolchains=@llvm_toolchain_linux//:all \
//ci:eagle_server_image \
//ci:admin_server_image \
//ci:jfr_sidecar_image \
//src/main/go/net/eagle0/warmup:warmup_linux_amd64
# Also build the warmup tool for Linux
bazel build //src/main/go/net/eagle0/warmup:warmup_linux_amd64
# Copy warmup binary to scripts/ for deployment
# Copy warmup binary to scripts/ so it gets deployed with other scripts
mkdir -p scripts/bin
cp bazel-bin/src/main/go/net/eagle0/warmup/warmup_linux_amd64_/warmup_linux_amd64 scripts/bin/warmup
# Save all image paths before any other bazel command changes bazel-bin symlink
EAGLE_PATH=$(readlink -f bazel-bin/ci/eagle_server_image)
ADMIN_PATH=$(readlink -f bazel-bin/ci/admin_server_image)
JFR_PATH=$(readlink -f bazel-bin/ci/jfr_sidecar_image)
# Save the resolved path before any other bazel command changes bazel-bin symlink
IMAGE_PATH=$(readlink -f bazel-bin/ci/eagle_server_image)
echo "Image path: $IMAGE_PATH"
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
echo "eagle_path=$EAGLE_PATH" >> $GITHUB_OUTPUT
echo "admin_path=$ADMIN_PATH" >> $GITHUB_OUTPUT
echo "jfr_path=$JFR_PATH" >> $GITHUB_OUTPUT
- name: Login to DigitalOcean Container Registry
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
run: |
mkdir -p ~/.docker
AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
# Also set for current directory in case Bazel uses different home
mkdir -p .docker
cp ~/.docker/config.json .docker/
echo "=== Image paths ==="
echo "Eagle: $EAGLE_PATH"
echo "Admin: $ADMIN_PATH"
echo "JFR Sidecar: $JFR_PATH"
- name: Push Eagle image to DO registry
id: push-eagle
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DOCKER_CONFIG: ${{ github.workspace }}/.docker
run: |
set -ex
# Use cross-compiled image path from build step
EAGLE_IMAGE="${{ steps.build-eagle.outputs.image_path }}"
echo "Using Eagle image: $EAGLE_IMAGE"
if [ -z "$EAGLE_IMAGE" ] || [ ! -d "$EAGLE_IMAGE" ]; then
echo "ERROR: Eagle image not found at: $EAGLE_IMAGE"
exit 1
fi
# Debug: show OCI layout contents
echo "=== OCI Layout Contents ==="
cat "$EAGLE_IMAGE/index.json"
echo ""
echo "=== Blobs ==="
ls -la "$EAGLE_IMAGE/blobs/sha256/" | head -20
# Verify OCI layout consistency before pushing
echo "=== Verifying OCI layout consistency ==="
for digest in $(cat "$EAGLE_IMAGE/index.json" | grep -o '"sha256:[^"]*"' | tr -d '"'); do
blob_path="$EAGLE_IMAGE/blobs/${digest/://}"
if [ ! -f "$blob_path" ]; then
echo "ERROR: Blob not found: $blob_path"
exit 1
fi
actual_digest="sha256:$(shasum -a 256 "$blob_path" | cut -d' ' -f1)"
if [ "$digest" != "$actual_digest" ]; then
echo "ERROR: Digest mismatch for $blob_path"
echo " Index says: $digest"
echo " Actual: $actual_digest"
exit 1
fi
echo "✓ Verified: $digest"
done
# Build the push target to get crane in runfiles
bazel build //ci:eagle_server_push
# Use crane directly for push (avoids OCI->Docker digest mismatch)
CRANE="bazel-bin/ci/push_eagle_server_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
echo "Using crane: $CRANE"
# Push with SHA tag
GIT_SHA=$(git rev-parse --short=8 HEAD)
IMAGE_TAG="registry.digitalocean.com/eagle0/eagle-server:${GIT_SHA}"
echo "Pushing eagle image: $IMAGE_TAG"
$CRANE push "$EAGLE_IMAGE" "$IMAGE_TAG"
# Verify push by checking what's in the registry
echo "=== Verifying push ==="
$CRANE manifest "$IMAGE_TAG" | head -50
PUSHED_DIGEST=$($CRANE digest "$IMAGE_TAG")
echo "Registry reports digest: $PUSHED_DIGEST"
# Output the full image tag for deploy step
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
# Also update :latest for convenience (but deploy won't use it)
echo "Copying to :latest tag"
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/eagle-server:latest"
build-shardok:
runs-on: self-hosted
outputs:
image_tag: ${{ steps.push-shardok.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
- name: Build Shardok binary (cross-compile for Linux)
run: |
set -ex
# Step 1: Build JUST the binary with cross-compilation
# We need --extra_toolchains to force the Linux toolchain to be used
# because toolchains_llvm registers with dev_dependency=True
echo "=== Building shardok-server binary for linux-x86_64 ==="
bazel build \
--platforms=//:linux_x86_64 \
--extra_toolchains=@llvm_toolchain_linux//:all \
//src/main/cpp/net/eagle0/shardok:shardok-server
# Step 2: Check the binary directly from bazel-bin
# bazel-bin is a symlink that points to the correct output directory
LINUX_BIN="bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server"
echo "=== Checking binary at: $LINUX_BIN ==="
if [ ! -f "$LINUX_BIN" ]; then
echo "ERROR: Binary not found at $LINUX_BIN"
exit 1
fi
# Debug: show what bazel-bin points to
echo "bazel-bin symlink target: $(readlink bazel-bin || echo 'not a symlink')"
# Step 3: Verify it's ELF (Linux) not Mach-O (macOS)
echo "=== Verifying binary format ==="
MAGIC=$(head -c 4 "$LINUX_BIN" | xxd -p)
echo "Binary magic bytes: $MAGIC"
if [ "$MAGIC" = "7f454c46" ]; then
echo "SUCCESS: Binary is ELF format (Linux)"
elif [ "$MAGIC" = "cfaeedfe" ] || [ "$MAGIC" = "cffaedfe" ]; then
echo "ERROR: Binary is Mach-O format (macOS) - cross-compilation failed!"
echo ""
echo "Debug info:"
echo "- bazel-bin points to: $(readlink bazel-bin)"
file "$LINUX_BIN" || true
exit 1
else
echo "WARNING: Unknown binary format: $MAGIC"
file "$LINUX_BIN" || true
fi
- name: Build Shardok Docker image
id: build-shardok
run: |
set -ex
# Build the OCI image with cross-compilation flags
bazel build \
--platforms=//:linux_x86_64 \
--extra_toolchains=@llvm_toolchain_linux//:all \
//ci:shardok_server_image
# The image is output to bazel-bin which is a symlink.
# Resolve it now before any other bazel commands change where it points.
IMAGE_PATH=$(readlink -f bazel-bin/ci/shardok_server_image)
echo "Image path: $IMAGE_PATH"
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
# Verify the binary inside the tar layer is ELF
echo "=== Verifying binary in image tar ==="
BINARY_TAR="bazel-bin/ci/shardok_binary_layer.tar"
if [ -f "$BINARY_TAR" ]; then
echo "Checking binary in $BINARY_TAR"
# Extract just the first 4 bytes of the binary from the tar
MAGIC=$(tar -xOf "$BINARY_TAR" app/shardok-server 2>/dev/null | head -c 4 | xxd -p)
echo "Binary magic in tar: $MAGIC"
if [ "$MAGIC" = "7f454c46" ]; then
echo "SUCCESS: Binary in tar is ELF format (Linux)"
else
echo "ERROR: Binary in tar is NOT ELF format!"
echo "This means pkg_tar is packaging the wrong binary."
exit 1
fi
else
echo "WARNING: Could not find $BINARY_TAR"
fi
- name: Login to DigitalOcean Container Registry
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
run: |
mkdir -p ~/.docker
AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
# Also set for current directory in case Bazel uses different home
mkdir -p .docker
cp ~/.docker/config.json .docker/
- name: Push Shardok image to DO registry
id: push-shardok
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DOCKER_CONFIG: ${{ github.workspace }}/.docker
run: |
set -ex
# Use cross-compiled image path from build step
CROSS_IMAGE="${{ steps.build-shardok.outputs.image_path }}"
echo "Using cross-compiled image: $CROSS_IMAGE"
if [ -z "$CROSS_IMAGE" ] || [ ! -d "$CROSS_IMAGE" ]; then
echo "ERROR: Cross-compiled image not found at: $CROSS_IMAGE"
exit 1
fi
# Get crane from Eagle push target (which doesn't need cross-compilation)
# This gives us a macOS crane binary we can actually run.
# We can't build shardok_server_push with platform flags because it would
# download a Linux crane that can't run on macOS.
bazel build //ci:eagle_server_push
# Find the Darwin crane binary (may be a symlink)
RUNFILES="bazel-bin/ci/push_eagle_server_push.sh.runfiles"
CRANE=$(find "$RUNFILES" -path "*darwin*" -name crane 2>/dev/null | head -1)
if [ -z "$CRANE" ]; then
# Fallback to any crane
CRANE=$(find "$RUNFILES" -name crane 2>/dev/null | head -1)
fi
if [ -z "$CRANE" ] || [ ! -e "$CRANE" ]; then
echo "ERROR: crane not found. Listing runfiles:"
find "$RUNFILES" -name crane 2>/dev/null || true
exit 1
fi
echo "Using crane: $CRANE"
# Push the cross-compiled image with SHA tag
GIT_SHA=$(git rev-parse --short=8 HEAD)
IMAGE_TAG="registry.digitalocean.com/eagle0/shardok-server:${GIT_SHA}"
echo "Pushing shardok image: $IMAGE_TAG"
$CRANE push "$CROSS_IMAGE" "$IMAGE_TAG"
# Output the full image tag for deploy step
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
# Also update :latest for convenience (but deploy won't use it)
echo "Copying to :latest tag"
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/shardok-server:latest"
build-admin:
runs-on: self-hosted
outputs:
image_tag: ${{ steps.push-admin.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
- name: Build Admin Server Docker image
id: build-admin
run: |
set -ex
# Build admin server image (Go binary has explicit goos/goarch in BUILD.bazel)
bazel build //ci:admin_server_image
# Save the resolved path before any other bazel command changes bazel-bin symlink
IMAGE_PATH=$(readlink -f bazel-bin/ci/admin_server_image)
echo "Image path: $IMAGE_PATH"
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
- name: Login to DigitalOcean Container Registry
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
@@ -99,70 +321,123 @@ jobs:
mkdir -p .docker
cp ~/.docker/config.json .docker/
- name: Push all images to DO registry
id: push-images
- name: Push Admin image to DO registry
id: push-admin
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DOCKER_CONFIG: ${{ github.workspace }}/.docker
run: |
set -ex
GIT_SHA=$(git rev-parse --short=8 HEAD)
ADMIN_IMAGE="${{ steps.build-admin.outputs.image_path }}"
echo "Using Admin image: $ADMIN_IMAGE"
# Get crane from push target runfiles
bazel build //ci:eagle_server_push
CRANE="bazel-bin/ci/push_eagle_server_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
if [ ! -e "$CRANE" ]; then
# Fallback: find any Darwin crane
RUNFILES="bazel-bin/ci/push_eagle_server_push.sh.runfiles"
CRANE=$(find "$RUNFILES" -path "*darwin*" -name crane 2>/dev/null | head -1)
fi
if [ -z "$CRANE" ] || [ ! -e "$CRANE" ]; then
echo "ERROR: crane not found"
if [ -z "$ADMIN_IMAGE" ] || [ ! -d "$ADMIN_IMAGE" ]; then
echo "ERROR: Admin image not found at: $ADMIN_IMAGE"
exit 1
fi
# Build the push target to get crane in runfiles
bazel build //ci:admin_server_push
# Use crane directly for push
CRANE="bazel-bin/ci/push_admin_server_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
echo "Using crane: $CRANE"
# Push 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 with SHA tag
GIT_SHA=$(git rev-parse --short=8 HEAD)
IMAGE_TAG="registry.digitalocean.com/eagle0/admin-server:${GIT_SHA}"
echo "Pushing admin image: $IMAGE_TAG"
$CRANE push "$ADMIN_IMAGE" "$IMAGE_TAG"
# 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
# Output the full image tag for deploy step
echo "image_tag=$IMAGE_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
# Also update :latest for convenience
echo "Copying to :latest tag"
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/admin-server:latest"
echo "=== All images pushed successfully ==="
build-jfr-sidecar:
runs-on: self-hosted
outputs:
image_tag: ${{ steps.push-jfr-sidecar.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
- name: Build JFR Sidecar Docker image
id: build-jfr-sidecar
run: |
set -ex
# Build JFR sidecar image (Go binary has explicit goos/goarch in BUILD.bazel)
bazel build //ci:jfr_sidecar_image
# Save the resolved path before any other bazel command changes bazel-bin symlink
IMAGE_PATH=$(readlink -f bazel-bin/ci/jfr_sidecar_image)
echo "Image path: $IMAGE_PATH"
echo "image_path=$IMAGE_PATH" >> $GITHUB_OUTPUT
- name: Login to DigitalOcean Container Registry
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DO_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
run: |
mkdir -p ~/.docker
AUTH=$(echo -n "${DO_TOKEN}:${DO_TOKEN}" | base64)
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
mkdir -p .docker
cp ~/.docker/config.json .docker/
- name: Push JFR Sidecar image to DO registry
id: push-jfr-sidecar
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
env:
DOCKER_CONFIG: ${{ github.workspace }}/.docker
run: |
set -ex
JFR_IMAGE="${{ steps.build-jfr-sidecar.outputs.image_path }}"
echo "Using JFR Sidecar image: $JFR_IMAGE"
if [ -z "$JFR_IMAGE" ] || [ ! -d "$JFR_IMAGE" ]; then
echo "ERROR: JFR Sidecar image not found at: $JFR_IMAGE"
exit 1
fi
# Build the push target to get crane in runfiles
bazel build //ci:jfr_sidecar_push
# Use crane directly for push
CRANE="bazel-bin/ci/push_jfr_sidecar_push.sh.runfiles/rules_oci~~oci~oci_crane_darwin_arm64/crane"
echo "Using crane: $CRANE"
# Push with SHA tag
GIT_SHA=$(git rev-parse --short=8 HEAD)
IMAGE_TAG="registry.digitalocean.com/eagle0/jfr-sidecar:${GIT_SHA}"
echo "Pushing JFR sidecar image: $IMAGE_TAG"
$CRANE push "$JFR_IMAGE" "$IMAGE_TAG"
# Output the full image tag for deploy step
echo "image_tag=$IMAGE_TAG" >> $GITHUB_OUTPUT
# Also update :latest for convenience
echo "Copying to :latest tag"
$CRANE copy "$IMAGE_TAG" "registry.digitalocean.com/eagle0/jfr-sidecar:latest"
deploy:
runs-on: [self-hosted, bazel]
needs: [build-all]
runs-on: self-hosted
needs: [build-eagle, build-shardok, build-admin, build-jfr-sidecar]
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
environment: production
env:
EAGLE_IMAGE: ${{ needs.build-all.outputs.eagle_image_tag }}
ADMIN_IMAGE: ${{ needs.build-all.outputs.admin_image_tag }}
JFR_SIDECAR_IMAGE: ${{ needs.build-all.outputs.jfr_sidecar_image_tag }}
EAGLE_IMAGE: ${{ needs.build-eagle.outputs.image_tag }}
SHARDOK_IMAGE: ${{ needs.build-shardok.outputs.image_tag }}
ADMIN_IMAGE: ${{ needs.build-admin.outputs.image_tag }}
JFR_SIDECAR_IMAGE: ${{ needs.build-jfr-sidecar.outputs.image_tag }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GPT_MODEL_NAME: ${{ secrets.GPT_MODEL_NAME }}
EAGLE_ENABLE_S3: ${{ secrets.EAGLE_ENABLE_S3 }}
DO_SPACES_ACCESS_KEY: ${{ secrets.DO_SPACES_ACCESS_KEY }}
@@ -172,14 +447,6 @@ jobs:
DISCORD_CLIENT_SECRET: ${{ secrets.DISCORD_CLIENT_SECRET }}
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
GH_OAUTH_CLIENT_ID: ${{ secrets.GH_OAUTH_CLIENT_ID }}
GH_OAUTH_CLIENT_SECRET: ${{ secrets.GH_OAUTH_CLIENT_SECRET }}
APPLE_SIGNIN_CLIENT_ID: ${{ secrets.APPLE_SIGNIN_CLIENT_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_SIGNIN_KEY_ID: ${{ secrets.APPLE_SIGNIN_KEY_ID }}
APPLE_SIGNIN_PRIVATE_KEY: ${{ secrets.APPLE_SIGNIN_PRIVATE_KEY }}
TWITCH_CLIENT_ID: ${{ secrets.TWITCH_CLIENT_ID }}
TWITCH_CLIENT_SECRET: ${{ secrets.TWITCH_CLIENT_SECRET }}
SHARDOK_ADDRESS: ${{ secrets.SHARDOK_ADDRESS }}
SHARDOK_AUTH_TOKEN: ${{ secrets.SHARDOK_AUTH_TOKEN }}
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
@@ -208,17 +475,21 @@ jobs:
- name: Copy config files to droplet
run: |
# Create directory structure on remote
# Use -p to create parents, and test write access before copying
ssh -i ~/.ssh/deploy_key deploy@"$DO_DROPLET_IP" bash -s << 'SETUP_DIRS'
set -e
mkdir -p /opt/eagle0/scripts/bin /opt/eagle0/nginx
mkdir -p /opt/eagle0/scripts/bin /opt/eagle0/nginx /opt/eagle0/deploy
# Remove existing warmup binary (it may have read-only permissions from bazel)
rm -f /opt/eagle0/scripts/bin/warmup
SETUP_DIRS
# Copy files
# Copy files preserving structure
scp -i ~/.ssh/deploy_key docker-compose.prod.yml deploy@"$DO_DROPLET_IP":/opt/eagle0/
scp -i ~/.ssh/deploy_key nginx/nginx.conf deploy@"$DO_DROPLET_IP":/opt/eagle0/nginx/
scp -i ~/.ssh/deploy_key scripts/deploy-blue-green.sh scripts/warmup-eagle.sh scripts/eagle-exec.sh scripts/eagle-logs.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/
scp -i ~/.ssh/deploy_key scripts/deploy-blue-green.sh scripts/warmup-eagle.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/
scp -i ~/.ssh/deploy_key scripts/bin/warmup deploy@"$DO_DROPLET_IP":/opt/eagle0/scripts/bin/
scp -i ~/.ssh/deploy_key deploy/env.template deploy/update-env.sh deploy@"$DO_DROPLET_IP":/opt/eagle0/deploy/
- name: Deploy to production droplet
run: |
@@ -226,163 +497,156 @@ jobs:
set -ex
cd /opt/eagle0
# =================================================================
# CRITICAL: Validate environment variables before proceeding
# This catches GitHub Actions secret store hiccups early
# =================================================================
validate_env() {
local var_name="\$1"
local var_value="\$2"
local default_value="\${3:-}"
# Environment variables passed via heredoc
EAGLE_IMAGE="${EAGLE_IMAGE}"
SHARDOK_IMAGE="${SHARDOK_IMAGE}"
ADMIN_IMAGE="${ADMIN_IMAGE}"
JFR_SIDECAR_IMAGE="${JFR_SIDECAR_IMAGE}"
OPENAI_API_KEY="${OPENAI_API_KEY}"
GPT_MODEL_NAME="${GPT_MODEL_NAME}"
EAGLE_ENABLE_S3="${EAGLE_ENABLE_S3}"
DO_SPACES_ACCESS_KEY="${DO_SPACES_ACCESS_KEY}"
DO_SPACES_SECRET_KEY="${DO_SPACES_SECRET_KEY}"
JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
DISCORD_CLIENT_ID="${DISCORD_CLIENT_ID}"
DISCORD_CLIENT_SECRET="${DISCORD_CLIENT_SECRET}"
GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID}"
GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET}"
SHARDOK_ADDRESS="${SHARDOK_ADDRESS}"
SHARDOK_AUTH_TOKEN="${SHARDOK_AUTH_TOKEN}"
SENTRY_DSN="${SENTRY_DSN}"
FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
DO_REGISTRY_TOKEN="${DO_REGISTRY_TOKEN}"
if [ -z "\${var_value}" ]; then
echo "ERROR: \${var_name} is empty. GitHub Actions secrets may have failed to load."
echo "Please retry the workflow."
return 1
fi
# Check if we got a default value instead of the real secret
if [ -n "\${default_value}" ] && [ "\${var_value}" = "\${default_value}" ]; then
echo "ERROR: \${var_name} has default value '\${default_value}' instead of the actual secret."
echo "This indicates GitHub Actions secrets failed to load. Please retry the workflow."
return 1
fi
return 0
}
echo "Validating critical environment variables..."
# These are the raw values from GitHub Actions (before export)
# We check them before exporting to catch issues early
VALIDATION_FAILED=0
validate_env "SHARDOK_ADDRESS" "${SHARDOK_ADDRESS}" "" || VALIDATION_FAILED=1
validate_env "EAGLE_IMAGE" "${EAGLE_IMAGE}" "" || VALIDATION_FAILED=1
validate_env "JWT_PRIVATE_KEY" "${JWT_PRIVATE_KEY}" "" || VALIDATION_FAILED=1
validate_env "DO_REGISTRY_TOKEN" "${DO_REGISTRY_TOKEN}" "" || VALIDATION_FAILED=1
if [ "\${VALIDATION_FAILED}" -eq 1 ]; then
echo ""
echo "========================================="
echo "DEPLOYMENT ABORTED: Missing critical secrets"
echo "This is likely a transient GitHub Actions issue."
echo "Please retry the workflow."
echo "========================================="
exit 1
fi
echo "All critical environment variables validated successfully."
# =================================================================
# Export environment variables for docker compose
# These are passed via heredoc and exported so child processes (docker compose) can access them
export EAGLE_IMAGE="${EAGLE_IMAGE}"
export ADMIN_IMAGE="${ADMIN_IMAGE}"
export JFR_SIDECAR_IMAGE="${JFR_SIDECAR_IMAGE}"
export OPENAI_API_KEY="${OPENAI_API_KEY}"
export ANTHROPIC_API_KEY="${ANTHROPIC_API_KEY}"
export GEMINI_API_KEY="${GEMINI_API_KEY}"
export GPT_MODEL_NAME="${GPT_MODEL_NAME:-gpt-4o}"
export EAGLE_ENABLE_S3="${EAGLE_ENABLE_S3:-false}"
export DO_SPACES_ACCESS_KEY="${DO_SPACES_ACCESS_KEY}"
export DO_SPACES_SECRET_KEY="${DO_SPACES_SECRET_KEY}"
export JWT_PRIVATE_KEY="${JWT_PRIVATE_KEY}"
export DISCORD_CLIENT_ID="${DISCORD_CLIENT_ID}"
export DISCORD_CLIENT_SECRET="${DISCORD_CLIENT_SECRET}"
export GOOGLE_CLIENT_ID="${GOOGLE_CLIENT_ID}"
export GOOGLE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET}"
export GH_OAUTH_CLIENT_ID="${GH_OAUTH_CLIENT_ID}"
export GH_OAUTH_CLIENT_SECRET="${GH_OAUTH_CLIENT_SECRET}"
export APPLE_SIGNIN_CLIENT_ID="${APPLE_SIGNIN_CLIENT_ID}"
export APPLE_TEAM_ID="${APPLE_TEAM_ID}"
export APPLE_SIGNIN_KEY_ID="${APPLE_SIGNIN_KEY_ID}"
export APPLE_SIGNIN_PRIVATE_KEY="${APPLE_SIGNIN_PRIVATE_KEY}"
export TWITCH_CLIENT_ID="${TWITCH_CLIENT_ID}"
export TWITCH_CLIENT_SECRET="${TWITCH_CLIENT_SECRET}"
export SHARDOK_ADDRESS="${SHARDOK_ADDRESS}"
export SHARDOK_AUTH_TOKEN="${SHARDOK_AUTH_TOKEN}"
export SENTRY_DSN="${SENTRY_DSN}"
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
export FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
export DO_REGISTRY_TOKEN="${DO_REGISTRY_TOKEN}"
# Check Docker has IPv6 support
# Check Docker has IPv6 support for connecting to Hetzner Shardok
# (One-time setup: sudo tee /etc/docker/daemon.json <<< '{"ipv6": true, "ip6tables": true, "experimental": true, "fixed-cidr-v6": "fd00::/80"}' && sudo systemctl restart docker)
if ! cat /etc/docker/daemon.json 2>/dev/null | grep -q '"ip6tables"'; then
echo "WARNING: Docker IPv6 not configured. Eagle may not reach Hetzner Shardok."
echo "Run: sudo tee /etc/docker/daemon.json <<< '{\"ipv6\": true, \"ip6tables\": true, \"experimental\": true, \"fixed-cidr-v6\": \"fd00::/80\"}' && sudo systemctl restart docker"
fi
# Update env vars using shared script (preserves vars set by other workflows)
chmod +x update-env.sh
./update-env.sh \
"EAGLE_IMAGE=\${EAGLE_IMAGE}" \
"SHARDOK_IMAGE=\${SHARDOK_IMAGE}" \
"ADMIN_IMAGE=\${ADMIN_IMAGE}" \
"JFR_SIDECAR_IMAGE=\${JFR_SIDECAR_IMAGE}" \
"OPENAI_API_KEY=\${OPENAI_API_KEY}" \
"GPT_MODEL_NAME=\${GPT_MODEL_NAME:-gpt-4o}" \
"EAGLE_ENABLE_S3=\${EAGLE_ENABLE_S3:-false}" \
"DO_SPACES_ACCESS_KEY=\${DO_SPACES_ACCESS_KEY}" \
"DO_SPACES_SECRET_KEY=\${DO_SPACES_SECRET_KEY}" \
"JWT_PRIVATE_KEY=\${JWT_PRIVATE_KEY}" \
"DISCORD_CLIENT_ID=\${DISCORD_CLIENT_ID}" \
"DISCORD_CLIENT_SECRET=\${DISCORD_CLIENT_SECRET}" \
"GOOGLE_CLIENT_ID=\${GOOGLE_CLIENT_ID}" \
"GOOGLE_CLIENT_SECRET=\${GOOGLE_CLIENT_SECRET}" \
"SHARDOK_ADDRESS=\${SHARDOK_ADDRESS:-shardok:40042}" \
"SHARDOK_AUTH_TOKEN=\${SHARDOK_AUTH_TOKEN}" \
"SENTRY_DSN=\${SENTRY_DSN}" \
"FASTMAIL_API_TOKEN=\${FASTMAIL_API_TOKEN}" \
"FASTMAIL_FROM_EMAIL=\${FASTMAIL_FROM_EMAIL}" \
"FASTMAIL_FROM_NAME=\${FASTMAIL_FROM_NAME}"
# Login to registry
echo "\$DO_REGISTRY_TOKEN" | docker login registry.digitalocean.com -u "\$DO_REGISTRY_TOKEN" --password-stdin
echo "Using images: \$EAGLE_IMAGE, \$ADMIN_IMAGE, \$JFR_SIDECAR_IMAGE"
# Use exact image tags passed from build jobs (no :latest fallback)
# Note: AUTH_IMAGE is managed separately by auth_build.yml
echo "Using images: \$EAGLE_IMAGE, \$SHARDOK_IMAGE, \$ADMIN_IMAGE, \$JFR_SIDECAR_IMAGE"
# Install crane for pulling OCI images
# Use crane to pull images (handles OCI format correctly) then load into Docker
# This avoids digest mismatch from DO registry's OCI->Docker format conversion
echo "Installing crane..."
rm -f crane
curl -sL https://github.com/google/go-containerregistry/releases/download/v0.20.2/go-containerregistry_Linux_x86_64.tar.gz | tar xzf - crane
chmod +x crane
if [ ! -x crane ]; then
echo "ERROR: Failed to install crane"
ls -la crane || true
exit 1
fi
echo "Crane installed"
ls -la crane
# Pull and load all images
echo "Pulling Eagle image..."
./crane pull "\${EAGLE_IMAGE}" eagle.tar && docker load -i eagle.tar && rm eagle.tar
# crane uses Docker config for auth
echo "Pulling Eagle image with crane..."
./crane pull "\${EAGLE_IMAGE}" eagle.tar || { echo "ERROR: Failed to pull eagle image"; exit 1; }
echo "Loading Eagle image into Docker..."
docker load -i eagle.tar
rm eagle.tar
echo "Pulling Admin image..."
./crane pull "\${ADMIN_IMAGE}" admin.tar && docker load -i admin.tar && rm admin.tar
echo "Pulling Shardok image with crane..."
./crane pull "\${SHARDOK_IMAGE}" shardok.tar || { echo "ERROR: Failed to pull shardok image"; exit 1; }
echo "Loading Shardok image into Docker..."
docker load -i shardok.tar
rm shardok.tar
echo "Pulling JFR Sidecar image..."
./crane pull "\${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar && docker load -i jfr-sidecar.tar && rm jfr-sidecar.tar
echo "Pulling Admin image with crane..."
./crane pull "\${ADMIN_IMAGE}" admin.tar || { echo "ERROR: Failed to pull admin image"; exit 1; }
echo "Loading Admin image into Docker..."
docker load -i admin.tar
rm admin.tar
# Pull other compose images
echo "Pulling JFR Sidecar image with crane..."
./crane pull "\${JFR_SIDECAR_IMAGE}" jfr-sidecar.tar || { echo "ERROR: Failed to pull jfr-sidecar image"; exit 1; }
echo "Loading JFR Sidecar image into Docker..."
docker load -i jfr-sidecar.tar
rm jfr-sidecar.tar
# Keep crane for blue-green deploys (don't delete it)
echo "Crane kept at ./crane for future blue-green deploys"
# Also pull other compose images
docker pull nginx:alpine || true
docker pull certbot/certbot || true
echo "All images pulled successfully"
# =================================================================
# Verify Shardok connectivity before proceeding with deployment
# This catches network/firewall issues early
# =================================================================
echo "Verifying Shardok connectivity..."
SHARDOK_HOST=\$(echo "\${SHARDOK_ADDRESS}" | cut -d: -f1)
SHARDOK_PORT=\$(echo "\${SHARDOK_ADDRESS}" | cut -d: -f2)
# Recreate non-Eagle services (not auth - managed by auth_build.yml)
# Note: jfr-sidecar must start after eagle-blue due to PID namespace sharing
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate shardok admin
# Try to connect to Shardok (timeout after 10 seconds)
if nc -z -w 10 "\${SHARDOK_HOST}" "\${SHARDOK_PORT}" 2>/dev/null; then
echo "Shardok connectivity verified: \${SHARDOK_ADDRESS} is reachable"
# Deploy Eagle with blue-green (zero-downtime) if scripts are available
if [ -x "/opt/eagle0/scripts/deploy-blue-green.sh" ]; then
echo "Using blue-green deployment for Eagle..."
chmod +x /opt/eagle0/scripts/*.sh
# Make warmup binary executable if present
if [ -f "/opt/eagle0/scripts/bin/warmup" ]; then
chmod +x /opt/eagle0/scripts/bin/warmup
fi
GIT_SHA=\$(echo "\${EAGLE_IMAGE}" | sed 's/.*://')
/opt/eagle0/scripts/deploy-blue-green.sh "\${GIT_SHA}" || {
echo "Blue-green deployment failed, falling back to direct restart"
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle-blue
}
else
echo ""
echo "========================================="
echo "ERROR: Cannot reach Shardok at \${SHARDOK_ADDRESS}"
echo "This may indicate:"
echo " - Shardok server is not running on Hetzner"
echo " - Network/firewall issues between DigitalOcean and Hetzner"
echo " - Incorrect SHARDOK_ADDRESS configuration"
echo ""
echo "DEPLOYMENT ABORTED: Shardok must be reachable for battles to work."
echo "========================================="
exit 1
echo "Blue-green scripts not found, using direct restart..."
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate eagle-blue
fi
# Stop local shardok container if running (now runs on Hetzner)
docker stop shardok-server 2>/dev/null || true
docker rm shardok-server 2>/dev/null || true
# Start jfr-sidecar after eagle-blue (shares PID namespace with eagle-blue)
docker compose -f docker-compose.prod.yml up -d --no-deps --force-recreate jfr-sidecar
# Deploy Eagle with blue-green (handles eagle, nginx, admin, jfr-sidecar)
# Note: Shardok runs on Hetzner, deployed separately via shardok_arm64_build.yml
# Note: Auth is deployed separately via auth_build.yml - do NOT touch auth here
chmod +x /opt/eagle0/scripts/*.sh
[ -f "/opt/eagle0/scripts/bin/warmup" ] && chmod +x /opt/eagle0/scripts/bin/warmup
GIT_SHA=\$(echo "\${EAGLE_IMAGE}" | sed 's/.*://')
/opt/eagle0/scripts/deploy-blue-green.sh "\${GIT_SHA}"
# Ensure auth is running (but don't force-recreate it)
docker compose -f docker-compose.prod.yml up -d auth
# Verify
# Restart nginx to pick up new container IPs
# (nginx caches DNS at startup, so it needs restart after eagle/shardok)
docker compose -f docker-compose.prod.yml up -d --force-recreate nginx
# Wait for health checks
sleep 10
docker compose -f docker-compose.prod.yml ps
# Verify containers are using correct images
echo "=== Verifying container image tags ==="
docker compose -f docker-compose.prod.yml images
# Cleanup
# Cleanup stopped containers and old images
docker container prune -f
docker image prune -f
DEPLOY_SCRIPT
-37
View File
@@ -1,37 +0,0 @@
name: Eagle Build
on:
push:
branches: [ "main" ]
paths:
- 'src/main/scala/**'
- 'src/main/protobuf/net/eagle0/eagle/**'
- 'src/main/protobuf/net/eagle0/common/**'
- 'WORKSPACE'
- 'MODULE.bazel'
- 'BUILD.bazel'
- '.github/workflows/eagle_build.yml'
pull_request:
paths:
- 'src/main/scala/**'
- 'src/main/protobuf/net/eagle0/eagle/**'
- 'src/main/protobuf/net/eagle0/common/**'
- 'WORKSPACE'
- 'MODULE.bazel'
- 'BUILD.bazel'
- '.github/workflows/eagle_build.yml'
permissions:
contents: read
jobs:
build:
runs-on: [self-hosted, bazel]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
lfs: false
- name: Build Eagle server
run: bazel build //src/main/scala/net/eagle0/eagle:eagle_server
+30 -73
View File
@@ -5,20 +5,18 @@ on:
branches: [ "main" ]
paths:
- ".github/workflows/installer_build.yml"
- "src/main/go/net/eagle0/clients/win/installer/**"
- "src/main/csharp/net/eagle0/clients/win/installer/**"
pull_request:
paths:
- ".github/workflows/installer_build.yml"
- "src/main/go/net/eagle0/clients/win/installer/**"
workflow_dispatch:
- "src/main/csharp/net/eagle0/clients/win/installer/**"
permissions:
contents: read
actions: write # Required to delete artifacts after deploy
jobs:
build-installer:
runs-on: [self-hosted, bazel]
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
@@ -26,47 +24,34 @@ jobs:
lfs: false
clean: false
- name: Build Go installer for Windows
env:
MANIFEST_PUBLIC_KEY: ${{ secrets.MANIFEST_PUBLIC_KEY }}
run: |
# Require manifest public key for production builds
if [ -z "$MANIFEST_PUBLIC_KEY" ]; then
echo "ERROR: MANIFEST_PUBLIC_KEY secret is not set"
echo "The installer requires a public key for manifest signature verification"
exit 1
fi
- name: Setup .NET 8
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
# Build Windows installer with WebView GUI (uses CGO cross-compilation)
# Use --action_env to pass the signing key into the genrule sandbox
bazel build //src/main/go/net/eagle0/clients/win/installer:eagle_installer_windows_amd64_webview --stamp --action_env=MANIFEST_PUBLIC_KEY
- name: Restore dependencies
run: dotnet restore src/main/csharp/net/eagle0/clients/win/installer/EagleInstaller/EagleInstaller.csproj
# Copy to output directory
rm -rf ./installer-output
mkdir -p ./installer-output
cp bazel-bin/src/main/go/net/eagle0/clients/win/installer/Eagle0.exe ./installer-output/Eagle0.exe
echo "Go installer size: $(ls -lh ./installer-output/Eagle0.exe | awk '{print $5}')"
- name: Build installer
run: dotnet publish src/main/csharp/net/eagle0/clients/win/installer/EagleInstaller/EagleInstaller.csproj -c Release -r win-x64 --self-contained true --output ./installer-output
- name: Archive installer binary
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: eagle-installer
path: ./installer-output/
retention-days: 1
path: ./installer-output/EagleInstaller.exe
- name: Verify installer exists
if: success()
run: |
echo "=== Installer output directory ==="
ls -lh ./installer-output/
if [ ! -f "./installer-output/Eagle0.exe" ]; then
echo "ERROR: Eagle0.exe not found"
if [ ! -f "./installer-output/EagleInstaller.exe" ]; then
echo "ERROR: EagleInstaller.exe not found at expected location"
echo "Directory contents:"
ls -la ./installer-output/
exit 1
fi
echo "Installer found"
echo "Installer found at correct location"
- name: Deploy installer
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
@@ -74,52 +59,24 @@ jobs:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: |
INSTALLER_PATH="$(pwd)/installer-output/Eagle0.exe"
echo "Deploying Go installer to installer/Eagle0.exe"
bazel run //src/main/go/net/eagle0/build/installer_build_handler:installer_build_handler -- "$INSTALLER_PATH" "installer/Eagle0.exe"
INSTALLER_PATH="$(pwd)/installer-output/EagleInstaller.exe"
echo "Using absolute path: $INSTALLER_PATH"
bazel run //src/main/go/net/eagle0/build/installer_build_handler:installer_build_handler -- "$INSTALLER_PATH"
- name: Update manifest
- name: Update unified manifest
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
MANIFEST_SIGNING_KEY: ${{ secrets.MANIFEST_SIGNING_KEY }}
run: |
INSTALLER_SHA=$(sha256sum ./installer-output/Eagle0.exe | cut -d' ' -f1)
# Create installer manifest content
INSTALLER_SHA=$(sha256sum ./installer-output/EagleInstaller.exe | cut -d' ' -f1)
echo "installer_version=$INSTALLER_SHA" > /tmp/installer_manifest.txt
echo "installer_url=installer/Eagle0.exe" >> /tmp/installer_manifest.txt
echo "=== Manifest content ==="
echo "installer_url=installer/EagleInstaller.exe" >> /tmp/installer_manifest.txt
echo "=== Installer manifest content ==="
cat /tmp/installer_manifest.txt
echo "========================"
# Write signing key to temp file (if available)
SIGNING_ARGS=""
if [ -n "$MANIFEST_SIGNING_KEY" ]; then
echo "$MANIFEST_SIGNING_KEY" > /tmp/manifest_signing_key
chmod 600 /tmp/manifest_signing_key
SIGNING_ARGS="/tmp/manifest_signing_key"
echo "Manifest signing key available"
else
echo "Warning: MANIFEST_SIGNING_KEY not set, manifest will be unsigned"
fi
# Update the v2 manifest at installer/v2/eagle0_manifest.txt
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- installer-v2 /tmp/installer_manifest.txt $SIGNING_ARGS
rm -f /tmp/manifest_signing_key
- name: Delete all installer artifacts
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
env:
GH_TOKEN: ${{ github.token }}
run: |
# Delete ALL eagle-installer artifacts to free up storage
echo "Fetching all eagle-installer artifacts..."
artifact_ids=$(gh api "repos/${{ github.repository }}/actions/artifacts" \
--paginate -q '.artifacts[] | select(.name == "eagle-installer") | .id')
for id in $artifact_ids; do
echo "Deleting artifact ID: $id"
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$id" || true
done
echo "Cleanup complete"
echo "=================================="
# Update the unified manifest
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- installer /tmp/installer_manifest.txt
@@ -1,66 +0,0 @@
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
jobs:
build-ios-addressables:
runs-on: [self-hosted, macOS, unity-mac]
steps:
- uses: actions/checkout@v4
with:
lfs: true
clean: true
- name: Pull LFS files
run: git lfs pull
- name: Restore Library/
env:
UNITY_CACHE_PLATFORM: ios
run: ./ci/github_actions/restore_library.sh
- name: Build iOS Addressables
run: ./ci/github_actions/build_ios_addressables.sh "/tmp/eagle0/editor_ios_addressables.log"
- name: Persist Library/
if: success()
env:
UNITY_CACHE_PLATFORM: ios
run: ./ci/github_actions/persist_library.sh
- name: Upload Addressables to CDN
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: ./ci/github_actions/upload_addressables.sh iOS
- name: Archive Build Log
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: editor_ios_addressables.log
path: /tmp/eagle0/editor_ios_addressables.log
retention-days: 5
+27 -210
View File
@@ -6,91 +6,60 @@ on:
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/**"
- "src/main/proto/**"
- "scripts/build_protos.sh"
- "scripts/build_mac_plugin.sh"
- "scripts/inject_sparkle.sh"
- "scripts/codesign_mac_app.sh"
- "scripts/notarize_submit.sh"
- "scripts/notarize_wait.sh"
- "scripts/notarize_mac_app.sh"
- "ci/github_actions/build_mac.sh"
- "ci/github_actions/build_unity_mac.sh"
- "ci/github_actions/upload_addressables.sh"
- "ci/mac/**"
pull_request:
paths:
- ".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/inject_sparkle.sh"
- "scripts/codesign_mac_app.sh"
- "scripts/notarize_submit.sh"
- "scripts/notarize_wait.sh"
- "scripts/notarize_mac_app.sh"
- "ci/github_actions/build_mac.sh"
- "ci/github_actions/build_unity_mac.sh"
- "ci/github_actions/upload_addressables.sh"
- "ci/mac/**"
workflow_dispatch:
inputs:
skip_signing:
description: 'Skip code signing, notarization, and deploy (build only)'
skip_notarization:
description: 'Skip notarization (for testing)'
required: false
default: 'false'
type: boolean
permissions:
contents: read
actions: write # Required to delete artifacts after deploy
jobs:
build-and-sign:
runs-on: [self-hosted, macOS, unity-mac]
outputs:
submission_id: ${{ steps.notarize-submit.outputs.submission_id }}
should_deploy: ${{ steps.check-deploy.outputs.should_deploy }}
mac-unity:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
with:
lfs: true
clean: true # Remove untracked files like old SparklePlugin.bundle
clean: false
fetch-depth: 0 # For version numbering from git history
- name: Pull LFS files
run: git lfs pull
- 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 "/tmp/eagle0/eagle0MAC"
- name: Persist Library/
if: success()
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'
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: ./ci/github_actions/upload_addressables.sh StandaloneOSX
- name: Inject Sparkle Framework
if: success()
env:
@@ -99,21 +68,8 @@ jobs:
chmod +x ./scripts/inject_sparkle.sh
./scripts/inject_sparkle.sh "/tmp/eagle0/eagle0MAC/eagle0.app"
- name: Check if should deploy
id: check-deploy
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.skip_signing }}" == "true" ]]; then
echo "should_deploy=false" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "should_deploy=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "should_deploy=true" >> $GITHUB_OUTPUT
else
echo "should_deploy=false" >> $GITHUB_OUTPUT
fi
- name: Import Code Signing Certificate
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
if: success() && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
@@ -125,11 +81,8 @@ jobs:
# Decode certificate
echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
# Delete any existing keychain from previous runs
security delete-keychain build.keychain 2>/dev/null || true
# Create temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain || true
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
@@ -143,125 +96,25 @@ jobs:
rm certificate.p12
- name: Code Sign App
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
if: success() && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
env:
SIGNING_IDENTITY: ${{ secrets.SIGNING_IDENTITY }}
run: |
chmod +x ./scripts/codesign_mac_app.sh
./scripts/codesign_mac_app.sh "/tmp/eagle0/eagle0MAC/eagle0.app" "ci/mac/eagle0.entitlements"
- name: Submit for Notarization
id: notarize-submit
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
- name: Notarize App
if: success() && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.event.inputs.skip_notarization != 'true'
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
chmod +x ./scripts/notarize_submit.sh
./scripts/notarize_submit.sh "/tmp/eagle0/eagle0MAC/eagle0.app" >> $GITHUB_OUTPUT
- name: Cleanup Keychain
if: always()
run: |
security delete-keychain build.keychain 2>/dev/null || true
- name: Zip signed app for artifact
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
run: |
cd /tmp/eagle0/eagle0MAC
ditto -c -k --keepParent eagle0.app eagle0.app.zip
- name: Upload signed app
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
uses: actions/upload-artifact@v4
with:
name: signed-mac-app-${{ github.run_id }}
path: /tmp/eagle0/eagle0MAC/eagle0.app.zip
retention-days: 1
- name: Archive Build Log
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: editor_mac.log
path: /tmp/eagle0/editor_mac.log
retention-days: 5
wait-notarization:
needs: build-and-sign
if: needs.build-and-sign.outputs.should_deploy == 'true'
runs-on: [self-hosted, macOS, notarize]
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: scripts
- name: Clean download directory
run: rm -rf /tmp/eagle0/eagle0MAC
- name: Download signed app
uses: actions/download-artifact@v4
with:
name: signed-mac-app-${{ github.run_id }}
path: /tmp/eagle0/eagle0MAC
- name: Unzip signed app
run: |
cd /tmp/eagle0/eagle0MAC
ditto -x -k eagle0.app.zip .
rm eagle0.app.zip
ls -la /tmp/eagle0/eagle0MAC/eagle0.app/
- name: Wait for Notarization and Staple
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
chmod +x ./scripts/notarize_wait.sh
./scripts/notarize_wait.sh "${{ needs.build-and-sign.outputs.submission_id }}" "/tmp/eagle0/eagle0MAC/eagle0.app"
- name: Zip notarized app for artifact
run: |
cd /tmp/eagle0/eagle0MAC
rm -f eagle0.app.zip
ditto -c -k --keepParent eagle0.app eagle0.app.zip
- name: Upload notarized app
uses: actions/upload-artifact@v4
with:
name: notarized-mac-app-${{ github.run_id }}
path: /tmp/eagle0/eagle0MAC/eagle0.app.zip
retention-days: 1
deploy:
needs: [build-and-sign, wait-notarization]
if: needs.build-and-sign.outputs.should_deploy == 'true'
runs-on: [self-hosted, macOS, unity-mac]
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # For version numbering
- name: Clean download directory
run: rm -rf /tmp/eagle0/eagle0MAC
- name: Download notarized app
uses: actions/download-artifact@v4
with:
name: notarized-mac-app-${{ github.run_id }}
path: /tmp/eagle0/eagle0MAC
- name: Unzip notarized app
run: |
cd /tmp/eagle0/eagle0MAC
ditto -x -k eagle0.app.zip .
rm eagle0.app.zip
chmod +x ./scripts/notarize_mac_app.sh
./scripts/notarize_mac_app.sh "/tmp/eagle0/eagle0MAC/eagle0.app"
- name: Deploy Mac Build
if: success() && github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
@@ -272,61 +125,25 @@ jobs:
echo "$SPARKLE_EDDSA_PRIVATE_KEY" > "$SPARKLE_PRIVATE_KEY_PATH"
chmod 600 "$SPARKLE_PRIVATE_KEY_PATH"
# Install dmgbuild (creates .DS_Store programmatically, no AppleScript needed)
pip3 install dmgbuild
# Background image for styled DMG
BACKGROUND_PATH="$(pwd)/ci/mac/dmg/background.png"
# Read version from the built app's Info.plist to ensure appcast matches the actual app
APP_PATH="/tmp/eagle0/eagle0MAC/eagle0.app"
BUILD_NUMBER=$(/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$APP_PATH/Contents/Info.plist")
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$APP_PATH/Contents/Info.plist")
VERSION=$(git describe --tags --always)
BUILD_NUMBER=$(git rev-list --count HEAD)
bazel run //src/main/go/net/eagle0/build/mac_build_handler:mac_build_handler -- \
"/tmp/eagle0/eagle0MAC/eagle0.app" \
"$VERSION" \
"$BUILD_NUMBER" \
"$BACKGROUND_PATH" \
"$SPARKLE_PRIVATE_KEY_PATH"
rm "$SPARKLE_PRIVATE_KEY_PATH"
- name: Delete this run's Mac app artifacts
env:
GH_TOKEN: ${{ github.token }}
- name: Cleanup Keychain
if: always()
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"
security delete-keychain build.keychain 2>/dev/null || true
# Cleanup job runs regardless of success/failure to prevent artifact accumulation
cleanup:
needs: [build-and-sign, wait-notarization, deploy]
if: always()
runs-on: ubuntu-latest
steps:
- name: Delete this run's Mac app artifacts
env:
GH_TOKEN: ${{ github.token }}
run: |
# Delete this run's artifacts (names include run ID to avoid conflicts)
for artifact_name in signed-mac-app-${{ github.run_id }} notarized-mac-app-${{ github.run_id }}; do
echo "Deleting artifact: $artifact_name"
artifact_id=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
-q ".artifacts[] | select(.name == \"$artifact_name\") | .id")
if [ -n "$artifact_id" ]; then
echo "Deleting artifact ID: $artifact_id"
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" || true
fi
done
echo "Cleanup complete"
- name: Archive Build Log
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: editor_mac.log
path: /tmp/eagle0/editor_mac.log
+7 -7
View File
@@ -46,26 +46,26 @@ jobs:
for REPO in $REPOS; do
echo "=== Processing repository: ${REPO} ==="
# Get all manifests with their tags and dates using JSON output for reliable parsing
MANIFESTS_JSON=$(doctl registry repository list-manifests "${REPO}" --output json 2>/dev/null || echo "[]")
# Get all manifests with their tags and dates
# Filter out header row and empty lines
MANIFESTS=$(doctl registry repository list-manifests "${REPO}" --format Digest,UpdatedAt,Tags --no-header 2>/dev/null | grep -v '^Digest' | grep -v '^$' || echo "")
if [ "$MANIFESTS_JSON" = "[]" ] || [ -z "$MANIFESTS_JSON" ]; then
if [ -z "$MANIFESTS" ]; then
echo " No manifests found"
continue
fi
# Parse JSON and process each manifest
echo "$MANIFESTS_JSON" | jq -r '.[] | "\(.digest) \(.updated_at) \(.tags // [] | join(","))"' | while read -r DIGEST UPDATED_AT TAGS; do
echo "$MANIFESTS" | while read -r DIGEST UPDATED_AT TAGS; do
# Skip if no digest or if it doesn't look like a valid digest (sha256:...)
if [ -z "$DIGEST" ] || ! echo "$DIGEST" | grep -q '^sha256:'; then
continue
fi
# Parse the date (ISO 8601 format from JSON)
# Parse the date
MANIFEST_DATE=$(date -d "$UPDATED_AT" +%s 2>/dev/null || echo "0")
# Skip protected tags (latest, arm64-latest)
if echo ",$TAGS," | grep -qE ',(latest|arm64-latest),'; then
if echo "$TAGS" | grep -qE '(^|,)(latest|arm64-latest)(,|$)'; then
echo " KEEP: ${DIGEST:0:20}... (protected tag: $TAGS)"
continue
fi
+2 -2
View File
@@ -24,7 +24,7 @@ permissions:
jobs:
build-shardok-arm64:
runs-on: [self-hosted, bazel]
runs-on: self-hosted
outputs:
image_tag: ${{ steps.push-shardok.outputs.image_tag }}
steps:
@@ -159,7 +159,7 @@ jobs:
echo "Also tagged as: registry.digitalocean.com/eagle0/shardok-server:arm64-latest"
deploy-hetzner:
runs-on: [self-hosted, bazel]
runs-on: self-hosted
needs: [build-shardok-arm64]
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
environment: production
+1 -1
View File
@@ -28,7 +28,7 @@ permissions:
jobs:
build:
runs-on: [self-hosted, bazel]
runs-on: self-hosted
steps:
- name: Checkout repository
+8 -46
View File
@@ -6,71 +6,51 @@ on:
paths:
- ".github/workflows/unity_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/proto/**"
- "scripts/build_protos.sh"
- "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"
- "MODULE.bazel"
- "WORKSPACE"
workflow_dispatch:
- "src/main/proto/net/eagle0/eagle/**/BUILD.bazel"
pull_request:
paths:
- ".github/workflows/unity_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/proto/**"
- "scripts/build_protos.sh"
- "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"
- "MODULE.bazel"
- "WORKSPACE"
- "src/main/proto/**/BUILD.bazel"
permissions:
contents: read
jobs:
windows-unity:
runs-on: [self-hosted, macOS, unity-windows]
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
with:
lfs: true
clean: true # Remove untracked files from previous builds
clean: false
- name: Pull lfs files
run: git lfs pull
- 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 "/tmp/eagle0/eagle0WIN"
- name: Persist Library/
if: success()
env:
UNITY_CACHE_PLATFORM: windows
run: ./ci/github_actions/persist_library.sh
- name: Upload Addressables to CDN
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
run: ./ci/github_actions/upload_addressables.sh StandaloneWindows64
- name: Deploy Windows unity
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
env:
@@ -83,28 +63,10 @@ jobs:
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
SECRET_KEY: ${{ secrets.SECRET_KEY }}
MANIFEST_SIGNING_KEY: ${{ secrets.MANIFEST_SIGNING_KEY }}
run: |
# Write signing key to temp file (if available)
SIGNING_ARGS=""
if [ -n "$MANIFEST_SIGNING_KEY" ]; then
echo "$MANIFEST_SIGNING_KEY" > /tmp/manifest_signing_key
chmod 600 /tmp/manifest_signing_key
SIGNING_ARGS="/tmp/manifest_signing_key"
echo "Manifest signing key available"
else
echo "Warning: MANIFEST_SIGNING_KEY not set, manifest will be unsigned"
fi
# Update the v2 manifest at installer/v2/eagle0_manifest.txt
bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- unity3d-v2 /tmp/unity_manifest.txt $SIGNING_ARGS
# Cleanup
rm -f /tmp/manifest_signing_key
run: bazel run //src/main/go/net/eagle0/build/manifest_manager:manifest_manager -- unity3d /tmp/unity_manifest.txt
- name: Archive build log
if: success() || failure()
uses: actions/upload-artifact@v4
with:
name: editor_win.log
path: /tmp/eagle0/editor_win.log
retention-days: 5
path: /tmp/eagle0/editor_win.log
-1
View File
@@ -38,4 +38,3 @@ scripts/refresh_name_layers/refresh_name_layers.zip
api_keys.txt
src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/Packages/com.unity.dedicated-server/
node_modules/
-12
View File
@@ -32,15 +32,3 @@ nogo(
vet = True,
visibility = ["//visibility:public"],
)
# Dependency constraint tests
# These verify architectural boundaries are maintained
sh_test(
name = "build_deps_test",
srcs = ["scripts/check_build_deps.sh"],
args = ["--ci"],
tags = [
"local", # Needs bazel query access
"no-sandbox",
],
)
+1 -5
View File
@@ -4,18 +4,14 @@
**NEVER push directly to main/master.** No exceptions. Not for "small changes." Not for docs. Not ever.
**NEVER merge PRs.** You create PRs. The user merges them. No exceptions.
**ALWAYS use this workflow:**
1. Create a feature branch from origin/main
2. Commit to that branch
3. Create a PR with `gh pr create`
4. Wait for user to merge (DO NOT run `gh pr merge`)
4. Wait for user to merge
If you catch yourself about to run `git push origin main` or `git push origin <branch>:main`, STOP. You are about to violate a critical rule. Create a PR instead.
If you catch yourself about to run `gh pr merge`, STOP. Only the user merges PRs.
---
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+213
View File
@@ -0,0 +1,213 @@
# Deproto Migration Plan
This document tracks the migration from protobuf types to native Scala models inside the Eagle game engine.
## Architectural Decisions
1. **Keep proto for persistence**: Yes - protobuf is used for persisting game state
2. **Keep proto for Shardok communication**: Yes - protobuf is used for Eagle-Shardok gRPC communication
3. **Use Scala views inside the library**: Yes - use native Scala types like `ProvinceView`, `FactionView`, `HeroView`, etc. within the library code
## Recent Completed Work
### GameState Round-Trip Elimination (PRs #4913, #4914, #4915)
Eliminated wasteful Scala→proto→Scala conversions in the hot path:
1. **LLM Pipeline** (#4913): `LlmRequestWithGameState` now uses Scala `GameState` instead of proto. All ~38 prompt generators updated to use Scala model types (`FactionT`, `HeroT`, `ProvinceT`).
2. **ActionWithResultingState Caching** (#4914): Added `precomputedScalaState: Option[GameState]` to cache Scala state when available, avoiding `fromProto()` conversion in `stateAfter()`.
3. **PostResults Simplification** (#4915): Changed `PostResults.gameState` from proto to `Option[GameState]` (Scala), eliminating `toProto()` calls when creating PostResults.
## Migration Pattern
The codebase follows a **Legacy* pattern** for separating proto-dependent and protoless code:
- **Protoless utilities**: `FactionUtils`, `HeroUtils`, `ProvinceUtils`, `ProvinceDistances`, etc.
- **Proto-dependent utilities**: `LegacyFactionUtils`, `LegacyHeroUtils`, `LegacyProvinceUtils`, `LegacyProvinceDistances`, etc.
When migrating a file:
1. Create a `Legacy*` version containing the proto-dependent methods
2. Keep the original file name for protoless methods
3. Update callers to use the appropriate version based on their context
## Migration Status
### Fully Protoless (no proto imports)
**Utilities:**
- [x] `FactionUtils` - has protoless `ownedNeighbors` method
- [x] `ProvinceDistances` - split into protoless + `LegacyProvinceDistances`
- [x] `SwornBrotherChooser` - fully protoless (removed `bestChoiceProto`)
**Command Selectors (all use native GameState):**
- [x] `AllianceOfferCommandSelector`
- [x] `AlmsCommandSelector`
- [x] `AttackCommandChooser`
- [x] `ExpandCommandSelector`
- [x] `HeroGiftCommandSelector`
- [x] `ImproveCommandSelector`
- [x] `MarchTowardProvinceCommandChooser` - in AI folder, uses native GameState (callers convert)
- [x] `OrganizeCommandSelector`
- [x] `RansomOfferHelpers`
- [x] `SeekMoreLeadersCommandChooser` - in AI folder, uses native GameState
- [x] `TruceOfferCommandSelector`
- [x] `TrustForDiplomacy`
**Quest Command Selectors (all protoless):**
- [x] `AllianceQuestCommandChooser`
- [x] `AlmsAcrossRealmQuestCommandChooser`
- [x] `AlmsToProvinceQuestCommandChooser`
- [x] `DismissSpecificVassalCommandChooser`
- [x] `GiveToHeroesAcrossRealmQuestCommandChooser`
- [x] `GiveToHeroesInProvinceQuestCommandChooser`
- [x] `ImproveQuestCommandChooser`
- [x] `QuestCommandChooser`
- [x] `TruceCountQuestCommandChooser`
- [x] `TruceWithFactionQuestCommandChooser`
### Fully Protoless
- [x] `AIClientUtils` - has protoless overloads (`takenHeroIdsForMarchTowardFocus`, `mostPowerfulHeroes`)
- [x] `AttackCommandChooser` - uses Scala `GameState` and `BattalionViewC` (Scala view type)
- [x] `BattalionPower` - has `estimatedPower(BattalionViewC)` for recon data with optional stats
- [x] `CommandChoiceHelpers` - fully protoless, uses Scala `GameState` throughout
- [x] `ProvinceGoldSurplusCalculator` - fully protoless (callers use converters)
- [x] `HeroSelector` - fully protoless (removed dead `minimallyFatiguedHeroesProto`)
### AI Layer ✅ COMPLETE
All AI and command chooser code is now fully protoless:
- [x] `AttackDecisionCommandChooser` - uses Scala GameState
- [x] `CommandChooser` - trait uses Scala GameState
- [x] `FulfillQuestsCommandSelector` - uses Scala GameState
- [x] `MidGameAIClient` - uses Scala GameState internally
### Still Using Proto GameState (Boundary Code)
These files use proto GameState because they're at system boundaries:
**View Filters (client projection):**
- `view_filters/GameStateViewFilter` - has Scala overload, uses Scala sub-filters
- `view_filters/ProvinceViewFilter` - has Scala overloads for some methods
- `view_filters/FactionViewFilter` - has Scala overload
- `view_filters/HeroViewFilter` - has Scala overload
- `view_filters/BattalionNameFilter` - has Scala overload
- `view_filters/BattleFilter` - has Scala overload
**Legacy Utilities (to be deprecated):**
- `LegacyProvinceDistances`, `LegacyFactionUtils`, `LegacyHeroUtils`, etc.
- Used by code that still needs proto GameState
**Persistence/Action System:**
- `ActionResultTApplier`, `ActionResultProtoApplier` - apply results to proto state
- `ActionWithResultingState` - caches both proto and Scala state
**Shardok Interface (gRPC boundary):**
- `ShardokInterfaceGrpcClient`, `ShardokInterfaceProxy` - must use proto for C++ communication
## Next Steps
### Phase 1-3: AI Layer ✅ COMPLETE
The entire AI decision-making layer is now protoless.
### Phase 4: View Filters ✅ COMPLETE
The view_filters package migration is complete:
**Completed:**
- [x] `GameStateViewFilter` - added Scala GameState overload (uses Scala sub-filters)
- [x] `ProvinceViewFilter` - already has Scala overloads for server-side views
- [x] `HumanPlayerClientConnectionState` - updated to pass Scala GameState directly
- [x] `HeroViewFilter` - added Scala overload
- [x] `FactionViewFilter` - added Scala overload
- [x] `Visibility` - added Scala overloads
**Still Using Proto:**
- [x] `BattalionNameFilter` - has Scala overload
- [x] `BattleFilter` - has Scala overload
- [ ] `ActionResultFilter` - uses proto internally (boundary code)
**Strategy:**
1. Add Scala GameState overloads to view filter methods
2. Update callers to pass Scala GameState where available
3. Eventually deprecate proto versions
### Phase 5: Legacy Utility Cleanup (IN PROGRESS)
Remove Legacy* utilities by migrating remaining callers:
1. Identify callers of each Legacy* util
2. Update callers to use protoless versions
3. Delete Legacy* files when no longer needed
**Deleted (no production callers):**
- [x] `LegacyProvinceDistances` - deleted (no callers)
- [x] `LegacyBattalionSuitability` - deleted (no callers)
- [x] `LegacyFoodConsumptionUtils` - deleted (no callers)
- [x] `LegacyHandleRiotUtils` - deleted (no callers)
**Refactored to Thin Wrappers (delegating to protoless versions):**
- [x] `LegacyRansomValidity` - already a thin wrapper delegating to `RansomValidity`
- [x] `LegacyRecruitmentOdds` - refactored to delegate to `RecruitmentOdds`
**Parallel Implementations (proto mirrors protoless):**
- [x] `FactionUtils` / `LegacyFactionUtils` - both have matching APIs; LegacyFactionUtils used by boundary code (24+ callers)
- [x] `HeroUtils` / `LegacyHeroUtils` - both have matching APIs; LegacyHeroUtils used by boundary code (10 callers)
- [x] `ProvinceUtils` / `LegacyProvinceUtils` - both have matching APIs; LegacyProvinceUtils used by boundary code (20 callers: availability factories, view filters)
**Parallel Implementations (awaiting migration of callers):**
- [x] `BattalionUtils` / `LegacyBattalionUtils` - both have matching core methods; LegacyBattalionUtils used by boundary code (4 callers)
- [x] `BattalionViewFilter` / `LegacyBattalionViewFilter` - protoless version exists; Legacy used by view filters, action appliers (3 callers)
- [x] `BattalionTypeFinder` / `LegacyBattalionTypeFinder` - protoless version exists; Legacy used by validators (1 caller: RuntimeValidator)
### Recent Caller Migration
**CheckForFulfilledQuestsAction** - migrated to use protoless `BattalionTypeFinder`:
- Changed `battalionTypes` parameter from proto `Vector[BattalionType]` to Scala `Vector[BattalionType]`
- Updated callers (EngineImpl, EndVassalCommandsPhaseAction) to pass Scala types directly
- Eliminated wasteful `BattalionTypeConverter.toProto()` conversions
**ExpandedUnaffiliatedHeroUtils** - added Scala overload:
- New overload takes Scala `GameState` and `UnaffiliatedHeroT` directly
- Added `UnaffiliatedHeroConverter.unaffiliatedHeroTypeToProto()` helper for efficient enum conversion
- Proto overload retained for backward compatibility
**AvailablePleaseRecruitMeCommandFactory** - eliminated wasteful proto conversions:
- Now uses `ExpandedUnaffiliatedHeroUtils.expandedUnaffiliatedHero(ScalaGameState, UnaffiliatedHeroT)` directly
- Removed `GameStateConverter.toProto()` and `UnaffiliatedHeroConverter.toProto()` calls
- Factory is now fully protoless internally (still returns proto types for API boundary)
**ProvinceViewFilter** - added Scala overload with faction filtering:
- New `filteredProvinceView(province: ProvinceT, gs: ScalaGameState, factionId: FactionId)` overload
- Uses protoless `FactionUtils.hasAlliance`, `Visibility.hasFullVisibility`, and `ProvinceUtils.incomingOthers`
- Handles reconned provinces directly from Scala `FactionT.reconnedProvinces` (already Scala type)
- Added helper methods: `fullProvinceInfoScala`, `maybeIncomingAttackersScala`, `unaffiliatedHeroInfoScala`
- Events still converted to proto at the end (ProvinceView.knownEvents uses proto events)
**GameStateViewFilter** - eliminated GameStateConverter.toProto() call:
- Scala overload now fully protoless internally
- Uses the new ProvinceViewFilter Scala overload with faction filtering
- Converts `battalionTypes` and `chronicleEntries` to proto only at output boundary
## Key Files
### Protoless Model Types
- `src/main/scala/net/eagle0/eagle/model/state/game_state/GameState.scala` - native Scala GameState
- `src/main/scala/net/eagle0/eagle/model/state/province/ProvinceView.scala` - province view type
- `src/main/scala/net/eagle0/eagle/model/state/faction/FactionView.scala` - faction view type
- `src/main/scala/net/eagle0/eagle/model/state/hero/HeroView.scala` - hero view type
### Proto Converters
- `src/main/scala/net/eagle0/eagle/model/proto_converters/game_state/` - converts between proto and Scala types
## Notes
- The AI client code (`src/main/scala/net/eagle0/eagle/ai/`) is now fully protoless
- Proto GameState is still needed at boundaries: persistence, gRPC to Shardok
- `PerformUnaffiliatedHeroesAction` and the LLM pipeline use protoless `GameState`
- `GameStateViewFilter` Scala overload is now fully protoless internally (converts to proto only at output)
- `ProvinceViewFilter` has Scala overloads for all three modes: no filtering, faction filtering, and withdrawn-from view
+10 -44
View File
@@ -13,8 +13,8 @@ AWS_SDK_VERSION = "2.28.1"
# Core Build Tools
#
bazel_dep(name = "bazel_skylib", version = "1.9.0")
bazel_dep(name = "rules_pkg", version = "1.2.0")
bazel_dep(name = "bazel_skylib", version = "1.8.1")
bazel_dep(name = "rules_pkg", version = "1.1.0")
#
# Language Support - Scala
@@ -102,12 +102,11 @@ sysroot(
# Language Support - Go
#
bazel_dep(name = "rules_go", version = "0.59.0", repo_name = "io_bazel_rules_go")
bazel_dep(name = "gazelle", version = "0.47.0", repo_name = "bazel_gazelle")
bazel_dep(name = "rules_go", version = "0.56.1", repo_name = "io_bazel_rules_go")
bazel_dep(name = "gazelle", version = "0.45.0", repo_name = "bazel_gazelle")
go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
go_sdk.download(version = "1.23.3")
use_repo(go_sdk, "go_default_sdk")
go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps")
go_deps.from_file(go_mod = "//:go.mod")
@@ -119,10 +118,8 @@ use_repo(
"com_github_aws_aws_sdk_go_v2_service_s3",
"com_github_golang_jwt_jwt_v5",
"com_github_google_uuid",
"com_github_webview_webview_go",
"org_golang_google_grpc",
"org_golang_google_protobuf",
"org_golang_x_sys",
)
#
@@ -133,13 +130,6 @@ bazel_dep(name = "apple_support", version = "1.21.1", repo_name = "build_bazel_a
bazel_dep(name = "rules_apple", version = "4.3.3", repo_name = "build_bazel_rules_apple")
bazel_dep(name = "rules_swift", version = "2.4.0", repo_name = "build_bazel_rules_swift")
# 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")
#
# Protocol Buffers & RPC
#
@@ -147,7 +137,7 @@ use_repo(apple_cc_configure, "local_config_apple_cc")
bazel_dep(name = "protobuf", version = "29.2", repo_name = "com_google_protobuf")
bazel_dep(name = "grpc", version = "1.71.0")
bazel_dep(name = "grpc-java", version = "1.71.0")
bazel_dep(name = "flatbuffers", version = "25.9.23")
bazel_dep(name = "flatbuffers", version = "25.2.10")
#
# Testing
@@ -159,8 +149,8 @@ bazel_dep(name = "googletest", version = "1.17.0")
# Container Images (OCI)
#
bazel_dep(name = "rules_oci", version = "2.2.7")
bazel_dep(name = "aspect_bazel_lib", version = "2.22.4")
bazel_dep(name = "rules_oci", version = "2.2.6")
bazel_dep(name = "aspect_bazel_lib", version = "2.16.0")
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
@@ -196,7 +186,7 @@ use_repo(oci, "alpine_linux", "alpine_linux_linux_amd64", "eclipse_temurin_17",
# Java/Scala Dependencies
#
bazel_dep(name = "rules_jvm_external", version = "6.9")
bazel_dep(name = "rules_jvm_external", version = "6.3")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
maven.install(
@@ -303,25 +293,14 @@ http_archive(
],
)
# Sparkle framework for macOS auto-updates
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),
)
# Busybox static binary for Docker health checks (provides nc, wget, etc.)
# Primary: DigitalOcean Spaces (public, reliable)
# Primary: GitHub release mirror (reliable)
# Fallback: busybox.net (can be unreliable/slow)
http_file(
name = "busybox_x86_64",
sha256 = "6e123e7f3202a8c1e9b1f94d8941580a25135382b99e8d3e34fb858bba311348",
urls = [
"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/busybox/busybox-1.35.0-x86_64-linux-musl",
"https://github.com/nolen777/eagle0/releases/download/busybox-1.35.0/busybox-1.35.0-x86_64-linux-musl",
"https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox",
],
downloaded_file_path = "busybox",
@@ -339,19 +318,6 @@ http_file(
executable = True,
)
# LLVM MinGW toolchain for Windows cross-compilation from macOS
# This provides a complete toolchain for building Windows executables including
# the MinGW-w64 libraries needed for CGO cross-compilation
LLVM_MINGW_VERSION = "20250305"
http_archive(
name = "llvm_mingw",
build_file = "@//external:BUILD.llvm_mingw",
sha256 = "32c24fc62fc8b9f8a900bf2c730b78b36767688f816f9d21e97a168289ff44e0",
strip_prefix = "llvm-mingw-%s-ucrt-macos-14.4.1-universal" % LLVM_MINGW_VERSION,
urls = ["https://github.com/fathonix/llvm-mingw-arm64ec-macos/releases/download/%s/llvm-mingw-%s-ucrt-macos-14.4.1-universal.tar.xz" % (LLVM_MINGW_VERSION, LLVM_MINGW_VERSION)],
)
#
# Toolchain Registration
#
+30 -680
View File
@@ -27,9 +27,9 @@
"https://bcr.bazel.build/modules/aspect_bazel_lib/1.40.3/MODULE.bazel": "668e6bcb4d957fc0e284316dba546b705c8d43c857f87119619ee83c4555b859",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.11.0/MODULE.bazel": "cb1ba9f9999ed0bc08600c221f532c1ddd8d217686b32ba7d45b0713b5131452",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.14.0/MODULE.bazel": "2b31ffcc9bdc8295b2167e07a757dbbc9ac8906e7028e5170a3708cecaac119f",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.16.0/MODULE.bazel": "852f9ebbda017572a7c113a2434592dd3b2f55cd9a0faea3d4be5a09a59e4900",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/MODULE.bazel": "253d739ba126f62a5767d832765b12b59e9f8d2bc88cc1572f4a73e46eb298ca",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.4/MODULE.bazel": "a05cbd9bc16712a58dc27ffe0dceaefd0da59a9bd87a227379b2a934b26a39ab",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.22.4/source.json": "9780bc57f521968ee82b7c3e85b7d0c71518fb7ce83ed7a9e5077ce20923207b",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.19.3/source.json": "ffab9254c65ba945f8369297ad97ca0dec213d3adc6e07877e23a48624a8b456",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.2/MODULE.bazel": "780d1a6522b28f5edb7ea09630748720721dfe27690d65a2d33aa7509de77e07",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.7.7/MODULE.bazel": "491f8681205e31bb57892d67442ce448cda4f472a8e6b3dc062865e29a64f89c",
"https://bcr.bazel.build/modules/aspect_bazel_lib/2.8.1/MODULE.bazel": "812d2dd42f65dca362152101fbec418029cc8fd34cbad1a2fde905383d705838",
@@ -39,11 +39,11 @@
"https://bcr.bazel.build/modules/aspect_rules_js/1.33.1/MODULE.bazel": "db3e7f16e471cf6827059d03af7c21859e7a0d2bc65429a3a11f005d46fc501b",
"https://bcr.bazel.build/modules/aspect_rules_js/1.39.0/MODULE.bazel": "aece421d479e3c31dc3e5f6d49a12acc2700457c03c556650ec7a0ff23fc0d95",
"https://bcr.bazel.build/modules/aspect_rules_js/2.0.0/MODULE.bazel": "b45b507574aa60a92796e3e13c195cd5744b3b8aff516a9c0cb5ae6a048161c5",
"https://bcr.bazel.build/modules/aspect_rules_js/2.3.8/MODULE.bazel": "74bf20a7a6bd5f2be09607fdb4196cfd6f203422ea271752ec2b1afe95426101",
"https://bcr.bazel.build/modules/aspect_rules_js/2.3.8/source.json": "411ec9d79d6f5fe8a083359588c21d01a5b48d88a2cbd334a4c90365015b7836",
"https://bcr.bazel.build/modules/aspect_rules_js/2.1.3/MODULE.bazel": "47cc48eec374d69dced3cf9b9e5926beac2f927441acfb1a3568bbb709b25666",
"https://bcr.bazel.build/modules/aspect_rules_js/2.1.3/source.json": "6b0fe67780c101430be087381b7a79d75eeebe1a1eae6a2cee937713603634ac",
"https://bcr.bazel.build/modules/aspect_rules_lint/0.12.0/MODULE.bazel": "e767c5dbfeb254ec03275a7701b5cfde2c4d2873676804bc7cb27ddff3728fed",
"https://bcr.bazel.build/modules/aspect_rules_ts/3.6.0/MODULE.bazel": "d0045b5eabb012be550a609589b3e5e47eba682344b19cfd9365d4d896ed07df",
"https://bcr.bazel.build/modules/aspect_rules_ts/3.6.0/source.json": "5593e3f1cd0dd5147f7748e163307fd5c2e1077913d6945b58739ad8d770a290",
"https://bcr.bazel.build/modules/aspect_rules_ts/3.4.0/MODULE.bazel": "5b554d5de90d96ee14117527c0519037713dd33884f3212eae391beccb2e94ff",
"https://bcr.bazel.build/modules/aspect_rules_ts/3.4.0/source.json": "9ada3722b716853b6dccdb7b650d8e776a23bc8a190de0c59bd15f21afea6f8a",
"https://bcr.bazel.build/modules/bazel_features/0.1.0/MODULE.bazel": "47011d645b0f949f42ee67f2e8775188a9cf4a0a1528aa2fa4952f2fd00906fd",
"https://bcr.bazel.build/modules/bazel_features/1.1.0/MODULE.bazel": "cfd42ff3b815a5f39554d97182657f8c4b9719568eb7fded2b9135f084bf760b",
"https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd",
@@ -57,15 +57,12 @@
"https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65",
"https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d",
"https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9",
"https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87",
"https://bcr.bazel.build/modules/bazel_features/1.34.0/MODULE.bazel": "e8475ad7c8965542e0c7aac8af68eb48c4af904be3d614b6aa6274c092c2ea1e",
"https://bcr.bazel.build/modules/bazel_features/1.38.0/MODULE.bazel": "f9b8a9c890ebd216b4049fd12a31d3c2602e3403c7af636b04fbbd7453edc9c9",
"https://bcr.bazel.build/modules/bazel_features/1.38.0/source.json": "31ba776c122b54a2885e23651642e32f087a87bf025465f8040751894b571277",
"https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7",
"https://bcr.bazel.build/modules/bazel_features/1.9.0/MODULE.bazel": "885151d58d90d8d9c811eb75e3288c11f850e1d6b481a8c9f766adee4712358b",
"https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a",
"https://bcr.bazel.build/modules/bazel_lib/3.0.0/MODULE.bazel": "22b70b80ac89ad3f3772526cd9feee2fa412c2b01933fea7ed13238a448d370d",
"https://bcr.bazel.build/modules/bazel_lib/3.0.0/source.json": "895f21909c6fba01d7c17914bb6c8e135982275a1b18cdaa4e62272217ef1751",
"https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8",
"https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e",
"https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686",
@@ -80,8 +77,7 @@
"https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67",
"https://bcr.bazel.build/modules/bazel_skylib/1.9.0/MODULE.bazel": "72997b29dfd95c3fa0d0c48322d05590418edef451f8db8db5509c57875fb4b7",
"https://bcr.bazel.build/modules/bazel_skylib/1.9.0/source.json": "7ad77c1e8c1b84222d9b3f3cae016a76639435744c19330b0b37c0a3c9da7dc0",
"https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb",
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.6/MODULE.bazel": "fd1f9432ca04c947e91b500df69ce7c5b6dbfe1bc45ab1820338205dae3383a6",
"https://bcr.bazel.build/modules/bazel_worker_api/0.0.6/source.json": "5d68545f224904745a3cabd35aea6bc2b6cc5a78b7f49f3f69660eab2eeeb273",
"https://bcr.bazel.build/modules/boringssl/0.0.0-20211025-d4f1ab9/MODULE.bazel": "6ee6353f8b1a701fe2178e1d925034294971350b6d3ac37e67e5a7d463267834",
@@ -108,8 +104,8 @@
"https://bcr.bazel.build/modules/envoy_api/0.0.0-20241214-918efc9/MODULE.bazel": "24e05f6f52f37be63a795192848555a2c8c855e7814dbc1ed419fb04a7005464",
"https://bcr.bazel.build/modules/envoy_api/0.0.0-20250128-4de3c74/MODULE.bazel": "1fe72489212c530086e3ffb0e018b2bfef4663200ca03571570f9f006bef1d75",
"https://bcr.bazel.build/modules/envoy_api/0.0.0-20250128-4de3c74/source.json": "028519164a2e24563f4b43d810fdedc702daed90e71e7042d45ba82ad807b46f",
"https://bcr.bazel.build/modules/flatbuffers/25.9.23/MODULE.bazel": "32753ba60bf3bacfe7737c0f3e8e3e55624b19af5d398c485580d57492d145d8",
"https://bcr.bazel.build/modules/flatbuffers/25.9.23/source.json": "a2116f0017f6896353fd3abf65ef2b89b0a257e8a87f395c5000f53934829f31",
"https://bcr.bazel.build/modules/flatbuffers/25.2.10/MODULE.bazel": "dab15cafe8512d2c4a8daa44c2d7968c5c79f01e220d40076cdc260bf58605e2",
"https://bcr.bazel.build/modules/flatbuffers/25.2.10/source.json": "7eae7ea3eb913b9802426e4d5df11d6c6072a3573a548f8cabf1e965f5cca4d0",
"https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/MODULE.bazel": "cdf8cbe5ee750db04b78878c9633cc76e80dcf4416cbe982ac3a9222f80713c8",
"https://bcr.bazel.build/modules/gawk/5.3.2.bcr.1/source.json": "fa7b512dfcb5eafd90ce3959cf42a2a6fe96144ebbb4b3b3928054895f2afac2",
"https://bcr.bazel.build/modules/gazelle/0.27.0/MODULE.bazel": "3446abd608295de6d90b4a8a118ed64a9ce11dcb3dda2dc3290a22056bd20996",
@@ -119,8 +115,8 @@
"https://bcr.bazel.build/modules/gazelle/0.34.0/MODULE.bazel": "abdd8ce4d70978933209db92e436deb3a8b737859e9354fb5fd11fb5c2004c8a",
"https://bcr.bazel.build/modules/gazelle/0.36.0/MODULE.bazel": "e375d5d6e9a6ca59b0cb38b0540bc9a05b6aa926d322f2de268ad267a2ee74c0",
"https://bcr.bazel.build/modules/gazelle/0.37.0/MODULE.bazel": "d1327ba0907d0275ed5103bfbbb13518f6c04955b402213319d0d6c0ce9839d4",
"https://bcr.bazel.build/modules/gazelle/0.47.0/MODULE.bazel": "b61bb007c4efad134aa30ee7f4a8e2a39b22aa5685f005edaa022fbd1de43ebc",
"https://bcr.bazel.build/modules/gazelle/0.47.0/source.json": "aeb2e5df14b7fb298625d75d08b9c65bdb0b56014c5eb89da9e5dd0572280ae6",
"https://bcr.bazel.build/modules/gazelle/0.45.0/MODULE.bazel": "ecd19ebe9f8e024e1ccffb6d997cc893a974bcc581f1ae08f386bdd448b10687",
"https://bcr.bazel.build/modules/gazelle/0.45.0/source.json": "111d182facc5f5e80f0b823d5f077b74128f40c3fd2eccc89a06f34191bd3392",
"https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb",
"https://bcr.bazel.build/modules/google_benchmark/1.8.4/MODULE.bazel": "c6d54a11dcf64ee63545f42561eda3fd94c1b5f5ebe1357011de63ae33739d5e",
"https://bcr.bazel.build/modules/google_benchmark/1.8.5/MODULE.bazel": "9ba9b31b984022828a950e3300410977eda2e35df35584c6b0b2d0c2e52766b7",
@@ -180,7 +176,6 @@
"https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/MODULE.bazel": "b3925269f63561b8b880ae7cf62ccf81f6ece55b62cd791eda9925147ae116ec",
"https://bcr.bazel.build/modules/opentracing-cpp/1.6.0/source.json": "da1cb1add160f5e5074b7272e9db6fd8f1b3336c15032cd0a653af9d2f484aed",
"https://bcr.bazel.build/modules/package_metadata/0.0.2/MODULE.bazel": "fb8d25550742674d63d7b250063d4580ca530499f045d70748b1b142081ebb92",
"https://bcr.bazel.build/modules/package_metadata/0.0.3/MODULE.bazel": "77890552ecea9e284b5424c9de827a58099348763a4359e975c359a83d4faa83",
"https://bcr.bazel.build/modules/package_metadata/0.0.5/MODULE.bazel": "ef4f9439e3270fdd6b9fd4dbc3d2f29d13888e44c529a1b243f7a31dfbc2e8e4",
"https://bcr.bazel.build/modules/package_metadata/0.0.5/source.json": "2326db2f6592578177751c3e1f74786b79382cd6008834c9d01ec865b9126a85",
"https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5",
@@ -234,9 +229,9 @@
"https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8",
"https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e",
"https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a",
"https://bcr.bazel.build/modules/rules_apple/3.16.1/MODULE.bazel": "8294474defa70af2534a558ab905c083d69203344145e6f7d544d5098611ec7d",
"https://bcr.bazel.build/modules/rules_apple/3.16.1/source.json": "9190fd9d34a5d048bfbba8a530a57f2c2bf3f61e5634a9ab0b6ab005458857f9",
"https://bcr.bazel.build/modules/rules_apple/3.5.1/MODULE.bazel": "3d1bbf65ad3692003d36d8a29eff54d4e5c1c5f4bfb60f79e28646a924d9101c",
"https://bcr.bazel.build/modules/rules_apple/4.3.3/MODULE.bazel": "c5c2c4adeeac5f3f2f9b7f16abfa8be7ffefa596171d0d92bed4cae9ade0a498",
"https://bcr.bazel.build/modules/rules_apple/4.3.3/source.json": "3cb1d69c8243ffcc42ecbf84ae8b9cccd7b1e2f091b0aee5a3e9c9a45267f312",
"https://bcr.bazel.build/modules/rules_buf/0.1.1/MODULE.bazel": "6189aec18a4f7caff599ad41b851ab7645d4f1e114aa6431acf9b0666eb92162",
"https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647",
"https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002",
@@ -251,8 +246,6 @@
"https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e",
"https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5",
"https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513",
"https://bcr.bazel.build/modules/rules_cc/0.1.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0",
"https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8",
"https://bcr.bazel.build/modules/rules_cc/0.2.14/MODULE.bazel": "353c99ed148887ee89c54a17d4100ae7e7e436593d104b668476019023b58df8",
"https://bcr.bazel.build/modules/rules_cc/0.2.14/source.json": "55d0a4587c5592fad350f6e698530f4faf0e7dd15e69d43f8d87e220c78bea54",
"https://bcr.bazel.build/modules/rules_foreign_cc/0.10.1/MODULE.bazel": "b9527010e5fef060af92b6724edb3691970a5b1f76f74b21d39f7d433641be60",
@@ -270,8 +263,8 @@
"https://bcr.bazel.build/modules/rules_go/0.48.0/MODULE.bazel": "d00ebcae0908ee3f5e6d53f68677a303d6d59a77beef879598700049c3980a03",
"https://bcr.bazel.build/modules/rules_go/0.50.1/MODULE.bazel": "b91a308dc5782bb0a8021ad4330c81fea5bda77f96b9e4c117b9b9c8f6665ee0",
"https://bcr.bazel.build/modules/rules_go/0.53.0/MODULE.bazel": "a4ed760d3ac0dbc0d7b967631a9a3fd9100d28f7d9fcf214b4df87d4bfff5f9a",
"https://bcr.bazel.build/modules/rules_go/0.59.0/MODULE.bazel": "b7e43e7414a3139a7547d1b4909b29085fbe5182b6c58cbe1ed4c6272815aeae",
"https://bcr.bazel.build/modules/rules_go/0.59.0/source.json": "1df17bb7865cfc029492c30163cee891d0dd8658ea0d5bfdf252c4b6db5c1ef6",
"https://bcr.bazel.build/modules/rules_go/0.56.1/MODULE.bazel": "d5b835c548ac917345f1780cd2da52edc1130a908fe091c92096895303ae78a0",
"https://bcr.bazel.build/modules/rules_go/0.56.1/source.json": "0c902f7272e8d4e47e459af97be472bc19dadbbe6023a0719d1adce8483ac75a",
"https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74",
"https://bcr.bazel.build/modules/rules_java/5.1.0/MODULE.bazel": "324b6478b0343a3ce7a9add8586ad75d24076d6d43d2f622990b9c1cfd8a1b15",
"https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86",
@@ -299,8 +292,7 @@
"https://bcr.bazel.build/modules/rules_jvm_external/6.0/MODULE.bazel": "37c93a5a78d32e895d52f86a8d0416176e915daabd029ccb5594db422e87c495",
"https://bcr.bazel.build/modules/rules_jvm_external/6.1/MODULE.bazel": "75b5fec090dbd46cf9b7d8ea08cf84a0472d92ba3585b476f44c326eda8059c4",
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0",
"https://bcr.bazel.build/modules/rules_jvm_external/6.9/MODULE.bazel": "07c5db05527db7744a54fcffd653e1550d40e0540207a7f7e6d0a4de5bef8274",
"https://bcr.bazel.build/modules/rules_jvm_external/6.9/source.json": "b12970214f3cc144b26610caeb101fa622d910f1ab3d98f0bae1058edbd00bd4",
"https://bcr.bazel.build/modules/rules_jvm_external/6.3/source.json": "6f5f5a5a4419ae4e37c35a5bb0a6ae657ed40b7abc5a5189111b47fcebe43197",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.0/MODULE.bazel": "ef85697305025e5a61f395d4eaede272a5393cee479ace6686dba707de804d59",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3",
"https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5",
@@ -314,12 +306,12 @@
"https://bcr.bazel.build/modules/rules_nodejs/6.3.0/MODULE.bazel": "45345e4aba35dd6e4701c1eebf5a4e67af4ed708def9ebcdc6027585b34ee52d",
"https://bcr.bazel.build/modules/rules_nodejs/6.3.3/MODULE.bazel": "b66eadebd10f1f1b25f52f95ab5213a57e82c37c3f656fcd9a57ad04d2264ce7",
"https://bcr.bazel.build/modules/rules_nodejs/6.3.3/source.json": "45bd343155bdfed2543f0e39b80ff3f6840efc31975da4b5795797f4c94147ad",
"https://bcr.bazel.build/modules/rules_oci/2.2.7/MODULE.bazel": "f6150e4b224d459f7f6523ef65967464ca4efdd266c7fbf2f5a2a51011957e0c",
"https://bcr.bazel.build/modules/rules_oci/2.2.7/source.json": "b099f02af330f47f19dc67fc9300ef6e1937a8c86882690db0e7a2fcea8c7f6b",
"https://bcr.bazel.build/modules/rules_oci/2.2.6/MODULE.bazel": "2ba6ddd679269e00aeffe9ca04faa2d0ca4129650982c9246d0d459fe2da47d9",
"https://bcr.bazel.build/modules/rules_oci/2.2.6/source.json": "94e7decb8f95d9465b0bbea71c65064cd16083be1350c7468f131818641dc4a5",
"https://bcr.bazel.build/modules/rules_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.2.0/MODULE.bazel": "c7db3c2b407e673c7a39e3625dc05dc9f12d6682cbd82a3a5924a13b491eda7e",
"https://bcr.bazel.build/modules/rules_pkg/1.2.0/source.json": "9062e00845bf91a4247465d371baa837adf9b6ff44c542f73ba084f07667e1dc",
"https://bcr.bazel.build/modules/rules_pkg/1.1.0/MODULE.bazel": "9db8031e71b6ef32d1846106e10dd0ee2deac042bd9a2de22b4761b0c3036453",
"https://bcr.bazel.build/modules/rules_pkg/1.1.0/source.json": "fef768df13a92ce6067e1cd0cdc47560dace01354f1d921cfb1d632511f7d608",
"https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06",
"https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7",
"https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483",
@@ -342,8 +334,7 @@
"https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c",
"https://bcr.bazel.build/modules/rules_python/0.40.0/MODULE.bazel": "9d1a3cd88ed7d8e39583d9ffe56ae8a244f67783ae89b60caafc9f5cf318ada7",
"https://bcr.bazel.build/modules/rules_python/1.0.0/MODULE.bazel": "898a3d999c22caa585eb062b600f88654bf92efb204fa346fb55f6f8edffca43",
"https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13",
"https://bcr.bazel.build/modules/rules_python/1.3.0/source.json": "25932f917cd279c7baefa6cb1d3fa8750a7a29de522024449b19af6eab51f4a0",
"https://bcr.bazel.build/modules/rules_python/1.0.0/source.json": "b0162a65c6312e45e7912e39abd1a7f8856c2c7e41ecc9b6dc688a6f6400a917",
"https://bcr.bazel.build/modules/rules_rust/0.45.1/MODULE.bazel": "a69d0db3a958fab2c6520961e1b2287afcc8b36690fd31bbc4f6f7391397150d",
"https://bcr.bazel.build/modules/rules_scala/7.1.1/MODULE.bazel": "b1f80c52ae49b27d41b9291d8b328b69247de2b7596d35d09afe6147b82cf562",
"https://bcr.bazel.build/modules/rules_scala/7.1.1/source.json": "5038cb231d4020c5965c920681cf961a7bf137b40315025e40f3a7b6a0ac1f0f",
@@ -354,8 +345,8 @@
"https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca",
"https://bcr.bazel.build/modules/rules_swift/1.18.0/MODULE.bazel": "a6aba73625d0dc64c7b4a1e831549b6e375fbddb9d2dde9d80c9de6ec45b24c9",
"https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046",
"https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd",
"https://bcr.bazel.build/modules/rules_swift/2.4.0/source.json": "a6577f57f9febbdc015a01f2a8f3487422032f134d6c61d18ed8e8ca3b9acc7c",
"https://bcr.bazel.build/modules/rules_swift/2.3.1/MODULE.bazel": "0b42093600d9226bcbdb31fb86d25d4204293d716fdbb2e50a1852547032a660",
"https://bcr.bazel.build/modules/rules_swift/2.3.1/source.json": "87d28609c37d2061db2f6fc3aae8ab7fbda9adf556cd88fbd0c7d520b8d81391",
"https://bcr.bazel.build/modules/stardoc/0.5.0/MODULE.bazel": "f9f1f46ba8d9c3362648eea571c6f9100680efc44913618811b58cc9c02cd678",
"https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8",
"https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c",
@@ -369,7 +360,6 @@
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91",
"https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/source.json": "32bd87e5f4d7acc57c5b2ff7c325ae3061d5e242c0c4c214ae87e0f1c13e54cb",
"https://bcr.bazel.build/modules/tar.bzl/0.2.1/MODULE.bazel": "52d1c00a80a8cc67acbd01649e83d8dd6a9dc426a6c0b754a04fe8c219c76468",
"https://bcr.bazel.build/modules/tar.bzl/0.5.1/MODULE.bazel": "7c2eb3dcfc53b0f3d6f9acdfd911ca803eaf92aadf54f8ca6e4c1f3aee288351",
"https://bcr.bazel.build/modules/tar.bzl/0.6.0/MODULE.bazel": "a3584b4edcfafcabd9b0ef9819808f05b372957bbdff41601429d5fd0aac2e7c",
"https://bcr.bazel.build/modules/tar.bzl/0.6.0/source.json": "4a620381df075a16cb3a7ed57bd1d05f7480222394c64a20fa51bdb636fda658",
"https://bcr.bazel.build/modules/toolchains_llvm/1.6.0/MODULE.bazel": "39603859cafb1c6830160fcd6370552e836790e6abb2bfb8d13bff53c0c10a64",
@@ -396,7 +386,7 @@
"@@apple_support~//crosstool:setup.bzl%apple_cc_configure_extension": {
"general": {
"bzlTransitiveDigest": "Z3yAd66IJL0GAZUTSeMOjoHiE1SZPPwiIs/XQui5BvE=",
"usagesDigest": "DqQsfZN5lA8z+nLEEY+EpKGzQ8M73mDm/A8lofDSyus=",
"usagesDigest": "TOb4CUri5UsTKxgIDTNzR0ddIc21eYLCRIm+jqQmjlg=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -423,7 +413,7 @@
},
"@@aspect_rules_esbuild~//esbuild:extensions.bzl%esbuild": {
"general": {
"bzlTransitiveDigest": "8L5Llfl6uxIWXd5GR+Qmmm04/jxp6TuJH5LFhIZIUCA=",
"bzlTransitiveDigest": "8jv3p0xDR/oitFeH8y0+Y5xlyrUbfsTRlc9TSwYkwl8=",
"usagesDigest": "iDVoyPxUeADmfK8ssoyG3Ehq1bj6p7A43LpEiE266os=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
@@ -502,7 +492,6 @@
"extra_build_content": "",
"generate_bzl_library_targets": false,
"extract_full_archive": false,
"exclude_package_contents": [],
"system_tar": "auto"
}
},
@@ -527,17 +516,11 @@
"package_visibility": [
"//visibility:public"
],
"replace_package": "",
"exclude_package_contents": []
"replace_package": ""
}
}
},
"recordedRepoMappingEntries": [
[
"aspect_bazel_lib~",
"bazel_lib",
"bazel_lib~"
],
[
"aspect_bazel_lib~",
"bazel_skylib",
@@ -548,11 +531,6 @@
"bazel_tools",
"bazel_tools"
],
[
"aspect_bazel_lib~",
"tar.bzl",
"tar.bzl~"
],
[
"aspect_rules_esbuild~",
"aspect_rules_js",
@@ -568,11 +546,6 @@
"aspect_bazel_lib",
"aspect_bazel_lib~"
],
[
"aspect_rules_js~",
"aspect_rules_js",
"aspect_rules_js~"
],
[
"aspect_rules_js~",
"bazel_skylib",
@@ -582,470 +555,6 @@
"aspect_rules_js~",
"bazel_tools",
"bazel_tools"
],
[
"bazel_lib~",
"bazel_skylib",
"bazel_skylib~"
],
[
"bazel_lib~",
"bazel_tools",
"bazel_tools"
],
[
"tar.bzl~",
"aspect_bazel_lib",
"aspect_bazel_lib~"
],
[
"tar.bzl~",
"bazel_skylib",
"bazel_skylib~"
],
[
"tar.bzl~",
"tar.bzl",
"tar.bzl~"
]
]
}
},
"@@aspect_rules_js~//npm:extensions.bzl%pnpm": {
"general": {
"bzlTransitiveDigest": "T22SdPzhLxF1CM+j9RD/Rq03yJ0NDfH2eE6hAbUcOII=",
"usagesDigest": "6rWte4KDbiluq1s7w98bc4+2NjA8w67DKHDj4+DNw/Y=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"pnpm": {
"bzlFile": "@@aspect_rules_js~//npm/private:npm_import.bzl",
"ruleClassName": "npm_import_rule",
"attributes": {
"package": "pnpm",
"version": "8.6.7",
"root_package": "",
"link_workspace": "",
"link_packages": {},
"integrity": "sha512-vRIWpD/L4phf9Bk2o/O2TDR8fFoJnpYrp2TKqTIZF/qZ2/rgL3qKXzHofHgbXsinwMoSEigz28sqk3pQ+yMEQQ==",
"url": "",
"commit": "",
"patch_args": [
"-p0"
],
"patches": [],
"custom_postinstall": "",
"npm_auth": "",
"npm_auth_basic": "",
"npm_auth_username": "",
"npm_auth_password": "",
"lifecycle_hooks": [],
"extra_build_content": "load(\"@aspect_rules_js//js:defs.bzl\", \"js_binary\")\njs_binary(name = \"pnpm\", data = glob([\"package/**\"]), entry_point = \"package/dist/pnpm.cjs\", visibility = [\"//visibility:public\"])",
"generate_bzl_library_targets": false,
"extract_full_archive": true,
"exclude_package_contents": [],
"system_tar": "auto"
}
},
"pnpm__links": {
"bzlFile": "@@aspect_rules_js~//npm/private:npm_import.bzl",
"ruleClassName": "npm_import_links",
"attributes": {
"package": "pnpm",
"version": "8.6.7",
"dev": false,
"root_package": "",
"link_packages": {},
"deps": {},
"transitive_closure": {},
"lifecycle_build_target": false,
"lifecycle_hooks_env": [],
"lifecycle_hooks_execution_requirements": [
"no-sandbox"
],
"lifecycle_hooks_use_default_shell_env": false,
"bins": {},
"package_visibility": [
"//visibility:public"
],
"replace_package": "",
"exclude_package_contents": []
}
}
},
"recordedRepoMappingEntries": [
[
"aspect_bazel_lib~",
"bazel_lib",
"bazel_lib~"
],
[
"aspect_bazel_lib~",
"bazel_skylib",
"bazel_skylib~"
],
[
"aspect_bazel_lib~",
"bazel_tools",
"bazel_tools"
],
[
"aspect_bazel_lib~",
"tar.bzl",
"tar.bzl~"
],
[
"aspect_rules_js~",
"aspect_bazel_lib",
"aspect_bazel_lib~"
],
[
"aspect_rules_js~",
"aspect_rules_js",
"aspect_rules_js~"
],
[
"aspect_rules_js~",
"bazel_features",
"bazel_features~"
],
[
"aspect_rules_js~",
"bazel_skylib",
"bazel_skylib~"
],
[
"aspect_rules_js~",
"bazel_tools",
"bazel_tools"
],
[
"bazel_features~",
"bazel_features_globals",
"bazel_features~~version_extension~bazel_features_globals"
],
[
"bazel_features~",
"bazel_features_version",
"bazel_features~~version_extension~bazel_features_version"
],
[
"bazel_lib~",
"bazel_skylib",
"bazel_skylib~"
],
[
"bazel_lib~",
"bazel_tools",
"bazel_tools"
],
[
"tar.bzl~",
"aspect_bazel_lib",
"aspect_bazel_lib~"
],
[
"tar.bzl~",
"bazel_skylib",
"bazel_skylib~"
],
[
"tar.bzl~",
"tar.bzl",
"tar.bzl~"
]
]
}
},
"@@aspect_rules_ts~//ts:extensions.bzl%ext": {
"general": {
"bzlTransitiveDigest": "h1hftyFCdJgiHD9blfFcMAiEk5ltEoUdNkuHPIkg/hM=",
"usagesDigest": "v0aTa4/gasWF2bvssXYr1bqcYWc3kjV48hcj0z2QVT0=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"npm_typescript": {
"bzlFile": "@@aspect_rules_ts~//ts/private:npm_repositories.bzl",
"ruleClassName": "http_archive_version",
"attributes": {
"bzlmod": true,
"version": "5.8.3",
"integrity": "",
"build_file": "@@aspect_rules_ts~//ts:BUILD.typescript",
"build_file_substitutions": {
"bazel_worker_version": "5.4.2",
"google_protobuf_version": "3.20.1"
},
"urls": [
"https://registry.npmjs.org/typescript/-/typescript-{}.tgz"
]
}
}
},
"recordedRepoMappingEntries": [
[
"aspect_rules_ts~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@cel-spec~//:extensions.bzl%non_module_dependencies": {
"general": {
"bzlTransitiveDigest": "/2uyuQa5purSharolRXYOGYMGSGjDeByo6JfQDWsraA=",
"usagesDigest": "2f6juplOpWu+UdD1kVgi773xavnFQ+OcH0PRuQduDxY=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"com_google_googleapis": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"sha256": "bd8e735d881fb829751ecb1a77038dda4a8d274c45490cb9fcf004583ee10571",
"strip_prefix": "googleapis-07c27163ac591955d736f3057b1619ece66f5b99",
"urls": [
"https://github.com/googleapis/googleapis/archive/07c27163ac591955d736f3057b1619ece66f5b99.tar.gz"
]
}
}
},
"recordedRepoMappingEntries": [
[
"cel-spec~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@cel-spec~//:googleapis_ext.bzl%googleapis_ext": {
"general": {
"bzlTransitiveDigest": "yun2jmsomFi3bs5bjQWXApBzqQf66zBJ39JEBYigzdc=",
"usagesDigest": "OK8FsLndSl2AbwGM1Npe5NdHR1kDAebcw7Ee+KkekE0=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"com_google_googleapis_imports": {
"bzlFile": "@@cel-spec~~non_module_dependencies~com_google_googleapis//:repository_rules.bzl",
"ruleClassName": "switched_rules",
"attributes": {
"rules": {
"proto_library_with_info": [
"",
""
],
"moved_proto_library": [
"",
""
],
"java_proto_library": [
"",
""
],
"java_grpc_library": [
"",
""
],
"java_gapic_library": [
"",
""
],
"java_gapic_test": [
"",
""
],
"java_gapic_assembly_gradle_pkg": [
"",
""
],
"py_proto_library": [
"",
""
],
"py_grpc_library": [
"",
""
],
"py_gapic_library": [
"",
""
],
"py_test": [
"",
""
],
"py_gapic_assembly_pkg": [
"",
""
],
"py_import": [
"",
""
],
"go_proto_library": [
"",
""
],
"go_library": [
"",
""
],
"go_test": [
"",
""
],
"go_gapic_library": [
"",
""
],
"go_gapic_assembly_pkg": [
"",
""
],
"cc_proto_library": [
"native.cc_proto_library",
""
],
"cc_grpc_library": [
"",
""
],
"cc_gapic_library": [
"",
""
],
"php_proto_library": [
"",
"php_proto_library"
],
"php_grpc_library": [
"",
"php_grpc_library"
],
"php_gapic_library": [
"",
"php_gapic_library"
],
"php_gapic_assembly_pkg": [
"",
"php_gapic_assembly_pkg"
],
"nodejs_gapic_library": [
"",
"typescript_gapic_library"
],
"nodejs_gapic_assembly_pkg": [
"",
"typescript_gapic_assembly_pkg"
],
"ruby_proto_library": [
"",
""
],
"ruby_grpc_library": [
"",
""
],
"ruby_ads_gapic_library": [
"",
""
],
"ruby_cloud_gapic_library": [
"",
""
],
"ruby_gapic_assembly_pkg": [
"",
""
],
"csharp_proto_library": [
"",
""
],
"csharp_grpc_library": [
"",
""
],
"csharp_gapic_library": [
"",
""
],
"csharp_gapic_assembly_pkg": [
"",
""
]
}
}
}
},
"recordedRepoMappingEntries": [
[
"cel-spec~",
"com_google_googleapis",
"cel-spec~~non_module_dependencies~com_google_googleapis"
]
]
}
},
"@@envoy_api~//bazel:repositories.bzl%non_module_deps": {
"general": {
"bzlTransitiveDigest": "6TqmRfVELxZJRPQYuJpC4JBX4QvdrHqTDOUJGOGODSo=",
"usagesDigest": "IivjlawPvqhHPUJ3c6dLiPsH22mn/g/dJtlmv3zdimM=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"prometheus_metrics_model": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/prometheus/client_model/archive/v0.6.1.tar.gz"
],
"sha256": "b9b690bc35d80061f255faa7df7621eae39fe157179ccd78ff6409c3b004f05e",
"strip_prefix": "client_model-0.6.1",
"build_file_content": "\nload(\"@envoy_api//bazel:api_build_system.bzl\", \"api_cc_py_proto_library\")\nload(\"@io_bazel_rules_go//proto:def.bzl\", \"go_proto_library\")\n\napi_cc_py_proto_library(\n name = \"client_model\",\n srcs = [\n \"io/prometheus/client/metrics.proto\",\n ],\n visibility = [\"//visibility:public\"],\n)\n\ngo_proto_library(\n name = \"client_model_go_proto\",\n importpath = \"github.com/prometheus/client_model/go\",\n proto = \":client_model\",\n visibility = [\"//visibility:public\"],\n)\n"
}
},
"com_github_bufbuild_buf": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/bufbuild/buf/releases/download/v1.49.0/buf-Linux-x86_64.tar.gz"
],
"sha256": "ee8da9748249f7946d79191e36469ce7bc3b8ba80019bff1fa4289a44cbc23bf",
"strip_prefix": "buf",
"build_file_content": "\npackage(\n default_visibility = [\"//visibility:public\"],\n)\n\nfilegroup(\n name = \"buf\",\n srcs = [\n \"@com_github_bufbuild_buf//:bin/buf\",\n ],\n tags = [\"manual\"], # buf is downloaded as a linux binary; tagged manual to prevent build for non-linux users\n)\n"
}
},
"envoy_toolshed": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/envoyproxy/toolshed/archive/bazel-v0.2.2.tar.gz"
],
"sha256": "443fe177aba0cef8c17b7a48905c925c67b09005b10dd70ff12cd9f729a72d51",
"strip_prefix": "toolshed-bazel-v0.2.2/bazel"
}
}
},
"recordedRepoMappingEntries": [
[
"envoy_api~",
"bazel_tools",
"bazel_tools"
],
[
"envoy_api~",
"envoy_api",
"envoy_api~"
]
]
}
@@ -1218,37 +727,6 @@
"recordedRepoMappingEntries": []
}
},
"@@pybind11_bazel~//:internal_configure.bzl%internal_configure_extension": {
"general": {
"bzlTransitiveDigest": "CyAKLVVonohnkTSqg9II/HA7M49sOlnMkgMHL3CmDuc=",
"usagesDigest": "mFrTHX5eCiNU/OIIGVHH3cOILY9Zmjqk8RQYv8o6Thk=",
"recordedFileInputs": {
"@@pybind11_bazel~//MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34"
},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"pybind11": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file": "@@pybind11_bazel~//:pybind11-BUILD.bazel",
"strip_prefix": "pybind11-2.12.0",
"urls": [
"https://github.com/pybind/pybind11/archive/v2.12.0.zip"
]
}
}
},
"recordedRepoMappingEntries": [
[
"pybind11_bazel~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@rules_foreign_cc~//foreign_cc:extensions.bzl%tools": {
"general": {
"bzlTransitiveDigest": "a7qnESofmIRYId6wwGNPJ9kvExU80KrkxL281P3+lBE=",
@@ -1589,97 +1067,6 @@
]
}
},
"@@rules_fuzzing~//fuzzing/private:extensions.bzl%non_module_dependencies": {
"general": {
"bzlTransitiveDigest": "hVgJRQ3Er45/UUAgNn1Yp2Khcp/Y8WyafA2kXIYmQ5M=",
"usagesDigest": "YnIrdgwnf3iCLfChsltBdZ7yOJh706lpa2vww/i2pDI=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"platforms": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://mirror.bazel.build/github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz",
"https://github.com/bazelbuild/platforms/releases/download/0.0.8/platforms-0.0.8.tar.gz"
],
"sha256": "8150406605389ececb6da07cbcb509d5637a3ab9a24bc69b1101531367d89d74"
}
},
"rules_python": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"sha256": "d70cd72a7a4880f0000a6346253414825c19cdd40a28289bdf67b8e6480edff8",
"strip_prefix": "rules_python-0.28.0",
"url": "https://github.com/bazelbuild/rules_python/releases/download/0.28.0/rules_python-0.28.0.tar.gz"
}
},
"bazel_skylib": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"sha256": "cd55a062e763b9349921f0f5db8c3933288dc8ba4f76dd9416aac68acee3cb94",
"urls": [
"https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz",
"https://github.com/bazelbuild/bazel-skylib/releases/download/1.5.0/bazel-skylib-1.5.0.tar.gz"
]
}
},
"com_google_absl": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"urls": [
"https://github.com/abseil/abseil-cpp/archive/refs/tags/20240116.1.zip"
],
"strip_prefix": "abseil-cpp-20240116.1",
"integrity": "sha256-7capMWOvWyoYbUaHF/b+I2U6XLMaHmky8KugWvfXYuk="
}
},
"rules_fuzzing_oss_fuzz": {
"bzlFile": "@@rules_fuzzing~//fuzzing/private/oss_fuzz:repository.bzl",
"ruleClassName": "oss_fuzz_repository",
"attributes": {}
},
"honggfuzz": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_archive",
"attributes": {
"build_file": "@@rules_fuzzing~//:honggfuzz.BUILD",
"sha256": "6b18ba13bc1f36b7b950c72d80f19ea67fbadc0ac0bb297ec89ad91f2eaa423e",
"url": "https://github.com/google/honggfuzz/archive/2.5.zip",
"strip_prefix": "honggfuzz-2.5"
}
},
"rules_fuzzing_jazzer": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_jar",
"attributes": {
"sha256": "ee6feb569d88962d59cb59e8a31eb9d007c82683f3ebc64955fd5b96f277eec2",
"url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer/0.20.1/jazzer-0.20.1.jar"
}
},
"rules_fuzzing_jazzer_api": {
"bzlFile": "@@bazel_tools//tools/build_defs/repo:http.bzl",
"ruleClassName": "http_jar",
"attributes": {
"sha256": "f5a60242bc408f7fa20fccf10d6c5c5ea1fcb3c6f44642fec5af88373ae7aa1b",
"url": "https://repo1.maven.org/maven2/com/code-intelligence/jazzer-api/0.20.1/jazzer-api-0.20.1.jar"
}
}
},
"recordedRepoMappingEntries": [
[
"rules_fuzzing~",
"bazel_tools",
"bazel_tools"
]
]
}
},
"@@rules_java~//java:rules_java_deps.bzl%compatibility_proxy": {
"general": {
"bzlTransitiveDigest": "KIX40nDfygEWbU+rq3nYpt3tVgTK/iO8PKh5VMBlN7M=",
@@ -1775,7 +1162,7 @@
"@@rules_nodejs~//nodejs:extensions.bzl%node": {
"general": {
"bzlTransitiveDigest": "q44Ox2Nwogn6OsO0Xw5lhjkd/xmxkvvpwVOn5P4pmHQ=",
"usagesDigest": "Py5Wgc5kr5fTMe1FKrlFK276B6SodesXp6nw2Fq5XA8=",
"usagesDigest": "WQpLKLujnBfrx9sMWCJgyaK9P04binseT6CGBy3vP4E=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -1905,8 +1292,8 @@
},
"@@rules_oci~//oci:extensions.bzl%oci": {
"general": {
"bzlTransitiveDigest": "AOLP47LtVHSKSDiukosQymx543OwcgeoQP666wwuj3o=",
"usagesDigest": "3Xsv1/UEV8MOARW4BZScPw3Gxtx19OQ5EXOqcL1p9bI=",
"bzlTransitiveDigest": "FaY+7xb13bB3hmxqwAWaGp3Tf3Q4Nfdlr+F38CP5mcg=",
"usagesDigest": "39yHQmifPoGf+JSYpnQSnJGugxbrFVge/+ENmUiqZ6M=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
@@ -2193,43 +1580,6 @@
]
}
},
"@@rules_python~//python/uv:uv.bzl%uv": {
"general": {
"bzlTransitiveDigest": "Xpqjnjzy6zZ90Es9Wa888ZLHhn7IsNGbph/e6qoxzw8=",
"usagesDigest": "qI5PVlIum/YAnGJg5oXGHzDkMFWt2aNSUZY4G8PBbic=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
"generatedRepoSpecs": {
"uv": {
"bzlFile": "@@rules_python~//python/uv/private:uv_toolchains_repo.bzl",
"ruleClassName": "uv_toolchains_repo",
"attributes": {
"toolchain_type": "'@@rules_python~//python/uv:uv_toolchain_type'",
"toolchain_names": [
"none"
],
"toolchain_implementations": {
"none": "'@@rules_python~//python:none'"
},
"toolchain_compatible_with": {
"none": [
"@platforms//:incompatible"
]
},
"toolchain_target_settings": {}
}
}
},
"recordedRepoMappingEntries": [
[
"rules_python~",
"platforms",
"platforms"
]
]
}
},
"@@rules_scala~//scala/extensions:config.bzl%scala_config": {
"general": {
"bzlTransitiveDigest": "TdBxhkZTM7VU6teIFS+KoonKU7wmb5BL7leCWWx7yX8=",
@@ -5706,7 +5056,7 @@
"@@rules_swift~//swift:extensions.bzl%non_module_deps": {
"general": {
"bzlTransitiveDigest": "PAIMhc1bVKfcyoHeg0xO8LMS9KN5yzbsMGwa5O2ifJM=",
"usagesDigest": "l2vIL7SL4tJqHIVLWd78Y/ym+r0II64lmvoX8o+0Bb0=",
"usagesDigest": "A3fzk5iHsrLdI3PokT1bHIdeJ2j9tc09H3/3Old6IfU=",
"recordedFileInputs": {},
"recordedDirentsInputs": {},
"envVariables": {},
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
/bin/mkdir -p win_output
/usr/bin/dotnet publish src/main/csharp/net/eagle0/clients/win/installer/EagleInstaller.sln -o win_output
SHA=`sha256sum /tmp/EagleInstaller.exe | awk '{print $1 }'`
ZIP_FILE="updater__$SHA.zip"
/usr/bin/zip win_output/$ZIP_FILE win_output/EagleInstaller.exe
rm win_output/EagleInstaller.exe
rm win_output/EagleInstaller.pdb
DATE=`date +"%Y-%m-%d %T"`
cat > win_output/updater.html <<-EOF
<html>
<head>
<title>Download Eagle Updater</title>
</head>
<body>
<a href="http://eagle0.net/assets/$ZIP_FILE">$ZIP_FILE</a> (updated $DATE)
</body>
</html>
EOF
SSH_KEY_FILE=$1
SSH_USER_NAME=$2
/usr/bin/rsync -r --copy-links -e "/usr/bin/ssh -i $SSH_KEY_FILE -p 9022" win_output/ $SSH_USER_NAME@eagle0.net:/www/assets/
@@ -1,32 +0,0 @@
#!/usr/bin/env bash
# Build iOS Addressables only (no player build)
# This switches Unity to iOS target and builds addressables for CDN upload
set -euxo pipefail
. ./ci/unity_version.sh
WORKSPACE=$(pwd)
echo "Building protos"
./scripts/build_protos.sh
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
LOG_PATH=${1:-"/tmp/eagle0/editor_ios_addressables.log"}
echo "Building iOS Addressables"
mkdir -p "$(dirname "$LOG_PATH")"
# Build Addressables for iOS target
# Uses BuildiOSAddressables which explicitly switches build target
${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"
echo "iOS Addressables build complete"
echo "Bundles should be in: $WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0/ServerData/iOS/"
+1 -3
View File
@@ -15,12 +15,10 @@ echo "Cleaning up $BUILD_DIR"
/bin/rm -rf "$BUILD_DIR"
/bin/mkdir -p "$BUILD_DIR"
# Use custom build script that builds Addressables before the player
${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
-nographics \
-batchmode \
-quit \
-executeMethod BuildScript.BuildMacPlayer \
-buildPath "$BUILD_DIR/eagle0.app" \
-buildOSXUniversalPlayer "$BUILD_DIR/eagle0.app" \
-logFile "$LOG_PATH" \
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
+3
View File
@@ -9,6 +9,9 @@ COMMIT=$(/usr/bin/git rev-parse --short HEAD)
/bin/echo "build protos"
./scripts/build_protos.sh
/bin/echo "build plugins"
./scripts/build_windows_plugin.sh
git log -3
/bin/echo "build Windows"
+2 -2
View File
@@ -7,8 +7,8 @@ COMMIT=$(/usr/bin/git rev-parse --short HEAD)
/bin/echo "build protos"
./scripts/build_protos.sh
/bin/echo "build Sparkle plugin"
./scripts/build_sparkle_plugin.sh
/bin/echo "build Mac plugin"
./scripts/build_mac_plugin.sh
git log -3
+1 -3
View File
@@ -15,12 +15,10 @@ echo "Cleaning up $1"
/bin/rm -rf $1
/bin/mkdir -p $1
# Use custom build script that builds Addressables before the player
${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
-nographics \
-batchmode \
-quit \
-executeMethod BuildScript.BuildWindowsPlayer \
-buildPath "$BUILD_DIR/eagle0.exe" \
-buildWindows64Player $BUILD_DIR/eagle0.exe \
-logFile $LOG_PATH \
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
+3 -30
View File
@@ -1,33 +1,6 @@
#!/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
set -euxo 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
/bin/echo "persist Library/"
/usr/bin/rsync -rtlDvq src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ /tmp/eagle0/Library/
+3 -12
View File
@@ -1,16 +1,7 @@
#!/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/
/bin/echo "restore Library/"
/bin/mkdir -p /tmp/eagle0/Library
/usr/bin/rsync -rtlDvq /tmp/eagle0/Library/ src/main/csharp/net/eagle0/clients/unity/eagle0/Library/
-44
View File
@@ -1,44 +0,0 @@
#!/usr/bin/env bash
# Upload Addressables bundles to DigitalOcean Spaces
# Usage: ./upload_addressables.sh <build_target>
# Example: ./upload_addressables.sh StandaloneOSX
#
# Required environment variables:
# ACCESS_KEY_ID - DigitalOcean Spaces access key (same as other deploys)
# SECRET_KEY - DigitalOcean Spaces secret key (same as other deploys)
set -euxo pipefail
BUILD_TARGET=$1
WORKSPACE=$(pwd)
UNITY_PROJECT="$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
SERVER_DATA="$UNITY_PROJECT/ServerData/$BUILD_TARGET"
# DigitalOcean Spaces configuration (same region as other eagle0 buckets)
DO_ENDPOINT="https://sfo3.digitaloceanspaces.com"
DO_BUCKET="eagle0-assets"
if [ ! -d "$SERVER_DATA" ]; then
echo "No Addressables bundles found at $SERVER_DATA"
echo "Skipping upload (this is expected if Addressables are bundled locally)"
exit 0
fi
echo "Uploading Addressables bundles from $SERVER_DATA"
echo "Target: s3://$DO_BUCKET/addressables/$BUILD_TARGET/"
# Configure AWS CLI for DigitalOcean Spaces
export AWS_ACCESS_KEY_ID="$ACCESS_KEY_ID"
export AWS_SECRET_ACCESS_KEY="$SECRET_KEY"
# Sync bundles to Spaces
# --delete removes files in destination that don't exist in source
# --acl public-read makes files publicly accessible
aws s3 sync "$SERVER_DATA" "s3://$DO_BUCKET/addressables/$BUILD_TARGET/" \
--endpoint-url "$DO_ENDPOINT" \
--acl public-read \
--delete
echo "Addressables upload complete"
echo "Files available at: https://assets.eagle0.net/addressables/$BUILD_TARGET/"
Binary file not shown.
+40
View File
@@ -0,0 +1,40 @@
# Environment template for production deployment
# This file defines all env vars used by docker-compose.prod.yml
# Workflows should update their specific vars without overwriting others
# Container images (managed by respective build workflows)
EAGLE_IMAGE=registry.digitalocean.com/eagle0/eagle-server:latest
SHARDOK_IMAGE=registry.digitalocean.com/eagle0/shardok-server:latest
ADMIN_IMAGE=registry.digitalocean.com/eagle0/admin-server:latest
JFR_SIDECAR_IMAGE=registry.digitalocean.com/eagle0/jfr-sidecar:latest
AUTH_IMAGE=registry.digitalocean.com/eagle0/auth-server:latest
# OpenAI / LLM
OPENAI_API_KEY=
GPT_MODEL_NAME=gpt-4o
# DigitalOcean Spaces (S3-compatible storage)
EAGLE_ENABLE_S3=false
DO_SPACES_ACCESS_KEY=
DO_SPACES_SECRET_KEY=
# JWT authentication
JWT_PRIVATE_KEY=
# OAuth providers
DISCORD_CLIENT_ID=
DISCORD_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# Shardok connection
SHARDOK_ADDRESS=shardok:40042
SHARDOK_AUTH_TOKEN=
# Monitoring
SENTRY_DSN=
# Email (Fastmail JMAP)
FASTMAIL_API_TOKEN=
FASTMAIL_FROM_EMAIL=
FASTMAIL_FROM_NAME=
+59
View File
@@ -0,0 +1,59 @@
#!/bin/bash
# Update .env file without losing other variables
# Usage: ./update-env.sh KEY1=value1 KEY2=value2 ...
#
# This script:
# 1. Creates .env from template if it doesn't exist
# 2. Updates only the specified KEY=value pairs
# 3. Preserves all other existing values
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENV_FILE="${ENV_FILE:-/opt/eagle0/.env}"
TEMPLATE_FILE="${TEMPLATE_FILE:-$SCRIPT_DIR/env.template}"
# Create .env from template if it doesn't exist
if [ ! -f "$ENV_FILE" ]; then
if [ -f "$TEMPLATE_FILE" ]; then
echo "Creating .env from template..."
grep -v '^#' "$TEMPLATE_FILE" | grep -v '^$' > "$ENV_FILE"
else
echo "Creating empty .env..."
touch "$ENV_FILE"
fi
chmod 600 "$ENV_FILE"
fi
# Process each KEY=VALUE argument
for arg in "$@"; do
# Skip empty args
[ -z "$arg" ] && continue
# Parse KEY=VALUE
KEY="${arg%%=*}"
VALUE="${arg#*=}"
# Skip if no key
[ -z "$KEY" ] && continue
# Skip setting empty values (keeps existing value)
if [ -z "$VALUE" ]; then
echo "Skipping $KEY (empty value)"
continue
fi
# Remove existing line for this key and add new one
if grep -q "^${KEY}=" "$ENV_FILE" 2>/dev/null; then
# Key exists, update it
sed -i "s|^${KEY}=.*|${KEY}=${VALUE}|" "$ENV_FILE"
echo "Updated $KEY"
else
# Key doesn't exist, add it
echo "${KEY}=${VALUE}" >> "$ENV_FILE"
echo "Added $KEY"
fi
done
chmod 600 "$ENV_FILE"
echo "Done updating $ENV_FILE"
+46 -61
View File
@@ -1,13 +1,11 @@
# Docker Compose for production deployment
#
# Local testing:
# Build images: bazel run //ci:eagle_server_load && bazel run //ci:auth_server_load
# Build images: bazel run //ci:eagle_server_load && bazel run //ci:shardok_server_load && bazel run //ci:auth_server_load
# Run: docker compose -f docker-compose.prod.yml up
#
# Production deployment:
# Run: docker compose -f docker-compose.prod.yml up -d
#
# Note: Shardok runs on Hetzner ARM64 server, deployed via shardok_arm64_build.yml workflow.
services:
# Blue-green deployment: eagle-blue is the primary (production) instance
@@ -18,23 +16,24 @@ services:
image: ${EAGLE_IMAGE:-registry.digitalocean.com/eagle0/eagle-server:latest}
container_name: eagle-blue
command:
- "--gpt-model-name"
- "${GPT_MODEL_NAME:-gpt-5.1}"
- "--shardok-interface-remote-address"
- "${SHARDOK_ADDRESS}"
- "${SHARDOK_ADDRESS:-shardok:40042}"
- "--auth-service-url"
- "auth:40033"
ports:
- "40032:40032"
environment:
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
GEMINI_API_KEY: "${GEMINI_API_KEY:-}"
EAGLE_ENABLE_S3: "${EAGLE_ENABLE_S3:-false}"
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
# JWT public key for token validation (auth service handles signing)
# Reads from /etc/eagle0/keys/public.pem via shared volume
# Auth token for Shardok on Hetzner (required)
JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}"
DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}"
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
# Auth token for remote Shardok on Hetzner (only used when shardok address contains .eagle0.net)
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
# Use persistent volume for save data (users, games, etc.)
EAGLE_SAVE_DIR: "/app/saves"
@@ -46,8 +45,9 @@ services:
- ./archived:/app/archived # Archived completed games
- ./jfr:/app/jfr # JFR recordings - dump with: docker exec eagle-blue jcmd 1 JFR.dump filename=/app/jfr/profile.jfr
- jvm-tmp:/tmp # Shared with jfr-sidecar for JVM attach socket files
- jwt-keys:/etc/eagle0/keys:ro # JWT public key from auth service (read-only)
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
depends_on:
- shardok
- auth
restart: unless-stopped
logging:
@@ -67,22 +67,23 @@ services:
container_name: eagle-green
profiles: ["blue-green"] # Only started during blue-green deployment
command:
- "--gpt-model-name"
- "${GPT_MODEL_NAME:-gpt-5.1}"
- "--shardok-interface-remote-address"
- "${SHARDOK_ADDRESS}"
- "${SHARDOK_ADDRESS:-shardok:40042}"
- "--auth-service-url"
- "auth:40033"
ports:
- "40034:40032" # Different host port for staging
environment:
OPENAI_API_KEY: "${OPENAI_API_KEY:-}"
ANTHROPIC_API_KEY: "${ANTHROPIC_API_KEY:-}"
GEMINI_API_KEY: "${GEMINI_API_KEY:-}"
EAGLE_ENABLE_S3: "${EAGLE_ENABLE_S3:-false}"
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
# JWT public key for token validation (auth service handles signing)
# Reads from /etc/eagle0/keys/public.pem via shared volume
JWT_PRIVATE_KEY: "${JWT_PRIVATE_KEY:-}"
DISCORD_CLIENT_ID: "${DISCORD_CLIENT_ID:-}"
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
EAGLE_SAVE_DIR: "/app/saves"
EAGLE_ARCHIVE_DIR: "/app/archived"
@@ -91,10 +92,9 @@ services:
volumes:
- ./saves:/app/saves # Same save directory as blue
- ./archived:/app/archived # Same archive directory as blue
- ./jfr:/app/jfr # JFR recordings (same as blue)
- jvm-tmp:/tmp # Shared with jfr-sidecar-green for JVM attach socket files
- jwt-keys:/etc/eagle0/keys:ro # JWT public key from auth service (read-only)
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with auth service
depends_on:
- shardok
- auth
restart: "no" # Don't auto-restart during deployment
logging:
@@ -131,16 +131,6 @@ services:
DISCORD_CLIENT_SECRET: "${DISCORD_CLIENT_SECRET:-}"
GOOGLE_CLIENT_ID: "${GOOGLE_CLIENT_ID:-}"
GOOGLE_CLIENT_SECRET: "${GOOGLE_CLIENT_SECRET:-}"
GH_OAUTH_CLIENT_ID: "${GH_OAUTH_CLIENT_ID:-}"
GH_OAUTH_CLIENT_SECRET: "${GH_OAUTH_CLIENT_SECRET:-}"
# Apple Sign-In credentials
APPLE_SIGNIN_CLIENT_ID: "${APPLE_SIGNIN_CLIENT_ID:-}"
APPLE_TEAM_ID: "${APPLE_TEAM_ID:-}"
APPLE_SIGNIN_KEY_ID: "${APPLE_SIGNIN_KEY_ID:-}"
APPLE_SIGNIN_PRIVATE_KEY: "${APPLE_SIGNIN_PRIVATE_KEY:-}"
# Twitch OAuth credentials
TWITCH_CLIENT_ID: "${TWITCH_CLIENT_ID:-}"
TWITCH_CLIENT_SECRET: "${TWITCH_CLIENT_SECRET:-}"
# Server base URL for OAuth callbacks
SERVER_BASE_URL: "${SERVER_BASE_URL:-https://prod.eagle0.net}"
# JWT keys - PEM files in volume, bootstrapped from JWK on first run
@@ -150,8 +140,6 @@ services:
FASTMAIL_API_TOKEN: "${FASTMAIL_API_TOKEN:-}"
FASTMAIL_FROM_EMAIL: "${FASTMAIL_FROM_EMAIL:-}"
FASTMAIL_FROM_NAME: "${FASTMAIL_FROM_NAME:-}"
# Require invitation codes for new user registration
REQUIRE_INVITATION_CODE: "true"
# Note: port 40033 is exposed via nginx, not directly
volumes:
- jwt-keys:/etc/eagle0/keys # Shared JWT keys with Eagle
@@ -170,8 +158,30 @@ services:
retries: 3
start_period: 10s
# Note: Shardok runs on Hetzner ARM64 server, not in this docker-compose.
# Configure SHARDOK_ADDRESS to point to the Hetzner instance.
shardok:
image: ${SHARDOK_IMAGE:-registry.digitalocean.com/eagle0/shardok-server:latest}
container_name: shardok-server
mem_limit: 1g
memswap_limit: 1g # Prevent swap, OOM-kill cleanly instead
ports:
- "40042:40042"
- "40052:40052"
environment:
SHARDOK_RESOURCES_PATH: "/app/resources"
SHARDOK_MAPS_PATH: "/app/resources/maps"
SHARDOK_EAGLE_INTERFACE_ADDRESS: "0.0.0.0:40042"
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
healthcheck:
test: ["CMD-SHELL", "nc -z localhost 40042 || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
nginx:
image: nginx:alpine
@@ -186,9 +196,8 @@ services:
- ./certbot/www:/var/www/certbot:ro
- ./auth:/etc/nginx/auth:ro
depends_on:
- eagle-blue
- admin
# Note: nginx connects to eagle via EAGLE_ADDR (default: eagle-blue:40032)
# For blue-green deployments, update EAGLE_ADDR in .env before switching
restart: unless-stopped
logging:
driver: "json-file"
@@ -205,14 +214,14 @@ services:
- "--auth-addr"
- "auth:40033"
- "--jfr-sidecar-addr"
- "${JFR_SIDECAR_ADDR:-jfr-sidecar:8081}" # Can be switched for blue-green
- "jfr-sidecar:8081"
- "--http-port"
- "8080"
# No external port - accessed via nginx at admin.eagle0.net
depends_on:
- eagle-blue
- auth
# Note: admin connects to eagle via EAGLE_ADDR and jfr-sidecar via JFR_SIDECAR_ADDR
# For blue-green deployments, set both in .env before switching
- jfr-sidecar
restart: unless-stopped
logging:
driver: "json-file"
@@ -230,7 +239,6 @@ services:
image: ${JFR_SIDECAR_IMAGE:-registry.digitalocean.com/eagle0/jfr-sidecar:latest}
container_name: jfr-sidecar
# Share PID namespace with Eagle to access its JVM via jcmd
# For blue-green: use JFR_SIDECAR_ADDR=jfr-sidecar-green:8081 when green is active
pid: "service:eagle-blue"
volumes:
- jvm-tmp:/tmp # Shared with Eagle for JVM attach socket files
@@ -249,29 +257,6 @@ services:
retries: 3
start_period: 10s
jfr-sidecar-green:
image: ${JFR_SIDECAR_IMAGE:-registry.digitalocean.com/eagle0/jfr-sidecar:latest}
container_name: jfr-sidecar-green
profiles: ["blue-green"] # Only started during blue-green deployment
# Share PID namespace with Eagle green instance
pid: "service:eagle-green"
volumes:
- jvm-tmp:/tmp # Shared with Eagle for JVM attach socket files
depends_on:
- eagle-green
restart: "no" # Don't auto-restart during deployment
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "2"
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://localhost:8081/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
certbot:
image: certbot/certbot
container_name: certbot
+46 -67
View File
@@ -109,20 +109,11 @@ All 26 tracks have CC licenses with proper attribution:
| Dragon Castle | Makai Symphony | CC BY-SA 3.0 |
| Durandal | Makai Symphony | CC BY-SA 3.0 |
**Tracks with non-CC licenses:**
| Track | Artist | License | Source |
|-------|--------|---------|--------|
| Market Day | RandomMind | Free without attribution | [Chosic](https://www.chosic.com/download-audio/27016/) |
| Shopping List | Komiku | Free without attribution | [Chosic](https://www.chosic.com/download-audio/24714/) |
| Medieval: Victory Theme | RandomMind | CC0 Public Domain | [Chosic](https://www.chosic.com/download-audio/28492/) |
| No Time for Greatness | Dima Koltsov | CC BY 4.0 | [YouTube](https://www.youtube.com/watch?v=cQh0OWIFdgM) |
| Warriors of Demacia | Dima Koltsov | CC BY 4.0 | [YouTube](https://www.youtube.com/watch?v=yktSUMJn9ao) |
| Forest Queen Tale | Dima Koltsov | Presumed CC BY 4.0 | Not found on YouTube; other Dima Koltsov tracks are CC BY 4.0 |
| Valor | Dima Koltsov | CC BY 4.0 | [YouTube](https://www.youtube.com/watch?v=uoHYJRPcS2Y) |
| Clouds | Dima Koltsov | Presumed CC BY 4.0 | Not found on YouTube; other Dima Koltsov tracks are CC BY 4.0 |
**Note on Dima Koltsov tracks:** 3 of 5 tracks confirmed CC BY 4.0 via YouTube. 2 remaining tracks (Forest Queen Tale, Clouds) presumed same license but not verified.
**Tracks without specific license (verify):**
- Market Day
- Shopping List
- Medieval: Victory Theme
- Tracks by Dima Koltsov (AUDIUS): No Time for Greatness, Warriors of Demacia, Forest Queen Tale, Valor, Clouds
---
@@ -138,55 +129,42 @@ All 26 tracks have CC licenses with proper attribution:
## 4. Potentially Problematic Assets (Review Needed)
### ~~Clip Art (Unknown License)~~ RESOLVED
| File | Status |
|------|--------|
| ~~`Assets/Shardok/commandImages/bridge.png`~~ | **REPLACED** (2026-01-23) with AI-generated wooden rope bridge icon (ChatGPT/DALL-E 3, 512x512 PNG). No licensing restrictions - AI-generated for this project. |
| ~~`Assets/Images/startFire.png`~~ | **REPLACED** (2026-01-23) with "Flame Icon" from [UXWing](https://uxwing.com/flame-icon/) (free for commercial use, no attribution required). Consolidated duplicate removed. |
| ~~`Assets/Shardok/commandImages/startFire.png`~~ | **DELETED** (2026-01-23) - duplicate removed, all references updated to use `Assets/Images/startFire.png` |
### Stock Images (Possible License Issues)
These appear to be stock images that may have been used as placeholders:
| File | Concern |
|------|---------|
| ~~`Assets/Eagle/79066358-stock-illustration-raster-illustration-medieval-purse-bag...jpg`~~ | **DELETED** (2025-01-04) |
| ~~`Assets/Images/kisspng-hammer-hand-saws-tool-clip-art...jpg`~~ | **DELETED** (2025-01-04) |
| ~~`Assets/Images/lee-ermy-cropped.jpg`~~ | **DELETED** (2025-01-04) |
| ~~`Assets/Eagle/images.jpeg`~~ | **DELETED** (2025-01-04) |
### Clip Art (Unknown License)
| File | Concern |
|------|---------|
| `Assets/Shardok/commandImages/bridge.png` | Clip art style wooden bridge, unknown source - **needs replacement** |
| `Assets/Images/startFire.png` | Icon, unknown source - **needs verification or replacement** |
### Shardok Sound Effects
- **Location:** `Assets/Shardok/soundEffects/`
- **Count:** 37 audio files (was incorrectly counted as 56 including .meta files)
- **Count:** 56 MP3 files
- **Contents:** Spell effects, movement, combat sounds
**Verified from [Zombie Monster - Undead Collection](https://assetstore.unity.com/packages/audio/sound-fx/creatures/zombie-monster-undead-collection-70662) (Unity Asset Store):**
- `raise_undead.mp3`
- `undead_break_control.mp3`
- `undead_grew.wav`
**⚠️ MUST REPLACE:**
- ~~`anybody.mp3`~~ - **REPLACED** (2026-01-23) with `Positive Effect 6.wav` from Magic Spells Sound Effects LITE
- ~~`burnination.mp3`~~ - **REPLACED** (2026-01-23) with `Magic Element Fire 04.wav` from Medieval Combat Sounds
- `failure_horn.mp3` - licensing issue, no replacement found in purchased assets
- `runaway.mp3` - licensing issue, no replacement found in purchased assets
**Presumed from Unity Asset Store purchases (31 files):**
Owner believes these are from: Fantasy Interface Sounds, Medieval Combat Sounds, Magic Spells Sound Effects LITE, and/or Medieval Battle Sound Pack.
- `archery.mp3`, `battle_shout.mp3`, `boo.mp3`, `braved_water.mp3`
- `build_bridge.mp3`, `build_bridge_failure.mp3`, `charge.mp3`
- `dismiss_unit.mp3`, `duel_challenged.mp3`, `failure_horn.mp3`, `fear.mp3`, `fear_failed.mp3`
- `fire_extinguish.mp3`, `fire_spread.mp3`, `fire_start.mp3`, `fire_start_failure.mp3`
- `freeze.mp3`, `holy_wave.mp3`, `holy_wave_damage.mp3`, `jail_door.mp3`, `lightning.mp3`
- `melee.mp3`, `meteor.mp3`, `mind_control.mp3`, `move.mp3`, `move 1.mp3`
- `raging_fire.mp3`, `reduce.mp3`, `repair.mp3`, `repair_failed.mp3`, `splash.mp3`
- **Status:** Unknown origin - may be custom or need verification
### Free Icons
- **Location:** `Assets/free_icons/`
- **Count:** 8 PNG weather icons
- **Status:** Verify "free" means commercially usable
### ~~Terrain Hexes~~ VERIFIED
### Terrain Hexes
- **Location:** `Assets/Terrain Hexes/`
- **Count:** 85 PNG files
- **Status:** ✓ Confirmed Unity Asset Store purchase (2026-01-23)
- **Status:** Unknown source - verify licensing
### ~~StrategyGameIcons~~ VERIFIED
### StrategyGameIcons
- **Location:** `Assets/StrategyGameIcons/`
- **Count:** 138 PNG files
- **Publisher:** REXARD
- **Asset Store Link:** https://assetstore.unity.com/packages/2d/gui/icons/strategy-game-icons-64816
- **Status:** ✓ Confirmed Unity Asset Store purchase (2026-01-23)
- **Status:** Unknown source - verify licensing
---
@@ -218,24 +196,26 @@ NuGet packages in `Assets/Packages/` all include LICENSE.TXT files:
### Must Verify Before Opening Public Access:
1. ~~**Clip art images**~~ - **RESOLVED** (2025-01-23): Replaced with game-icons.net CC BY 3.0 icons
1. ~~**Stock images** - The JPG files with stock image filenames need review.~~ **DONE** - Deleted lee-ermy, kisspng, stock-illustration, Yosemite Sam, and images.jpeg (2025-01-04)
2. **Shardok sound effects** - 3 files must be replaced:
- `anybody.mp3` - licensing issue
- `burnination.mp3` - licensing issue
- `runaway.mp3` - licensing issue
2. **Clip art images** - Unknown license, need replacement with properly licensed alternatives:
- `Assets/Shardok/commandImages/bridge.png` - wooden bridge icon
- `Assets/Images/startFire.png` - fire icon
Remaining 31 files presumed from Asset Store purchases; 3 verified from Zombie Monster Undead Collection.
3. **Shardok sound effects** - 56 MP3 files of unknown origin. Either:
- Document their source
- Replace with known-licensed alternatives
- Confirm they were custom-created
3. ~~**Terrain Hexes**~~ - **VERIFIED** (2026-01-23): Confirmed Unity Asset Store purchase
4. **Terrain Hexes** - 85 hex tiles of unknown source
- **TODO:** Confirm if this is a Unity Asset Store purchase (owner believes it is)
4. ~~**StrategyGameIcons**~~ - **VERIFIED** (2026-01-23): Unity Asset Store purchase (REXARD)
5. **StrategyGameIcons** - 138 icons of unknown source
- **TODO:** Investigate origin - check Unity Asset Store purchase history
5. ~~**Medieval: Victory Theme**~~ - **VERIFIED** (2026-01-23): CC0 Public Domain by RandomMind ([Chosic](https://www.chosic.com/download-audio/28492/))
6. **AUDIUS music tracks** - Verify Dima Koltsov tracks are licensed for commercial use
6. ~~**Dima Koltsov tracks**~~ - **MOSTLY VERIFIED** (2026-01-23): 3 of 5 confirmed CC BY 4.0 via YouTube. 2 remaining (Forest Queen Tale, Clouds) presumed same license.
7. ~~**Discord logo**~~ - **OK** (2026-01-23): Usage complies with Discord brand guidelines for "Login with Discord" button
7. **Discord logo** (`Eagle/Discord-Logo-Blurple.png`) - Likely fine for "Login with Discord" button per Discord brand guidelines, but verify usage complies with their terms
### Already Safe:
@@ -249,13 +229,12 @@ NuGet packages in `Assets/Packages/` all include LICENSE.TXT files:
## Recommendation
Before public release:
Before removing HTTP basic auth:
1. ~~Replace clip art images (`bridge.png`, `startFire.png`) with properly licensed alternatives~~ **DONE** - see Section 4
2. ~~Verify source of `Assets/Shardok/soundEffects/` MP3s~~ **MOSTLY DONE** - 3 files flagged for replacement, rest presumed Asset Store
3. ~~Verify source of `Assets/Terrain Hexes/`~~ **DONE** - confirmed Asset Store purchase
4. ~~Verify source of `Assets/StrategyGameIcons/`~~ **DONE** - Unity Asset Store (REXARD)
5. ~~Replace Dima Koltsov Audius tracks~~ **MOSTLY DONE** - 3/5 confirmed CC BY 4.0, 2 presumed same
6. ~~Find source of Medieval: Victory Theme or replace~~ **DONE** - CC0 Public Domain by RandomMind
1. ~~Delete or replace the 4 suspicious JPG/JPEG files in `Assets/Eagle/` and `Assets/Images/`~~ **DONE** (2025-01-04)
2. Replace clip art images (`bridge.png`, `startFire.png`) with properly licensed alternatives (e.g., from [game-icons.net](https://game-icons.net) CC BY 3.0)
3. Verify source of `Assets/Shardok/soundEffects/` MP3s
4. Verify source of `Assets/Terrain Hexes/` and `Assets/StrategyGameIcons/`
5. If any are from early development with unclear licensing, replace them
The bulk of your assets (95%+) are properly licensed Asset Store purchases or CC content.
+388
View File
@@ -0,0 +1,388 @@
# Deproto Migration Plan
## Vision
**Protocol buffers should only be used at the edges** — for network serialization (gRPC) and disk persistence. Inside the Eagle game engine, all logic should operate on native Scala models.
```
┌─────────────────────────────────────────────────────────────────────┐
│ GRPC BOUNDARY │
│ EagleServiceImpl.scala ←→ Proto Messages ←→ Unity Client │
└─────────────────────────────────────────────────────────────────────┘
GameStateConverter
┌─────────────────────────────────────────────────────────────────────┐
│ SCALA ENGINE │
│ │
│ GameStateC ───→ Actions ───→ ActionResultT ───→ New GameStateC │
│ ↑ │ │
│ │ (Pure Scala models) │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ HeroC, FactionC, ProvinceC, BattalionC, ArmyC, etc. │
└─────────────────────────────────────────────────────────────────────┘
GameStateConverter
┌─────────────────────────────────────────────────────────────────────┐
│ PERSISTENCE BOUNDARY │
│ GameHistory.scala ←→ Proto Messages ←→ File/Database │
└─────────────────────────────────────────────────────────────────────┘
```
---
## Current State
### Completed Phases
| Phase | Status | Summary |
|-------|--------|---------|
| Phase 1: GameStateC | **Complete** | Scala `GameState` model with 22 fields |
| Phase 2: EngineImpl | **Complete** | Holds Scala `GameState` internally |
| Phase 3: GameHistory | **Complete** | `stateAfter` returns Scala GameState |
| Phase 4: ActionResultT | **Complete** | All 59 actions return `ActionResultT` |
| Phase 5: Action Base Classes | **Complete** | All `RandomSequentialResultsAction` and `DeterministicSingleResultAction` converted to T-type base classes |
| Phase 5b: Base Class Cleanup | **Complete** | `RandomSequentialResultsAction` and `DeterministicSingleResultAction` deleted |
| Phase 5c: RoundPhaseAdvancer Actions | **Complete** | All actions called by RoundPhaseAdvancer accept Scala GameState |
| Phase 5d: RoundPhaseAdvancer Itself | **Complete** | RoundPhaseAdvancer.checkForPhaseAdvancement takes Scala GameState |
### Phase 5c/5d Progress (Complete)
`RoundPhaseAdvancer.checkForPhaseAdvancement` now accepts Scala `GameState` and `ActionResultApplier` directly (PR #4677).
| Action | PR | Status |
|--------|-----|--------|
| `PrisonerExchangeAction` | #4670 | ✅ Merged |
| `PerformForcedTurnBackAction` | #4671 | ✅ Merged |
| `PerformHeroDeparturesAction` | #4672 | ✅ Merged |
| `RequestFreeForAllBattlesAction` | #4673 | ✅ Merged |
| `EndPlayerCommandsPhaseAction` | #4674 | ✅ Merged |
| `EndDiplomacyResolutionPhaseAction` | #4675 | ✅ Merged |
| `RoundPhaseAdvancer` itself | #4677 | ✅ Merged |
### EngineImpl Progress
| Change | PR | Status |
|--------|-----|--------|
| `recursiveTransform` deleted | #4677 | ✅ Merged |
| `recursiveTransformT` uses `RandomStateTSequencer` | #4677 | ✅ Merged |
### Current Architecture
**ActionResultT Production (100% Complete):**
- All actions produce `ActionResultT`
- Conversion to `ActionResultProto` happens via `ActionResultProtoConverter.toProto()`
- No direct `ActionResultProto` construction outside the converter
**ActionResultProto Consumption (Next Target):**
- `ActionResultProtoApplierImpl` - applies proto results to proto GameState
- `RoundPhaseAdvancer` - calls converter, passes protos to applier
- `InMemoryHistory` / `PersistedHistory` - stores proto results
- Service layer (`GameController`, `GamesManager`, etc.) - uses proto for client communication
---
## Phase 6: Migrate to ActionResultT Consumers
### Objective
Eliminate internal consumption of `ActionResultProto`. Everything inside the engine should work with `ActionResultT`.
### Current Flow (Proto-Heavy)
```
Action.execute()
→ ActionResultT
→ ActionResultProtoConverter.toProto()
→ ActionResultProto
→ ActionResultProtoApplierImpl.applyActionResults()
→ GameStateProto
→ GameStateConverter.fromProto()
→ GameStateC
```
### Target Flow (T-Types Throughout)
```
Action.execute()
→ ActionResultT
→ ActionResultApplier.applyActionResults()
→ GameStateC
(Proto conversion only at boundaries)
```
### Key Files to Convert
**Tier 1 - Core Applier:****Complete**
```
src/main/scala/net/eagle0/eagle/library/actions/applier/ActionResultApplierImpl.scala
```
`ActionResultApplier` applies `ActionResultT` directly to Scala `GameState`. The legacy `ActionResultTApplierImpl` wraps it and converts to/from proto for callers that still need proto types.
**Tier 2 - RoundPhaseAdvancer:****Complete**
```
src/main/scala/net/eagle0/eagle/library/RoundPhaseAdvancer.scala
```
Now accepts Scala `GameState` and `ActionResultApplier`. Only converts to proto lazily for `AvailableCommandsFactory` calls.
**Tier 3 - Sequencers:**
```
src/main/scala/net/eagle0/eagle/library/actions/impl/common/RandomStateTSequencer.scala
src/main/scala/net/eagle0/eagle/library/actions/impl/common/RandomStateProtoSequencer.scala
```
Modify `RandomStateTSequencer` to thread Scala `GameState` throughout (currently converts to proto internally). Then evaluate whether `RandomStateProtoSequencer` is still needed at all.
**Current State**: `RandomStateTSequencer` accepts Scala `GameState` via its `apply()` method but internally converts to proto. All callback methods (`withRandomActionResult`, `withActionResults`, etc.) pass `GameStateProto` to callers, forcing actions that use the sequencer to work with proto types internally.
**Target State**: Create a fully protoless sequencer where:
1. `lastState` returns Scala `GameState` (not `lastStateProto`)
2. All callback methods pass Scala `GameState` to callers
3. Actions using the sequencer can be fully protoless
**Migration Path**:
1. Add `lastState: GameState` method alongside `lastStateProto` (non-breaking)
2. Add parallel callback methods that pass Scala GameState (e.g., `withScalaActionResult`)
3. Migrate actions one by one to use the new Scala-based callbacks
4. Once all actions migrated, deprecate/remove proto-based callbacks
5. Remove `lastStateProto` once no longer used
**RandomStateSequencer Migration Progress** (PR #4679 introduced protoless `RandomStateSequencer`):
| Action | Status |
|--------|--------|
| `TruceTurnBackPhaseAction` | ✅ Migrated (PR #4680) |
| `EndHandleRiotsPhaseAction` | ✅ Migrated (PR #4684) |
| `PerformVassalCommandsPhaseAction` | ✅ Migrated |
| `PerformVassalDefenseDecisionsAction` | ✅ Migrated |
| `EndVassalCommandsPhaseAction` | ✅ Migrated |
| `PerformReconResolutionAction` | ✅ Migrated |
| `NewRoundAction` | ✅ Migrated (PR #4698) |
| `EndBattleAftermathPhaseAction` | ✅ Migrated (PR #4699) |
| `EndDiplomacyResolutionPhaseAction` | ✅ Migrated |
| `PerformUnaffiliatedHeroesAction` | ✅ Migrated |
| `EngineImpl.recursiveTransformT` | ✅ Migrated (PR #4704) |
| `ProtolessSequentialResultsActionWrapper` | ✅ Migrated (PR #4705) |
| `LegacyRandomStateTSequencer` | ✅ **Deleted** (PR #4705) |
**TCommandFactory Extraction** (PR #4684):
To enable lightweight mocking of command creation in tests, `TCommandFactory` trait was extracted from `CommandFactory`. This allows tests to mock just the `makeTCommand` method without pulling in all 40+ command dependencies that `CommandFactory` requires.
- `TCommandFactory` - lightweight trait with just `makeTCommand`
- `CommandFactory extends TCommandFactory` - maintains backward compatibility
- Actions accepting command factories now use `TCommandFactory` type for better testability
**Tier 4 - History APIs:**
```
src/main/scala/net/eagle0/eagle/service/InMemoryHistory.scala
src/main/scala/net/eagle0/eagle/service/PersistedHistory.scala
```
Change APIs to vend Scala `GameState` and `ActionResultT` instead of proto versions. `PersistedHistory` converts to proto internally for disk persistence; `InMemoryHistory` doesn't need proto at all.
### ActionResultProto Consumer Inventory
| File | Usage | Status |
|------|-------|--------|
| `ActionResultApplierImpl.scala` | Applies ActionResultT to Scala GameState | ✅ **Complete** |
| `ActionResultTApplierImpl.scala` | Legacy wrapper - converts to/from proto | Keep until all callers migrated |
| `RoundPhaseAdvancer.scala` | Uses Scala GameState | ✅ **Complete** |
| `RandomStateSequencer.scala` | Threads Scala GameState | ✅ **Complete** |
| `VigorXPApplier.scala` | Has both proto and Scala methods | Scala method exists, delete proto method when unused |
| `PerformForcedTurnBackAction.scala` | Fully protoless | ✅ **Complete** |
| `ResolveBattleAction.scala` | ✅ Fully protoless | Complete (PR #5048) |
| `InMemoryHistory.scala` | Stores proto results | Pending - vend Scala types |
| `PersistedHistory.scala` | Stores proto results | Pending - vend Scala types, convert for disk |
| `GameController.scala` | Uses proto for client communication | Keep proto (gRPC boundary) |
### Remaining Proto Usage in Actions
**Progress: 52 of 52 action files (100%) are fully protoless.**
All action files have been migrated to use Scala types:
| Action | Status | Notes |
|--------|--------|-------|
| `ResolveBattleAction` | ✅ Complete | PR #5048 - uses Scala GameState and ActionResultApplier |
| `PerformVassalCommandsPhaseAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
| `EndHandleRiotsPhaseAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
| `PerformVassalDefenseDecisionsAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
| `EndVassalCommandsPhaseAction` | ✅ Complete | Uses Scala types via CommandChoiceHelpers |
**Note:** `NewRoundAction` is now fully protoless after converting `ChronicleEventGenerator` to return Scala `ChronicleEvent` types directly.
**Deleted Dead Code:**
- `UnaffiliatedHeroMovedAction` - Was never called; `PerformUnaffiliatedHeroesAction.heroMovedResult` constructs `ActionResultC` directly
- `HeroBackstoryUpdateActionGenerator.fromGameState` - Dead method that converted proto to Scala; only `apply(GameState)` is used
**Note**: `PerformReconResolutionAction` and `EndBattleAftermathPhaseAction` are now fully protoless after:
1. Migrating `FactionT.reconnedProvinces` and `ChangedFactionC.updatedReconnedProvinces` to use Scala `ProvinceView`
2. Adding Scala overload of `ProvinceViewFilter.withdrawnFromProvinceView`
### Estimated Effort (Remaining)
| Component | Lines | Complexity | Blocks |
|-----------|-------|------------|--------|
| History API updates | ~100 | Low | - |
| **Total Remaining** | **~100** | | |
**Completed:**
-`ChronicleEventGenerator` converted to return Scala `ChronicleEvent` types directly
-`CommandChoiceHelpers` migrated to Scala types
-`ResolveBattleAction` refactored to use Scala GameState and ActionResultApplier (PR #5048)
### Enum Type Migrations
Proto enums are being converted to Scala sealed traits with converters at boundaries:
| Enum | Scala Type | Status | Notes |
|------|------------|--------|-------|
| `DiplomacyOfferStatus` | `Status` sealed trait | ✅ **Complete** | PR #5093 - `EligibleDiplomacyStatuses` uses Scala types internally |
| `RoundPhase` | `RoundPhase` sealed trait | Partial | Some usages converted |
| `BattalionType` | `BattalionType` sealed trait | Partial | Some usages converted |
**DiplomacyOfferStatus Migration (PR #5093):**
- `EligibleDiplomacyStatuses.scala` now returns `Vector[Status]` instead of `Vector[DiplomacyOfferStatus]`
- Call sites in `AvailableResolve*CommandFactory` files convert to proto via `StatusConverter.toProto` at the boundary
- This pattern should be applied to other proto enums
### CommandChoiceHelpers Migration Status
Several command selectors have already been converted to use Scala types:
| File | Status | Notes |
|------|--------|-------|
| `AttackCommandChooser.scala` | ✅ **Protoless** | Uses Scala `GameState`, `HeroT`, `ProvinceT` |
| `AlmsCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `HeroT`, `ProvinceT` |
| `FoodConsumptionUtils.scala` | ✅ **Protoless** | Uses Scala `GameState`, `ProvinceT`, `RoundPhase` |
| `MarchSuppliesHelpers.scala` | ✅ **Protoless** | Uses `BattalionT` |
| `CombatUnitSelector.scala` | ✅ **Protoless** | Uses `HeroT`, `BattalionT`, `BattalionType` |
| `ExpandCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `ProvinceT`, `FactionT` |
| `ImproveCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `ProvinceT`, `HeroT` |
| `OrganizeCommandSelector.scala` | ✅ **Protoless** | Uses Scala `GameState`, `BattalionT`, `BattalionType` |
| `RansomOfferHelpers.scala` | ✅ **Protoless** | Uses Scala `GameState`, `FactionT` |
| `CommandChoiceHelpers.scala` | ✅ **Protoless** | Uses Scala `GameState` throughout |
| `ProvinceGoldSurplusCalculator.scala` | ✅ **Protoless** | Uses Scala types |
**All CommandChoiceHelpers selectors have been migrated to Scala types.**
### Progress Summary
| Metric | Value |
|--------|-------|
| Action files fully protoless | 52 / 52 (100%) ✅ |
| Proto usages in remaining actions | 0 |
| Next target | See "Next Candidates" section below |
### Next Candidates
Priority candidates for further deproto work:
1. **More Enum Migrations** - Apply the `DiplomacyOfferStatus` pattern to other proto enums:
- Files importing `net.eagle0.eagle.common.round_phase.RoundPhase` (proto) could use Scala `RoundPhase`
- Files importing `net.eagle0.eagle.common.battalion_type.BattalionType` (proto) could use Scala `BattalionType`
2. **AvailableCommandsFactory Files** - Many still use proto `GameState` internally:
- These files build proto `AvailableCommand` messages but could use Scala types for internal logic
- Convert to accept Scala `GameState`, only convert fields to proto when building the response
3. **History APIs** - `InMemoryHistory` and `PersistedHistory`:
- Change to vend Scala `GameState` and `ActionResultT` instead of proto versions
- `PersistedHistory` converts to proto internally for disk persistence
### Validation
- [x] `ActionResultApplier` created and tested
- [x] `RandomStateSequencer` threads Scala GameState throughout
- [x] `RoundPhaseAdvancer` uses T-types internally
- [x] `ProvinceViewFilter` has Scala overload for server-side use (PR #4752)
- [x] `FactionT.reconnedProvinces` and `ChangedFactionC.updatedReconnedProvinces` use Scala `ProvinceView`
- [x] `ProvinceViewFilter.withdrawnFromProvinceView` has Scala overload
- [x] `CommandChoiceHelpers` uses Scala types ✅
- [x] All action files (52/52) are fully protoless ✅
- [ ] `ProvinceViewFilter` faction-filtered views use Scala types
- [ ] History APIs vend Scala types
- [ ] No `ActionResultProtoConverter.toProto()` calls except at persistence/gRPC boundaries
- [ ] All tests pass
---
## Phase 7: Clean Up Legacy Utilities
### Objective
Remove remaining direct proto imports from utility classes.
### Files to Modify
| File | Status |
|------|--------|
| `CommandChoiceHelpers.scala` | Accepts proto `GameState`; blocks full deproto of `PerformVassalCommandsPhaseAction` and `PerformVassalDefenseDecisionsAction` |
| `LegacyProvinceUtils.scala` | Replace with `ProvinceUtils.scala` - `hasImminentRiot` added (PR #4683) |
| `LegacyFactionUtils.scala` | Replace proto imports with `FactionT` |
| `LegacyUnaffiliatedHeroUtils.scala` | Replace proto imports with Scala models |
| `BattalionTypeLoader.scala` | Keep proto for file loading, convert immediately after |
| `BeastUtils.scala` | **Complete** - now uses Scala `BeastInfo` only |
### View Filters (Partially Complete)
The view filter utilities now have Scala overloads for server-side use:
| File | Status | Notes |
|------|--------|-------|
| `ProvinceViewFilter.scala` | **Partial** | `filteredProvinceView(ProvinceT, ScalaGameState)` added (PR #4752) |
| `ArmyFilter.scala` | **Partial** | `filterArmy(ScalaArmy, Map[BattalionId, BattalionT], Option[FactionId])` added |
| `BattalionViewFilter.scala` | **Complete** | Uses Scala `BattalionT` throughout |
| `GameStateViewFilter.scala` | Pending | Uses proto types throughout |
| `GameStateViewDiffer.scala` | Pending | Works with view protos |
**Unblocked Actions** (PR #4752):
- `EndBattleAftermathPhaseAction` - can now use `filteredProvinceView(province, scalaGameState)`
- `PerformReconResolutionAction` - can now use Scala overload
- `GameStateFactionExtensions` - can now use `updatedReconnedProvinces` with Scala types
**Remaining Work**:
- Faction-filtered `filteredProvinceView(Province, GameState, FactionId)` still uses proto types
- `withdrawnFromProvinceView` still uses proto types
- These are needed for client-facing views with visibility restrictions
---
## Phase 8: Verify Boundaries
### Objective
Confirm protos are used correctly at boundaries — and ONLY there.
### Expected Proto Usage (Keep)
- `EagleServiceImpl.scala` - gRPC boundary
- `InMemoryHistory.scala` / `PersistedHistory.scala` - Persistence boundary
- `*Converter.scala` - Explicit conversion utilities
- `*Loader.scala` - File loading utilities
### Expected No Proto Usage (Verify)
- `/library/actions/impl/` - Pure Scala models
- `/library/util/` - Pure Scala models (except loaders)
- `/model/state/` - Pure Scala models
---
## Open Questions
1. **Persistence Format**: Currently game state is persisted as proto. Should we keep proto for persistence (good for schema evolution) or switch to a different format?
2. **Shardok Integration**: `ResolveBattleAction` communicates with Shardok. Should the Shardok interface use protos (external service) or Scala models?
3. **View Generation**: `GameStateViewDiffer` works with view protos for client updates. Views need Scala models (`ProvinceViewT`, etc.) to allow actions like `EndBattleAftermathPhaseAction` to be fully protoless. The Scala views would be converted to proto only at the gRPC boundary when sending updates to clients.
---
## Success Criteria
### Code Quality
- [ ] Zero proto imports in `/library/actions/` (except boundaries)
- [ ] Zero proto imports in `/library/` utilities (except loaders)
- [ ] `GameStateT` used throughout engine internals
- [ ] Proto usage limited to: `EagleServiceImpl`, loaders, converters, persistence
### Architecture
- [ ] Clear separation: Scala models (internal) vs Proto (boundaries)
- [ ] Converters as the only bridge between domains
- [ ] No "proto creep" into business logic
-94
View File
@@ -1,94 +0,0 @@
# LLM Model Comparison
This document compares streaming latency (time-to-first-token) and pricing across OpenAI, Anthropic (Claude), and Google (Gemini) models for use in Eagle's narrative text generation.
## Test Methodology
All tests were performed locally using curl with streaming enabled. Each model was tested 3 times with the same prompt:
> "Write a short paragraph about a brave knight who discovers a hidden cave. Make it vivid and descriptive."
Time-to-first-token (TTFT) was measured from request initiation to the first text content appearing in the stream.
## Streaming Latency Results (January 2026)
| Model | Run 1 | Run 2 | Run 3 | Average TTFT |
|-------|-------|-------|-------|--------------|
| **Gemini 2.5 Flash-Lite** | 0.76s | 0.54s | 0.53s | **~0.6s** |
| **gpt-4.1-mini** | 1.65s | 1.72s | 1.68s | **~1.7s** |
| **claude-3-5-haiku** | 1.85s | 1.92s | 1.88s | **~1.9s** |
| gpt-5.2 | 3.25s | 3.38s | 3.32s | **~3.3s** |
| gpt-5-mini | 2.52s | 5.82s | 3.12s | **~3.8s** (high variance) |
| Gemini 3 Flash Preview | 4.11s | 4.77s | 4.28s | **~4.4s** |
| claude-sonnet-4 | 4.89s | 5.12s | 4.98s | **~5.0s** |
| Gemini 2.5 Flash | 5.60s | 7.00s | 7.79s | **~6.8s** |
## Pricing Comparison (per 1M tokens)
| Model | Input Price | Output Price | Notes |
|-------|-------------|--------------|-------|
| **Gemini 2.5 Flash-Lite** | $0.10 | $0.40 | Cheapest and fastest |
| Gemini 2.5 Flash | $0.15 | $0.60 | |
| gpt-5-mini | $0.25 | $2.00 | |
| **gpt-4.1-mini** | $0.40 | $1.60 | Best OpenAI value |
| Gemini 3 Flash Preview | $0.50 | $3.00 | Includes thinking tokens |
| **claude-3-5-haiku** | $0.80 | $4.00 | Best Anthropic value |
| gpt-5.2 | ~$1.00 | ~$10.00 | Full reasoning model |
| Gemini 2.5 Pro | $1.25 | $10.00 | |
| Gemini 3 Pro Preview | $2.00 | $12.00 | ≤200K context |
| claude-sonnet-4 | $3.00 | $15.00 | |
## Recommendations
### For Narrative Text Generation (Default)
**Gemini 2.5 Flash-Lite** is recommended as the default:
- Fastest TTFT (~0.6s) - nearly 3x faster than alternatives
- Cheapest pricing ($0.10/$0.40 per 1M tokens)
- Quality is acceptable for short narrative snippets
### Alternative Options
| Priority | Model | When to Use |
|----------|-------|-------------|
| Speed + Cost | Gemini 2.5 Flash-Lite | Default for most use cases |
| Speed + Quality | gpt-4.1-mini | When you need OpenAI quality with good speed |
| Instruction Following | claude-3-5-haiku | Complex multi-step prompts, consistent tone |
| Maximum Quality | claude-sonnet-4 or gpt-5.2 | When output quality is paramount |
### Quality Trade-offs
For short narrative snippets (1-3 paragraphs):
- **Flash-Lite vs Haiku/4.1-mini**: Minor quality difference, significant speed gain
- **Haiku vs Sonnet**: Noticeable quality difference in creative writing variety
- **gpt-4.1-mini vs gpt-5.2**: Moderate quality difference, significant cost savings
## Configuration
LLM settings can be changed at runtime via the admin console:
1. Navigate to Admin Console → Settings
2. Change `LlmProvider` to select vendor (gemini, openai, claude)
3. Change the corresponding model name setting:
- `GeminiModelName` (default: gemini-2.5-flash-lite)
- `OpenAiModelName` (default: gpt-4.1-mini)
- `ClaudeModelName` (default: claude-3-5-haiku-20241022)
Changes take effect on the next LLM request.
## Environment Variables
For production deployment, ensure API keys are set:
```bash
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
GEMINI_API_KEY=AIza...
```
## Notes
- **gpt-5-mini** showed high latency variance (2.5s - 5.8s) in testing
- **Gemini 2.5 Flash** was surprisingly slower than Flash-Lite, possibly due to internal reasoning overhead
- **Gemini 3 Flash** is a frontier model with better quality but higher latency than 2.5 Flash-Lite
- All Gemini models have a generous free tier (up to 1,000 daily requests)
-78
View File
@@ -1,78 +0,0 @@
# The Small Eagle TODO
## Goals
Be able to support a small (10-50 user) private alpha, including with strangers.
Larger set of goals in [The Big Eagle TODO](https://docs.google.com/document/d/17RTt3-4Wl2AAVMRLodaC3a84E4de6xuQTWBPvCRM484/edit?pli=1&tab=t.0), but most of that is not necessary for MVP.
## Required
### Gameplay Productionization
- [x] ~~All functionality works on production eagle / shardok servers~~
- [x] ~~Acceptable latency in all regions~~
- [x] ~~Shardok performance similar to QA~~
- [x] ~~Error logging & alerting~~
- [x] ~~Fix long disconnects on deployments~~
- [ ] Fix the Mac installer
- [x] ~~Still not reconnecting after deployments~~
- [ ] Notify about client updates, button to come directly back
- [ ] Generatedtext healing
- [ ] Kill outstanding shardok requests when game is deleted
### Other Productionization
- [x] ~~Oauth sign-in~~
- [x] ~~Add Google, others?~~
- [ ] User management
- [x] ~~Invite codes~~
- [ ] Link accounts
- [x] ~~Choose display name~~
- [x] ~~Just do account setup from the landing page?~~
- [x] ~~Download client directly from DO, avoid basic auth/my home network~~
- [ ] "Message of the day"
- [ ] Support plan
### Alpha Tester Support
- [ ] Feedback channel (Discord server? Bug report form?)
- [ ] Crash reporting from Unity client
- [ ] Known issues doc (so testers don't report the same things)
### IP / Legal
- [ ] Document and make available licenses for art & music
- [ ] Required open source disclosures
- [ ] Audit assets for anything we don't have rights to and replace it
- [ ] Replace heroes that are based on real 20th or 21st century people or IP
- [ ] Privacy policy (collecting accounts, OAuth data, gameplay data)
- [ ] Terms of service (basic liability protection)
- [ ] Data deletion capability (user requests account removal)
### Basic Gameplay
- [ ] Tutorial
- [ ] In the Your Warlord panel, say what the profession is
- [x] ~~And separate panels for each profession when you encounter one~~
- [x] ~~Command tutorial for each command the first time it's clicked~~
- [x] ~~Time to recruit / expand~~
- [x] ~~And how expansion works~~
- [ ] Province events
- [ ] Running low on food
- [x] ~~Time to swear brotherhood~~
- [x] ~~When you get large, or~~
- [x] ~~When you get a good candidate~~
- [ ] Shardok tutorial!
- [ ] Basic Shardok AI stuff fixed
- [ ] Lobby fixes
- [ ] Have goals / ending
- [ ] First-session onboarding (beyond mechanics tutorial)
- [ ] Narrative hook in first few minutes - why should I care about my warlord?
- [ ] Clear first-session goal ("try to capture your first province" or similar)
- [ ] Early small victory to build momentum
- [ ] Guided first scenario vs. overwhelming sandbox?
## Nice to have
<!-- Add nice-to-have items here as they come up -->
-568
View File
@@ -1,568 +0,0 @@
# Sparkle Delta Updates Implementation Plan
## Overview
This document outlines the implementation plan for adding delta update support to the Eagle0 macOS auto-update system using Sparkle's BinaryDelta feature.
### Current State
- Full DMG downloads (~200MB) for every update
- `mac_build_handler.go` creates DMG, signs it, uploads to S3, updates appcast.xml
- Keeps last 10 versions in appcast, deletes older DMGs
- Users must download full app even for small changes
### Goals
- Reduce update download size from ~200MB to ~10-30MB (85% reduction)
- Maintain backward compatibility with full DMG downloads
- Automatic fallback for users who are many versions behind
## Sparkle Delta Update Architecture
Sparkle supports binary delta updates through the `<sparkle:deltas>` element in the appcast. When a user updates, Sparkle:
1. Checks if a delta patch exists from their current version to the new version
2. If found, downloads the smaller delta patch instead of the full DMG
3. Applies the patch locally to create the new app version
4. Falls back to full DMG if no matching delta exists
### Appcast XML Structure with Deltas
```xml
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
<channel>
<title>Eagle0</title>
<link>https://assets.eagle0.net/mac/appcast.xml</link>
<description>Eagle0 game updates</description>
<language>en</language>
<item>
<title>Version 1.0.9615</title>
<pubDate>Sun, 19 Jan 2026 12:00:00 -0800</pubDate>
<sparkle:version>9615</sparkle:version>
<sparkle:shortVersionString>1.0.9615</sparkle:shortVersionString>
<enclosure
url="https://assets.eagle0.net/mac/builds/eagle0-1.0.9615.dmg"
length="200000000"
type="application/octet-stream"
sparkle:edSignature="..." />
<sparkle:deltas>
<enclosure
url="https://assets.eagle0.net/mac/deltas/9614-9615.delta"
sparkle:deltaFrom="9614"
length="15000000"
type="application/octet-stream"
sparkle:edSignature="..." />
<enclosure
url="https://assets.eagle0.net/mac/deltas/9613-9615.delta"
sparkle:deltaFrom="9613"
length="18000000"
type="application/octet-stream"
sparkle:edSignature="..." />
<enclosure
url="https://assets.eagle0.net/mac/deltas/9612-9615.delta"
sparkle:deltaFrom="9612"
length="22000000"
type="application/octet-stream"
sparkle:edSignature="..." />
</sparkle:deltas>
</item>
<!-- older versions... -->
</channel>
</rss>
```
## Implementation Plan
### Phase 1: Add S3 Utility Functions
**File:** `src/main/go/net/eagle0/util/aws/bucket_basics.go`
Add two new functions to support delta generation:
```go
// ListObjectsWithPrefix returns all object keys matching the given prefix
func (bb BucketBasics) ListObjectsWithPrefix(bucket, prefix string) ([]string, error) {
var keys []string
paginator := s3.NewListObjectsV2Paginator(bb.S3Client, &s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
Prefix: aws.String(prefix),
})
for paginator.HasMorePages() {
page, err := paginator.NextPage(context.TODO())
if err != nil {
return nil, err
}
for _, obj := range page.Contents {
keys = append(keys, *obj.Key)
}
}
return keys, nil
}
// DownloadFile downloads an object to a local file path
func (bb BucketBasics) DownloadFile(bucket, key, localPath string) error {
result, err := bb.S3Client.GetObject(context.TODO(), &s3.GetObjectInput{
Bucket: aws.String(bucket),
Key: aws.String(key),
})
if err != nil {
return err
}
defer result.Body.Close()
file, err := os.Create(localPath)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(file, result.Body)
return err
}
```
### Phase 2: Store App Bundles for Delta Generation
**File:** `src/main/go/net/eagle0/build/mac_build_handler/mac_build_handler.go`
Add storage paths:
```go
var appsRoot = "mac/apps/" // Zipped app bundles for delta generation
var deltasRoot = "mac/deltas/" // Delta patches
```
After DMG creation, upload the zipped app bundle:
```go
func uploadAppBundle(bb aws.BucketBasics, appPath string, buildNumber string) error {
appZipPath := filepath.Join("/tmp", fmt.Sprintf("eagle0-%s.app.zip", buildNumber))
// Create zip of app bundle using ditto (preserves metadata)
cmd := exec.Command("ditto", "-c", "-k", "--keepParent", appPath, appZipPath)
if output, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("failed to zip app: %s: %w", string(output), err)
}
defer os.Remove(appZipPath)
// Upload to S3
remotePath := appsRoot + fmt.Sprintf("eagle0-%s.app.zip", buildNumber)
log.Printf("Uploading app bundle to S3: %s", remotePath)
return bb.UploadFilePublic(bucketName, remotePath, appZipPath)
}
```
### Phase 3: Add Delta XML Structures
**File:** `src/main/go/net/eagle0/build/mac_build_handler/mac_build_handler.go`
Add new structs for delta representation:
```go
// Delta represents a delta patch from a previous version
type Delta struct {
XMLName xml.Name `xml:"enclosure"`
URL string `xml:"url,attr"`
DeltaFrom string `xml:"sparkle:deltaFrom,attr"`
Length int64 `xml:"length,attr"`
Type string `xml:"type,attr"`
EdSig string `xml:"sparkle:edSignature,attr"`
}
// Deltas wraps the sparkle:deltas element
type Deltas struct {
XMLName xml.Name `xml:"sparkle:deltas"`
Items []Delta `xml:"enclosure"`
}
// Update Item struct to include Deltas
type Item struct {
Title string `xml:"title"`
PubDate string `xml:"pubDate"`
SparkleVersion string `xml:"sparkle:version"`
SparkleShortVersion string `xml:"sparkle:shortVersionString"`
Description string `xml:"description,omitempty"`
Enclosure Enclosure `xml:"enclosure"`
Deltas *Deltas `xml:"sparkle:deltas,omitempty"`
}
```
### Phase 4: Generate Delta Patches
**File:** `src/main/go/net/eagle0/build/mac_build_handler/mac_build_handler.go`
```go
// Maximum number of versions to generate deltas from
const maxDeltaVersions = 5
// generateDeltas creates delta patches from previous versions to the new version
func generateDeltas(bb aws.BucketBasics, newBuildNumber string, newAppPath string, privateKeyPath string) ([]Delta, error) {
var deltas []Delta
// Ensure BinaryDelta tool is available
binaryDeltaPath, err := ensureBinaryDelta()
if err != nil {
return nil, fmt.Errorf("failed to get BinaryDelta: %w", err)
}
// List available app bundles
appKeys, err := bb.ListObjectsWithPrefix(bucketName, appsRoot+"eagle0-")
if err != nil {
log.Printf("Warning: failed to list app bundles: %v", err)
return deltas, nil // Continue without deltas
}
// Parse build numbers from keys and sort descending
var buildNumbers []string
for _, key := range appKeys {
// Extract build number from "mac/apps/eagle0-9614.app.zip"
base := filepath.Base(key)
if strings.HasPrefix(base, "eagle0-") && strings.HasSuffix(base, ".app.zip") {
bn := strings.TrimSuffix(strings.TrimPrefix(base, "eagle0-"), ".app.zip")
if bn != newBuildNumber {
buildNumbers = append(buildNumbers, bn)
}
}
}
// Sort descending (most recent first) and limit to maxDeltaVersions
sort.Sort(sort.Reverse(sort.StringSlice(buildNumbers)))
if len(buildNumbers) > maxDeltaVersions {
buildNumbers = buildNumbers[:maxDeltaVersions]
}
// Generate delta from each previous version
for _, oldBuild := range buildNumbers {
delta, err := generateSingleDelta(bb, binaryDeltaPath, oldBuild, newBuildNumber, newAppPath, privateKeyPath)
if err != nil {
log.Printf("Warning: failed to generate delta from %s: %v", oldBuild, err)
continue // Skip this delta but continue with others
}
deltas = append(deltas, delta)
}
return deltas, nil
}
func generateSingleDelta(bb aws.BucketBasics, binaryDeltaPath, oldBuild, newBuild, newAppPath, privateKeyPath string) (Delta, error) {
// Download old app bundle
oldAppZipKey := appsRoot + fmt.Sprintf("eagle0-%s.app.zip", oldBuild)
oldAppZipLocal := filepath.Join("/tmp", fmt.Sprintf("eagle0-%s.app.zip", oldBuild))
defer os.Remove(oldAppZipLocal)
if err := bb.DownloadFile(bucketName, oldAppZipKey, oldAppZipLocal); err != nil {
return Delta{}, fmt.Errorf("failed to download old app: %w", err)
}
// Unzip old app
oldAppDir := filepath.Join("/tmp", fmt.Sprintf("old-app-%s", oldBuild))
defer os.RemoveAll(oldAppDir)
cmd := exec.Command("ditto", "-x", "-k", oldAppZipLocal, oldAppDir)
if output, err := cmd.CombinedOutput(); err != nil {
return Delta{}, fmt.Errorf("failed to unzip old app: %s: %w", string(output), err)
}
oldAppPath := filepath.Join(oldAppDir, "eagle0.app")
// Generate delta
deltaPath := filepath.Join("/tmp", fmt.Sprintf("%s-%s.delta", oldBuild, newBuild))
defer os.Remove(deltaPath)
cmd = exec.Command(binaryDeltaPath, "create", oldAppPath, newAppPath, deltaPath)
if output, err := cmd.CombinedOutput(); err != nil {
return Delta{}, fmt.Errorf("failed to create delta: %s: %w", string(output), err)
}
// Get delta size
deltaSize, err := getFileSize(deltaPath)
if err != nil {
return Delta{}, fmt.Errorf("failed to get delta size: %w", err)
}
log.Printf("Delta %s->%s size: %d bytes (%.1f MB)", oldBuild, newBuild, deltaSize, float64(deltaSize)/1024/1024)
// Sign delta
signature, err := signWithSparkle(deltaPath, privateKeyPath)
if err != nil {
return Delta{}, fmt.Errorf("failed to sign delta: %w", err)
}
// Upload delta
deltaKey := deltasRoot + fmt.Sprintf("%s-%s.delta", oldBuild, newBuild)
if err := bb.UploadFilePublic(bucketName, deltaKey, deltaPath); err != nil {
return Delta{}, fmt.Errorf("failed to upload delta: %w", err)
}
deltaURL := fmt.Sprintf("https://assets.eagle0.net/%s", deltaKey)
return Delta{
URL: deltaURL,
DeltaFrom: oldBuild,
Length: deltaSize,
Type: "application/octet-stream",
EdSig: signature,
}, nil
}
func ensureBinaryDelta() (string, error) {
binaryDeltaPath := "/tmp/sparkle-cache/Sparkle-2.6.4/bin/BinaryDelta"
if _, err := os.Stat(binaryDeltaPath); os.IsNotExist(err) {
log.Println("Sparkle BinaryDelta not found, downloading...")
cmd := exec.Command("bash", "-c", `
mkdir -p /tmp/sparkle-cache
curl -sL https://github.com/sparkle-project/Sparkle/releases/download/2.6.4/Sparkle-2.6.4.tar.xz | tar -xJ -C /tmp/sparkle-cache
`)
if output, err := cmd.CombinedOutput(); err != nil {
return "", fmt.Errorf("failed to download Sparkle: %s: %w", string(output), err)
}
}
return binaryDeltaPath, nil
}
```
### Phase 5: Update Main Deploy Flow
**File:** `src/main/go/net/eagle0/build/mac_build_handler/mac_build_handler.go`
Modify `main()` to integrate delta generation:
```go
func main() {
// ... existing argument parsing ...
// Create DMG (existing)
if err := createDMG(appPath, dmgPath, "Eagle0"); err != nil {
log.Fatalf("Failed to create DMG: %v", err)
}
// ... existing DMG upload ...
if privateKeyPath != "" {
// Upload app bundle for future delta generation (NEW)
log.Println("Uploading app bundle for delta generation...")
if err := uploadAppBundle(bb, appPath, buildNumber); err != nil {
log.Printf("Warning: failed to upload app bundle: %v", err)
// Continue - delta generation is optional
}
// Generate deltas from previous versions (NEW)
log.Println("Generating delta patches...")
deltas, err := generateDeltas(bb, buildNumber, appPath, privateKeyPath)
if err != nil {
log.Printf("Warning: failed to generate deltas: %v", err)
} else {
log.Printf("Generated %d delta patches", len(deltas))
}
// Update appcast with deltas
log.Println("Updating appcast.xml...")
appcast, err := fetchAppcast(bb)
if err != nil {
log.Fatalf("Failed to fetch appcast: %v", err)
}
// Create new item with deltas
newItem := Item{
Title: fmt.Sprintf("Version %s", version),
PubDate: time.Now().Format(time.RFC1123Z),
SparkleVersion: buildNumber,
SparkleShortVersion: version,
Description: "",
Enclosure: Enclosure{
URL: downloadURL,
Length: fileSize,
Type: "application/octet-stream",
EdSig: signature,
},
}
// Add deltas if any were generated
if len(deltas) > 0 {
newItem.Deltas = &Deltas{Items: deltas}
}
// ... rest of appcast handling ...
}
}
```
### Phase 6: Cleanup Old Artifacts
**File:** `src/main/go/net/eagle0/build/mac_build_handler/mac_build_handler.go`
When pruning old versions from appcast, also delete associated artifacts:
```go
// In the appcast pruning section, after removing old items:
if len(appcast.Channel.Items) > 10 {
oldItems := appcast.Channel.Items[10:]
for _, item := range oldItems {
oldBuild := item.SparkleVersion
// Delete old DMG (existing)
dmgKey := strings.TrimPrefix(item.Enclosure.URL, "https://assets.eagle0.net/")
log.Printf("Deleting old build: %s", dmgKey)
bb.DeleteObject(bucketName, dmgKey)
// Delete old app bundle (NEW)
appKey := appsRoot + fmt.Sprintf("eagle0-%s.app.zip", oldBuild)
log.Printf("Deleting old app bundle: %s", appKey)
bb.DeleteObject(bucketName, appKey)
// Delete deltas TO this version (NEW)
deltaKeys, _ := bb.ListObjectsWithPrefix(bucketName, deltasRoot)
for _, key := range deltaKeys {
if strings.HasSuffix(key, fmt.Sprintf("-%s.delta", oldBuild)) {
log.Printf("Deleting old delta: %s", key)
bb.DeleteObject(bucketName, key)
}
}
}
appcast.Channel.Items = appcast.Channel.Items[:10]
}
```
## S3 Storage Structure
After implementation, the S3 bucket will have this structure:
```
eagle0-windows/
├── mac/
│ ├── appcast.xml # Update feed with delta info
│ ├── builds/ # Full DMG downloads
│ │ ├── eagle0-1.0.9620.dmg
│ │ ├── eagle0-1.0.9619.dmg
│ │ ├── ...
│ │ └── eagle0-latest.dmg # Symlink to latest
│ ├── apps/ # Zipped app bundles (NEW)
│ │ ├── eagle0-9620.app.zip
│ │ ├── eagle0-9619.app.zip
│ │ ├── eagle0-9618.app.zip
│ │ ├── eagle0-9617.app.zip
│ │ └── eagle0-9616.app.zip # Keep last 5 for delta gen
│ └── deltas/ # Delta patches (NEW)
│ ├── 9619-9620.delta
│ ├── 9618-9620.delta
│ ├── 9617-9620.delta
│ ├── 9616-9620.delta
│ ├── 9615-9620.delta
│ ├── 9618-9619.delta
│ ├── 9617-9619.delta
│ └── ...
```
## Storage Impact Analysis
### Current Storage (without deltas)
- 10 DMGs × 200MB = **~2GB**
### Estimated Storage (with deltas)
- 10 DMGs × 200MB = 2GB
- 5 app bundles × 150MB = 0.75GB (zip compression)
- ~25 delta files × 20MB avg = 0.5GB
- **Total: ~3.25GB**
### Trade-offs
- **+1.25GB storage** (~60% increase)
- **-170MB per user update** (~85% bandwidth savings)
- Break-even: ~8 user updates to recoup storage cost
## Bandwidth Savings
| Scenario | Without Deltas | With Deltas | Savings |
|----------|---------------|-------------|---------|
| 1 version behind | 200MB | ~15MB | 92% |
| 2 versions behind | 200MB | ~20MB | 90% |
| 3 versions behind | 200MB | ~25MB | 87% |
| 5 versions behind | 200MB | ~35MB | 82% |
| 6+ versions behind | 200MB | 200MB (full) | 0% |
## Migration Strategy
The implementation is backward-compatible and requires no changes to existing clients:
1. **First deploy after implementation:**
- Stores app bundle for the first time
- No deltas generated (no previous app bundles exist)
- Appcast has no `<sparkle:deltas>` element
2. **Second deploy:**
- Generates delta from previous version
- Appcast now has `<sparkle:deltas>` with one entry
- Users on previous version get delta update
3. **Subsequent deploys:**
- Generate deltas from last 5 versions
- Users within 5 versions get delta updates
- Users more than 5 versions behind get full DMG
4. **Client behavior:**
- Sparkle automatically checks for matching delta
- Falls back to full DMG if no delta matches
- No client code changes required
## Error Handling
The implementation handles failures gracefully:
1. **S3 list/download fails:** Skip delta generation, use full DMG
2. **BinaryDelta fails for one version:** Log warning, continue with other versions
3. **Signing fails:** Skip that delta, continue with others
4. **Upload fails:** Skip that delta, continue with others
The deploy never fails due to delta issues - deltas are optional enhancements.
## Verification Plan
### Manual Testing
1. **Deploy version N:**
- Verify app bundle uploaded to `mac/apps/eagle0-N.app.zip`
- Verify appcast has no deltas (first deploy)
2. **Deploy version N+1:**
- Verify delta generated at `mac/deltas/N-(N+1).delta`
- Verify appcast contains `<sparkle:deltas>` element
- Verify delta signature is valid
3. **Test update from N to N+1:**
- Install version N manually
- Check for updates
- Monitor download size in Player.log (should be ~15-30MB, not 200MB)
- Verify app updated successfully
4. **Test fresh install:**
- Download latest DMG directly
- Verify installation works normally
5. **Test fallback scenario:**
- Install a version more than 5 versions behind
- Update should download full DMG
### Automated Verification
Add to CI workflow (optional):
```yaml
- name: Verify delta generation
run: |
# Check app bundle exists
aws s3 ls s3://eagle0-windows/mac/apps/ | grep eagle0-${BUILD_NUMBER}.app.zip
# Check deltas exist (after second deploy)
aws s3 ls s3://eagle0-windows/mac/deltas/ | head -5
# Verify appcast has deltas
curl -s https://assets.eagle0.net/mac/appcast.xml | grep "sparkle:deltas"
```
## Security Considerations
1. **All deltas are EdDSA signed:** Same signature verification as full DMG
2. **BinaryDelta is Sparkle's official tool:** Well-audited, production-ready
3. **App bundles in S3 are public:** Same as DMGs, no additional exposure
4. **Cleanup removes old artifacts:** No indefinite storage of old versions
## Future Enhancements
1. **Parallel delta generation:** Generate multiple deltas concurrently
2. **Delta size threshold:** Skip uploading deltas larger than X% of full DMG
3. **Delta metrics:** Track delta download rates vs full DMG
4. **Configurable delta count:** Allow adjusting how many versions to keep
-181
View File
@@ -1,181 +0,0 @@
# Tutorial Content Guide
This document defines all tutorial content. Edit this to refine the text, then update `TutorialContentDefinitions.cs` to match.
---
## Onboarding Sequence
Shown to first-time players. Guides them through the basics of strategic and tactical gameplay.
| Step | ID | Display | Trigger | Title | Description |
|------|-----|---------|---------|-------|-------------|
| 1 | `welcome` | Modal | Auto (game start) | Welcome to Eagle0 | Command your armies, recruit heroes, and conquer provinces in this turn-based strategy game.<br><br>Let's walk through the basics! |
| 2 | `select_province` | Overlay | Completes on: `province_selected` | The Strategic Map | This is your kingdom. Each colored region is a province.<br><br>Tap a province you control (shown in your color) to see what you can do there. |
| 3 | `province_panel` | Modal | Button click | Province Information | This panel shows province details: its name, terrain, any armies present, and the commands available to you.<br><br>Commands let you move troops, recruit heroes, and more. |
| 4 | `try_march` | Overlay | Completes on: `command_issued` | Issue a Command | Try issuing a March command to move your army to an adjacent province.<br><br>Select a destination and confirm the order. |
| 5 | `turn_cycle` | Modal | Button click | The Turn Cycle | Eagle0 uses simultaneous turns. All players give orders at the same time, then turns resolve together.<br><br>When all players are ready, the server processes everyone's commands and shows the results. |
| 6 | `wait_for_battle` | Hidden | Completes on: `first_battle_available` | *(none)* | *(Invisible step - waits for a battle to become available)* |
| 7 | `battle_intro` | Modal | Button click | Battle Time! | When armies collide, you'll fight tactical battles on a hex grid.<br><br>You command individual units - infantry, cavalry, archers, and heroes with special abilities. |
| 8 | `enter_battle` | Overlay | Completes on: `battle_entered` | Enter the Battle | Tap the Battle button to enter tactical combat. |
| 9 | `tactical_overview` | Modal | Button click | Tactical Combat | Each unit has movement points and attack power. Position your troops wisely!<br><br>Units attack adjacent enemies. Flanking (attacking from multiple sides) deals bonus damage. |
| 10 | `move_unit` | Overlay | Completes on: `battle_action` | Move Your Units | Tap one of your units to select it, then tap a highlighted hex to move there.<br><br>Blue hexes show where you can move. |
| 11 | `attack_enemy` | Overlay | Completes on: `battle_action` | Attack! | Move next to an enemy unit, then tap the enemy to attack.<br><br>Red highlights show valid attack targets. |
| 12 | `end_turn` | Overlay | Completes on: `turn_ended` | End Your Turn | When you've moved all units or want to pass, tap End Turn.<br><br>The enemy will then take their turn. |
| 13 | `complete` | Modal | Button click (no skip) | You're Ready! | You now know the basics of Eagle0!<br><br>Explore diplomacy, recruit powerful heroes, and conquer the realm. Good luck, commander! |
### Notes on Onboarding Flow
- Steps 1-5 cover strategic gameplay
- Step 6 is invisible - just waits for a battle
- Steps 7-12 cover tactical combat
- Step 13 celebrates completion
**Questions to consider:**
- Should we skip tactical tutorial if player skips to first battle themselves?
- Should there be a "skip all" option visible from step 1?
- Is the step order correct for typical first-game flow?
---
## Strategic Contextual Tutorials
Triggered when players encounter features for the first time.
### Diplomacy Introduction
| Field | Value |
|-------|-------|
| ID | `diplomacy_intro` |
| Trigger | `diplomacy_available` (diplomacy commands appear) |
| Display | Modal |
| Title | Diplomacy |
| Description | You can negotiate with other factions!<br><br>Offer alliances, declare war, or propose tribute. Your diplomatic choices shape the realm. |
### Hero Recruitment
| Field | Value |
|-------|-------|
| ID | `hero_recruitment` |
| Trigger | `hero_recruitment_available` (free heroes detected) |
| Display | Modal |
| Title | Heroes Available |
| Description | Free heroes wander the realm seeking a lord to serve.<br><br>Recruit them to lead your armies! Heroes have unique abilities and grow stronger with experience. |
### Weather Control
| Field | Value |
|-------|-------|
| ID | `weather_control` |
| Trigger | `weather_control_available` (weather command appears) |
| Display | Overlay |
| Title | Weather Magic |
| Description | Your mages can influence the weather!<br><br>Rain slows movement, storms disrupt enemies, and clear skies speed your march. |
### Prisoner Management
| Field | Value |
|-------|-------|
| ID | `prisoner_management` |
| Trigger | `prisoner_command_issued` (player uses prisoner command) |
| Display | Modal |
| Title | Prisoners Captured |
| Description | You've captured enemy soldiers!<br><br>You can ransom them for gold, recruit them into your army, or execute them as a warning. |
---
## Tactical Contextual Tutorials
Triggered during battles when players encounter spells, terrain, or abilities.
### Lightning Bolt Spell
| Field | Value |
|-------|-------|
| ID | `spell_lightning` |
| Trigger | `spell_lightning_available` |
| Display | Tooltip |
| Title | Lightning Bolt |
| Description | Your mage can cast Lightning Bolt!<br><br>This spell strikes a single target for heavy damage. Great for eliminating key enemy units. |
### Meteor Strike Spell
| Field | Value |
|-------|-------|
| ID | `spell_meteor` |
| Trigger | `spell_meteor_available` |
| Display | Modal |
| Title | Meteor Strike |
| Description | Meteor is a devastating area spell!<br><br>It takes a turn to cast: first select target, then it lands next turn. Plan ahead! |
### Holy Wave Spell
| Field | Value |
|-------|-------|
| ID | `spell_holywave` |
| Trigger | `spell_holywave_available` |
| Display | Tooltip |
| Title | Holy Wave |
| Description | Holy Wave heals your units and damages undead!<br><br>Position your troops carefully to maximize its effect. |
### Raise Dead Spell
| Field | Value |
|-------|-------|
| ID | `spell_raisedead` |
| Trigger | `spell_raisedead_available` |
| Display | Modal |
| Title | Raise Dead |
| Description | Dark magic can raise fallen soldiers as undead!<br><br>They fight for you, but beware - they may crumble if your necromancer falls. |
### Fire Terrain
| Field | Value |
|-------|-------|
| ID | `terrain_fire` |
| Trigger | `terrain_fire_encountered` (fire damage occurs) |
| Display | Tooltip |
| Title | Fire Hazard |
| Description | Fire spreads across the battlefield!<br><br>Units in burning hexes take damage. Use fire to block enemy routes or avoid it yourself. |
### Water Crossing
| Field | Value |
|-------|-------|
| ID | `terrain_water` |
| Trigger | `terrain_water_encountered` (water crossing attempted) |
| Display | Tooltip |
| Title | Water Crossing |
| Description | Units can cross shallow water, but it's risky.<br><br>Crossing takes extra movement and may fail. Some units swim better than others. |
### Cavalry Charge
| Field | Value |
|-------|-------|
| ID | `ability_charge` |
| Trigger | `ability_charge_available` |
| Display | Overlay |
| Title | Cavalry Charge |
| Description | Your cavalry can Charge!<br><br>Charging deals bonus damage based on distance traveled. Use open terrain for maximum impact. |
---
## Display Modes
| Mode | Description | Use For |
|------|-------------|---------|
| **Modal** | Full popup with dimmed background, blocks interaction | Important concepts, multi-paragraph explanations |
| **Overlay** | Semi-transparent overlay, can highlight UI elements | Guiding player to interact with specific UI |
| **Tooltip** | Small popup near target element | Quick tips, less important info |
| **Hint** | Pulsing dot indicator only | Subtle suggestions |
| **None** | Invisible, just waits for event | Transition steps |
---
## Adding New Tutorials
1. Add entry to this document
2. Update `TutorialContentDefinitions.cs`:
- For onboarding: add to `CreateOnboardingSequence()`
- For contextual: add to `RegisterStrategicTutorials()` or `RegisterTacticalTutorials()`
3. Ensure trigger event exists in `TutorialTriggerRegistry.cs`
4. Test the flow
---
## Content Guidelines
- Keep descriptions to 2-3 short paragraphs max
- Use `<br><br>` for paragraph breaks (renders as newlines in Unity)
- Avoid jargon - explain game terms when first introduced
- Be encouraging, not condescending
- Focus on "what to do" not exhaustive "how it works"
-48
View File
@@ -1,48 +0,0 @@
# LLVM MinGW toolchain for Windows cross-compilation
# Provides x86_64-w64-mingw32 target compiler and libraries
package(default_visibility = ["//visibility:public"])
filegroup(
name = "all_files",
srcs = glob(["**/*"]),
)
# Compiler binaries
filegroup(
name = "compiler_files",
srcs = glob([
"bin/x86_64-w64-mingw32-*",
"bin/clang*",
"bin/llvm-*",
"bin/lld*",
]),
)
# Windows x86_64 sysroot (headers and libraries)
filegroup(
name = "windows_x86_64_sysroot",
srcs = glob([
"x86_64-w64-mingw32/**/*",
"generic-w64-mingw32/include/**/*",
]),
)
# All library files needed for linking
filegroup(
name = "linker_files",
srcs = glob([
"bin/x86_64-w64-mingw32-*",
"bin/lld*",
"bin/ld.lld*",
"lib/**/*",
"x86_64-w64-mingw32/lib/**/*",
]),
)
# The main C compiler wrapper script path for CGO
# CGO needs CC to point to the cross-compiler
exports_files([
"bin/x86_64-w64-mingw32-clang",
"bin/x86_64-w64-mingw32-clang++",
])
-8
View File
@@ -1,8 +0,0 @@
load("@build_bazel_rules_apple//apple:apple.bzl", "apple_dynamic_framework_import")
# Import pre-built Sparkle framework
apple_dynamic_framework_import(
name = "Sparkle",
framework_imports = glob(["Sparkle.framework/**"]),
visibility = ["//visibility:public"],
)
-2
View File
@@ -11,8 +11,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
golang.org/x/sys v0.28.0
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.36.3
)
-4
View File
@@ -41,10 +41,6 @@ github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 h1:VQpB2SpK88C6B5lPHTuSZKb2Qee1QWwiFlC5CKY4AW0=
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk=
golang.org/x/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=
+11 -29
View File
@@ -3,9 +3,6 @@ events {
}
http {
# Allow large request bodies for game uploads (default is 1MB)
client_max_body_size 50M;
# Logging
log_format grpc_json escape=json '{'
'"time":"$time_iso8601",'
@@ -27,12 +24,12 @@ http {
# This prevents stale IP caching when containers restart
resolver 127.0.0.11 valid=10s ipv6=off;
# Eagle backend - blue-green deployment with variable-based routing
# Uses a variable so nginx only resolves the configured backend (not all backends).
# This allows nginx to start/reload even when the inactive backend is stopped.
# The deploy script updates this map, then recreates nginx.
map $host $eagle_backend {
default "eagle-blue:40032";
# Upstream for Eagle gRPC server
# For blue-green deployments, this is switched between eagle-blue:40032 and eagle-green:40032
# by scripts/deploy-blue-green.sh, then nginx is reloaded with 'nginx -s reload'
upstream eagle_grpc {
server eagle-blue:40032;
keepalive 100;
}
# HTTP server for Let's Encrypt challenge and redirect
@@ -76,8 +73,8 @@ http {
# Rate limiting
limit_req zone=grpc_limit burst=50 nodelay;
# gRPC proxy - uses variable for blue-green deployment
grpc_pass grpc://$eagle_backend;
# gRPC proxy
grpc_pass grpc://eagle_grpc;
# Timeouts for long-running streams
grpc_read_timeout 1200s;
@@ -88,14 +85,13 @@ http {
error_page 502 = /error502grpc;
}
# gRPC proxy for Auth service (routes to Go auth service, not Eagle)
# gRPC proxy for Auth service
location /net.eagle0.eagle.api.auth.Auth {
# Rate limiting
limit_req zone=grpc_limit burst=50 nodelay;
# Route to auth service directly (not through Eagle)
set $auth_backend "auth:40033";
grpc_pass grpc://$auth_backend;
# gRPC proxy
grpc_pass grpc://eagle_grpc;
# Timeouts
grpc_read_timeout 30s;
@@ -112,20 +108,6 @@ http {
proxy_set_header X-Real-IP $remote_addr;
}
# Apple OAuth callback (Apple uses POST with form_post response mode)
location /oauth/apple/callback {
proxy_pass http://auth:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Steam OAuth callback (Steam uses OpenID 2.0)
location /oauth/steam/callback {
proxy_pass http://auth:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Invitation landing page (proxied to Go auth service)
location /invite/ {
proxy_pass http://auth:8080;
-3
View File
@@ -8,6 +8,3 @@ ZIP_LOCATION=$(bazel cquery --config=mactools --output=files @net_eagle0_unity_g
/usr/bin/unzip -o $ZIP_LOCATION -d src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/
/usr/bin/plutil -convert xml1 src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/DarwinGodiceBundle.bundle/Contents/Info.plist
/bin/echo "building sparkle plugin"
./scripts/build_sparkle_plugin.sh
-30
View File
@@ -1,30 +0,0 @@
#!/usr/bin/env bash
#
# Build the SparklePlugin native library for Unity using Bazel
#
# Usage: build_sparkle_plugin.sh [output_dir]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
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 ==="
bazel build --config=mactools //src/main/objc/net/eagle0/sparkle:SparklePlugin
# Get the zip path from bazel
ZIP_PATH=$(bazel cquery --config=mactools --output=files //src/main/objc/net/eagle0/sparkle:SparklePlugin 2>/dev/null)
echo "=== Extracting SparklePlugin.bundle ==="
mkdir -p "$OUTPUT_DIR"
rm -rf "$OUTPUT_DIR/SparklePlugin.bundle"
unzip -o "$ZIP_PATH" -d "$OUTPUT_DIR/"
# Convert Info.plist from binary to XML format (Unity requires XML)
/usr/bin/plutil -convert xml1 "$OUTPUT_DIR/SparklePlugin.bundle/Contents/Info.plist"
echo "=== SparklePlugin built successfully ==="
ls -la "$OUTPUT_DIR/SparklePlugin.bundle/"
-129
View File
@@ -1,129 +0,0 @@
#!/bin/bash
# Check BUILD.bazel dependency constraints
# This script enforces architectural boundaries in the codebase.
#
# Usage:
# ./scripts/check_build_deps.sh # Check all rules
# ./scripts/check_build_deps.sh --ci # CI mode (fail on any violation)
# ./scripts/check_build_deps.sh --count # Just count current violations (for tracking progress)
# ./scripts/check_build_deps.sh --strict # Same as --ci (strict enforcement)
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
cd "$REPO_ROOT"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
MODE="${1:-check}"
EXIT_CODE=0
# Rule 1: src/main should not depend on src/test
check_main_depends_on_test() {
echo -e "${YELLOW}Checking: src/main should not depend on src/test...${NC}"
violations=$(bazel query 'deps(//src/main/...) intersect //src/test/...' 2>/dev/null || true)
if [ -n "$violations" ]; then
echo -e "${RED}VIOLATION: src/main depends on src/test:${NC}"
echo "$violations"
return 1
else
echo -e "${GREEN}✓ No violations${NC}"
return 0
fi
}
# Rule 2: library/ should not depend on Scala proto types
# C++/Go proto deps are allowed (they're build-time deps for map generation tools)
check_library_depends_on_scala_proto() {
echo -e "${YELLOW}Checking: library/ should not depend on Scala proto types...${NC}"
violations=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | grep "_scala_proto" || true)
if [ -z "$violations" ]; then
count=0
else
count=$(echo "$violations" | grep -c "^//" || true)
fi
if [ "$count" -gt 0 ]; then
echo -e "${RED}VIOLATION: Found $count Scala proto dependencies in library/:${NC}"
echo "$violations"
return 1
else
echo -e "${GREEN}✓ No Scala proto dependencies in library/${NC}"
return 0
fi
}
# Rule 3: library/ should not depend on proto_converters
# Proto conversions should happen at service boundaries, not in library code
check_library_depends_on_proto_converters() {
echo -e "${YELLOW}Checking: library/ should not depend on proto_converters...${NC}"
violations=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/scala/net/eagle0/eagle/model/proto_converters/...' 2>/dev/null | grep "^//" || true)
if [ -n "$violations" ]; then
count=$(echo "$violations" | wc -l | tr -d ' ')
echo -e "${RED}VIOLATION: library/ depends on $count proto_converters targets:${NC}"
echo "$violations"
echo ""
echo "Proto conversions should happen at service boundaries (ShardokInterfaceGrpcClient,"
echo "EagleServiceImpl, etc.), not in library code."
return 1
else
echo -e "${GREEN}✓ No proto_converters dependencies in library/${NC}"
return 0
fi
}
# Count proto deps for informational purposes
count_proto_deps() {
echo -e "${YELLOW}=== Proto dependency counts ===${NC}"
scala_proto_results=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | grep "_scala_proto" || true)
if [ -z "$scala_proto_results" ]; then
scala_proto_count=0
else
scala_proto_count=$(echo "$scala_proto_results" | wc -l | tr -d ' ')
fi
echo "library/ Scala proto deps: $scala_proto_count"
# C++/Go proto deps are expected (map generation tools)
all_proto_count=$(bazel query 'deps(//src/main/scala/net/eagle0/eagle/library/...) intersect //src/main/protobuf/...' 2>/dev/null | wc -l | tr -d ' ')
echo "library/ all proto deps (includes C++/Go build tools): $all_proto_count"
}
echo "=== BUILD.bazel Dependency Check ==="
echo ""
case "$MODE" in
--count)
count_proto_deps
;;
--ci|--strict)
check_main_depends_on_test || EXIT_CODE=1
check_library_depends_on_scala_proto || EXIT_CODE=1
check_library_depends_on_proto_converters || EXIT_CODE=1
;;
*)
check_main_depends_on_test || EXIT_CODE=1
check_library_depends_on_scala_proto || EXIT_CODE=1
check_library_depends_on_proto_converters || EXIT_CODE=1
echo ""
count_proto_deps
;;
esac
echo ""
if [ $EXIT_CODE -eq 0 ]; then
echo -e "${GREEN}=== All checks passed ===${NC}"
else
echo -e "${RED}=== Some checks failed ===${NC}"
fi
exit $EXIT_CODE
+6 -25
View File
@@ -39,51 +39,32 @@ find "$APP_PATH" -name "*.bundle" -print0 | while IFS= read -r -d '' item; do
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign XPC services (but skip ones inside Sparkle.framework - they're already signed)
# Sign XPC services (inside Sparkle framework)
find "$APP_PATH" -name "*.xpc" -print0 | while IFS= read -r -d '' item; do
if [[ "$item" == *"Sparkle.framework"* ]]; then
echo "Skipping Sparkle XPC service (pre-signed): $item"
continue
fi
echo "Signing XPC service: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign nested apps (but skip ones inside Sparkle.framework - they're already signed)
# Sign nested apps (like Sparkle's Updater.app)
find "$APP_PATH" -path "*/Frameworks/*.app" -print0 | while IFS= read -r -d '' item; do
if [[ "$item" == *"Sparkle.framework"* ]]; then
echo "Skipping Sparkle nested app (pre-signed): $item"
continue
fi
echo "Signing nested app: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign standalone executables inside frameworks (but skip Sparkle.framework internals)
# Sign standalone executables inside frameworks (like Autoupdate)
find "$APP_PATH" -path "*/Frameworks/*/Versions/*/Autoupdate" -type f -print0 | while IFS= read -r -d '' item; do
if [[ "$item" == *"Sparkle.framework"* ]]; then
echo "Skipping Sparkle executable (pre-signed): $item"
continue
fi
echo "Signing executable: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
done
# Sign all frameworks (after their contents are signed)
# Use --deep for Sparkle.framework to handle its XPC services
find "$APP_PATH" -name "*.framework" -print0 | while IFS= read -r -d '' item; do
if [[ "$item" == *"Sparkle.framework" ]]; then
echo "Signing Sparkle framework with --deep: $item"
codesign --deep --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
else
echo "Signing framework: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
fi
echo "Signing framework: $item"
codesign --force --verify --verbose --timestamp --options runtime \
--sign "$SIGNING_IDENTITY" "$item"
done
echo "=== Signing main app bundle ==="
+56 -199
View File
@@ -2,21 +2,14 @@
#
# Blue-Green Deployment Script for Eagle Server
#
# This script performs a zero-downtime deployment with state consistency:
# 1. Create .deployment_in_progress marker (signals deployment started)
# 2. Start the staging instance (green) with new image
# 3. Run warmup/smoke tests against staging (warms JIT)
# 4. Switch nginx to staging (zero downtime - users immediately route to staging)
# 5. Stop the active instance (blue) - blocks until flush completes
# 6. Create .flush_complete marker (signals disk state is fresh)
#
# The flush marker coordination ensures green never serves stale game data:
# - When users reconnect to green and trigger lazy-load, the code checks for markers
# - If .deployment_in_progress exists, lazy-load WAITS for .flush_complete
# - Once blue's flush completes and marker is created, lazy-load proceeds with fresh data
#
# Key insight: nginx switches to green BEFORE blue stops, achieving zero downtime.
# Users who trigger lazy-load during blue's shutdown will wait for the flush marker.
# This script performs a zero-downtime deployment by:
# 1. Starting the new version on a staging port (green)
# 2. Waiting for it to become healthy
# 3. Running warmup traffic to pre-heat the JIT
# 4. Stopping the old version (blue) - which flushes state to disk
# 5. Telling green to reload games from disk
# 6. Switching nginx to route traffic to green
# 7. Cleaning up
#
# Usage: ./deploy-blue-green.sh [NEW_IMAGE_TAG]
#
@@ -32,10 +25,6 @@ APP_DIR="${APP_DIR:-/opt/eagle0}"
NGINX_CONF="${APP_DIR}/nginx/nginx.conf"
COMPOSE_FILE="${APP_DIR}/docker-compose.prod.yml"
WARMUP_SCRIPT="${SCRIPT_DIR}/warmup-eagle.sh"
SAVES_DIR="${APP_DIR}/saves"
FLUSH_MARKER="${SAVES_DIR}/.flush_complete"
DEPLOYMENT_IN_PROGRESS="${SAVES_DIR}/.deployment_in_progress"
ACTIVE_INSTANCE_FILE="${APP_DIR}/.active-instance"
# Colors for output
RED='\033[0;31m'
@@ -47,57 +36,15 @@ log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Marker file operations use docker exec because saves directory is owned by root (Docker).
# We run commands inside a container that has the saves directory mounted.
create_deployment_marker() {
local deploy_id=$1
local container=$2 # Container to use for file operations
docker exec "${container}" rm -f /app/saves/.flush_complete
docker exec "${container}" sh -c "echo '${deploy_id}' > /app/saves/.deployment_in_progress"
}
create_flush_marker() {
local deploy_id=$1
local container=$2 # Container to use for file operations
docker exec "${container}" sh -c "echo '${deploy_id}' > /app/saves/.flush_complete"
docker exec "${container}" rm -f /app/saves/.deployment_in_progress
}
cleanup_markers_on_failure() {
local container=$1 # Container to use for file operations
docker exec "${container}" rm -f /app/saves/.deployment_in_progress 2>/dev/null || true
docker exec "${container}" touch /app/saves/.flush_complete 2>/dev/null || true
}
remove_stale_deployment_marker() {
# Try any running eagle container
local container
container=$(docker ps --filter "name=eagle-" --format "{{.Names}}" | head -1)
if [ -n "${container}" ]; then
docker exec "${container}" rm -f /app/saves/.deployment_in_progress 2>/dev/null || true
fi
}
# Determine which instance is currently running (not from nginx config)
get_running_instance() {
local blue_running green_running
blue_running=$(docker inspect --format='{{.State.Running}}' eagle-blue 2>/dev/null || echo "false")
green_running=$(docker inspect --format='{{.State.Running}}' eagle-green 2>/dev/null || echo "false")
if [ "$blue_running" = "true" ] && [ "$green_running" = "true" ]; then
# Both running - use nginx config to determine primary
if grep -q "server eagle-blue:40032;" "${NGINX_CONF}" | head -1 | grep -qv backup; then
echo "blue"
else
echo "green"
fi
elif [ "$blue_running" = "true" ]; then
# Determine which instance is currently active
get_active_instance() {
if grep -q "eagle-blue:40032" "${NGINX_CONF}"; then
echo "blue"
elif [ "$green_running" = "true" ]; then
elif grep -q "eagle-green:40032" "${NGINX_CONF}"; then
echo "green"
else
# Neither running - default to blue (first deploy or recovery)
echo "none"
log_error "Cannot determine active instance from nginx config"
exit 1
fi
}
@@ -107,12 +54,6 @@ pull_with_retry() {
local max_attempts=${2:-3}
local attempt=1
# Skip pull if image already exists locally (e.g., CI already pulled it)
if docker image inspect "${image}" &>/dev/null; then
log_info "Image ${image} already exists locally, skipping pull"
return 0
fi
# Use crane if available (handles OCI format correctly)
if [ -x "${APP_DIR}/crane" ]; then
while [ $attempt -le $max_attempts ]; do
@@ -173,63 +114,46 @@ main() {
local registry="registry.digitalocean.com/eagle0/eagle-server"
local new_image="${registry}:${new_tag}"
# Generate deployment ID for log correlation with server logs
local deploy_id
deploy_id=$(date +%s)
local deploy_start_time=$deploy_id
log_info "========================================="
log_info "Starting blue-green deployment"
log_info "Deployment ID: ${deploy_id}"
log_info "New image: ${new_image}"
log_info "========================================="
cd "${APP_DIR}"
# Determine current active instance (need this before creating marker)
local active=$(get_running_instance)
# Determine current active instance
local active=$(get_active_instance)
local staging
if [ "$active" = "blue" ] || [ "$active" = "none" ]; then
if [ "$active" = "blue" ]; then
staging="green"
active="blue" # Normalize "none" to "blue" for first deploy
else
staging="blue"
fi
# Step 1: Signal deployment in progress
log_info "[DEPLOY:${deploy_id}] Step 1: Signaling deployment in progress..."
# Use active container for marker operations (it's the one currently running)
if [ "$active" != "none" ] && docker ps --filter "name=eagle-${active}" --format "{{.Names}}" | grep -q .; then
create_deployment_marker "${deploy_id}" "eagle-${active}"
log_info "[DEPLOY:${deploy_id}] Deployment marker created via eagle-${active}"
else
log_warn "[DEPLOY:${deploy_id}] No running container to create marker (first deploy?)"
fi
log_info "Active instance: eagle-${active}"
log_info "Staging instance: eagle-${staging}"
# Pull the new image (with retry for intermittent registry issues)
if ! pull_with_retry "${new_image}" 3; then
log_error "Failed to pull new image, aborting deployment"
cleanup_markers_on_failure "eagle-${active}"
exit 1
fi
# Step 2: Start staging instance with new image
log_info "Step 2: Starting eagle-${staging} with new image..."
# Start staging instance with new image
log_info "Starting eagle-${staging} with new image..."
if [ "$staging" = "green" ]; then
EAGLE_IMAGE_NEW="${new_image}" docker compose -f "${COMPOSE_FILE}" --profile blue-green up -d eagle-green jfr-sidecar-green
EAGLE_IMAGE_NEW="${new_image}" docker compose -f "${COMPOSE_FILE}" --profile blue-green up -d eagle-green
else
EAGLE_IMAGE="${new_image}" docker compose -f "${COMPOSE_FILE}" up -d eagle-blue jfr-sidecar
EAGLE_IMAGE="${new_image}" docker compose -f "${COMPOSE_FILE}" up -d eagle-blue
fi
# Wait for staging to be healthy
if ! wait_for_healthy "eagle-${staging}" 90; then
log_error "Staging instance failed health check, aborting deployment"
docker compose -f "${COMPOSE_FILE}" stop "eagle-${staging}"
cleanup_markers_on_failure "eagle-${active}"
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${staging}"
exit 1
fi
# Step 3: Run warmup/smoke test
# Run warmup/smoke test
local staging_port
if [ "$staging" = "green" ]; then
staging_port=40034
@@ -237,12 +161,12 @@ main() {
staging_port=40032
fi
log_info "Step 3: Running warmup against eagle-${staging}..."
log_info "Running warmup against eagle-${staging}..."
if [ -x "${WARMUP_SCRIPT}" ]; then
if ! "${WARMUP_SCRIPT}" "localhost:${staging_port}"; then
log_error "Warmup/smoke test failed, aborting deployment"
docker compose -f "${COMPOSE_FILE}" stop "eagle-${staging}"
cleanup_markers_on_failure "eagle-${active}"
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${staging}"
exit 1
fi
else
@@ -250,104 +174,49 @@ main() {
log_warn "JIT will be cold on first requests"
fi
# Step 4: Switch nginx to staging BEFORE stopping active
# This achieves zero downtime - users immediately route to staging.
# Any lazy-loads will wait for the flush marker (created in step 6).
local nginx_switch_start
nginx_switch_start=$(date +%s)
log_info "[DEPLOY:${deploy_id}] Step 4: Switching nginx to eagle-${staging}..."
# Stop the active instance (this flushes state to disk)
log_info "Stopping eagle-${active} (flushing state to disk)..."
docker compose -f "${COMPOSE_FILE}" stop "eagle-${active}"
# Update nginx config (variable-based routing)
# Tell staging to reload games from disk
log_info "Telling eagle-${staging} to reload games from disk..."
if command -v grpcurl &> /dev/null; then
grpcurl -plaintext -d '{}' "localhost:${staging_port}" net.eagle0.eagle.api.Eagle/ReloadGames || true
else
log_warn "grpcurl not installed, skipping game reload"
log_warn "New instance will use games loaded at startup"
fi
# Switch nginx upstream
log_info "Switching nginx upstream to eagle-${staging}..."
if [ "$staging" = "green" ]; then
sed -i.bak 's/eagle-blue:40032/eagle-green:40032/g' "${NGINX_CONF}"
else
sed -i.bak 's/eagle-green:40032/eagle-blue:40032/g' "${NGINX_CONF}"
fi
# Recreate nginx to pick up new config
# Use --no-deps to prevent cascading to auth (which has secrets not available here)
docker compose -f "${COMPOSE_FILE}" up -d --force-recreate --no-deps nginx
# Verify nginx picked up the correct config
local nginx_backend
nginx_backend=$(docker exec nginx grep -o 'eagle-[a-z]*:40032' /etc/nginx/nginx.conf | head -1 || echo "unknown")
if [ "$nginx_backend" = "eagle-${staging}:40032" ]; then
log_info "[DEPLOY:${deploy_id}] Verified: nginx routing to eagle-${staging}"
else
log_error "[DEPLOY:${deploy_id}] nginx config mismatch! Expected eagle-${staging}:40032, got ${nginx_backend}"
exit 1
fi
log_info "[DEPLOY:${deploy_id}] Traffic switched to eagle-${staging} (lazy-loads will wait for flush marker)"
local nginx_switch_end
nginx_switch_end=$(date +%s)
# Step 5: Stop active instance (blocks until exit, ensuring flush completes)
# Users may be lazy-loading on staging during this time - they'll wait for the marker.
local flush_start
flush_start=$(date +%s)
log_info "[DEPLOY:${deploy_id}] Step 5: Stopping eagle-${active} (waiting for flush)..."
docker compose -f "${COMPOSE_FILE}" stop "eagle-${active}"
local flush_end
flush_end=$(date +%s)
local flush_duration=$((flush_end - flush_start))
log_info "[DEPLOY:${deploy_id}] eagle-${active} stopped, flush completed in ${flush_duration}s"
# Step 6: Create flush marker - signals that disk state is fresh
# Any waiting lazy-loads on staging will now proceed with fresh data.
# The Eagle server automatically detects the flush marker update and invalidates any stale cached games.
log_info "[DEPLOY:${deploy_id}] Step 6: Creating flush marker..."
create_flush_marker "${deploy_id}" "eagle-${staging}"
log_info "[DEPLOY:${deploy_id}] Flush marker created - server will auto-invalidate stale cache"
# Write active instance file for eagle-exec helper
echo "eagle-${staging}" > "${ACTIVE_INSTANCE_FILE}"
log_info "[DEPLOY:${deploy_id}] Active instance file updated: eagle-${staging}"
# Update .env for admin service
local env_file="${APP_DIR}/.env"
if [ "$staging" = "green" ]; then
log_info "Updating .env for green instance..."
sed -i.bak '/^EAGLE_ADDR=/d; /^JFR_SIDECAR_ADDR=/d' "${env_file}" 2>/dev/null || true
echo "EAGLE_ADDR=eagle-green:40032" >> "${env_file}"
echo "JFR_SIDECAR_ADDR=jfr-sidecar-green:8081" >> "${env_file}"
else
log_info "Updating .env for blue instance..."
sed -i.bak '/^EAGLE_ADDR=/d; /^JFR_SIDECAR_ADDR=/d' "${env_file}" 2>/dev/null || true
echo "EAGLE_ADDR=eagle-blue:40032" >> "${env_file}"
echo "JFR_SIDECAR_ADDR=jfr-sidecar:8081" >> "${env_file}"
fi
# Restart admin to pick up new .env
# Use --no-deps to prevent cascading to auth (which has secrets not available here)
log_info "Restarting admin service..."
docker compose -f "${COMPOSE_FILE}" up -d --force-recreate --no-deps admin
# Reload nginx
log_info "Reloading nginx..."
docker compose -f "${COMPOSE_FILE}" exec nginx nginx -s reload
# Clean up old instance
log_info "Cleaning up old eagle-${active}..."
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${active}" 2>/dev/null || true
log_info "Removing old eagle-${active} container..."
docker compose -f "${COMPOSE_FILE}" rm -f "eagle-${active}"
# Stop the old jfr-sidecar (it can't attach to removed container anyway)
if [ "$active" = "green" ]; then
docker compose -f "${COMPOSE_FILE}" stop "jfr-sidecar-green" 2>/dev/null || true
docker compose -f "${COMPOSE_FILE}" rm -f "jfr-sidecar-green" 2>/dev/null || true
# Update the staging instance's restart policy and image var
# For blue, we need to update EAGLE_IMAGE; for green, update EAGLE_IMAGE_NEW
if [ "$staging" = "blue" ]; then
log_info "Updating EAGLE_IMAGE to ${new_image} for future restarts"
# User should update their .env file
else
docker compose -f "${COMPOSE_FILE}" stop "jfr-sidecar" 2>/dev/null || true
docker compose -f "${COMPOSE_FILE}" rm -f "jfr-sidecar" 2>/dev/null || true
log_info "Green is now active. Consider switching to blue on next deployment."
fi
local deploy_end_time
deploy_end_time=$(date +%s)
local total_duration=$((deploy_end_time - deploy_start_time))
local user_wait_window=$((flush_end - nginx_switch_end))
log_info "Deployment complete!"
log_info "Active instance: eagle-${staging}"
log_info ""
log_info "========================================="
log_info "[DEPLOY:${deploy_id}] Deployment complete!"
log_info " Active instance: eagle-${staging}"
log_info " Total duration: ${total_duration}s"
log_info " Flush duration: ${flush_duration}s"
log_info " Max user wait window: ${user_wait_window}s"
log_info "========================================="
log_info "Note: Update your .env file with EAGLE_IMAGE=${new_image}"
log_info " if you want future 'docker compose up' to use this version."
}
# Check for required tools
@@ -371,18 +240,6 @@ check_requirements() {
log_error "docker-compose file not found at ${COMPOSE_FILE}"
exit 1
fi
# Ensure saves directory exists
if [ ! -d "${SAVES_DIR}" ]; then
log_info "Creating saves directory at ${SAVES_DIR}"
mkdir -p "${SAVES_DIR}"
fi
# Clean up any stale deployment-in-progress marker from a previous failed deploy
if [ -f "${DEPLOYMENT_IN_PROGRESS}" ]; then
log_warn "Found stale deployment-in-progress marker, removing it"
remove_stale_deployment_marker
fi
}
# Run
-46
View File
@@ -1,46 +0,0 @@
#!/bin/bash
#
# Helper to run docker exec against the active Eagle instance.
# Reads the active instance from /opt/eagle0/.active-instance (set by deploy-blue-green.sh).
#
# Usage:
# eagle-exec printenv GEMINI_API_KEY
# eagle-exec jcmd 1 VM.flags
# eagle-exec sh # Get a shell
#
# To create an alias, add to ~/.bashrc:
# alias eagle-exec='/opt/eagle0/scripts/eagle-exec.sh'
#
set -euo pipefail
APP_DIR="${APP_DIR:-/opt/eagle0}"
ACTIVE_FILE="${APP_DIR}/.active-instance"
# Read active instance from file, with fallback
if [ -f "$ACTIVE_FILE" ]; then
ACTIVE=$(cat "$ACTIVE_FILE")
else
# Fallback: check which container is actually running
if docker inspect --format='{{.State.Running}}' eagle-blue 2>/dev/null | grep -q true; then
ACTIVE="eagle-blue"
elif docker inspect --format='{{.State.Running}}' eagle-green 2>/dev/null | grep -q true; then
ACTIVE="eagle-green"
else
ACTIVE=""
fi
fi
if [ -z "$ACTIVE" ]; then
echo "Error: No active Eagle instance found" >&2
exit 1
fi
if [ $# -eq 0 ]; then
echo "Active instance: $ACTIVE"
echo "Usage: $0 <command> [args...]"
echo "Example: $0 printenv GEMINI_API_KEY"
exit 0
fi
exec docker exec "$ACTIVE" "$@"
-44
View File
@@ -1,44 +0,0 @@
#!/bin/bash
#
# Helper to tail logs from the active Eagle instance.
# Reads the active instance from /opt/eagle0/.active-instance (set by deploy-blue-green.sh).
#
# Usage:
# eagle-logs # Tail logs (follow mode)
# eagle-logs -n 100 # Show last 100 lines and follow
# eagle-logs --no-follow -n 50 # Show last 50 lines without following
#
# To create an alias, add to ~/.bashrc:
# alias eagle-logs='/opt/eagle0/scripts/eagle-logs.sh'
#
set -euo pipefail
APP_DIR="${APP_DIR:-/opt/eagle0}"
ACTIVE_FILE="${APP_DIR}/.active-instance"
# Read active instance from file, with fallback
if [ -f "$ACTIVE_FILE" ]; then
ACTIVE=$(cat "$ACTIVE_FILE")
else
# Fallback: check which container is actually running
if docker inspect --format='{{.State.Running}}' eagle-blue 2>/dev/null | grep -q true; then
ACTIVE="eagle-blue"
elif docker inspect --format='{{.State.Running}}' eagle-green 2>/dev/null | grep -q true; then
ACTIVE="eagle-green"
else
ACTIVE=""
fi
fi
if [ -z "$ACTIVE" ]; then
echo "Error: No active Eagle instance found" >&2
exit 1
fi
# Default to follow mode if no args provided
if [ $# -eq 0 ]; then
exec docker logs -f "$ACTIVE"
else
exec docker logs "$@" "$ACTIVE"
fi
-41
View File
@@ -1,41 +0,0 @@
// +build ignore
// Script to generate Ed25519 key pair for manifest signing.
// Run with: go run scripts/generate_manifest_keys.go
//
// This will output:
// - Private key (base64): Store as MANIFEST_SIGNING_KEY GitHub secret
// - Public key (base64): Embed in EagleInstaller for verification
package main
import (
"crypto/ed25519"
"crypto/rand"
"encoding/base64"
"fmt"
"log"
)
func main() {
// Generate Ed25519 key pair
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
log.Fatalf("Failed to generate key pair: %v", err)
}
// Encode keys as base64
privateKeyB64 := base64.StdEncoding.EncodeToString(privateKey)
publicKeyB64 := base64.StdEncoding.EncodeToString(publicKey)
fmt.Println("=== Ed25519 Key Pair for Manifest Signing ===")
fmt.Println()
fmt.Println("PRIVATE KEY (store as GitHub secret MANIFEST_SIGNING_KEY):")
fmt.Println(privateKeyB64)
fmt.Println()
fmt.Println("PUBLIC KEY (embed in EagleInstaller.cs for verification):")
fmt.Println(publicKeyB64)
fmt.Println()
fmt.Printf("Private key size: %d bytes\n", len(privateKey))
fmt.Printf("Public key size: %d bytes\n", len(publicKey))
}
+20 -47
View File
@@ -27,59 +27,33 @@ if [ -z "${SPARKLE_EDDSA_PUBLIC_KEY:-}" ]; then
exit 1
fi
# Always use a fresh Sparkle download to avoid cache corruption issues
# Download Sparkle if not cached
SPARKLE_DIR="$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION"
echo "=== Clearing Sparkle cache and downloading fresh copy ==="
rm -rf "$SPARKLE_DIR"
mkdir -p "$SPARKLE_DIR"
SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
echo "Downloading from: $SPARKLE_URL"
curl -L "$SPARKLE_URL" -o /tmp/sparkle.tar.xz
tar -xJf /tmp/sparkle.tar.xz -C "$SPARKLE_DIR"
rm /tmp/sparkle.tar.xz
# Show what was extracted
echo "=== Extracted contents ==="
ls -la "$SPARKLE_DIR/"
# The tarball extracts files directly, not into a subdirectory
# Verify the framework has proper symlink structure
echo "=== Verifying Sparkle.framework structure ==="
ls -la "$SPARKLE_DIR/Sparkle.framework/"
if [ ! -L "$SPARKLE_DIR/Sparkle.framework/Sparkle" ]; then
echo "ERROR: Sparkle.framework/Sparkle is not a symlink"
file "$SPARKLE_DIR/Sparkle.framework/Sparkle"
exit 1
if [ ! -d "$SPARKLE_DIR/Sparkle.framework" ]; then
echo "=== Downloading Sparkle $SPARKLE_VERSION ==="
mkdir -p "$SPARKLE_CACHE_DIR"
SPARKLE_URL="https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
curl -L "$SPARKLE_URL" | tar -xJ -C "$SPARKLE_CACHE_DIR"
mv "$SPARKLE_CACHE_DIR/Sparkle-$SPARKLE_VERSION" "$SPARKLE_DIR" 2>/dev/null || true
# If the extracted directory doesn't match version pattern, it may just be "Sparkle"
if [ ! -d "$SPARKLE_DIR" ]; then
mkdir -p "$SPARKLE_DIR"
mv "$SPARKLE_CACHE_DIR/Sparkle.framework" "$SPARKLE_DIR/" 2>/dev/null || true
mv "$SPARKLE_CACHE_DIR/bin" "$SPARKLE_DIR/" 2>/dev/null || true
fi
fi
if [ ! -L "$SPARKLE_DIR/Sparkle.framework/Versions/Current" ]; then
echo "ERROR: Sparkle.framework/Versions/Current is not a symlink"
ls -la "$SPARKLE_DIR/Sparkle.framework/Versions/"
exit 1
fi
echo "Sparkle framework structure verified OK"
echo "=== Injecting Sparkle framework ==="
FRAMEWORKS_DIR="$APP_PATH/Contents/Frameworks"
mkdir -p "$FRAMEWORKS_DIR"
# Remove any existing Sparkle.framework in the app
rm -rf "$FRAMEWORKS_DIR/Sparkle.framework"
# Copy Sparkle framework
cp -R "$SPARKLE_DIR/Sparkle.framework" "$FRAMEWORKS_DIR/"
# Copy Sparkle framework (use ditto to preserve symlinks and bundle structure)
ditto "$SPARKLE_DIR/Sparkle.framework" "$FRAMEWORKS_DIR/Sparkle.framework"
# Verify the copied framework still has proper structure
echo "=== Verifying copied Sparkle.framework structure ==="
ls -la "$FRAMEWORKS_DIR/Sparkle.framework/"
if [ ! -L "$FRAMEWORKS_DIR/Sparkle.framework/Sparkle" ]; then
echo "ERROR: Copied framework lost symlink structure"
exit 1
# Also copy the XPC services if present
if [ -d "$SPARKLE_DIR/Sparkle.framework/Versions/B/XPCServices" ]; then
echo "Sparkle XPC services present"
fi
echo "Copied framework structure OK"
echo "=== Updating Info.plist ==="
PLIST_PATH="$APP_PATH/Contents/Info.plist"
@@ -95,9 +69,8 @@ PLIST_PATH="$APP_PATH/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Add :SUEnableAutomaticChecks bool true" "$PLIST_PATH"
# Set bundle version from git for Sparkle version comparison
# Use commit count for automatic incrementing versions (e.g., 1.0.9548)
VERSION=$(git describe --tags --always 2>/dev/null || echo "1.0.0")
BUILD_NUMBER=$(git rev-list --count HEAD 2>/dev/null || echo "1")
VERSION="1.0.${BUILD_NUMBER}"
echo "Setting version: $VERSION (build $BUILD_NUMBER)"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$PLIST_PATH" 2>/dev/null || \
@@ -110,7 +83,7 @@ echo "=== Adding URL scheme for invitation codes ==="
/usr/libexec/PlistBuddy -c "Delete :CFBundleURLTypes" "$PLIST_PATH" 2>/dev/null || true
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes array" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0 dict" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLName string 'net.eagle0.eagle0'" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLName string 'com.Shardok-Games.eagle0'" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes array" "$PLIST_PATH"
/usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes:0:CFBundleURLSchemes:0 string 'eagle0'" "$PLIST_PATH"
+5 -39
View File
@@ -31,51 +31,17 @@ echo "=== Creating ZIP for notarization: $ZIP_PATH ==="
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
echo "=== Submitting to Apple for notarization ==="
SUBMIT_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \
xcrun notarytool submit "$ZIP_PATH" \
--apple-id "$APPLE_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--team-id "$TEAM_ID" \
--wait 2>&1) || true
--wait
echo "$SUBMIT_OUTPUT"
# Extract submission ID and status (look for " status:" to avoid matching "Current status:")
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep "id:" | head -1 | awk '{print $2}')
STATUS=$(echo "$SUBMIT_OUTPUT" | grep "^ status:" | awk '{print $2}')
echo "Submission ID: $SUBMISSION_ID"
echo "Status: $STATUS"
# Clean up the zip (use -f to avoid failure if already deleted)
rm -f "$ZIP_PATH"
if [ "$STATUS" != "Accepted" ]; then
echo "=== Notarization failed! Fetching log for details ==="
xcrun notarytool log "$SUBMISSION_ID" \
--apple-id "$APPLE_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--team-id "$TEAM_ID"
exit 1
fi
# Clean up the zip
rm "$ZIP_PATH"
echo "=== Stapling notarization ticket to app ==="
# Retry stapling - Apple's CloudKit can have a brief delay after notarization completes
MAX_STAPLE_ATTEMPTS=5
STAPLE_ATTEMPT=1
while [ $STAPLE_ATTEMPT -le $MAX_STAPLE_ATTEMPTS ]; do
echo "Stapling attempt $STAPLE_ATTEMPT/$MAX_STAPLE_ATTEMPTS..."
if xcrun stapler staple "$APP_PATH"; then
echo "Stapling successful"
break
fi
if [ $STAPLE_ATTEMPT -eq $MAX_STAPLE_ATTEMPTS ]; then
echo "ERROR: Stapling failed after $MAX_STAPLE_ATTEMPTS attempts"
exit 1
fi
echo "Stapling failed, waiting 10 seconds before retry..."
sleep 10
STAPLE_ATTEMPT=$((STAPLE_ATTEMPT + 1))
done
xcrun stapler staple "$APP_PATH"
echo "=== Verifying notarization ==="
xcrun stapler validate "$APP_PATH"
-56
View File
@@ -1,56 +0,0 @@
#!/usr/bin/env bash
#
# Submit a macOS .app bundle to Apple for notarization (no waiting)
# Usage: notarize_submit.sh <app_path>
# Outputs: submission_id=<id> to stdout (for GitHub Actions)
#
# Environment variables (required):
# APPLE_ID - Apple Developer account email
# APP_SPECIFIC_PASSWORD - App-specific password for notarytool
# TEAM_ID - Apple Developer Team ID
set -euo pipefail
APP_PATH="$1"
if [ ! -d "$APP_PATH" ]; then
echo "ERROR: App not found at $APP_PATH" >&2
exit 1
fi
if [ -z "${APPLE_ID:-}" ] || [ -z "${APP_SPECIFIC_PASSWORD:-}" ] || [ -z "${TEAM_ID:-}" ]; then
echo "ERROR: Required environment variables not set" >&2
echo " APPLE_ID: ${APPLE_ID:-<not set>}" >&2
echo " APP_SPECIFIC_PASSWORD: ${APP_SPECIFIC_PASSWORD:+<set>}" >&2
echo " TEAM_ID: ${TEAM_ID:-<not set>}" >&2
exit 1
fi
# Create ZIP for notarization submission
ZIP_PATH="${APP_PATH%.app}.zip"
echo "=== Creating ZIP for notarization: $ZIP_PATH ===" >&2
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
echo "=== Submitting to Apple for notarization ===" >&2
SUBMIT_OUTPUT=$(xcrun notarytool submit "$ZIP_PATH" \
--apple-id "$APPLE_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--team-id "$TEAM_ID" 2>&1)
echo "$SUBMIT_OUTPUT" >&2
# Extract submission ID
SUBMISSION_ID=$(echo "$SUBMIT_OUTPUT" | grep "id:" | head -1 | awk '{print $2}')
if [ -z "$SUBMISSION_ID" ]; then
echo "ERROR: Failed to get submission ID" >&2
exit 1
fi
# Clean up the zip
rm "$ZIP_PATH"
echo "Submission ID: $SUBMISSION_ID" >&2
# Output for GitHub Actions
echo "submission_id=$SUBMISSION_ID"
-88
View File
@@ -1,88 +0,0 @@
#!/usr/bin/env bash
#
# Wait for Apple notarization to complete and staple the ticket
# Usage: notarize_wait.sh <submission_id> <app_path>
#
# Environment variables (required):
# APPLE_ID - Apple Developer account email
# APP_SPECIFIC_PASSWORD - App-specific password for notarytool
# TEAM_ID - Apple Developer Team ID
set -euo pipefail
SUBMISSION_ID="$1"
APP_PATH="$2"
if [ -z "$SUBMISSION_ID" ]; then
echo "ERROR: submission_id is required" >&2
exit 1
fi
if [ ! -d "$APP_PATH" ]; then
echo "ERROR: App not found at $APP_PATH" >&2
exit 1
fi
if [ -z "${APPLE_ID:-}" ] || [ -z "${APP_SPECIFIC_PASSWORD:-}" ] || [ -z "${TEAM_ID:-}" ]; then
echo "ERROR: Required environment variables not set" >&2
exit 1
fi
echo "=== Waiting for notarization of submission $SUBMISSION_ID ==="
WAIT_OUTPUT=$(xcrun notarytool wait "$SUBMISSION_ID" \
--apple-id "$APPLE_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--team-id "$TEAM_ID" 2>&1) || true
echo "$WAIT_OUTPUT"
# Extract status (look for " status:" to avoid matching "Current status:")
STATUS=$(echo "$WAIT_OUTPUT" | grep "^ status:" | awk '{print $2}')
echo "Status: $STATUS"
if [ "$STATUS" != "Accepted" ]; then
echo "=== Notarization failed! Fetching log for details ==="
xcrun notarytool log "$SUBMISSION_ID" \
--apple-id "$APPLE_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--team-id "$TEAM_ID"
exit 1
fi
echo "=== Verifying code signature before stapling ==="
if ! codesign --verify --deep --strict "$APP_PATH" 2>&1; then
echo "ERROR: Code signature verification failed - app may have been damaged during transfer"
echo "Attempting to show signature details:"
codesign -dvvv "$APP_PATH" 2>&1 || true
exit 1
fi
echo "Code signature verified successfully"
echo "=== Stapling notarization ticket to app ==="
# Retry stapling - Apple's CloudKit can take several minutes to propagate the ticket
MAX_STAPLE_ATTEMPTS=10
STAPLE_WAIT_SECONDS=30
STAPLE_ATTEMPT=1
while [ $STAPLE_ATTEMPT -le $MAX_STAPLE_ATTEMPTS ]; do
echo "Stapling attempt $STAPLE_ATTEMPT/$MAX_STAPLE_ATTEMPTS..."
if STAPLE_OUTPUT=$(xcrun stapler staple "$APP_PATH" 2>&1); then
echo "$STAPLE_OUTPUT"
echo "Stapling successful"
break
fi
echo "Stapler output: $STAPLE_OUTPUT"
if [ $STAPLE_ATTEMPT -eq $MAX_STAPLE_ATTEMPTS ]; then
echo "ERROR: Stapling failed after $MAX_STAPLE_ATTEMPTS attempts (total wait: $((MAX_STAPLE_ATTEMPTS * STAPLE_WAIT_SECONDS)) seconds)"
exit 1
fi
echo "Stapling failed, waiting $STAPLE_WAIT_SECONDS seconds before retry..."
sleep $STAPLE_WAIT_SECONDS
STAPLE_ATTEMPT=$((STAPLE_ATTEMPT + 1))
done
echo "=== Verifying notarization ==="
xcrun stapler validate "$APP_PATH"
spctl --assess --type exec -v "$APP_PATH"
echo "Notarization complete: $APP_PATH"
+1 -2
View File
@@ -65,8 +65,7 @@ fi
# If we found the Go tool, use it
if [ -n "${WARMUP_TOOL}" ]; then
log_info "Using Go warmup tool: ${WARMUP_TOOL}"
# Use 5 minute timeout to allow for slow operations on cold JVM
if "${WARMUP_TOOL}" --address="${HOST}" --timeout=300s; then
if "${WARMUP_TOOL}" --address="${HOST}" --timeout=60s; then
log_info "Warmup complete!"
exit 0
else
@@ -20,9 +20,8 @@ auto UnitIdsRequiringWaterCrossing(
fb::HexMapW mapCopy = fb::CopyHexMap(gameState->hex_map());
for (uint32_t index = 0; index < mapCopy->terrain()->size(); index++) {
if (IsWater(mapCopy->terrain()->Get(index)->type())) continue;
// const_cast is safe because we own the mutable buffer (mapCopy)
const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
mapCopy->mutable_terrain()->GetMutableObject(index))
mapCopy->mutable_terrain()
->GetMutableObject(index)
->mutable_modifier()
.mutable_fire()
.mutate_present(false);
@@ -165,11 +164,16 @@ auto WaterCrossingTiles(
if (modifier.ice().present() && !modifier.fire().present()) continue;
// Now try adding a bridge to the tile to see if it helps
// const_cast is safe because we own the mutable buffer (mapCopy)
auto *terr = const_cast<net::eagle0::shardok::storage::fb::Terrain *>(
mapCopy->mutable_terrain()->GetMutableObject(index));
terr->mutable_modifier().mutable_bridge().mutate_present(true);
terr->mutable_modifier().mutable_fire().mutate_present(false);
mapCopy->mutable_terrain()
->GetMutableObject(index)
->mutable_modifier()
.mutable_bridge()
.mutate_present(true);
mapCopy->mutable_terrain()
->GetMutableObject(index)
->mutable_modifier()
.mutable_fire()
.mutate_present(false);
auto hash = ActionPointDistancesCache::GetMapId(mapCopy);
@@ -179,7 +183,11 @@ auto WaterCrossingTiles(
}
// Undo the new bridge for the next iteration of the loop
terr->mutable_modifier().mutable_bridge().mutate_present(false);
mapCopy->mutable_terrain()
->GetMutableObject(index)
->mutable_modifier()
.mutable_bridge()
.mutate_present(false);
}
return returnCoords;
@@ -76,13 +76,11 @@ static auto CreateIceClearedMap(const HexMap* map) -> fb::HexMapW {
// Now modify the ice on the mutable copy
auto* mutableMap = mapCopy.Get();
auto* terrainVec = mutableMap->mutable_terrain();
const auto* terrainVec = mutableMap->mutable_terrain();
for (size_t i = 0; i < terrainVec->size(); i++) {
// Only process tiles with ice
// const_cast is safe here because we own the mutable buffer (mapCopy)
if (auto* terrain = const_cast<Terrain*>(terrainVec->GetMutableObject(i));
terrain->modifier().ice().present()) {
if (auto* terrain = terrainVec->GetMutableObject(i); terrain->modifier().ice().present()) {
terrain->mutable_modifier().mutable_ice().mutate_present(false);
terrain->mutable_modifier().mutable_ice().mutate_integrity(0.0f);
}
@@ -22,13 +22,6 @@ using std::unique_ptr;
using ResolvedUnitProto = net::eagle0::shardok::storage::ResolvedUnit;
using GameState = net::eagle0::shardok::storage::fb::GameState;
using GameStateT = net::eagle0::shardok::storage::fb::GameStateT;
using Unit = net::eagle0::shardok::storage::fb::Unit;
// Helper to get a mutable unit from the units vector.
// const_cast is safe because we're accessing through a mutable GameState pointer.
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;
@@ -44,7 +37,7 @@ void ApplyResolvedUnit(
if (unit.has_attached_hero() &&
unit.attached_hero().control_info().controlled_unit_id() != -1) {
const UnitId controlledUnitId = unit.attached_hero().control_info().controlled_unit_id();
auto *controlledUnit = GetMutableUnit(inoutState, controlledUnitId);
auto *controlledUnit = inoutState->mutable_units()->GetMutableObject(controlledUnitId);
internalAssert(controlledUnit->unit_id() == controlledUnitId);
internalAssert(controlledUnit->commanding_unit_id() == unitId);
controlledUnit->mutate_commanding_unit_id(-1);
@@ -54,7 +47,7 @@ void ApplyResolvedUnit(
if (unit.commanding_unit_id() != -1) {
const UnitId commandingUnitId = unit.commanding_unit_id();
auto *commandingUnit = GetMutableUnit(inoutState, commandingUnitId);
auto *commandingUnit = inoutState->mutable_units()->GetMutableObject(commandingUnitId);
internalAssert(commandingUnit->unit_id() == commandingUnitId);
internalAssert(
commandingUnit->attached_hero().control_info().controlled_unit_id() == unitId);
@@ -65,7 +58,7 @@ void ApplyResolvedUnit(
if (status == net::eagle0::shardok::storage::fb::UnitStatus_CAPTURED_UNIT &&
unit.has_attached_hero() && unit.attached_hero().is_vip()) {
for (uint32_t i = 0; i < inoutState->units()->size(); i++) {
auto *playerUnit = GetMutableUnit(inoutState, i);
auto *playerUnit = inoutState->mutable_units()->GetMutableObject(i);
if (playerUnit->player_id() != unit.player_id()) continue;
if (playerUnit->unit_id() == unit.unit_id()) continue;
@@ -77,7 +70,7 @@ void ApplyResolvedUnit(
}
}
GetMutableUnit(inoutState, unitId)->mutate_status(status);
inoutState->mutable_units()->GetMutableObject(unitId)->mutate_status(status);
}
void ApplyResolvedUnit(
@@ -196,7 +189,7 @@ void MutatingAddUnits(GameStateW &mutatingState, const ActionResultProto &result
// We only need to process the units that are being changed
for (const auto &unitBytes : result.changed_units_fb()) {
const auto *unit = (Unit *)unitBytes.data();
auto *mutableUnit = GetMutableUnit(mutatingState.Get(), unit->unit_id());
auto *mutableUnit = mutatingState->mutable_units()->GetMutableObject(unit->unit_id());
if (mutableUnit->status() ==
net::eagle0::shardok::storage::fb::UnitStatus_RESERVED_SLOT) {
// Convert this reserved slot to a real unit
@@ -347,7 +340,7 @@ void MutatingApplyResult(
}
// Capture old position before applying changes
auto *mutableUnit = GetMutableUnit(mutatingGameState.Get(), changedUnit->unit_id());
auto *mutableUnit = mutatingGameState->units()->GetMutableObject(changedUnit->unit_id());
const auto oldLocation = mutableUnit->location();
fb::ApplyUnit(mutableUnit, changedUnit, status);
@@ -191,7 +191,6 @@ auto MoveCommand::GetCommandProto() const -> CommandProto {
proto.mutable_action_points()->set_value(pointCost);
proto.mutable_actor()->set_value(moverId);
*proto.mutable_target() = ToCoordsProto(interimTargets.back());
for (const auto& coords : interimTargets) { *proto.add_path() = ToCoordsProto(coords); }
for (const auto& fup : followUpCommandTypes) { proto.add_follow_up_command_types(fup); }
proto.set_will_unhide(willUnhide);
@@ -64,16 +64,8 @@ auto GetTerrain(const HexMap *map, const Coords &coords) -> const Terrain * {
return map->terrain()->Get(coords.row() * map->column_count() + coords.column());
}
auto GetMutableTerrain(HexMap *map, const Coords &coords) -> Terrain * {
// const_cast is safe because we're accessing through a mutable HexMap pointer
return const_cast<Terrain *>(map->mutable_terrain()->GetMutableObject(
coords.row() * map->column_count() + coords.column()));
}
auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain * {
// const_cast is safe when the underlying buffer is known to be mutable
return const_cast<Terrain *>(
map->terrain()->Get(coords.row() * map->column_count() + coords.column()));
return map->terrain()->GetMutableObject(coords.row() * map->column_count() + coords.column());
}
auto HasForestAccess(
@@ -605,9 +597,7 @@ void MutatingSetTileModifier(
const int row,
const int column,
const TileModifierProto &TileModifierProto) {
// const_cast is safe because we're accessing through a mutable HexMap pointer
auto *terr = const_cast<Terrain *>(
hexMap->mutable_terrain()->GetMutableObject(row * hexMap->column_count() + column));
auto *terr = hexMap->mutable_terrain()->GetMutableObject(row * hexMap->column_count() + column);
if (TileModifierProto.has_bridge()) {
terr->mutable_modifier().mutable_bridge().mutate_present(true);
@@ -82,9 +82,6 @@ auto HasForestAccess(
PlayerId player) -> bool;
auto GetTerrain(const HexMap *map, const Coords &coords) -> const Terrain *;
auto GetMutableTerrain(HexMap *map, const Coords &coords) -> Terrain *;
// Overload for const HexMap - uses const_cast internally. Safe when the underlying buffer is
// mutable.
auto GetMutableTerrain(const HexMap *map, const Coords &coords) -> Terrain *;
auto CoordsAreValid(const HexMap *map, const Coords &coords) -> bool;
@@ -29,13 +29,6 @@ using UnitViewProto = net::eagle0::shardok::api::UnitView;
using Coords = net::eagle0::shardok::storage::fb::Coords;
using Unit = net::eagle0::shardok::storage::fb::Unit;
using GameState = net::eagle0::shardok::storage::fb::GameState;
// Helper to get a mutable unit from the units vector.
// const_cast is safe because we're accessing through a mutable GameState pointer.
inline auto GetMutableUnit(GameState *state, UnitId unitId) -> Unit * {
return const_cast<Unit *>(state->mutable_units()->GetMutableObject(unitId));
}
constexpr int8_t kGuessedHeroStat = 75;
constexpr int8_t kGuessedBattalionStat = 0;
@@ -419,13 +412,16 @@ auto GameStateGuesser::GuessedState(
if (unit->has_attached_hero()) {
UnitId controlledUnitId = unit->attached_hero().control_info().controlled_unit_id();
if (controlledUnitId != -1) {
GetMutableUnit(gsw.Get(), controlledUnitId)->mutate_commanding_unit_id(unitId);
gsw->mutable_units()
->GetMutableObject(controlledUnitId)
->mutate_commanding_unit_id(unitId);
}
}
UnitId commandingUnitId = unit->commanding_unit_id();
if (unit->commanding_unit_id() != -1) {
GetMutableUnit(gsw.Get(), commandingUnitId)
gsw->mutable_units()
->GetMutableObject(commandingUnitId)
->mutable_attached_hero()
.mutable_control_info()
.mutate_controlled_unit_id(unitId);
@@ -37,7 +37,4 @@ sysinfo.txt
*.apk
*.unitypackage
mono_crash.*
# Local server data
ServerData/
mono_crash.*
@@ -26,7 +26,7 @@
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>Temp\Bin\Debug\Assembly-CSharp\</OutputPath>
<DefineConstants>UNITY_6000_3_0;UNITY_6000_3;UNITY_6000;UNITY_5_3_OR_NEWER;UNITY_5_4_OR_NEWER;UNITY_5_5_OR_NEWER;UNITY_5_6_OR_NEWER;UNITY_2017_1_OR_NEWER;UNITY_2017_2_OR_NEWER;UNITY_2017_3_OR_NEWER;UNITY_2017_4_OR_NEWER;UNITY_2018_1_OR_NEWER;UNITY_2018_2_OR_NEWER;UNITY_2018_3_OR_NEWER;UNITY_2018_4_OR_NEWER;UNITY_2019_1_OR_NEWER;UNITY_2019_2_OR_NEWER;UNITY_2019_3_OR_NEWER;UNITY_2019_4_OR_NEWER;UNITY_2020_1_OR_NEWER;UNITY_2020_2_OR_NEWER;UNITY_2020_3_OR_NEWER;UNITY_2021_1_OR_NEWER;UNITY_2021_2_OR_NEWER;UNITY_2021_3_OR_NEWER;UNITY_2022_1_OR_NEWER;UNITY_2022_2_OR_NEWER;UNITY_2022_3_OR_NEWER;UNITY_2023_1_OR_NEWER;UNITY_2023_2_OR_NEWER;UNITY_2023_3_OR_NEWER;UNITY_6000_0_OR_NEWER;UNITY_6000_1_OR_NEWER;UNITY_6000_2_OR_NEWER;UNITY_6000_3_OR_NEWER;UNITY_INCLUDE_TESTS;UNITY_ANALYTICS;ENABLE_AR;ENABLE_AUDIO;ENABLE_AUDIO_SCRIPTABLE_PIPELINE;ENABLE_CACHING;ENABLE_CLOTH;ENABLE_MICROPHONE;ENABLE_MULTIPLE_DISPLAYS;ENABLE_PHYSICS;ENABLE_TEXTURE_STREAMING;ENABLE_LZMA;ENABLE_UNITYEVENTS;ENABLE_VR;ENABLE_WEBCAM;ENABLE_UNITYWEBREQUEST;ENABLE_WWW;ENABLE_CLOUD_SERVICES;ENABLE_CLOUD_SERVICES_ADS;ENABLE_CLOUD_SERVICES_USE_WEBREQUEST;ENABLE_UNITY_CONSENT;ENABLE_UNITY_CLOUD_IDENTIFIERS;ENABLE_CLOUD_SERVICES_CRASH_REPORTING;ENABLE_CLOUD_SERVICES_PURCHASING;ENABLE_CLOUD_SERVICES_ANALYTICS;ENABLE_CLOUD_SERVICES_BUILD;ENABLE_EDITOR_GAME_SERVICES;ENABLE_UNITY_GAME_SERVICES_ANALYTICS_SUPPORT;ENABLE_CLOUD_LICENSE;ENABLE_EDITOR_HUB_LICENSE;ENABLE_WEBSOCKET_CLIENT;ENABLE_GENERATE_NATIVE_PLUGINS_FOR_ASSEMBLIES_API;ENABLE_DIRECTOR_AUDIO;ENABLE_DIRECTOR_TEXTURE;ENABLE_MANAGED_JOBS;ENABLE_MANAGED_TRANSFORM_JOBS;ENABLE_MANAGED_ANIMATION_JOBS;ENABLE_MANAGED_AUDIO_JOBS;ENABLE_ENGINE_CODE_STRIPPING;ENABLE_ONSCREEN_KEYBOARD;ENABLE_MANAGED_UNITYTLS;INCLUDE_DYNAMIC_GI;ENABLE_SCRIPTING_GC_WBARRIERS;PLATFORM_SUPPORTS_MONO;ENABLE_MARSHALLING_TESTS;ENABLE_VIDEO;ENABLE_NAVIGATION_OFFMESHLINK_TO_NAVMESHLINK;ENABLE_ACCELERATOR_CLIENT_DEBUGGING;ENABLE_ACCESSIBILITY_SCREEN_READER;ENABLE_ACCESSIBILITY_SETTINGS;TEXTCORE_1_0_OR_NEWER;EDITOR_ONLY_NAVMESH_BUILDER_DEPRECATED;ENABLE_RUNTIME_GI;ENABLE_GAMECENTER;ENABLE_NETWORK;ENABLE_IOS_ON_DEMAND_RESOURCES;ENABLE_IOS_APP_SLICING;PLAYERCONNECTION_LISTENS_FIXED_PORT;DEBUGGER_LISTENS_FIXED_PORT;PLATFORM_SUPPORTS_ADS_ID;PLATFORM_SUPPORTS_INSIGHTS_IDFA;PLATFORM_SUPPORTS_INSIGHTS_DEVICE_INFO;SUPPORT_ENVIRONMENT_VARIABLES;PLATFORM_SUPPORTS_PROFILER;ENABLE_CRUNCH_TEXTURE_COMPRESSION;ENABLE_ETC_COMPRESSION;UNITY_IPHONE_API;PLATFORM_SUPPORTS_SPLIT_GRAPHICS_JOBS;ENABLE_INSIGHTS_PLATFORM_SPECIFIC_RESOURCES;ENABLE_UNITYADS_RUNTIME;UNITY_UNITYADS_API;PLATFORM_IOS;UNITY_IOS;PLATFORM_IPHONE;UNITY_IPHONE;UNITY_HAS_GOOGLEVR;ENABLE_SPATIALTRACKING;ENABLE_CLOUD_SERVICES_ENGINE_DIAGNOSTICS;ENABLE_FIREBASE_IDENTIFIERS;ENABLE_MONO;NET_4_6;NET_UNITY_4_8;ENABLE_PROFILER;DEBUG;TRACE;UNITY_ASSERTIONS;UNITY_EDITOR;UNITY_EDITOR_64;UNITY_EDITOR_OSX;ENABLE_UNITY_COLLECTIONS_CHECKS;ENABLE_BURST_AOT;UNITY_TEAM_LICENSE;ENABLE_CUSTOM_RENDER_TEXTURE;ENABLE_DIRECTOR;ENABLE_LOCALIZATION;ENABLE_SPRITES;ENABLE_TERRAIN;ENABLE_TILEMAP;ENABLE_TIMELINE;ENABLE_LEGACY_INPUT_MANAGER;TEXTCORE_FONT_ENGINE_1_5_OR_NEWER;TEXTCORE_TEXT_ENGINE_1_5_OR_NEWER;TEXTCORE_FONT_ENGINE_1_6_OR_NEWER;CSHARP_7_OR_LATER;CSHARP_7_3_OR_NEWER</DefineConstants>
<DefineConstants>UNITY_6000_3_0;UNITY_6000_3;UNITY_6000;UNITY_5_3_OR_NEWER;UNITY_5_4_OR_NEWER;UNITY_5_5_OR_NEWER;UNITY_5_6_OR_NEWER;UNITY_2017_1_OR_NEWER;UNITY_2017_2_OR_NEWER;UNITY_2017_3_OR_NEWER;UNITY_2017_4_OR_NEWER;UNITY_2018_1_OR_NEWER;UNITY_2018_2_OR_NEWER;UNITY_2018_3_OR_NEWER;UNITY_2018_4_OR_NEWER;UNITY_2019_1_OR_NEWER;UNITY_2019_2_OR_NEWER;UNITY_2019_3_OR_NEWER;UNITY_2019_4_OR_NEWER;UNITY_2020_1_OR_NEWER;UNITY_2020_2_OR_NEWER;UNITY_2020_3_OR_NEWER;UNITY_2021_1_OR_NEWER;UNITY_2021_2_OR_NEWER;UNITY_2021_3_OR_NEWER;UNITY_2022_1_OR_NEWER;UNITY_2022_2_OR_NEWER;UNITY_2022_3_OR_NEWER;UNITY_2023_1_OR_NEWER;UNITY_2023_2_OR_NEWER;UNITY_2023_3_OR_NEWER;UNITY_6000_0_OR_NEWER;UNITY_6000_1_OR_NEWER;UNITY_6000_2_OR_NEWER;UNITY_6000_3_OR_NEWER;PLATFORM_ARCH_64;UNITY_64;UNITY_INCLUDE_TESTS;UNITY_ANALYTICS;ENABLE_AR;ENABLE_AUDIO;ENABLE_AUDIO_SCRIPTABLE_PIPELINE;ENABLE_CACHING;ENABLE_CLOTH;ENABLE_MICROPHONE;ENABLE_MULTIPLE_DISPLAYS;ENABLE_PHYSICS;ENABLE_TEXTURE_STREAMING;ENABLE_VIRTUALTEXTURING;ENABLE_LZMA;ENABLE_UNITYEVENTS;ENABLE_VR;ENABLE_WEBCAM;ENABLE_UNITYWEBREQUEST;ENABLE_WWW;ENABLE_CLOUD_SERVICES;ENABLE_CLOUD_SERVICES_ADS;ENABLE_CLOUD_SERVICES_USE_WEBREQUEST;ENABLE_UNITY_CONSENT;ENABLE_UNITY_CLOUD_IDENTIFIERS;ENABLE_CLOUD_SERVICES_CRASH_REPORTING;ENABLE_CLOUD_SERVICES_NATIVE_CRASH_REPORTING;ENABLE_CLOUD_SERVICES_PURCHASING;ENABLE_CLOUD_SERVICES_ANALYTICS;ENABLE_CLOUD_SERVICES_BUILD;ENABLE_EDITOR_GAME_SERVICES;ENABLE_UNITY_GAME_SERVICES_ANALYTICS_SUPPORT;ENABLE_CLOUD_LICENSE;ENABLE_EDITOR_HUB_LICENSE;ENABLE_WEBSOCKET_CLIENT;ENABLE_GENERATE_NATIVE_PLUGINS_FOR_ASSEMBLIES_API;ENABLE_DIRECTOR_AUDIO;ENABLE_DIRECTOR_TEXTURE;ENABLE_MANAGED_JOBS;ENABLE_MANAGED_TRANSFORM_JOBS;ENABLE_MANAGED_ANIMATION_JOBS;ENABLE_MANAGED_AUDIO_JOBS;ENABLE_MANAGED_UNITYTLS;INCLUDE_DYNAMIC_GI;ENABLE_SCRIPTING_GC_WBARRIERS;PLATFORM_SUPPORTS_MONO;RENDER_SOFTWARE_CURSOR;ENABLE_MARSHALLING_TESTS;ENABLE_VIDEO;ENABLE_NAVIGATION_OFFMESHLINK_TO_NAVMESHLINK;ENABLE_ACCELERATOR_CLIENT_DEBUGGING;ENABLE_ACCESSIBILITY_SCREEN_READER;TEXTCORE_1_0_OR_NEWER;EDITOR_ONLY_NAVMESH_BUILDER_DEPRECATED;PLATFORM_STANDALONE_OSX;PLATFORM_STANDALONE;UNITY_STANDALONE_OSX;UNITY_STANDALONE;ENABLE_GAMECENTER;ENABLE_RUNTIME_GI;ENABLE_MOVIES;ENABLE_NETWORK;ENABLE_CRUNCH_TEXTURE_COMPRESSION;ENABLE_CLOUD_SERVICES_ENGINE_DIAGNOSTICS;ENABLE_CLUSTER_SYNC;ENABLE_CLUSTERINPUT;ENABLE_SPATIALTRACKING;PLATFORM_HAS_CUSTOM_MUTEX;PLATFORM_UPDATES_TIME_OUTSIDE_OF_PLAYER_LOOP;PLATFORM_SUPPORTS_SPLIT_GRAPHICS_JOBS;ENABLE_MONO;NET_STANDARD_2_0;NET_STANDARD;NET_STANDARD_2_1;NETSTANDARD;NETSTANDARD2_1;ENABLE_PROFILER;DEBUG;TRACE;UNITY_ASSERTIONS;UNITY_EDITOR;UNITY_EDITOR_64;UNITY_EDITOR_OSX;ENABLE_UNITY_COLLECTIONS_CHECKS;ENABLE_BURST_AOT;UNITY_TEAM_LICENSE;ENABLE_CUSTOM_RENDER_TEXTURE;ENABLE_DIRECTOR;ENABLE_LOCALIZATION;ENABLE_SPRITES;ENABLE_TERRAIN;ENABLE_TILEMAP;ENABLE_TIMELINE;ENABLE_LEGACY_INPUT_MANAGER;TEXTCORE_FONT_ENGINE_1_5_OR_NEWER;TEXTCORE_TEXT_ENGINE_1_5_OR_NEWER;TEXTCORE_FONT_ENGINE_1_6_OR_NEWER;CSHARP_7_OR_LATER;CSHARP_7_3_OR_NEWER</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<NoWarn>0169,0649</NoWarn>
@@ -87,6 +87,7 @@
<Compile Include="Assets/Modern UI Pack/Scripts/Demo/LaunchURL.cs" />
<Compile Include="Assets/Shardok/HexCoordinates.cs" />
<Compile Include="Assets/Eagle/Table Rows/OrganizeTroopsTableRow.cs" />
<Compile Include="Assets/Bluetooth/PickerRowController.cs" />
<Compile Include="Assets/TouchHandler.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/TravelCommandSelector.cs" />
<Compile Include="Assets/Eagle/ConnectionStatusUI.cs" />
@@ -97,6 +98,7 @@
<Compile Include="Assets/HoveringTooltipTextProvider.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/util/KeyModifiedAmount.cs" />
<Compile Include="Assets/HoveringTooltip.cs" />
<Compile Include="Assets/Bluetooth/DiceInterface.cs" />
<Compile Include="Assets/ButtonColors.cs" />
<Compile Include="Assets/Eagle/MapController.cs" />
<Compile Include="Assets/common/DisclosureTriangle.cs" />
@@ -124,7 +126,6 @@
<Compile Include="Assets/Modern UI Pack/Scripts/Notification/NotificationManagerEditor.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicEditor.cs" />
<Compile Include="Assets/ConnectionHandler/RunningGameItem.cs" />
<Compile Include="Assets/Tutorial/UI/TutorialCanvasBuilder.cs" />
<Compile Include="Assets/Shardok/Table Rows/ArmyRowController.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/PrisonerExchangeDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/Notifications/Notification.cs" />
@@ -146,12 +147,13 @@
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/RadialSlider.cs" />
<Compile Include="Assets/Eagle/Notifications/WeatherForcedTurnBackNotificationGenerator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManager.cs" />
<Compile Include="Assets/common/GUIUtils/TouchAwareTooltip.cs" />
<Compile Include="Assets/Eagle/GeneratedTextUpdater.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/OutlawApprehendedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/HexMetrics.cs" />
<Compile Include="Assets/Bluetooth/OneDiceRollController.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Switch/SwitchManager.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ProvinceHeldDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Bluetooth/DieInfo.cs" />
<Compile Include="Assets/common/ResourceFetcher.cs" />
<Compile Include="Assets/Tutorial/Content/TutorialStep.cs" />
<Compile Include="Assets/Auth/AuthClient.cs" />
@@ -159,10 +161,10 @@
<Compile Include="Assets/Eagle/Notifications/RiotSuppressedNotificationGenerator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Modal Window/ModalWindowManagerEditor.cs" />
<Compile Include="Assets/common/GUIUtils/AutoScrollingText.cs" />
<Compile Include="Assets/Bluetooth/NativeDiceInterfaceImports.cs" />
<Compile Include="Assets/Eagle/Notifications/ProvinceConqueredNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/Table Rows/ExtraTroopsRowController.cs" />
<Compile Include="Assets/Auth/JwtAuthInterceptor.cs" />
<Compile Include="Assets/Tutorial/Content/TutorialContentDefinitions.cs" />
<Compile Include="Assets/Eagle/CommandPanelController.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/ShatteredArmyDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/TurnHistoryPanelController.cs" />
@@ -175,9 +177,9 @@
<Compile Include="Assets/Shardok/BattalionTypeManager.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/DefendCommandSelector.cs" />
<Compile Include="Assets/Eagle/HeroesAndBattalionsPanelController.cs" />
<Compile Include="Assets/common/HexMapStorage.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasic.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/BreakAllianceAmbassadorImprisonedNotificationGenerator.cs" />
<Compile Include="Assets/common/HexMapStorage.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/PleaseRecruitMeCommandSelector.cs" />
<Compile Include="Assets/Eagle/Notifications/RiotAvertedNotificationGenerator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Notification/NotificationStacking.cs" />
@@ -204,10 +206,10 @@
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/RansomRejectedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/FactionsTableController.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/RangeSlider.cs" />
<Compile Include="Assets/Tutorial/UI/TutorialOverlayBuilder.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ExileVassalCommandSelector.cs" />
<Compile Include="Assets/Tutorial/TutorialState.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicWithIconEditor.cs" />
<Compile Include="Assets/Bluetooth/DiceVectors.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerProgressBarLoop.cs" />
<Compile Include="Assets/Eagle/MovingArmiesTableController.cs" />
<Compile Include="Assets/ConnectionHandler/ConnectionHandler.cs" />
@@ -228,11 +230,9 @@
<Compile Include="Assets/Eagle/CommandSelectors/DiplomacyCommandSelector.cs" />
<Compile Include="Assets/Tutorial/Triggers/TutorialTriggerRegistry.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ResolveBreakAllianceCommandSelector.cs" />
<Compile Include="Assets/common/WindowFocusManager.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ResolveAllianceCommandSelector.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/WithdrewForTruceDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/MarchCommandSelector.cs" />
<Compile Include="Assets/Tutorial/TutorialTargetRegistry.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/TrainCommandSelector.cs" />
<Compile Include="Assets/Eagle/CustomFileLogger.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/InvitationAcceptedDetailsNotificationGenerator.cs" />
@@ -266,6 +266,7 @@
<Compile Include="Assets/Shardok/AnimationTestController.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/TradeCommandSelector.cs" />
<Compile Include="Assets/Eagle/Table Rows/DominionTableRowController.cs" />
<Compile Include="Assets/Bluetooth/RollPanelController.cs" />
<Compile Include="Assets/Shardok/ChargeAnimator.cs" />
<Compile Include="Assets/Eagle/ClientTextProvider.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManager.cs" />
@@ -295,13 +296,11 @@
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerEditor.cs" />
<Compile Include="Assets/UI/Scripts/SceneMigrationHelper.cs" />
<Compile Include="Assets/Eagle/Table Rows/UnitSelectorHeroRowController.cs" />
<Compile Include="Assets/Shardok/BridgeRotationCalculator.cs" />
<Compile Include="Assets/ConnectionHandler/CustomBattleHandler.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/RansomPaidDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/ControlAnimator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Horizontal Selector/HorizontalSelector.cs" />
<Compile Include="Assets/Eagle/Table Rows/OrganizeTroopsResultRow.cs" />
<Compile Include="Assets/Eagle/SparkleUpdater.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Slider/SliderManagerEditor.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SwearBrotherhoodDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/PersistentClientConnection.cs" />
@@ -320,15 +319,14 @@
<Compile Include="Assets/Shardok/ShardokGameModel.cs" />
<Compile Include="Assets/common/GUIUtils/GeneralClickDetector.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerContextMenu.cs" />
<Compile Include="Assets/Eagle/SparkleInitializer.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/TruceAmbassadorImprisonedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Button/ButtonManagerBasicWithIcon.cs" />
<Compile Include="Assets/Tutorial/UI/TutorialHintIndicator.cs" />
<Compile Include="Assets/Bluetooth/UnityDieColors.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/OrganizeTroopsCommandSelector.cs" />
<Compile Include="Assets/Shardok/ExtinguishAnimator.cs" />
<Compile Include="Assets/Eagle/PanelPositions.cs" />
<Compile Include="Assets/Eagle/Notifications/NotificationGenerator.cs" />
<Compile Include="Assets/BuildInfo.cs" />
<Compile Include="Assets/Eagle/Notifications/DynamicTextNotification.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerToggle.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerAnimatedIcon.cs" />
@@ -337,6 +335,7 @@
<Compile Include="Assets/EagleConnection.cs" />
<Compile Include="Assets/GUI Pro Kit Fantasy RPG/Scripts/CtrPanel.cs" />
<Compile Include="Assets/UI/Scripts/SceneLoadTester.cs" />
<Compile Include="Assets/Bluetooth/RollFetcher.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/HandleRiotGiveCommandSelector.cs" />
<Compile Include="Assets/Eagle/ProvinceUtils.cs" />
<Compile Include="Assets/common/GUIUtils/ProvinceStatUtils.cs" />
@@ -370,22 +369,22 @@
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SuppressBeastsFailedDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Terrain Hexes/Example Scene/BasicHexArranger.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/ManagePrisonersCommandSelector.cs" />
<Compile Include="Assets/ConnectionHandler/StoredAccountButton.cs" />
<Compile Include="Assets/Tutorial/UI/TutorialUIManager.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/SendSuppliesCommandSelector.cs" />
<Compile Include="Assets/Tutorial/UI/TutorialUIManager.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/Window/WindowManager.cs" />
<Compile Include="Assets/Auth/InvitationCodeManager.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/CapturedHeroExiledDetailsNotificationGenerator.cs" />
<Compile Include="Assets/Eagle/Notifications/ARNNotifications/SuppressBeastsSucceededNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/ReservesTableController.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/CommandSelector.cs" />
<Compile Include="Assets/Eagle/CommandSelectors/RestCommandSelector.cs" />
<Compile Include="Assets/Bluetooth/DiceConfigurationPanelController.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerDropdown.cs" />
<Compile Include="Assets/Modern UI Pack/Scripts/UI Manager/UIManagerButton.cs" />
<Compile Include="Assets/common/GUIUtils/GUIUtils.cs" />
<Compile Include="Assets/Eagle/Table Rows/IncomingArmyTableRow.cs" />
<Compile Include="Assets/ConnectionHandler/CreateGameItem.cs" />
<Compile Include="Assets/Eagle/EagleGameController.cs" />
<Compile Include="Assets/Tutorial/TutorialTestSetup.cs" />
<Compile Include="Assets/Eagle/Notifications/FailedSwearBrotherhoodNotificationGenerator.cs" />
<Compile Include="Assets/Shardok/FireEffectAnimator.cs" />
<Compile Include="Assets/common/GUIUtils/EventBasedTable.cs" />
@@ -414,12 +413,10 @@
<None Include="Assets/Packages/Microsoft.Extensions.Options.8.0.0/lib/netstandard2.1/Microsoft.Extensions.Options.xml" />
<None Include="Assets/TextMesh Pro/Shaders/TMPro_Mobile.cginc" />
<None Include="Assets/TextMesh Pro/Shaders/TMP_SDF SSD.shader" />
<None Include="Assets/TextMesh Pro/Shaders/TMPro_Properties.cginc" />
<None Include="Assets/Shardok/Painted Medieval Fantasy Items 1/medieval_items1.txt" />
<None Include="Assets/TextMesh Pro/Resources/Shaders/TMP_SDF-Surface-Mobile.shader" />
<None Include="Assets/Eagle/maskShader.shader" />
<None Include="Assets/GUI Pro Kit Fantasy RPG/Extensions/UIParticle/hidden particle.shader" />
<None Include="Assets/TextMesh Pro/Shaders/TMPro.cginc" />
<None Include="Assets/Terrain Hexes/readme.txt" />
<None Include="Assets/Modern UI Pack/Read Me.txt" />
<None Include="Assets/TextMesh Pro/Shaders/SDFFunctions.hlsl" />
@@ -449,7 +446,7 @@
<None Include="Assets/Packages/System.ComponentModel.Annotations.5.0.0/useSharedDesignerContext.txt" />
<None Include="Assets/Packages/System.Runtime.CompilerServices.Unsafe.6.0.0/useSharedDesignerContext.txt" />
<None Include="Assets/TextMesh Pro/Resources/Shaders/TMP_SDF-Mobile Masking.shader" />
<None Include="Assets/Music/Music Credits.txt" />
<None Include="Assets/Resources/Music/Music Credits.txt" />
<None Include="Assets/Packages/System.Diagnostics.DiagnosticSource.8.0.0/useSharedDesignerContext.txt" />
<None Include="Assets/Packages/Microsoft.Extensions.Logging.8.0.0/useSharedDesignerContext.txt" />
<None Include="Assets/Packages/Microsoft.Extensions.DependencyInjection.Abstractions.8.0.0/useSharedDesignerContext.txt" />
@@ -495,6 +492,12 @@
<Reference Include="UnityEngine.ClothModule">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/Managed/UnityEngine/UnityEngine.ClothModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ClusterInputModule">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/Managed/UnityEngine/UnityEngine.ClusterInputModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ClusterRendererModule">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/Managed/UnityEngine/UnityEngine.ClusterRendererModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.ContentLoadModule">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/Managed/UnityEngine/UnityEngine.ContentLoadModule.dll</HintPath>
</Reference>
@@ -687,6 +690,9 @@
<Reference Include="UnityEngine.VideoModule">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/Managed/UnityEngine/UnityEngine.VideoModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.VirtualTexturingModule">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/Managed/UnityEngine/UnityEngine.VirtualTexturingModule.dll</HintPath>
</Reference>
<Reference Include="UnityEngine.WindModule">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/Managed/UnityEngine/UnityEngine.WindModule.dll</HintPath>
</Reference>
@@ -861,6 +867,9 @@
<Reference Include="Microsoft.Extensions.Logging.Abstractions">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Packages/Microsoft.Extensions.Logging.Abstractions.8.0.0/lib/netstandard2.0/Microsoft.Extensions.Logging.Abstractions.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.Annotations">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Packages/System.ComponentModel.Annotations.5.0.0/lib/netstandard2.1/System.ComponentModel.Annotations.dll</HintPath>
</Reference>
<Reference Include="nunit.framework">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/PackageCache/com.unity.ext.nunit@d8c07649098d/net40/unity-custom/nunit.framework.dll</HintPath>
</Reference>
@@ -915,392 +924,374 @@
<Reference Include="Unity.Analytics.StandardEvents">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/PackageCache/com.unity.analytics@c9d14a6bdec6/AnalyticsStandardEvents/Unity.Analytics.StandardEvents.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.iOS.Extensions.Xcode">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/PlaybackEngines/iOSSupport/UnityEditor.iOS.Extensions.Xcode.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.iOS.Extensions.Common">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/PlaybackEngines/iOSSupport/UnityEditor.iOS.Extensions.Common.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.Apple.Extensions.Common">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/PlaybackEngines/iOSSupport/UnityEditor.Apple.Extensions.Common.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.iOS.Extensions.Xcode">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/PlaybackEngines/MacStandaloneSupport/UnityEditor.iOS.Extensions.Xcode.dll</HintPath>
</Reference>
<Reference Include="mscorlib">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/mscorlib.dll</HintPath>
</Reference>
<Reference Include="System">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.dll</HintPath>
</Reference>
<Reference Include="System.Core">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.Core.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.Runtime.Serialization.dll</HintPath>
</Reference>
<Reference Include="System.Xml">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.Xml.dll</HintPath>
</Reference>
<Reference Include="System.Xml.Linq">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.Xml.Linq.dll</HintPath>
</Reference>
<Reference Include="System.Numerics">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.Numerics.dll</HintPath>
</Reference>
<Reference Include="System.Numerics.Vectors">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.Numerics.Vectors.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.Net.Http.dll</HintPath>
</Reference>
<Reference Include="System.IO.Compression">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.IO.Compression.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CSharp">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Microsoft.CSharp.dll</HintPath>
</Reference>
<Reference Include="System.Data">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.Data.dll</HintPath>
</Reference>
<Reference Include="System.Data.DataSetExtensions">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.Data.DataSetExtensions.dll</HintPath>
</Reference>
<Reference Include="System.Drawing">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.Drawing.dll</HintPath>
</Reference>
<Reference Include="System.IO.Compression.FileSystem">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.IO.Compression.FileSystem.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.Composition">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.ComponentModel.Composition.dll</HintPath>
</Reference>
<Reference Include="System.Transactions">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/System.Transactions.dll</HintPath>
<Reference Include="netstandard">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/ref/2.1.0/netstandard.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Win32.Primitives">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/Microsoft.Win32.Primitives.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/Microsoft.Win32.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.AppContext">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.AppContext.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.AppContext.dll</HintPath>
</Reference>
<Reference Include="System.Buffers">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Buffers.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Buffers.dll</HintPath>
</Reference>
<Reference Include="System.Collections.Concurrent">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Collections.Concurrent.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Collections.Concurrent.dll</HintPath>
</Reference>
<Reference Include="System.Collections.NonGeneric">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Collections.NonGeneric.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Collections.NonGeneric.dll</HintPath>
</Reference>
<Reference Include="System.Collections.Specialized">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Collections.Specialized.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Collections.Specialized.dll</HintPath>
</Reference>
<Reference Include="System.Collections">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Collections.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.Annotations">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.ComponentModel.Annotations.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Collections.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.EventBasedAsync">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.ComponentModel.EventBasedAsync.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.ComponentModel.EventBasedAsync.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.Primitives">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.ComponentModel.Primitives.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.ComponentModel.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.TypeConverter">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.ComponentModel.TypeConverter.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.ComponentModel.TypeConverter.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.ComponentModel.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.ComponentModel.dll</HintPath>
</Reference>
<Reference Include="System.Console">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Console.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Console.dll</HintPath>
</Reference>
<Reference Include="System.Data.Common">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Data.Common.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Data.Common.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.Contracts">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Diagnostics.Contracts.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Diagnostics.Contracts.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.Debug">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Diagnostics.Debug.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Diagnostics.Debug.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.FileVersionInfo">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Diagnostics.FileVersionInfo.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Diagnostics.FileVersionInfo.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.Process">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Diagnostics.Process.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Diagnostics.Process.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.StackTrace">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Diagnostics.StackTrace.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Diagnostics.StackTrace.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.TextWriterTraceListener">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Diagnostics.TextWriterTraceListener.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Diagnostics.TextWriterTraceListener.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.Tools">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Diagnostics.Tools.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Diagnostics.Tools.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.TraceSource">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Diagnostics.TraceSource.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Diagnostics.TraceSource.dll</HintPath>
</Reference>
<Reference Include="System.Diagnostics.Tracing">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Diagnostics.Tracing.dll</HintPath>
</Reference>
<Reference Include="System.Drawing.Primitives">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Drawing.Primitives.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Drawing.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Dynamic.Runtime">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Dynamic.Runtime.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Dynamic.Runtime.dll</HintPath>
</Reference>
<Reference Include="System.Globalization.Calendars">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Globalization.Calendars.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Globalization.Calendars.dll</HintPath>
</Reference>
<Reference Include="System.Globalization.Extensions">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Globalization.Extensions.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Globalization.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Globalization">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Globalization.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Globalization.dll</HintPath>
</Reference>
<Reference Include="System.IO.Compression.ZipFile">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.IO.Compression.ZipFile.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.IO.Compression.ZipFile.dll</HintPath>
</Reference>
<Reference Include="System.IO.Compression">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.IO.Compression.dll</HintPath>
</Reference>
<Reference Include="System.IO.FileSystem.DriveInfo">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.IO.FileSystem.DriveInfo.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.IO.FileSystem.DriveInfo.dll</HintPath>
</Reference>
<Reference Include="System.IO.FileSystem.Primitives">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.IO.FileSystem.Primitives.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.IO.FileSystem.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.IO.FileSystem.Watcher">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.IO.FileSystem.Watcher.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.IO.FileSystem.Watcher.dll</HintPath>
</Reference>
<Reference Include="System.IO.FileSystem">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.IO.FileSystem.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.IO.FileSystem.dll</HintPath>
</Reference>
<Reference Include="System.IO.IsolatedStorage">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.IO.IsolatedStorage.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.IO.IsolatedStorage.dll</HintPath>
</Reference>
<Reference Include="System.IO.MemoryMappedFiles">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.IO.MemoryMappedFiles.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.IO.MemoryMappedFiles.dll</HintPath>
</Reference>
<Reference Include="System.IO.Pipes">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.IO.Pipes.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.IO.Pipes.dll</HintPath>
</Reference>
<Reference Include="System.IO.UnmanagedMemoryStream">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.IO.UnmanagedMemoryStream.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.IO.UnmanagedMemoryStream.dll</HintPath>
</Reference>
<Reference Include="System.IO">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.IO.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.IO.dll</HintPath>
</Reference>
<Reference Include="System.Linq.Expressions">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Linq.Expressions.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Linq.Expressions.dll</HintPath>
</Reference>
<Reference Include="System.Linq.Parallel">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Linq.Parallel.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Linq.Parallel.dll</HintPath>
</Reference>
<Reference Include="System.Linq.Queryable">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Linq.Queryable.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Linq.Queryable.dll</HintPath>
</Reference>
<Reference Include="System.Linq">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Linq.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Linq.dll</HintPath>
</Reference>
<Reference Include="System.Memory">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Memory.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Memory.dll</HintPath>
</Reference>
<Reference Include="System.Net.Http.Rtc">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Net.Http.Rtc.dll</HintPath>
<Reference Include="System.Net.Http">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Net.Http.dll</HintPath>
</Reference>
<Reference Include="System.Net.NameResolution">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Net.NameResolution.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Net.NameResolution.dll</HintPath>
</Reference>
<Reference Include="System.Net.NetworkInformation">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Net.NetworkInformation.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Net.NetworkInformation.dll</HintPath>
</Reference>
<Reference Include="System.Net.Ping">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Net.Ping.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Net.Ping.dll</HintPath>
</Reference>
<Reference Include="System.Net.Primitives">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Net.Primitives.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Net.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Net.Requests">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Net.Requests.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Net.Requests.dll</HintPath>
</Reference>
<Reference Include="System.Net.Security">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Net.Security.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Net.Security.dll</HintPath>
</Reference>
<Reference Include="System.Net.Sockets">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Net.Sockets.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Net.Sockets.dll</HintPath>
</Reference>
<Reference Include="System.Net.WebHeaderCollection">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Net.WebHeaderCollection.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Net.WebHeaderCollection.dll</HintPath>
</Reference>
<Reference Include="System.Net.WebSockets.Client">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Net.WebSockets.Client.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Net.WebSockets.Client.dll</HintPath>
</Reference>
<Reference Include="System.Net.WebSockets">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Net.WebSockets.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Net.WebSockets.dll</HintPath>
</Reference>
<Reference Include="System.Numerics.Vectors">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Numerics.Vectors.dll</HintPath>
</Reference>
<Reference Include="System.ObjectModel">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.ObjectModel.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.ObjectModel.dll</HintPath>
</Reference>
<Reference Include="System.Reflection.DispatchProxy">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Reflection.DispatchProxy.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Reflection.DispatchProxy.dll</HintPath>
</Reference>
<Reference Include="System.Reflection.Emit.ILGeneration">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Reflection.Emit.ILGeneration.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Reflection.Emit.ILGeneration.dll</HintPath>
</Reference>
<Reference Include="System.Reflection.Emit.Lightweight">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Reflection.Emit.Lightweight.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Reflection.Emit.Lightweight.dll</HintPath>
</Reference>
<Reference Include="System.Reflection.Emit">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Reflection.Emit.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Reflection.Emit.dll</HintPath>
</Reference>
<Reference Include="System.Reflection.Extensions">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Reflection.Extensions.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Reflection.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Reflection.Primitives">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Reflection.Primitives.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Reflection.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Reflection">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Reflection.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Reflection.dll</HintPath>
</Reference>
<Reference Include="System.Resources.Reader">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Resources.Reader.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Resources.Reader.dll</HintPath>
</Reference>
<Reference Include="System.Resources.ResourceManager">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Resources.ResourceManager.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Resources.ResourceManager.dll</HintPath>
</Reference>
<Reference Include="System.Resources.Writer">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Resources.Writer.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Resources.Writer.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.CompilerServices.VisualC">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Runtime.CompilerServices.VisualC.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Runtime.CompilerServices.VisualC.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Extensions">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Runtime.Extensions.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Runtime.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Handles">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Runtime.Handles.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Runtime.Handles.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.InteropServices.RuntimeInformation">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.InteropServices.WindowsRuntime">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Runtime.InteropServices.WindowsRuntime.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Runtime.InteropServices.RuntimeInformation.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.InteropServices">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Runtime.InteropServices.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Runtime.InteropServices.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Numerics">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Runtime.Numerics.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Runtime.Numerics.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization.Formatters">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Runtime.Serialization.Formatters.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Runtime.Serialization.Formatters.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization.Json">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Runtime.Serialization.Json.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Runtime.Serialization.Json.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization.Primitives">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Runtime.Serialization.Primitives.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Runtime.Serialization.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization.Xml">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Runtime.Serialization.Xml.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Runtime.Serialization.Xml.dll</HintPath>
</Reference>
<Reference Include="System.Runtime">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Runtime.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Runtime.dll</HintPath>
</Reference>
<Reference Include="System.Security.Claims">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Security.Claims.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Security.Claims.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Algorithms">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Security.Cryptography.Algorithms.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Security.Cryptography.Algorithms.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Csp">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Security.Cryptography.Csp.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Security.Cryptography.Csp.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Encoding">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Security.Cryptography.Encoding.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Security.Cryptography.Encoding.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.Primitives">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Security.Cryptography.Primitives.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Security.Cryptography.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.Security.Cryptography.X509Certificates">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Security.Cryptography.X509Certificates.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Security.Cryptography.X509Certificates.dll</HintPath>
</Reference>
<Reference Include="System.Security.Principal">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Security.Principal.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Security.Principal.dll</HintPath>
</Reference>
<Reference Include="System.Security.SecureString">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Security.SecureString.dll</HintPath>
</Reference>
<Reference Include="System.ServiceModel.Duplex">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.ServiceModel.Duplex.dll</HintPath>
</Reference>
<Reference Include="System.ServiceModel.Http">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.ServiceModel.Http.dll</HintPath>
</Reference>
<Reference Include="System.ServiceModel.NetTcp">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.ServiceModel.NetTcp.dll</HintPath>
</Reference>
<Reference Include="System.ServiceModel.Primitives">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.ServiceModel.Primitives.dll</HintPath>
</Reference>
<Reference Include="System.ServiceModel.Security">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.ServiceModel.Security.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Security.SecureString.dll</HintPath>
</Reference>
<Reference Include="System.Text.Encoding.Extensions">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Text.Encoding.Extensions.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Text.Encoding.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Text.Encoding">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Text.Encoding.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Text.Encoding.dll</HintPath>
</Reference>
<Reference Include="System.Text.RegularExpressions">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Text.RegularExpressions.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Text.RegularExpressions.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Overlapped">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Threading.Overlapped.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Threading.Overlapped.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks.Extensions">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Threading.Tasks.Extensions.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Threading.Tasks.Extensions.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks.Parallel">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Threading.Tasks.Parallel.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Threading.Tasks.Parallel.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Tasks">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Threading.Tasks.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Threading.Tasks.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Thread">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Threading.Thread.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Threading.Thread.dll</HintPath>
</Reference>
<Reference Include="System.Threading.ThreadPool">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Threading.ThreadPool.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Threading.ThreadPool.dll</HintPath>
</Reference>
<Reference Include="System.Threading.Timer">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Threading.Timer.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Threading.Timer.dll</HintPath>
</Reference>
<Reference Include="System.Threading">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Threading.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Threading.dll</HintPath>
</Reference>
<Reference Include="System.ValueTuple">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.ValueTuple.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.ValueTuple.dll</HintPath>
</Reference>
<Reference Include="System.Xml.ReaderWriter">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Xml.ReaderWriter.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Xml.ReaderWriter.dll</HintPath>
</Reference>
<Reference Include="System.Xml.XDocument">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Xml.XDocument.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Xml.XDocument.dll</HintPath>
</Reference>
<Reference Include="System.Xml.XPath.XDocument">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Xml.XPath.XDocument.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Xml.XPath.XDocument.dll</HintPath>
</Reference>
<Reference Include="System.Xml.XPath">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Xml.XPath.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Xml.XPath.dll</HintPath>
</Reference>
<Reference Include="System.Xml.XmlDocument">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Xml.XmlDocument.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Xml.XmlDocument.dll</HintPath>
</Reference>
<Reference Include="System.Xml.XmlSerializer">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/System.Xml.XmlSerializer.dll</HintPath>
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netstandard/System.Xml.XmlSerializer.dll</HintPath>
</Reference>
<Reference Include="netstandard">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/UnityReferenceAssemblies/unity-4.8-api/Facades/netstandard.dll</HintPath>
<Reference Include="System.Runtime.InteropServices.WindowsRuntime">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/Extensions/2.0.0/System.Runtime.InteropServices.WindowsRuntime.dll</HintPath>
</Reference>
<Reference Include="System.ComponentModel.Composition">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.ComponentModel.Composition.dll</HintPath>
</Reference>
<Reference Include="System.Core">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.Core.dll</HintPath>
</Reference>
<Reference Include="System.Data">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.Data.dll</HintPath>
</Reference>
<Reference Include="System.Drawing">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.Drawing.dll</HintPath>
</Reference>
<Reference Include="System.IO.Compression.FileSystem">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.IO.Compression.FileSystem.dll</HintPath>
</Reference>
<Reference Include="System.Net">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.Net.dll</HintPath>
</Reference>
<Reference Include="System.Numerics">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.Numerics.dll</HintPath>
</Reference>
<Reference Include="System.Runtime.Serialization">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.Runtime.Serialization.dll</HintPath>
</Reference>
<Reference Include="System.ServiceModel.Web">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.ServiceModel.Web.dll</HintPath>
</Reference>
<Reference Include="System.Transactions">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.Transactions.dll</HintPath>
</Reference>
<Reference Include="System.Web">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.Web.dll</HintPath>
</Reference>
<Reference Include="System.Windows">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.Windows.dll</HintPath>
</Reference>
<Reference Include="System.Xml.Linq">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.Xml.Linq.dll</HintPath>
</Reference>
<Reference Include="System.Xml.Serialization">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.Xml.Serialization.dll</HintPath>
</Reference>
<Reference Include="System.Xml">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.Xml.dll</HintPath>
</Reference>
<Reference Include="System">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/System.dll</HintPath>
</Reference>
<Reference Include="mscorlib">
<HintPath>/Applications/Unity/Hub/Editor/6000.3.0f1/Unity.app/Contents/Resources/Scripting/NetStandard/compat/2.1.0/shims/netfx/mscorlib.dll</HintPath>
</Reference>
<Reference Include="Analytics">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Analytics.dll</HintPath>
@@ -1317,9 +1308,6 @@
<Reference Include="Unity.TextMeshPro">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.TextMeshPro.dll</HintPath>
</Reference>
<Reference Include="Unity.ResourceManager">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.ResourceManager.dll</HintPath>
</Reference>
<Reference Include="Unity.VisualStudio.Editor">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.VisualStudio.Editor.dll</HintPath>
</Reference>
@@ -1332,15 +1320,9 @@
<Reference Include="Unity.AI.Navigation">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.AI.Navigation.dll</HintPath>
</Reference>
<Reference Include="Unity.Profiling.Core">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.Profiling.Core.dll</HintPath>
</Reference>
<Reference Include="Unity.Timeline.Editor">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.Timeline.Editor.dll</HintPath>
</Reference>
<Reference Include="Unity.ScriptableBuildPipeline">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.ScriptableBuildPipeline.dll</HintPath>
</Reference>
<Reference Include="Cysharp.Net.Http.YetAnotherHttpHandler">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Cysharp.Net.Http.YetAnotherHttpHandler.dll</HintPath>
</Reference>
@@ -1365,18 +1347,12 @@
<Reference Include="Unity.AI.Navigation.Updater">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.AI.Navigation.Updater.dll</HintPath>
</Reference>
<Reference Include="Unity.ScriptableBuildPipeline.Editor">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.ScriptableBuildPipeline.Editor.dll</HintPath>
<Reference Include="Unity.Multiplayer.Center.Editor">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.Multiplayer.Center.Editor.dll</HintPath>
</Reference>
<Reference Include="Unity.PlasticSCM.Editor">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.PlasticSCM.Editor.dll</HintPath>
</Reference>
<Reference Include="Unity.Addressables">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.Addressables.dll</HintPath>
</Reference>
<Reference Include="Unity.Multiplayer.Center.Editor">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.Multiplayer.Center.Editor.dll</HintPath>
</Reference>
<Reference Include="Unity.Services.Core.Components">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.Services.Core.Components.dll</HintPath>
</Reference>
@@ -1392,9 +1368,6 @@
<Reference Include="Unity.AI.Navigation.Editor.ConversionSystem">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.AI.Navigation.Editor.ConversionSystem.dll</HintPath>
</Reference>
<Reference Include="Unity.Addressables.Editor">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/Unity.Addressables.Editor.dll</HintPath>
</Reference>
<Reference Include="UnityEditor.UI">
<HintPath>/Users/dancrosby/CodingProjects/github/eagle0-unity-client/src/main/csharp/net/eagle0/clients/unity/eagle0/Library/ScriptAssemblies/UnityEditor.UI.dll</HintPath>
</Reference>

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