mirror of
https://github.com/nolen777/eagle0.git
synced 2026-07-29 06:15:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
30f8a9d8c6 |
@@ -1,11 +1,20 @@
|
||||
# bazel-1.0.0.bazelrc
|
||||
bazel-1.0.0.bazelrc
|
||||
|
||||
# for now: filter out annoying TASTY warnings
|
||||
common --ui_event_filters=-INFO
|
||||
|
||||
common --enable_bzlmod
|
||||
|
||||
# Use pre-built protoc binary instead of compiling from source
|
||||
common --incompatible_enable_proto_toolchain_resolution
|
||||
common --@com_google_protobuf//bazel/toolchains:prefer_prebuilt_protoc=true
|
||||
|
||||
# Don't use toolchains_llvm for the swift app build
|
||||
common:mactools --ignore_dev_dependency
|
||||
|
||||
# Try to speed up sandboxes
|
||||
common --experimental_reuse_sandbox_directories
|
||||
|
||||
common --enable_platform_specific_config
|
||||
|
||||
common --strategy=Scalac=worker
|
||||
@@ -16,66 +25,21 @@ common --jobs=64
|
||||
|
||||
common --cxxopt="--std=c++23"
|
||||
common --cxxopt="-Wno-deprecated-non-prototype"
|
||||
common --per_file_copt=src/test/cpp/.*@-Wno-character-conversion
|
||||
common --per_file_copt=src/test/cpp/.*@-Wno-deprecated-enum-compare
|
||||
common --per_file_copt=src/test/cpp/.*@-Wno-sign-compare
|
||||
common --per_file_copt=src/test/cpp/.*@-Wno-unknown-warning-option
|
||||
common --host_cxxopt="--std=c++23"
|
||||
|
||||
# C++ sanitizer configs for targeted Shardok safety checks.
|
||||
# Example: bazel test --config=asan //src/test/cpp/net/eagle0/shardok/library/...
|
||||
build:asan --compilation_mode=dbg
|
||||
build:asan --strip=never
|
||||
build:asan --copt=-fno-omit-frame-pointer
|
||||
build:asan --copt=-Wno-macro-redefined
|
||||
build:asan --copt=-fsanitize=address
|
||||
build:asan --linkopt=-fsanitize=address
|
||||
test:asan --test_env=ASAN_OPTIONS=detect_leaks=0:strict_init_order=1
|
||||
|
||||
build:ubsan --compilation_mode=dbg
|
||||
build:ubsan --strip=never
|
||||
build:ubsan --copt=-fno-omit-frame-pointer
|
||||
build:ubsan --copt=-Wno-macro-redefined
|
||||
build:ubsan --copt=-fsanitize=undefined
|
||||
build:ubsan --linkopt=-fsanitize=undefined
|
||||
test:ubsan --test_env=UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1
|
||||
|
||||
build:tsan --compilation_mode=dbg
|
||||
build:tsan --strip=never
|
||||
build:tsan --copt=-fno-omit-frame-pointer
|
||||
build:tsan --copt=-Wno-macro-redefined
|
||||
build:tsan --copt=-fsanitize=thread
|
||||
build:tsan --linkopt=-fsanitize=thread
|
||||
test:tsan --test_env=TSAN_OPTIONS=halt_on_error=1
|
||||
|
||||
common --javacopt="-Xlint:-options"
|
||||
|
||||
# Prefer protobuf's prebuilt protoc toolchain instead of building protoc from
|
||||
# source when compatible binaries are available.
|
||||
common --@com_google_protobuf//bazel/toolchains:prefer_prebuilt_protoc
|
||||
|
||||
# std::filesystem and other modern C++ deps require macOS 10.15+.
|
||||
common:macos --macos_minimum_os=10.15
|
||||
common:macos --host_macos_minimum_os=10.15
|
||||
|
||||
# suppress warnings due to https://developer.apple.com/forums/thread/733317
|
||||
# Use host_linkopt for macOS-specific flags to avoid passing them to Linux cross-compilation
|
||||
common:macos --host_linkopt=-Wl,-no_warn_duplicate_libraries
|
||||
|
||||
# Pin default DEVELOPER_DIR so Apple repo rules use full Xcode instead of
|
||||
# Command Line Tools when they are needed.
|
||||
# Workaround for grpc cf_event_engine build error on macOS with Bazel 7.x
|
||||
# See: https://github.com/grpc/grpc/issues/37619
|
||||
common:macos --features=-module_maps
|
||||
|
||||
# Fix Xcode version caching issue - avoids need for `bazel clean --expunge` after Xcode updates
|
||||
common:macos --repo_env=DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer
|
||||
|
||||
# Local machine or CI overrides. This lets Bazel-only runners use Command Line
|
||||
# Tools without installing full Xcode. This must stay before .bazelrc.xcode so
|
||||
# generated Xcode settings win when both files exist.
|
||||
try-import %workspace%/.bazelrc.local
|
||||
|
||||
# Xcode config for Apple tool builds. Generated by scripts/sync_bazel_xcode.sh.
|
||||
# Bakes the Xcode build version into mactools action cache keys without making
|
||||
# ordinary macOS Bazel builds depend on the installed Xcode build number.
|
||||
try-import %workspace%/.bazelrc.xcode
|
||||
|
||||
common --java_language_version=25
|
||||
common --java_runtime_version=remotejdk_25
|
||||
common --tool_java_language_version=25
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
9.1.1
|
||||
8.5.1
|
||||
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
Checks: >
|
||||
-*,
|
||||
bugprone-*,
|
||||
performance-*,
|
||||
readability-*,
|
||||
modernize-*,
|
||||
cppcoreguidelines-*,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-cppcoreguidelines-avoid-magic-numbers,
|
||||
-cppcoreguidelines-macro-usage,
|
||||
-cppcoreguidelines-owning-memory,
|
||||
-cppcoreguidelines-pro-bounds-array-to-pointer-decay,
|
||||
-cppcoreguidelines-pro-bounds-constant-array-index,
|
||||
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
|
||||
-cppcoreguidelines-pro-type-const-cast,
|
||||
-cppcoreguidelines-pro-type-reinterpret-cast,
|
||||
-cppcoreguidelines-pro-type-union-access,
|
||||
-cppcoreguidelines-pro-type-vararg,
|
||||
-modernize-use-trailing-return-type,
|
||||
-readability-convert-member-functions-to-static,
|
||||
-readability-function-cognitive-complexity,
|
||||
-readability-identifier-length,
|
||||
-readability-magic-numbers
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: 'src/(main|test)/cpp/net/eagle0/(shardok|common)/.*'
|
||||
FormatStyle: file
|
||||
CheckOptions:
|
||||
readability-braces-around-statements.ShortStatementLines: '1'
|
||||
readability-function-size.LineThreshold: '160'
|
||||
readability-function-size.StatementThreshold: '80'
|
||||
readability-function-size.BranchThreshold: '20'
|
||||
modernize-use-nullptr.NullMacros: 'NULL'
|
||||
@@ -5,26 +5,12 @@
|
||||
*.tif filter=lfs diff=lfs merge=lfs -text
|
||||
*.bytes filter=lfs diff=lfs merge=lfs -text
|
||||
*.psd filter=lfs diff=lfs merge=lfs -text
|
||||
*.psb filter=lfs diff=lfs merge=lfs -text
|
||||
# PSB meta files contain bone/sprite import data and can be very large
|
||||
*.psb.meta filter=lfs diff=lfs merge=lfs -text
|
||||
*.ttf filter=lfs diff=lfs merge=lfs -text
|
||||
# Exclude pre-existing font files that were committed as blobs (not LFS pointers)
|
||||
src/main/csharp/**/GUI[[:space:]]Pro[[:space:]]Kit*/**/*.ttf !filter !diff !merge
|
||||
src/main/csharp/**/Modern[[:space:]]UI[[:space:]]Pack/**/*.ttf !filter !diff !merge
|
||||
*.herodata filter=lfs diff=lfs merge=lfs -text
|
||||
*.ogg filter=lfs diff=lfs merge=lfs -text
|
||||
*.tga filter=lfs diff=lfs merge=lfs -text
|
||||
*.fbx filter=lfs diff=lfs merge=lfs -text
|
||||
*.FBX filter=lfs diff=lfs merge=lfs -text
|
||||
*.pdf filter=lfs diff=lfs merge=lfs -text
|
||||
# Third-party asset packs: track all files via LFS (never manually edited)
|
||||
src/main/csharp/**/DungeonMonsters2D/** filter=lfs diff=lfs merge=lfs -text
|
||||
src/main/csharp/**/Raccoon/** filter=lfs diff=lfs merge=lfs -text
|
||||
src/main/csharp/**/Polytope[[:space:]]Studio/** filter=lfs diff=lfs merge=lfs -text
|
||||
src/main/csharp/**/RRFreelance-Characters/** filter=lfs diff=lfs merge=lfs -text
|
||||
src/main/csharp/**/Dragon/** filter=lfs diff=lfs merge=lfs -text
|
||||
*.jpg filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
# Unity Smart Merge - use UnityYAMLMerge for Unity files
|
||||
# Requires configuring unityyamlmerge in ~/.gitconfig:
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
name: Setup Bazel
|
||||
description: Ensure Bazel is installed on a self-hosted runner.
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Ensure Bazel installed
|
||||
shell: bash
|
||||
run: ./ci/github_actions/ensure_bazel_installed.sh
|
||||
@@ -0,0 +1,39 @@
|
||||
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:"
|
||||
# Save to temp file to avoid SIGPIPE/broken pipe errors with head
|
||||
gh api "repos/${{ github.repository }}/actions/artifacts" \
|
||||
--paginate -q '.artifacts[] | "\(.size_in_bytes)\t\(.name)\t\(.created_at)"' > /tmp/artifacts.txt
|
||||
sort -rn /tmp/artifacts.txt | head -20 | \
|
||||
awk -F'\t' '{printf "%d MB\t%s\t%s\n", $1/1024/1024, $2, $3}'
|
||||
rm -f /tmp/artifacts.txt
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Storage is within acceptable limits."
|
||||
@@ -6,38 +6,11 @@ on:
|
||||
paths:
|
||||
- 'src/main/go/net/eagle0/authservice/**'
|
||||
- 'src/main/go/net/eagle0/authcli/**'
|
||||
- 'src/main/go/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/util/**'
|
||||
- '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/net/eagle0/attributions.json'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/auth_build.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/go/net/eagle0/authservice/**'
|
||||
- 'src/main/go/net/eagle0/authcli/**'
|
||||
- 'src/main/go/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/util/**'
|
||||
- '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/net/eagle0/attributions.json'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/auth_build.yml'
|
||||
workflow_dispatch:
|
||||
@@ -51,45 +24,17 @@ on:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || 'deploy' }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
jobs:
|
||||
build-auth:
|
||||
runs-on: [self-hosted, bazel]
|
||||
outputs:
|
||||
image_tag: ${{ steps.push-auth.outputs.image_tag }}
|
||||
steps:
|
||||
- name: Prepare non-LFS checkout
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
run: |
|
||||
git config --global --unset-all filter.lfs.process || true
|
||||
git config --global filter.lfs.smudge "cat"
|
||||
git config --global filter.lfs.clean "cat"
|
||||
git config --global filter.lfs.required false
|
||||
if [ -d ".git" ]; then
|
||||
git config --local --unset-all filter.lfs.process || true
|
||||
git config --local filter.lfs.smudge "cat"
|
||||
git config --local filter.lfs.clean "cat"
|
||||
git config --local filter.lfs.required false
|
||||
fi
|
||||
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Build Auth Server Docker image
|
||||
id: build-auth
|
||||
run: |
|
||||
@@ -182,12 +127,10 @@ jobs:
|
||||
FASTMAIL_FROM_NAME: ${{ secrets.FASTMAIL_FROM_NAME }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy auth service to production
|
||||
uses: appleboy/ssh-action@v1.2.5
|
||||
uses: appleboy/ssh-action@v1.0.3
|
||||
with:
|
||||
host: ${{ secrets.DO_DROPLET_IP }}
|
||||
username: deploy
|
||||
@@ -195,7 +138,7 @@ jobs:
|
||||
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
|
||||
script: |
|
||||
set -e
|
||||
set -x
|
||||
cd /opt/eagle0
|
||||
|
||||
# Export env vars for docker compose (appleboy/ssh-action sets them but doesn't export)
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
name: Bazel Cache Cleanup
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run weekly on Sunday at 00:00 UTC
|
||||
- cron: '0 0 * * 0'
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Show disk usage before cleanup
|
||||
run: |
|
||||
echo "=== Disk usage before cleanup ==="
|
||||
BAZEL_USER_ROOT="/private/var/tmp/_bazel_$(whoami)"
|
||||
echo "Bazel user root: $BAZEL_USER_ROOT"
|
||||
du -sh "$BAZEL_USER_ROOT" 2>/dev/null || echo "Not found"
|
||||
df -h .
|
||||
|
||||
- name: Run bazel clean
|
||||
run: |
|
||||
echo "=== Running bazel clean ==="
|
||||
bazel clean
|
||||
echo "Clean complete"
|
||||
|
||||
- name: Show disk usage after cleanup
|
||||
run: |
|
||||
echo "=== Disk usage after cleanup ==="
|
||||
BAZEL_USER_ROOT="/private/var/tmp/_bazel_$(whoami)"
|
||||
du -sh "$BAZEL_USER_ROOT" 2>/dev/null || echo "Not found"
|
||||
df -h .
|
||||
@@ -1,136 +0,0 @@
|
||||
name: Bazel Cache Maintenance
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
description: 'Maintenance target'
|
||||
required: true
|
||||
default: 'parity'
|
||||
type: choice
|
||||
options:
|
||||
- parity
|
||||
- clean
|
||||
schedule:
|
||||
- cron: '17 11 * * *'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
warm-cache:
|
||||
if: github.event_name == 'schedule' || github.event.inputs.target == 'parity'
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- name: Clean workspace
|
||||
run: |
|
||||
git sparse-checkout disable 2>/dev/null || true
|
||||
git config --local core.sparseCheckout false 2>/dev/null || true
|
||||
git config --local --unset extensions.worktreeConfig 2>/dev/null || true
|
||||
rm -f .git/info/sparse-checkout .git/config.worktree 2>/dev/null || true
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
export PATH="$path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v git-lfs >/dev/null 2>&1; then
|
||||
git-lfs version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
- name: Warm remote cache
|
||||
run: ./scripts/check_bazel_remote_cache_parity.sh warm
|
||||
|
||||
verify-cache:
|
||||
needs: warm-cache
|
||||
if: github.event_name == 'schedule' || github.event.inputs.target == 'parity'
|
||||
runs-on: [self-hosted, bazel]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
probe: [a, b]
|
||||
|
||||
steps:
|
||||
- name: Clean workspace
|
||||
run: |
|
||||
git sparse-checkout disable 2>/dev/null || true
|
||||
git config --local core.sparseCheckout false 2>/dev/null || true
|
||||
git config --local --unset extensions.worktreeConfig 2>/dev/null || true
|
||||
rm -f .git/info/sparse-checkout .git/config.worktree 2>/dev/null || true
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
export PATH="$path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v git-lfs >/dev/null 2>&1; then
|
||||
git-lfs version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
- name: Verify remote cache parity
|
||||
run: ./scripts/check_bazel_remote_cache_parity.sh verify
|
||||
|
||||
clean-cache:
|
||||
if: github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'clean'
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
- name: Show disk usage before cleanup
|
||||
run: |
|
||||
echo "=== Disk usage before cleanup ==="
|
||||
BAZEL_USER_ROOT="/private/var/tmp/_bazel_$(whoami)"
|
||||
echo "Bazel user root: $BAZEL_USER_ROOT"
|
||||
du -sh "$BAZEL_USER_ROOT" 2>/dev/null || echo "Not found"
|
||||
df -h .
|
||||
|
||||
- name: Run bazel clean
|
||||
run: |
|
||||
echo "=== Running bazel clean ==="
|
||||
bazel clean
|
||||
echo "Clean complete"
|
||||
|
||||
- name: Show disk usage after cleanup
|
||||
run: |
|
||||
echo "=== Disk usage after cleanup ==="
|
||||
BAZEL_USER_ROOT="/private/var/tmp/_bazel_$(whoami)"
|
||||
du -sh "$BAZEL_USER_ROOT" 2>/dev/null || echo "Not found"
|
||||
df -h .
|
||||
@@ -7,12 +7,8 @@ on:
|
||||
- 'src/**'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'ci/github_actions/summarize_bazel_bep.py'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/bazel_test.yml'
|
||||
- '!src/main/csharp/**'
|
||||
- '!src/test/csharp/**'
|
||||
@@ -21,12 +17,8 @@ on:
|
||||
- 'src/**'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'ci/github_actions/summarize_bazel_bep.py'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/bazel_test.yml'
|
||||
- '!src/main/csharp/**'
|
||||
- '!src/test/csharp/**'
|
||||
@@ -48,32 +40,14 @@ jobs:
|
||||
git config --local core.sparseCheckout false 2>/dev/null || true
|
||||
git config --local --unset extensions.worktreeConfig 2>/dev/null || true
|
||||
rm -f .git/info/sparse-checkout .git/config.worktree 2>/dev/null || true
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
export PATH="$path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v git-lfs >/dev/null 2>&1; then
|
||||
git-lfs version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Check BUILD.bazel dependencies
|
||||
run: ./scripts/check_build_deps.sh --strict
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v6
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 'lts/*'
|
||||
- name: Check JavaScript syntax
|
||||
@@ -88,35 +62,12 @@ jobs:
|
||||
git config --local core.sparseCheckout false 2>/dev/null || true
|
||||
git config --local --unset extensions.worktreeConfig 2>/dev/null || true
|
||||
rm -f .git/info/sparse-checkout .git/config.worktree 2>/dev/null || true
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
export PATH="$path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v git-lfs >/dev/null 2>&1; then
|
||||
git-lfs version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Setup Bazel
|
||||
uses: ./.github/actions/setup-bazel
|
||||
- name: Run tests
|
||||
run: bazel test --build_event_json_file=test.json //src/test/... //src/main/go/...
|
||||
- name: Summarize Bazel build metrics
|
||||
if: always()
|
||||
run: python3 ci/github_actions/summarize_bazel_bep.py test.json
|
||||
- name: Collect failed test logs
|
||||
if: always()
|
||||
run: |
|
||||
@@ -151,16 +102,16 @@ jobs:
|
||||
ls -lh failed_test_logs/ 2>/dev/null || echo "No logs collected"
|
||||
- name: Archive test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: test.json
|
||||
path: test.json
|
||||
retention-days: 3
|
||||
retention-days: 5
|
||||
- name: Archive failed test logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: failed-test-logs
|
||||
path: failed_test_logs/
|
||||
if-no-files-found: ignore
|
||||
retention-days: 3
|
||||
retention-days: 5
|
||||
|
||||
@@ -22,36 +22,20 @@ permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build-sysroot:
|
||||
name: Build Linux Sysroot (${{ matrix.architecture }})
|
||||
build-sysroot-amd64:
|
||||
if: ${{ inputs.architecture == 'amd64' || inputs.architecture == 'both' }}
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(inputs.architecture == 'both' && '{"include":[{"architecture":"amd64","build_script":"./tools/sysroot/build_sysroot.sh","artifact_name":"ubuntu-noble-sysroot-amd64","archive_name":"ubuntu_noble_amd64_sysroot.tar.xz","checksum_name":"ubuntu_noble_amd64_sysroot.sha256","module_name":"linux_sysroot"},{"architecture":"arm64","build_script":"./tools/sysroot/build_sysroot_arm64.sh","artifact_name":"ubuntu-noble-sysroot-arm64","archive_name":"ubuntu_noble_arm64_sysroot.tar.xz","checksum_name":"ubuntu_noble_arm64_sysroot.sha256","module_name":"linux_sysroot_arm64"}]}' || inputs.architecture == 'arm64' && '{"include":[{"architecture":"arm64","build_script":"./tools/sysroot/build_sysroot_arm64.sh","artifact_name":"ubuntu-noble-sysroot-arm64","archive_name":"ubuntu_noble_arm64_sysroot.tar.xz","checksum_name":"ubuntu_noble_arm64_sysroot.sha256","module_name":"linux_sysroot_arm64"}]}' || '{"include":[{"architecture":"amd64","build_script":"./tools/sysroot/build_sysroot.sh","artifact_name":"ubuntu-noble-sysroot-amd64","archive_name":"ubuntu_noble_amd64_sysroot.tar.xz","checksum_name":"ubuntu_noble_amd64_sysroot.sha256","module_name":"linux_sysroot"}]}') }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up QEMU for ARM64 emulation
|
||||
if: ${{ matrix.architecture == 'arm64' }}
|
||||
uses: docker/setup-qemu-action@v4
|
||||
with:
|
||||
platforms: arm64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: ${{ matrix.architecture == 'arm64' }}
|
||||
uses: docker/setup-buildx-action@v4
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Build sysroot
|
||||
run: ${{ matrix.build_script }}
|
||||
run: ./tools/sysroot/build_sysroot.sh
|
||||
|
||||
- name: Upload sysroot artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.artifact_name }}
|
||||
name: ubuntu-noble-sysroot-amd64
|
||||
path: tools/sysroot/output/
|
||||
retention-days: 1
|
||||
|
||||
@@ -69,25 +53,87 @@ jobs:
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: |
|
||||
# Upload sysroot tarball to DO Spaces
|
||||
aws s3 cp "tools/sysroot/output/${{ matrix.archive_name }}" \
|
||||
"s3://eagle0-sysroot/${{ inputs.version }}/${{ matrix.archive_name }}" \
|
||||
aws s3 cp tools/sysroot/output/ubuntu_noble_amd64_sysroot.tar.xz \
|
||||
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz \
|
||||
--endpoint-url https://sfo3.digitaloceanspaces.com \
|
||||
--acl public-read
|
||||
|
||||
# Upload sha256 file
|
||||
aws s3 cp "tools/sysroot/output/${{ matrix.checksum_name }}" \
|
||||
"s3://eagle0-sysroot/${{ inputs.version }}/${{ matrix.checksum_name }}" \
|
||||
aws s3 cp tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256 \
|
||||
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.sha256 \
|
||||
--endpoint-url https://sfo3.digitaloceanspaces.com \
|
||||
--acl public-read
|
||||
|
||||
echo ""
|
||||
echo "=== ${{ matrix.architecture }} Sysroot uploaded ==="
|
||||
echo "URL: https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/${{ matrix.archive_name }}"
|
||||
echo "SHA256: $(cat "tools/sysroot/output/${{ matrix.checksum_name }}")"
|
||||
echo "=== AMD64 Sysroot uploaded ==="
|
||||
echo "URL: https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz"
|
||||
echo "SHA256: $(cat tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256)"
|
||||
echo ""
|
||||
echo "Update MODULE.bazel with:"
|
||||
echo "sysroot("
|
||||
echo " name = \"${{ matrix.module_name }}\","
|
||||
echo " sha256 = \"$(cat "tools/sysroot/output/${{ matrix.checksum_name }}")\","
|
||||
echo " urls = [\"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/${{ matrix.archive_name }}\"],"
|
||||
echo " name = \"linux_sysroot\","
|
||||
echo " sha256 = \"$(cat tools/sysroot/output/ubuntu_noble_amd64_sysroot.sha256)\","
|
||||
echo " urls = [\"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_amd64_sysroot.tar.xz\"],"
|
||||
echo ")"
|
||||
|
||||
build-sysroot-arm64:
|
||||
if: ${{ inputs.architecture == 'arm64' || inputs.architecture == 'both' }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU for ARM64 emulation
|
||||
uses: docker/setup-qemu-action@v3
|
||||
with:
|
||||
platforms: arm64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build ARM64 sysroot
|
||||
run: ./tools/sysroot/build_sysroot_arm64.sh
|
||||
|
||||
- name: Upload sysroot artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ubuntu-noble-sysroot-arm64
|
||||
path: tools/sysroot/output/
|
||||
retention-days: 1
|
||||
|
||||
- name: Install AWS CLI
|
||||
run: |
|
||||
if ! command -v aws &> /dev/null; then
|
||||
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
|
||||
unzip -q awscliv2.zip
|
||||
sudo ./aws/install
|
||||
fi
|
||||
|
||||
- name: Upload to DigitalOcean Spaces
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: |
|
||||
# Upload sysroot tarball to DO Spaces
|
||||
aws s3 cp tools/sysroot/output/ubuntu_noble_arm64_sysroot.tar.xz \
|
||||
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz \
|
||||
--endpoint-url https://sfo3.digitaloceanspaces.com \
|
||||
--acl public-read
|
||||
|
||||
# Upload sha256 file
|
||||
aws s3 cp tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256 \
|
||||
s3://eagle0-sysroot/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.sha256 \
|
||||
--endpoint-url https://sfo3.digitaloceanspaces.com \
|
||||
--acl public-read
|
||||
|
||||
echo ""
|
||||
echo "=== ARM64 Sysroot uploaded ==="
|
||||
echo "URL: https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz"
|
||||
echo "SHA256: $(cat tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256)"
|
||||
echo ""
|
||||
echo "Update MODULE.bazel with:"
|
||||
echo "sysroot("
|
||||
echo " name = \"linux_sysroot_arm64\","
|
||||
echo " sha256 = \"$(cat tools/sysroot/output/ubuntu_noble_arm64_sysroot.sha256)\","
|
||||
echo " urls = [\"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/${{ inputs.version }}/ubuntu_noble_arm64_sysroot.tar.xz\"],"
|
||||
echo ")"
|
||||
|
||||
@@ -18,34 +18,7 @@ on:
|
||||
- '!src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
|
||||
- 'src/main/resources/**'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- '.bazelrc'
|
||||
- 'docker-compose.prod.yml'
|
||||
- 'nginx/**'
|
||||
- '.github/workflows/docker_build.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
# Note: Auth changes trigger auth_build.yml instead
|
||||
# Note: Windows installer changes trigger installer_build.yml instead
|
||||
- 'src/main/go/net/eagle0/admin_server/**'
|
||||
- 'src/main/go/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/util/**'
|
||||
- 'src/main/scala/**'
|
||||
- 'src/main/protobuf/**'
|
||||
- '!src/main/protobuf/net/eagle0/eagle/api/auth.proto'
|
||||
- '!src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
|
||||
- 'src/main/resources/**'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'ci/github_actions/fetch_lfs.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- '.bazelrc'
|
||||
- 'docker-compose.prod.yml'
|
||||
- 'nginx/**'
|
||||
@@ -62,12 +35,12 @@ on:
|
||||
# The build-all job checks if it's still the latest commit on main and skips if not,
|
||||
# so intermediate commits don't waste time building when a newer one is already queued.
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || 'deploy' }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
group: docker-build-deploy
|
||||
cancel-in-progress: false
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write # Required to delete artifacts after deploy
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
# Single consolidated build job - builds all images with one bazel invocation
|
||||
@@ -79,10 +52,8 @@ jobs:
|
||||
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 }}
|
||||
skip_build: ${{ steps.check-latest.outputs.skip }}
|
||||
steps:
|
||||
- name: Skip if superseded
|
||||
if: github.event_name != 'pull_request'
|
||||
id: check-latest
|
||||
run: |
|
||||
# Check if there's a newer run of this workflow waiting in the queue.
|
||||
@@ -97,39 +68,15 @@ jobs:
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Ensure Git LFS available for checkout
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
run: |
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
fi
|
||||
done
|
||||
if ! command -v git-lfs >/dev/null 2>&1; then
|
||||
brew install git-lfs
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- name: Checkout repository
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Fetch LFS files needed for admin server
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: ./ci/github_actions/fetch_lfs.sh --include="src/main/go/net/eagle0/admin_server/static/tiles/*"
|
||||
run: git lfs pull --include="src/main/go/net/eagle0/admin_server/static/tiles/*"
|
||||
|
||||
- name: Build all Docker images
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
@@ -144,7 +91,7 @@ jobs:
|
||||
bazel build \
|
||||
--stamp \
|
||||
--platforms=//:linux_x86_64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux//:cc-toolchain-x86_64-linux \
|
||||
--extra_toolchains=@llvm_toolchain_linux//:all \
|
||||
//ci:eagle_server_image \
|
||||
//ci:admin_server_image \
|
||||
//ci:jfr_sidecar_image \
|
||||
@@ -169,8 +116,8 @@ jobs:
|
||||
echo "JFR Sidecar: $JFR_PATH"
|
||||
|
||||
- name: Upload warmup binary
|
||||
if: steps.check-latest.outputs.skip != 'true' && github.event_name != 'pull_request'
|
||||
uses: actions/upload-artifact@v7
|
||||
if: steps.check-latest.outputs.skip != 'true'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: warmup-binary
|
||||
path: scripts/bin/warmup
|
||||
@@ -189,7 +136,7 @@ jobs:
|
||||
|
||||
- name: Push all images to DO registry
|
||||
id: push-images
|
||||
if: steps.check-latest.outputs.skip != 'true' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true'))
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
env:
|
||||
DOCKER_CONFIG: ${{ github.workspace }}/.docker
|
||||
run: |
|
||||
@@ -255,9 +202,9 @@ jobs:
|
||||
echo "=== All images pushed successfully ==="
|
||||
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, bazel]
|
||||
needs: [build-all]
|
||||
if: needs.build-all.outputs.skip_build != 'true' && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true'))
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
environment: production
|
||||
env:
|
||||
EAGLE_IMAGE: ${{ needs.build-all.outputs.eagle_image_tag }}
|
||||
@@ -268,7 +215,6 @@ jobs:
|
||||
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
|
||||
GPT_MODEL_NAME: ${{ secrets.GPT_MODEL_NAME }}
|
||||
EAGLE_ENABLE_S3: ${{ secrets.EAGLE_ENABLE_S3 }}
|
||||
DO_SPACES_ENDPOINT: ${{ secrets.DO_SPACES_ENDPOINT }}
|
||||
DO_SPACES_ACCESS_KEY: ${{ secrets.DO_SPACES_ACCESS_KEY }}
|
||||
DO_SPACES_SECRET_KEY: ${{ secrets.DO_SPACES_SECRET_KEY }}
|
||||
# Admin server uses different credentials (for eagle0-assets bucket)
|
||||
@@ -293,37 +239,14 @@ jobs:
|
||||
FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }}
|
||||
FASTMAIL_FROM_EMAIL: ${{ secrets.FASTMAIL_FROM_EMAIL }}
|
||||
FASTMAIL_FROM_NAME: ${{ secrets.FASTMAIL_FROM_NAME }}
|
||||
EAGLE_HISTORY_BACKEND: ${{ secrets.EAGLE_HISTORY_BACKEND }}
|
||||
EAGLE_POSTGRES_HOST: ${{ secrets.EAGLE_POSTGRES_HOST }}
|
||||
EAGLE_POSTGRES_PORT: ${{ secrets.EAGLE_POSTGRES_PORT }}
|
||||
EAGLE_POSTGRES_DATABASE: ${{ secrets.EAGLE_POSTGRES_DATABASE }}
|
||||
EAGLE_POSTGRES_USER: ${{ secrets.EAGLE_POSTGRES_USER }}
|
||||
EAGLE_POSTGRES_PASSWORD: ${{ secrets.EAGLE_POSTGRES_PASSWORD }}
|
||||
DO_DROPLET_IP: ${{ secrets.DO_DROPLET_IP }}
|
||||
DO_REGISTRY_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
GITHUB_TOKEN_FOR_ADMIN: ${{ secrets.ADMIN_GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
fi
|
||||
done
|
||||
if ! command -v git-lfs >/dev/null 2>&1; then
|
||||
brew install git-lfs
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
- name: Setup SSH key
|
||||
@@ -334,7 +257,7 @@ jobs:
|
||||
ssh-keyscan -H "$DO_DROPLET_IP" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
|
||||
- name: Download warmup binary
|
||||
uses: actions/download-artifact@v8
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: warmup-binary
|
||||
path: scripts/bin/
|
||||
@@ -357,7 +280,7 @@ jobs:
|
||||
- name: Deploy to production droplet
|
||||
run: |
|
||||
ssh -i ~/.ssh/deploy_key deploy@"$DO_DROPLET_IP" bash -s << DEPLOY_SCRIPT
|
||||
set -e
|
||||
set -ex
|
||||
cd /opt/eagle0
|
||||
|
||||
# =================================================================
|
||||
@@ -395,10 +318,6 @@ jobs:
|
||||
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
|
||||
validate_env "EAGLE_POSTGRES_HOST" "${EAGLE_POSTGRES_HOST}" "" || VALIDATION_FAILED=1
|
||||
validate_env "EAGLE_POSTGRES_DATABASE" "${EAGLE_POSTGRES_DATABASE}" "" || VALIDATION_FAILED=1
|
||||
validate_env "EAGLE_POSTGRES_USER" "${EAGLE_POSTGRES_USER}" "" || VALIDATION_FAILED=1
|
||||
validate_env "EAGLE_POSTGRES_PASSWORD" "${EAGLE_POSTGRES_PASSWORD}" "" || VALIDATION_FAILED=1
|
||||
|
||||
if [ "\${VALIDATION_FAILED}" -eq 1 ]; then
|
||||
echo ""
|
||||
@@ -446,12 +365,6 @@ jobs:
|
||||
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
|
||||
export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
|
||||
export FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
|
||||
export EAGLE_HISTORY_BACKEND="${EAGLE_HISTORY_BACKEND:-postgres}"
|
||||
export EAGLE_POSTGRES_HOST="${EAGLE_POSTGRES_HOST}"
|
||||
export EAGLE_POSTGRES_PORT="${EAGLE_POSTGRES_PORT}"
|
||||
export EAGLE_POSTGRES_DATABASE="${EAGLE_POSTGRES_DATABASE}"
|
||||
export EAGLE_POSTGRES_USER="${EAGLE_POSTGRES_USER}"
|
||||
export EAGLE_POSTGRES_PASSWORD="${EAGLE_POSTGRES_PASSWORD}"
|
||||
export DO_REGISTRY_TOKEN="${DO_REGISTRY_TOKEN}"
|
||||
export NOTIFY_SECRET="${NOTIFY_SECRET}"
|
||||
export GITHUB_TOKEN="${GITHUB_TOKEN_FOR_ADMIN}"
|
||||
@@ -558,19 +471,3 @@ jobs:
|
||||
# Remove images older than 24h (keeps recent images for rollback)
|
||||
docker image prune -a -f --filter "until=24h" || true
|
||||
DEPLOY_SCRIPT
|
||||
|
||||
cleanup:
|
||||
needs: [build-all, deploy]
|
||||
if: always() && github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Delete warmup-binary artifact
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
ARTIFACT_ID=$(gh api "repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts" \
|
||||
-q '.artifacts[] | select(.name == "warmup-binary") | .id')
|
||||
if [ -n "$ARTIFACT_ID" ]; then
|
||||
echo "Deleting warmup-binary artifact (ID: $ARTIFACT_ID)"
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$ARTIFACT_ID" || true
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
name: Eagle Build
|
||||
|
||||
on:
|
||||
# Main pushes are covered by docker_build.yml which builds the same target
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/scala/**'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/**'
|
||||
- 'src/main/protobuf/net/eagle0/common/**'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/eagle_build.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
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
|
||||
@@ -5,16 +5,10 @@ on:
|
||||
branches: [ "main" ]
|
||||
paths:
|
||||
- ".github/workflows/installer_build.yml"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- ".bazelrc"
|
||||
- "src/main/go/net/eagle0/clients/win/installer/**"
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/installer_build.yml"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- ".bazelrc"
|
||||
- "src/main/go/net/eagle0/clients/win/installer/**"
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -27,32 +21,11 @@ jobs:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
export PATH="$path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v git-lfs >/dev/null 2>&1; then
|
||||
git-lfs version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
clean: false
|
||||
|
||||
- name: Setup Bazel
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Build Go installer for Windows
|
||||
env:
|
||||
MANIFEST_PUBLIC_KEY: ${{ secrets.MANIFEST_PUBLIC_KEY }}
|
||||
@@ -77,7 +50,7 @@ jobs:
|
||||
|
||||
- name: Archive installer binary
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: eagle-installer
|
||||
path: ./installer-output/
|
||||
@@ -139,29 +112,14 @@ jobs:
|
||||
- name: Delete all installer artifacts
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Delete ALL eagle-installer artifacts to free up storage
|
||||
echo "Fetching all eagle-installer artifacts..."
|
||||
page=1
|
||||
while true; do
|
||||
response=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts?per_page=100&page=$page")
|
||||
ids=$(echo "$response" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for a in data.get('artifacts', []):
|
||||
if a['name'] == 'eagle-installer':
|
||||
print(a['id'])
|
||||
" 2>/dev/null)
|
||||
if [ -z "$ids" ]; then
|
||||
break
|
||||
fi
|
||||
for id in $ids; do
|
||||
echo "Deleting artifact ID: $id"
|
||||
curl -s -X DELETE -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/artifacts/$id" || true
|
||||
done
|
||||
page=$((page + 1))
|
||||
artifact_ids=$(gh api "repos/${{ github.repository }}/actions/artifacts" \
|
||||
--paginate -q '.artifacts[] | select(.name == "eagle-installer") | .id')
|
||||
for id in $artifact_ids; do
|
||||
echo "Deleting artifact ID: $id"
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$id" || true
|
||||
done
|
||||
echo "Cleanup complete"
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
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:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
# Runner-specific build directory to allow parallel builds on multiple runners
|
||||
EAGLE0_BUILD_DIR: /tmp/eagle0-${{ github.run_id }}
|
||||
|
||||
jobs:
|
||||
build-ios-addressables:
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
run: |
|
||||
# Self-hosted runners persist .git between runs. When a PR is updated,
|
||||
# old local refs (refs/remotes/pull/*/merge) may point to commits whose
|
||||
# objects were never fetched or have been pruned. Remove these stale refs
|
||||
# before checkout to prevent "missing object" errors.
|
||||
if [ -d ".git" ]; then
|
||||
echo "Pruning stale PR refs..."
|
||||
git for-each-ref --format='%(refname)' refs/remotes/pull/ 2>/dev/null | \
|
||||
xargs -r git update-ref -d 2>/dev/null || true
|
||||
fi
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
rm -rf src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee/
|
||||
|
||||
- name: Fetch LFS files
|
||||
run: |
|
||||
git lfs install
|
||||
git lfs pull
|
||||
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh ios
|
||||
|
||||
- name: Build iOS Addressables
|
||||
run: ./ci/github_actions/build_ios_addressables.sh "$EAGLE0_BUILD_DIR/editor_ios_addressables.log"
|
||||
|
||||
- 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: Purge CDN cache for iOS addressables
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
env:
|
||||
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
||||
run: |
|
||||
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
||||
-H "Authorization: Bearer $DO_CDN_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"files": ["addressables/iOS/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
|
||||
- name: Archive Build Log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: editor_ios_addressables.log
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_ios_addressables.log
|
||||
retention-days: 5
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
@@ -1,8 +1,6 @@
|
||||
name: iOS TestFlight
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 5 * * *' # 9 PM Pacific (UTC-8) / 10 PM PDT (UTC-7)
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
skip_upload:
|
||||
@@ -13,7 +11,6 @@ on:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
|
||||
env:
|
||||
# Runner-specific build directory to allow parallel builds on multiple runners
|
||||
@@ -22,93 +19,7 @@ env:
|
||||
KEYCHAIN_NAME: ios-build-${{ github.run_id }}.keychain
|
||||
|
||||
jobs:
|
||||
check-changes:
|
||||
# Skip nightly builds when nothing has changed since the last successful
|
||||
# scheduled run. Manual workflow_dispatch always builds.
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_build: ${{ steps.check.outputs.should_build }}
|
||||
steps:
|
||||
- name: Check for relevant changes since last successful run
|
||||
id: check
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
if (context.eventName === 'workflow_dispatch') {
|
||||
core.setOutput('should_build', 'true');
|
||||
console.log('Manual trigger — building');
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: { workflow_runs: runs } } = await github.rest.actions.listWorkflowRuns({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
workflow_id: 'ios_testflight.yml',
|
||||
status: 'success',
|
||||
event: 'schedule',
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
if (runs.length === 0) {
|
||||
core.setOutput('should_build', 'true');
|
||||
console.log('No previous successful scheduled run — building');
|
||||
return;
|
||||
}
|
||||
|
||||
const lastSha = runs[0].head_sha;
|
||||
console.log(`Last successful scheduled run: ${lastSha}`);
|
||||
console.log(`Current SHA: ${context.sha}`);
|
||||
|
||||
if (lastSha === context.sha) {
|
||||
core.setOutput('should_build', 'false');
|
||||
console.log('No changes since last successful run — skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Paths that can affect the iOS Unity build.
|
||||
const relevantPrefixes = [
|
||||
'src/main/csharp/', // Unity project
|
||||
'src/main/protobuf/', // Generates C# code
|
||||
'src/main/resources/', // Game data & maps
|
||||
'scripts/', // Build scripts
|
||||
'ci/', // CI scripts & workflows
|
||||
'.github/workflows/ios_testflight.yml',
|
||||
'MODULE.bazel',
|
||||
'MODULE.bazel.lock',
|
||||
'.bazelrc',
|
||||
];
|
||||
|
||||
const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
basehead: `${lastSha}...${context.sha}`,
|
||||
per_page: 1, // We only need the file list, not patches
|
||||
});
|
||||
|
||||
// If the diff is too large, build to be safe
|
||||
if (comparison.files === undefined) {
|
||||
core.setOutput('should_build', 'true');
|
||||
console.log('Could not retrieve file list — building to be safe');
|
||||
return;
|
||||
}
|
||||
|
||||
const relevant = comparison.files.filter(f =>
|
||||
relevantPrefixes.some(p => f.filename.startsWith(p))
|
||||
);
|
||||
|
||||
if (relevant.length > 0) {
|
||||
core.setOutput('should_build', 'true');
|
||||
console.log(`${relevant.length} relevant file(s) changed:`);
|
||||
relevant.slice(0, 20).forEach(f => console.log(` ${f.filename}`));
|
||||
if (relevant.length > 20) console.log(` ... and ${relevant.length - 20} more`);
|
||||
} else {
|
||||
core.setOutput('should_build', 'false');
|
||||
console.log(`${comparison.files.length} file(s) changed, none in relevant paths — skipping`);
|
||||
}
|
||||
|
||||
build-unity:
|
||||
needs: check-changes
|
||||
if: needs.check-changes.outputs.should_build == 'true'
|
||||
runs-on: [self-hosted, macOS, testflight]
|
||||
|
||||
steps:
|
||||
@@ -123,26 +34,8 @@ jobs:
|
||||
git for-each-ref --format='%(refname)' refs/remotes/pull/ 2>/dev/null | \
|
||||
xargs -r git update-ref -d 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
fi
|
||||
done
|
||||
if ! command -v git-lfs >/dev/null 2>&1; then
|
||||
brew install git-lfs
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
fetch-depth: 0
|
||||
@@ -153,9 +46,9 @@ jobs:
|
||||
rm -rf src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee/
|
||||
|
||||
- name: Fetch LFS files
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: ./ci/github_actions/fetch_lfs.sh
|
||||
run: |
|
||||
git lfs install
|
||||
git lfs pull
|
||||
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh ios
|
||||
@@ -166,114 +59,36 @@ jobs:
|
||||
./ci/github_actions/build_unity_ios.sh "$EAGLE0_BUILD_DIR/eagle0iOS"
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
if: success() && github.event.inputs.skip_upload != 'true'
|
||||
if: success()
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ./ci/github_actions/upload_addressables.sh iOS
|
||||
|
||||
- name: Purge CDN cache for iOS addressables
|
||||
if: success() && github.event.inputs.skip_upload != 'true'
|
||||
if: success()
|
||||
env:
|
||||
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
||||
run: |
|
||||
UNITY_VERSION=$(grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //')
|
||||
UNITY_MAJOR_MINOR=$(echo "$UNITY_VERSION" | sed -E 's/^([0-9]+)\.([0-9]+).*/\1.\2/')
|
||||
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
||||
-H "Authorization: Bearer $DO_CDN_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"files\": [\"addressables/iOS/$UNITY_MAJOR_MINOR/*\"]}" \
|
||||
-d '{"files": ["addressables/iOS/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
|
||||
- name: Archive Build Log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: editor_ios.log
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_ios.log
|
||||
retention-days: 3
|
||||
|
||||
- name: Package generated iOS project
|
||||
if: success()
|
||||
run: |
|
||||
tar -czf "$RUNNER_TEMP/eagle0-ios-project.tar.gz" -C "$EAGLE0_BUILD_DIR" eagle0iOS
|
||||
|
||||
- name: Upload generated iOS project
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: eagle0-ios-project-${{ github.run_id }}
|
||||
path: ${{ runner.temp }}/eagle0-ios-project.tar.gz
|
||||
retention-days: 1
|
||||
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
retention-days: 5
|
||||
|
||||
archive-and-upload:
|
||||
needs: build-unity
|
||||
runs-on: [self-hosted, macOS, testflight]
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
fi
|
||||
done
|
||||
if ! command -v git-lfs >/dev/null 2>&1; then
|
||||
brew install git-lfs
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
- name: Clean build directory
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
- name: Download generated iOS project
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: eagle0-ios-project-${{ github.run_id }}
|
||||
path: ${{ runner.temp }}
|
||||
|
||||
- name: Extract generated iOS project
|
||||
run: |
|
||||
mkdir -p "$EAGLE0_BUILD_DIR"
|
||||
tar -xzf "$RUNNER_TEMP/eagle0-ios-project.tar.gz" -C "$EAGLE0_BUILD_DIR"
|
||||
|
||||
- name: Delete generated iOS project artifact
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const artifactName = 'eagle0-ios-project-${{ github.run_id }}';
|
||||
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: context.runId,
|
||||
});
|
||||
|
||||
const artifact = artifacts.find(a => a.name === artifactName);
|
||||
if (artifact === undefined) {
|
||||
console.log(`Artifact not found: ${artifactName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await github.rest.actions.deleteArtifact({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
artifact_id: artifact.id,
|
||||
});
|
||||
console.log(`Deleted artifact: ${artifactName} (${artifact.id})`);
|
||||
|
||||
- name: Install Signing Certificate
|
||||
env:
|
||||
IOS_CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }}
|
||||
@@ -325,20 +140,10 @@ jobs:
|
||||
- name: Archive and Export IPA
|
||||
env:
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
|
||||
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
|
||||
APP_STORE_CONNECT_API_KEY: ${{ secrets.APP_STORE_CONNECT_API_KEY }}
|
||||
run: |
|
||||
# Write API key to file for xcodebuild
|
||||
API_KEY_PATH=$(mktemp)
|
||||
echo "$APP_STORE_CONNECT_API_KEY" > "$API_KEY_PATH"
|
||||
export APP_STORE_CONNECT_API_KEY_PATH="$API_KEY_PATH"
|
||||
|
||||
chmod +x ./ci/github_actions/archive_ios.sh
|
||||
./ci/github_actions/archive_ios.sh "$EAGLE0_BUILD_DIR/eagle0iOS" "$EAGLE0_BUILD_DIR/archive" "$APPLE_TEAM_ID" "$PROFILE_UUID"
|
||||
|
||||
rm -f "$API_KEY_PATH"
|
||||
|
||||
- name: Upload to TestFlight
|
||||
if: ${{ github.event.inputs.skip_upload != 'true' }}
|
||||
env:
|
||||
@@ -363,7 +168,7 @@ jobs:
|
||||
- name: Upload IPA artifact
|
||||
# Only keep artifact if we skipped TestFlight upload (for debugging)
|
||||
if: success() && github.event.inputs.skip_upload == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: eagle0-ios-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/archive/eagle0.ipa
|
||||
|
||||
+123
-269
@@ -14,22 +14,16 @@ on:
|
||||
- "src/main/go/net/eagle0/build/mac_build_handler/**"
|
||||
- "scripts/build_protos.sh"
|
||||
- "scripts/build_mac_plugin.sh"
|
||||
- "scripts/build_sparkle_plugin.sh"
|
||||
- "scripts/inject_sparkle.sh"
|
||||
- "scripts/codesign_mac_app.sh"
|
||||
- "scripts/notarize_submit.sh"
|
||||
- "scripts/notarize_wait.sh"
|
||||
- "ci/github_actions/build_mac.sh"
|
||||
- "ci/github_actions/build_unity_mac.sh"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- "ci/github_actions/detect_addressables_changes.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- ".bazelrc"
|
||||
- "ci/mac/**"
|
||||
- "src/main/objc/net/eagle0/clients/unity/sparkle/**"
|
||||
pull_request:
|
||||
# On PRs, only build Mac when Mac-specific files change. Shared C#/proto
|
||||
# changes are covered by the Windows build — if it passes, Mac will too.
|
||||
@@ -38,25 +32,12 @@ on:
|
||||
- "src/main/go/net/eagle0/build/mac_build_handler/**"
|
||||
- "scripts/build_mac_plugin.sh"
|
||||
- "scripts/build_sparkle_plugin.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Editor/BuildScript.cs"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Eagle/SparkleInitializer.cs"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Eagle/SparkleUpdater.cs"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/common/UpdateNotification/UpdateNotificationManager.cs"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/common/WindowFocusManager.cs"
|
||||
- "src/main/objc/net/eagle0/clients/unity/sparkle/**"
|
||||
- "scripts/inject_sparkle.sh"
|
||||
- "scripts/codesign_mac_app.sh"
|
||||
- "scripts/notarize_submit.sh"
|
||||
- "scripts/notarize_wait.sh"
|
||||
- "ci/github_actions/build_mac.sh"
|
||||
- "ci/github_actions/build_unity_mac.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- "ci/github_actions/detect_addressables_changes.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectSettings.asset"
|
||||
- ".bazelrc"
|
||||
- "ci/mac/**"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
@@ -66,6 +47,10 @@ on:
|
||||
default: 'false'
|
||||
type: boolean
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write # Required to delete artifacts after deploy
|
||||
@@ -77,14 +62,11 @@ env:
|
||||
KEYCHAIN_NAME: build-${{ github.run_id }}.keychain
|
||||
|
||||
jobs:
|
||||
build-mac:
|
||||
build-and-sign:
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-build-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
outputs:
|
||||
submission_id: ${{ steps.notarize-submit.outputs.submission_id }}
|
||||
should_deploy: ${{ steps.check-deploy.outputs.should_deploy }}
|
||||
addressables_changed: ${{ steps.addressables.outputs.changed }}
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
@@ -98,26 +80,8 @@ jobs:
|
||||
git for-each-ref --format='%(refname)' refs/remotes/pull/ 2>/dev/null | \
|
||||
xargs -r git update-ref -d 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
fi
|
||||
done
|
||||
if ! command -v git-lfs >/dev/null 2>&1; then
|
||||
brew install git-lfs
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
fetch-depth: 0 # For version numbering from git history
|
||||
@@ -125,25 +89,11 @@ jobs:
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
LIBRARY_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library"
|
||||
VERSION_CACHE="$LIBRARY_DIR/.last_unity_version"
|
||||
PROJECT_VERSION_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
CURRENT_VERSION=$(grep "m_EditorVersion:" "$PROJECT_VERSION_FILE" | head -1 | sed 's/m_EditorVersion: //')
|
||||
|
||||
# Nuke Library/ when Unity version changes to avoid import loops
|
||||
if [ -f "$VERSION_CACHE" ]; then
|
||||
CACHED_VERSION=$(cat "$VERSION_CACHE")
|
||||
if [ "$CACHED_VERSION" != "$CURRENT_VERSION" ]; then
|
||||
echo "Unity version changed ($CACHED_VERSION -> $CURRENT_VERSION) -- clearing Library/"
|
||||
rm -rf "$LIBRARY_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Only clear Bee/ when C# files were added/deleted/renamed (structural
|
||||
# changes that stale the DAG). Content-only modifications are handled by
|
||||
# Bee's incremental compilation. See persist_library.sh for background.
|
||||
BEE_DIR="$LIBRARY_DIR/Bee"
|
||||
SHA_FILE="$LIBRARY_DIR/.last_built_sha"
|
||||
BEE_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee"
|
||||
SHA_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha"
|
||||
if [ -f "$SHA_FILE" ] && [ -d "$BEE_DIR" ]; then
|
||||
LAST_SHA=$(cat "$SHA_FILE")
|
||||
if git diff --diff-filter=ADR --name-only "$LAST_SHA" HEAD -- '*.cs' '*.csproj' '*.asmdef' 2>/dev/null | grep -q .; then
|
||||
@@ -158,53 +108,34 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Fetch LFS files
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: ./ci/github_actions/fetch_lfs.sh
|
||||
run: |
|
||||
git lfs install
|
||||
git lfs pull
|
||||
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh mac
|
||||
|
||||
- name: Ensure .NET SDK installed
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Sync Bazel Xcode config
|
||||
run: ./scripts/sync_bazel_xcode.sh
|
||||
|
||||
- name: Detect Addressables changes
|
||||
id: addressables
|
||||
if: success()
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
||||
else
|
||||
BASE_SHA="${{ github.event.before }}"
|
||||
fi
|
||||
|
||||
./ci/github_actions/detect_addressables_changes.sh "$BASE_SHA" "${{ github.sha }}"
|
||||
|
||||
- name: Build Mac Unity
|
||||
run: ./ci/github_actions/build_unity_mac.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC" "${{ steps.addressables.outputs.changed }}" "${{ steps.addressables.outputs.changed }}"
|
||||
run: ./ci/github_actions/build_unity_mac.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC"
|
||||
|
||||
- name: Save build SHA for Bee/ cache invalidation
|
||||
if: success()
|
||||
run: |
|
||||
mkdir -p src/main/csharp/net/eagle0/clients/unity/eagle0/Library
|
||||
git rev-parse HEAD > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha
|
||||
run: git rev-parse HEAD > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha
|
||||
|
||||
- name: Save Unity version for Library/ cache invalidation
|
||||
- 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:
|
||||
SPARKLE_EDDSA_PUBLIC_KEY: ${{ secrets.SPARKLE_EDDSA_PUBLIC_KEY }}
|
||||
run: |
|
||||
mkdir -p src/main/csharp/net/eagle0/clients/unity/eagle0/Library
|
||||
grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //' > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_unity_version
|
||||
chmod +x ./scripts/inject_sparkle.sh
|
||||
./scripts/inject_sparkle.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app"
|
||||
|
||||
- name: Check if should deploy
|
||||
id: check-deploy
|
||||
@@ -219,146 +150,8 @@ jobs:
|
||||
echo "should_deploy=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Zip unsigned app for deploy
|
||||
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
|
||||
run: |
|
||||
cd ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
ditto -c -k --keepParent eagle0.app eagle0.app.zip
|
||||
|
||||
- name: Upload unsigned app
|
||||
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unsigned-mac-app-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app.zip
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload Mac Addressables for deploy
|
||||
if: success() && steps.check-deploy.outputs.should_deploy == 'true' && steps.addressables.outputs.changed == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: mac-addressables-${{ github.run_id }}
|
||||
path: src/main/csharp/net/eagle0/clients/unity/eagle0/ServerData/StandaloneOSX
|
||||
if-no-files-found: ignore
|
||||
retention-days: 1
|
||||
|
||||
- name: Archive Build Log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: editor_mac.log
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_mac.log
|
||||
retention-days: 3
|
||||
- name: Archive Addressables build reports
|
||||
if: (success() || failure()) && steps.addressables.outputs.changed == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: mac-addressables-build-reports
|
||||
path: src/main/csharp/net/eagle0/clients/unity/eagle0/Library/com.unity.addressables/BuildReports
|
||||
if-no-files-found: ignore
|
||||
retention-days: 3
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
deploy-mac:
|
||||
needs: build-mac
|
||||
if: needs.build-mac.outputs.should_deploy == 'true'
|
||||
runs-on: [self-hosted, macOS, notarize]
|
||||
outputs:
|
||||
deployed_version: ${{ steps.deploy-mac.outputs.deployed_version }}
|
||||
deployed_unity_major_minor: ${{ steps.deploy-mac.outputs.deployed_unity_major_minor }}
|
||||
concurrency:
|
||||
group: mac-deploy-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
fi
|
||||
done
|
||||
if ! command -v git-lfs >/dev/null 2>&1; then
|
||||
brew install git-lfs
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0 # For version numbering
|
||||
|
||||
- name: Check if run is still latest main
|
||||
id: check-latest
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||
LATEST_SHA=$(curl -fsSL \
|
||||
-H "Authorization: Bearer ${{ github.token }}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/commits/main" \
|
||||
| python3 -c 'import json, sys; print(json.load(sys.stdin)["sha"])')
|
||||
if [[ "$LATEST_SHA" != "${{ github.sha }}" ]]; then
|
||||
echo "Skipping deploy because ${{ github.sha }} is no longer latest main ($LATEST_SHA is latest)"
|
||||
echo "should_deploy=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
echo "should_deploy=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Clean download directory
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
run: rm -rf ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
|
||||
- name: Download unsigned app
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: unsigned-mac-app-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
|
||||
- name: Unzip unsigned app
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
run: |
|
||||
cd ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
ditto -x -k eagle0.app.zip .
|
||||
rm eagle0.app.zip
|
||||
ls -la ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app/
|
||||
|
||||
- name: Clean Addressables directory
|
||||
if: steps.check-latest.outputs.should_deploy == 'true' && needs.build-mac.outputs.addressables_changed == 'true'
|
||||
run: rm -rf src/main/csharp/net/eagle0/clients/unity/eagle0/ServerData/StandaloneOSX
|
||||
|
||||
- name: Download Mac Addressables
|
||||
if: steps.check-latest.outputs.should_deploy == 'true' && needs.build-mac.outputs.addressables_changed == 'true'
|
||||
uses: actions/download-artifact@v8
|
||||
continue-on-error: true
|
||||
with:
|
||||
name: mac-addressables-${{ github.run_id }}
|
||||
path: src/main/csharp/net/eagle0/clients/unity/eagle0/ServerData/StandaloneOSX
|
||||
|
||||
- name: Upload Addressables to CDN
|
||||
if: steps.check-latest.outputs.should_deploy == 'true' && needs.build-mac.outputs.addressables_changed == 'true'
|
||||
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: steps.check-latest.outputs.should_deploy == 'true'
|
||||
env:
|
||||
SPARKLE_EDDSA_PUBLIC_KEY: ${{ secrets.SPARKLE_EDDSA_PUBLIC_KEY }}
|
||||
run: |
|
||||
chmod +x ./scripts/inject_sparkle.sh
|
||||
./scripts/inject_sparkle.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app"
|
||||
|
||||
- name: Import Code Signing Certificate
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
|
||||
env:
|
||||
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
|
||||
@@ -401,7 +194,7 @@ jobs:
|
||||
rm certificate.p12
|
||||
|
||||
- name: Code Sign App
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
|
||||
env:
|
||||
SIGNING_IDENTITY: ${{ secrets.SIGNING_IDENTITY }}
|
||||
run: |
|
||||
@@ -410,7 +203,7 @@ jobs:
|
||||
|
||||
- name: Submit for Notarization
|
||||
id: notarize-submit
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
|
||||
@@ -419,47 +212,116 @@ jobs:
|
||||
chmod +x ./scripts/notarize_submit.sh
|
||||
./scripts/notarize_submit.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cleanup Keychain
|
||||
if: always()
|
||||
run: |
|
||||
security delete-keychain "$KEYCHAIN_NAME" 2>/dev/null || true
|
||||
|
||||
- name: Zip signed app for artifact
|
||||
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
|
||||
run: |
|
||||
cd ${{ env.EAGLE0_BUILD_DIR }}/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: ${{ env.EAGLE0_BUILD_DIR }}/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: ${{ env.EAGLE0_BUILD_DIR }}/editor_mac.log
|
||||
retention-days: 5
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
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 ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
|
||||
- name: Download signed app
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: signed-mac-app-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
|
||||
- name: Unzip signed app
|
||||
run: |
|
||||
cd ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
ditto -x -k eagle0.app.zip .
|
||||
rm eagle0.app.zip
|
||||
ls -la ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app/
|
||||
|
||||
- name: Wait for Notarization and Staple
|
||||
if: steps.check-latest.outputs.should_deploy == '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_wait.sh
|
||||
./scripts/notarize_wait.sh "${{ steps.notarize-submit.outputs.submission_id }}" "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app"
|
||||
|
||||
- name: Cleanup Keychain
|
||||
if: always()
|
||||
run: |
|
||||
security delete-keychain "$KEYCHAIN_NAME" 2>/dev/null || true
|
||||
./scripts/notarize_wait.sh "${{ needs.build-and-sign.outputs.submission_id }}" "${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app"
|
||||
|
||||
- name: Zip notarized app for artifact
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
run: |
|
||||
cd ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
rm -f eagle0.app.zip
|
||||
ditto -c -k --keepParent eagle0.app eagle0.app.zip
|
||||
|
||||
- name: Upload notarized app
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: notarized-mac-app-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app.zip
|
||||
retention-days: 1
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
uses: ./.github/actions/setup-bazel
|
||||
deploy:
|
||||
needs: [build-and-sign, wait-notarization]
|
||||
if: needs.build-and-sign.outputs.should_deploy == 'true'
|
||||
runs-on: [self-hosted, macOS, unity-mac]
|
||||
outputs:
|
||||
deployed_version: ${{ steps.deploy-mac.outputs.deployed_version }}
|
||||
|
||||
- name: Sync Bazel Xcode config
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
run: ./scripts/sync_bazel_xcode.sh
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # For version numbering
|
||||
|
||||
- name: Clean download directory
|
||||
run: rm -rf ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
|
||||
- name: Download notarized app
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: notarized-mac-app-${{ github.run_id }}
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
|
||||
- name: Unzip notarized app
|
||||
run: |
|
||||
cd ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
|
||||
ditto -x -k eagle0.app.zip .
|
||||
rm eagle0.app.zip
|
||||
|
||||
- name: Deploy Mac Build
|
||||
id: deploy-mac
|
||||
if: steps.check-latest.outputs.should_deploy == 'true'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
@@ -470,13 +332,8 @@ jobs:
|
||||
echo "$SPARKLE_EDDSA_PRIVATE_KEY" > "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
chmod 600 "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
# Install dmgbuild in an isolated venv. Homebrew Python rejects system
|
||||
# installs under PEP 668, and mac_build_handler shells out to dmgbuild.
|
||||
DMGBUILD_VENV="${{ env.EAGLE0_BUILD_DIR }}/dmgbuild-venv"
|
||||
python3 -m venv "$DMGBUILD_VENV"
|
||||
"$DMGBUILD_VENV/bin/python" -m pip install --upgrade pip
|
||||
"$DMGBUILD_VENV/bin/python" -m pip install dmgbuild
|
||||
export PATH="$DMGBUILD_VENV/bin:$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"
|
||||
@@ -496,9 +353,6 @@ jobs:
|
||||
rm "$SPARKLE_PRIVATE_KEY_PATH"
|
||||
|
||||
echo "deployed_version=$VERSION" >> $GITHUB_OUTPUT
|
||||
UNITY_VERSION=$(grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //')
|
||||
UNITY_MAJOR_MINOR=$(echo "$UNITY_VERSION" | sed -E 's/^([0-9]+)\.([0-9]+).*/\1.\2/')
|
||||
echo "deployed_unity_major_minor=$UNITY_MAJOR_MINOR" >> $GITHUB_OUTPUT
|
||||
|
||||
# Artifact cleanup is handled by the cleanup job on ubuntu-latest
|
||||
- name: Cleanup build directory
|
||||
@@ -506,8 +360,8 @@ jobs:
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
notify-mac:
|
||||
needs: deploy-mac
|
||||
if: needs.deploy-mac.outputs.deployed_version != ''
|
||||
needs: deploy
|
||||
if: needs.deploy.outputs.deployed_version != ''
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Purge CDN cache and notify clients
|
||||
@@ -519,16 +373,16 @@ jobs:
|
||||
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
||||
-H "Authorization: Bearer $DO_CDN_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"files": ["mac/*", "addressables/StandaloneOSX/${{ needs.deploy-mac.outputs.deployed_unity_major_minor }}/*"]}' \
|
||||
-d '{"files": ["mac/*", "addressables/StandaloneOSX/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=mac&version=${{ needs.deploy-mac.outputs.deployed_version }}&required=false" \
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=mac&version=${{ needs.deploy.outputs.deployed_version }}&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
|
||||
# Cleanup job runs regardless of success/failure to prevent artifact accumulation
|
||||
cleanup:
|
||||
needs: [build-mac, deploy-mac, notify-mac]
|
||||
needs: [build-and-sign, wait-notarization, deploy, notify-mac]
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -538,7 +392,7 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Delete this run's artifacts (names include run ID to avoid conflicts)
|
||||
for artifact_name in unsigned-mac-app-${{ github.run_id }} notarized-mac-app-${{ github.run_id }} mac-addressables-${{ github.run_id }}; do
|
||||
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")
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
name: Cleanup Old Container Images
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Run daily at 3am UTC
|
||||
- cron: '0 3 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
dry_run:
|
||||
description: 'Dry run (show what would be deleted without deleting)'
|
||||
required: true
|
||||
default: 'true'
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install doctl
|
||||
uses: digitalocean/action-doctl@v2
|
||||
with:
|
||||
token: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
|
||||
- name: Cleanup old images
|
||||
env:
|
||||
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'true' }}
|
||||
run: |
|
||||
set -e
|
||||
|
||||
RETENTION_DAYS=5
|
||||
CUTOFF_DATE=$(date -d "-${RETENTION_DAYS} days" +%s)
|
||||
REGISTRY="eagle0"
|
||||
|
||||
echo "Cleaning up images older than ${RETENTION_DAYS} days"
|
||||
echo "Cutoff date: $(date -d "@${CUTOFF_DATE}" -Iseconds)"
|
||||
echo "Dry run: ${DRY_RUN}"
|
||||
echo ""
|
||||
|
||||
# List of repositories to clean
|
||||
# Use tail to skip header row in case --no-header doesn't work
|
||||
REPOS=$(doctl registry repository list-v2 --format Name --no-header | grep -v '^Name$' | grep -v '^$')
|
||||
|
||||
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 "[]")
|
||||
|
||||
if [ "$MANIFESTS_JSON" = "[]" ] || [ -z "$MANIFESTS_JSON" ]; 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
|
||||
# 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)
|
||||
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
|
||||
echo " KEEP: ${DIGEST:0:20}... (protected tag: $TAGS)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if older than cutoff
|
||||
if [ "$MANIFEST_DATE" -lt "$CUTOFF_DATE" ]; then
|
||||
echo " DELETE: ${DIGEST:0:20}... (updated: $UPDATED_AT, tags: $TAGS)"
|
||||
if [ "$DRY_RUN" != "true" ]; then
|
||||
doctl registry repository delete-manifest "${REPO}" "$DIGEST" --force
|
||||
fi
|
||||
else
|
||||
echo " KEEP: ${DIGEST:0:20}... (updated: $UPDATED_AT, tags: $TAGS)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
- name: Run garbage collection
|
||||
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'false')
|
||||
run: |
|
||||
echo "Starting garbage collection..."
|
||||
doctl registry garbage-collection start --force
|
||||
echo "Garbage collection started. It may take a few minutes to complete."
|
||||
@@ -1,219 +0,0 @@
|
||||
name: Renovate Dependency Artifacts
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
paths:
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- 'maven_install.json'
|
||||
- 'renovate.json'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/renovate_dependency_artifacts.yml'
|
||||
|
||||
pull_request_target:
|
||||
types: [opened, reopened, synchronize, ready_for_review]
|
||||
paths:
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- 'maven_install.json'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'renovate.json'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/renovate_dependency_artifacts.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.head.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
bazel: ${{ steps.changed-files.outputs.bazel }}
|
||||
go: ${{ steps.changed-files.outputs.go }}
|
||||
update_matrix: ${{ steps.changed-files.outputs.update_matrix }}
|
||||
steps:
|
||||
- name: Detect changed dependency files
|
||||
id: changed-files
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const files = await github.paginate(
|
||||
github.rest.pulls.listFiles,
|
||||
{
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
per_page: 100,
|
||||
},
|
||||
);
|
||||
|
||||
const changed = files.map((file) => file.filename);
|
||||
const bazelPatterns = [
|
||||
/^MODULE\.bazel$/,
|
||||
/^MODULE\.bazel\.lock$/,
|
||||
/^maven_install\.json$/,
|
||||
/^renovate\.json$/,
|
||||
/^ci\/github_actions\/ensure_bazel_installed\.sh$/,
|
||||
/^\.github\/actions\/setup-bazel\//,
|
||||
/^\.github\/workflows\/renovate_dependency_artifacts\.yml$/,
|
||||
];
|
||||
const goPatterns = [
|
||||
/^go\.mod$/,
|
||||
/^go\.sum$/,
|
||||
/^renovate\.json$/,
|
||||
/^\.github\/workflows\/renovate_dependency_artifacts\.yml$/,
|
||||
];
|
||||
|
||||
core.setOutput(
|
||||
'bazel',
|
||||
changed.some((file) => bazelPatterns.some((pattern) => pattern.test(file))),
|
||||
);
|
||||
core.setOutput(
|
||||
'go',
|
||||
changed.some((file) => goPatterns.some((pattern) => pattern.test(file))),
|
||||
);
|
||||
|
||||
const include = [];
|
||||
if (changed.some((file) => bazelPatterns.some((pattern) => pattern.test(file)))) {
|
||||
include.push({
|
||||
artifact: 'bazel',
|
||||
name: 'Bazel lockfiles',
|
||||
changed_files: 'MODULE.bazel.lock maven_install.json',
|
||||
commit_message: 'Update generated dependency lockfiles',
|
||||
});
|
||||
}
|
||||
if (changed.some((file) => goPatterns.some((pattern) => pattern.test(file)))) {
|
||||
include.push({
|
||||
artifact: 'go',
|
||||
name: 'Go artifacts',
|
||||
changed_files: 'go.sum',
|
||||
commit_message: 'Update generated Go artifacts',
|
||||
});
|
||||
}
|
||||
core.setOutput('update_matrix', JSON.stringify({include}));
|
||||
|
||||
check-lockfile:
|
||||
if: >-
|
||||
github.event_name == 'pull_request' &&
|
||||
github.event.pull_request.user.login != 'renovate[bot]'
|
||||
runs-on: [self-hosted, bazel]
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Update Maven lockfile
|
||||
env:
|
||||
REPIN: "1"
|
||||
run: bazel run @maven//:pin
|
||||
|
||||
- name: Refresh Bazel lockfile
|
||||
run: bazel build --nobuild //src/main/scala/net/eagle0/eagle:eagle_server //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
|
||||
- name: Verify generated lockfiles are current
|
||||
run: |
|
||||
if git diff --quiet -- MODULE.bazel.lock maven_install.json; then
|
||||
echo "Generated dependency lockfiles are current"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Generated dependency lockfiles are stale. Run the lockfile update commands and commit the result:"
|
||||
echo " REPIN=1 bazel run @maven//:pin"
|
||||
echo " bazel build --nobuild //src/main/scala/net/eagle0/eagle:eagle_server //src/main/cpp/net/eagle0/shardok:shardok-server"
|
||||
git diff -- MODULE.bazel.lock maven_install.json
|
||||
exit 1
|
||||
|
||||
update-artifacts:
|
||||
name: Update ${{ matrix.name }}
|
||||
needs: changes
|
||||
if: >-
|
||||
github.event_name == 'pull_request_target' &&
|
||||
github.event.pull_request.user.login == 'renovate[bot]' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
startsWith(github.event.pull_request.head.ref, 'renovate/') &&
|
||||
needs.changes.outputs.update_matrix != '{"include":[]}'
|
||||
runs-on: [self-hosted, bazel]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.changes.outputs.update_matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
export PATH="$path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v git-lfs >/dev/null 2>&1; then
|
||||
git-lfs version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
|
||||
- name: Checkout Renovate branch
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
token: ${{ secrets.RENOVATE_LOCKFILE_TOKEN || github.token }}
|
||||
lfs: false
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
if: matrix.artifact == 'bazel'
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Update Maven lockfile
|
||||
if: matrix.artifact == 'bazel'
|
||||
env:
|
||||
REPIN: "1"
|
||||
run: bazel run @maven//:pin
|
||||
|
||||
- name: Refresh Bazel lockfile
|
||||
if: matrix.artifact == 'bazel'
|
||||
run: bazel build --nobuild //src/main/scala/net/eagle0/eagle:eagle_server //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
|
||||
- name: Set up Go
|
||||
if: matrix.artifact == 'go'
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
cache: false
|
||||
|
||||
- name: Update Go module sums
|
||||
if: matrix.artifact == 'go'
|
||||
run: go mod download all
|
||||
|
||||
- name: Commit generated artifact updates
|
||||
run: |
|
||||
if git diff --quiet -- ${{ matrix.changed_files }}; then
|
||||
echo "${{ matrix.name }} are already up to date"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git add ${{ matrix.changed_files }}
|
||||
git commit -m "${{ matrix.commit_message }}"
|
||||
git push origin "HEAD:${{ github.event.pull_request.head.ref }}"
|
||||
@@ -1,189 +0,0 @@
|
||||
name: Repository Cleanup
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Registry cleanup: run daily at 03:00 UTC
|
||||
- cron: '0 3 * * *'
|
||||
# Artifact cleanup and storage check: run every 6 hours
|
||||
- cron: '0 */6 * * *'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
description: 'Cleanup target'
|
||||
required: true
|
||||
default: 'all'
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- artifacts
|
||||
- registry
|
||||
registry_dry_run:
|
||||
description: 'Dry run registry cleanup'
|
||||
required: true
|
||||
default: 'true'
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
cleanup-and-check-artifacts:
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
(github.event.inputs.target == 'all' || github.event.inputs.target == 'artifacts') ||
|
||||
github.event_name == 'schedule' &&
|
||||
github.event.schedule == '0 */6 * * *'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Delete expired artifacts and artifacts older than 3 days
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
echo "Fetching all artifacts..."
|
||||
gh api "repos/${{ github.repository }}/actions/artifacts" \
|
||||
--paginate -q '.artifacts[] | "\(.id)\t\(.created_at)\t\(.expired)\t\(.name)"' > /tmp/all_artifacts.txt
|
||||
|
||||
total=$(wc -l < /tmp/all_artifacts.txt)
|
||||
echo "Found $total total artifacts"
|
||||
|
||||
cutoff=$(date -u -d '3 days ago' '+%Y-%m-%dT%H:%M:%SZ')
|
||||
echo "Deleting expired artifacts and artifacts created before $cutoff"
|
||||
|
||||
deleted=0
|
||||
while IFS=$'\t' read -r id created_at expired name; do
|
||||
if [[ "$expired" == "true" || "$created_at" < "$cutoff" ]]; then
|
||||
gh api -X DELETE "repos/${{ github.repository }}/actions/artifacts/$id" 2>/dev/null && deleted=$((deleted + 1))
|
||||
if [ $((deleted % 50)) -eq 0 ]; then
|
||||
echo "Deleted $deleted artifacts so far..."
|
||||
fi
|
||||
fi
|
||||
done < /tmp/all_artifacts.txt
|
||||
|
||||
echo "Cleanup complete. Deleted $deleted artifacts out of $total total."
|
||||
rm -f /tmp/all_artifacts.txt
|
||||
|
||||
- name: Check artifact storage size
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Calculate active artifact storage. The artifacts API can list expired
|
||||
# artifacts until they are explicitly deleted, so do not count them.
|
||||
total_bytes=$(gh api "repos/${{ github.repository }}/actions/artifacts" \
|
||||
--paginate -q '.artifacts[] | select(.expired == false) | .size_in_bytes' | awk '{sum+=$1} END {print sum}')
|
||||
|
||||
total_mb=$(( ${total_bytes:-0} / 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:"
|
||||
# Save to temp file to avoid SIGPIPE/broken pipe errors with head
|
||||
gh api "repos/${{ github.repository }}/actions/artifacts" \
|
||||
--paginate -q '.artifacts[] | select(.expired == false) | "\(.size_in_bytes)\t\(.name)\t\(.created_at)"' > /tmp/artifacts.txt
|
||||
sort -rn /tmp/artifacts.txt | head -20 | \
|
||||
awk -F'\t' '{printf "%d MB\t%s\t%s\n", $1/1024/1024, $2, $3}'
|
||||
rm -f /tmp/artifacts.txt
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Storage is within acceptable limits."
|
||||
|
||||
cleanup-registry:
|
||||
if: >-
|
||||
github.event_name == 'workflow_dispatch' &&
|
||||
(github.event.inputs.target == 'all' || github.event.inputs.target == 'registry') ||
|
||||
github.event_name == 'schedule' &&
|
||||
github.event.schedule == '0 3 * * *'
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Install doctl
|
||||
uses: digitalocean/action-doctl@v2
|
||||
with:
|
||||
token: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
|
||||
- name: Cleanup old images
|
||||
env:
|
||||
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.registry_dry_run == 'true' }}
|
||||
run: |
|
||||
set -e
|
||||
|
||||
RETENTION_DAYS=5
|
||||
CUTOFF_DATE=$(date -d "-${RETENTION_DAYS} days" +%s)
|
||||
REGISTRY="eagle0"
|
||||
|
||||
echo "Cleaning up images older than ${RETENTION_DAYS} days"
|
||||
echo "Cutoff date: $(date -d "@${CUTOFF_DATE}" -Iseconds)"
|
||||
echo "Dry run: ${DRY_RUN}"
|
||||
echo ""
|
||||
|
||||
# List repositories via JSON. Text output can wrap table rows and make
|
||||
# headers/tags/digests look like repository names.
|
||||
REPOS=$(doctl registry repository list-v2 "${REGISTRY}" --output json | jq -r '.[] | .name // .Name // empty')
|
||||
|
||||
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 "[]")
|
||||
|
||||
if [ "$MANIFESTS_JSON" = "[]" ] || [ -z "$MANIFESTS_JSON" ]; 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
|
||||
# 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)
|
||||
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
|
||||
echo " KEEP: ${DIGEST:0:20}... (protected tag: $TAGS)"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Check if older than cutoff
|
||||
if [ "$MANIFEST_DATE" -lt "$CUTOFF_DATE" ]; then
|
||||
echo " DELETE: ${DIGEST:0:20}... (updated: $UPDATED_AT, tags: $TAGS)"
|
||||
if [ "$DRY_RUN" != "true" ]; then
|
||||
doctl registry repository delete-manifest "${REPO}" "$DIGEST" --force
|
||||
fi
|
||||
else
|
||||
echo " KEEP: ${DIGEST:0:20}... (updated: $UPDATED_AT, tags: $TAGS)"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
done
|
||||
|
||||
- name: Run garbage collection
|
||||
if: github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && github.event.inputs.registry_dry_run == 'false')
|
||||
run: |
|
||||
echo "Starting garbage collection..."
|
||||
set +e
|
||||
GC_OUTPUT=$(doctl registry garbage-collection start --force 2>&1)
|
||||
GC_STATUS=$?
|
||||
set -e
|
||||
|
||||
echo "$GC_OUTPUT"
|
||||
if [ "$GC_STATUS" -eq 0 ]; then
|
||||
echo "Garbage collection started. It may take a few minutes to complete."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if echo "$GC_OUTPUT" | grep -q "automated garbage collection is enabled"; then
|
||||
echo "Automated garbage collection is enabled for this registry; skipping manual garbage collection."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exit "$GC_STATUS"
|
||||
@@ -1,153 +0,0 @@
|
||||
name: Server Build
|
||||
|
||||
on:
|
||||
# Main pushes are covered by deploy workflows that build these targets.
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/scala/**'
|
||||
- 'src/main/cpp/**'
|
||||
- 'src/main/protobuf/net/eagle0/eagle/**'
|
||||
- 'src/main/protobuf/net/eagle0/shardok/**'
|
||||
- 'src/main/protobuf/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/build/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- 'scripts/build_eagle_ci.sh'
|
||||
- 'scripts/build_shardok_ci.sh'
|
||||
- 'scripts/build_shardok_linux_arm64_ci.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- '.github/workflows/server_build.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
eagle: ${{ steps.filter.outputs.eagle }}
|
||||
shardok: ${{ steps.filter.outputs.shardok }}
|
||||
build_matrix: ${{ steps.filter.outputs.build_matrix }}
|
||||
steps:
|
||||
- name: Detect changed server areas
|
||||
id: filter
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const files = await github.paginate(github.rest.pulls.listFiles, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
pull_number: context.payload.pull_request.number,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
const changed = files.map(file => file.filename);
|
||||
const matches = patterns => changed.some(file =>
|
||||
patterns.some(pattern =>
|
||||
pattern.endsWith('/') ? file.startsWith(pattern) : file === pattern
|
||||
)
|
||||
);
|
||||
|
||||
const sharedPatterns = [
|
||||
'src/main/protobuf/net/eagle0/common/',
|
||||
'src/main/go/net/eagle0/build/',
|
||||
'go.mod',
|
||||
'go.sum',
|
||||
'WORKSPACE',
|
||||
'MODULE.bazel',
|
||||
'MODULE.bazel.lock',
|
||||
'BUILD.bazel',
|
||||
'.bazelrc',
|
||||
'ci/github_actions/ensure_bazel_installed.sh',
|
||||
'.github/actions/setup-bazel/',
|
||||
'.github/workflows/server_build.yml',
|
||||
];
|
||||
|
||||
const eaglePatterns = [
|
||||
'src/main/scala/',
|
||||
'src/main/protobuf/net/eagle0/eagle/',
|
||||
'scripts/build_eagle_ci.sh',
|
||||
];
|
||||
|
||||
const shardokPatterns = [
|
||||
'src/main/cpp/',
|
||||
'src/main/protobuf/net/eagle0/shardok/',
|
||||
'scripts/build_shardok_ci.sh',
|
||||
'scripts/build_shardok_linux_arm64_ci.sh',
|
||||
];
|
||||
|
||||
const shared = matches(sharedPatterns);
|
||||
const eagle = shared || matches(eaglePatterns);
|
||||
const shardok = shared || matches(shardokPatterns);
|
||||
|
||||
core.info(`Changed files:\n${changed.join('\n')}`);
|
||||
core.info(`Run Eagle build: ${eagle}`);
|
||||
core.info(`Run Shardok build: ${shardok}`);
|
||||
core.setOutput('eagle', String(eagle));
|
||||
core.setOutput('shardok', String(shardok));
|
||||
const include = [];
|
||||
if (eagle) {
|
||||
include.push({
|
||||
name: 'Eagle server',
|
||||
command: './scripts/build_eagle_ci.sh',
|
||||
});
|
||||
}
|
||||
if (shardok) {
|
||||
include.push({
|
||||
name: 'Shardok server',
|
||||
command: './scripts/build_shardok_ci.sh',
|
||||
});
|
||||
include.push({
|
||||
name: 'Shardok Linux ARM64 server',
|
||||
command: './scripts/build_shardok_linux_arm64_ci.sh',
|
||||
});
|
||||
}
|
||||
core.setOutput('build_matrix', JSON.stringify({include}));
|
||||
|
||||
server-build:
|
||||
name: Build ${{ matrix.name }}
|
||||
needs: changes
|
||||
if: needs.changes.outputs.build_matrix != '{"include":[]}'
|
||||
runs-on: [self-hosted, bazel]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.changes.outputs.build_matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS=(/opt/homebrew/bin /usr/local/bin)
|
||||
for path in "${COMMON_PATHS[@]}"; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
export PATH="$path:$PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
if command -v git-lfs >/dev/null 2>&1; then
|
||||
git-lfs version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
brew install git-lfs
|
||||
git-lfs version
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
- name: Setup Bazel
|
||||
uses: ./.github/actions/setup-bazel
|
||||
- name: Build server
|
||||
run: ${{ matrix.command }}
|
||||
@@ -7,15 +7,9 @@ on:
|
||||
- 'src/main/cpp/**'
|
||||
- 'src/main/protobuf/net/eagle0/shardok/**'
|
||||
- 'src/main/protobuf/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/build/**'
|
||||
- 'src/main/resources/net/eagle0/shardok/**'
|
||||
- 'ci/BUILD.bazel'
|
||||
- 'ci/github_actions/ensure_bazel_installed.sh'
|
||||
- '.github/actions/setup-bazel/**'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'MODULE.bazel'
|
||||
- 'MODULE.bazel.lock'
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/shardok_arm64_build.yml'
|
||||
workflow_dispatch:
|
||||
@@ -39,35 +33,11 @@ jobs:
|
||||
outputs:
|
||||
image_tag: ${{ steps.push-shardok.outputs.image_tag }}
|
||||
steps:
|
||||
- name: Prepare non-LFS checkout
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
run: |
|
||||
git config --global --unset-all filter.lfs.process || true
|
||||
git config --global filter.lfs.smudge "cat"
|
||||
git config --global filter.lfs.clean "cat"
|
||||
git config --global filter.lfs.required false
|
||||
if [ -d ".git" ]; then
|
||||
git config --local --unset-all filter.lfs.process || true
|
||||
git config --local filter.lfs.smudge "cat"
|
||||
git config --local filter.lfs.clean "cat"
|
||||
git config --local filter.lfs.required false
|
||||
fi
|
||||
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Build Shardok ARM64 binary (cross-compile for Linux ARM64)
|
||||
run: |
|
||||
set -ex
|
||||
@@ -75,7 +45,7 @@ jobs:
|
||||
echo "=== Building shardok-server binary for linux-aarch64 ==="
|
||||
bazel build \
|
||||
--platforms=//:linux_arm64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux \
|
||||
--extra_toolchains=@llvm_toolchain_linux_arm64//:all \
|
||||
//src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
|
||||
LINUX_BIN="bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server"
|
||||
@@ -116,10 +86,9 @@ jobs:
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Keep this in sync with the standalone binary build above.
|
||||
bazel build \
|
||||
--platforms=//:linux_arm64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux \
|
||||
--extra_toolchains=@llvm_toolchain_linux_arm64//:all \
|
||||
//ci:shardok_server_image_arm64
|
||||
|
||||
IMAGE_PATH=$(readlink -f bazel-bin/ci/shardok_server_image_arm64)
|
||||
@@ -196,9 +165,7 @@ jobs:
|
||||
echo "Also tagged as: registry.digitalocean.com/eagle0/shardok-server:arm64-latest"
|
||||
|
||||
deploy-hetzner:
|
||||
# Keep this on a self-hosted Mac runner for IPv6 reachability to Hetzner,
|
||||
# but do not require a Bazel runner slot for artifact transport/deploy work.
|
||||
runs-on: [self-hosted, macOS, ARM64]
|
||||
runs-on: [self-hosted, bazel]
|
||||
needs: [build-shardok-arm64]
|
||||
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
|
||||
environment: production
|
||||
@@ -213,55 +180,33 @@ jobs:
|
||||
# Add host key to known_hosts to avoid prompt
|
||||
ssh-keyscan -H ${{ secrets.HETZNER_IP }} >> ~/.ssh/known_hosts 2>/dev/null || true
|
||||
|
||||
- name: Pull Shardok image tarball for Hetzner
|
||||
env:
|
||||
DO_REGISTRY_TOKEN: ${{ secrets.DO_REGISTRY_TOKEN }}
|
||||
run: |
|
||||
set -ex
|
||||
|
||||
# Hetzner is IPv6-only, while Docker can be redirected to IPv4-only
|
||||
# DigitalOcean registry/blob endpoints. Pull on the runner and copy
|
||||
# the Docker-loadable tarball over SSH instead.
|
||||
CRANE_VERSION="v0.20.2"
|
||||
CRANE_DIR="${HOME}/.local/bin"
|
||||
CRANE="${CRANE_DIR}/crane"
|
||||
if [ ! -x "$CRANE" ] || ! "$CRANE" version 2>/dev/null | grep -q "0.20.2"; then
|
||||
echo "Installing crane ${CRANE_VERSION}..."
|
||||
mkdir -p "$CRANE_DIR"
|
||||
curl -sL "https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Darwin_arm64.tar.gz" | tar xzf - -C "$CRANE_DIR" crane
|
||||
chmod +x "$CRANE"
|
||||
else
|
||||
echo "Using cached crane at $CRANE"
|
||||
fi
|
||||
|
||||
AUTH=$(echo -n "${DO_REGISTRY_TOKEN}:${DO_REGISTRY_TOKEN}" | base64)
|
||||
mkdir -p ~/.docker
|
||||
echo "{\"auths\":{\"registry.digitalocean.com\":{\"auth\":\"${AUTH}\"}}}" > ~/.docker/config.json
|
||||
|
||||
IMAGE_TAR="${RUNNER_TEMP}/shardok-arm64-image.tar"
|
||||
"$CRANE" pull "${SHARDOK_IMAGE}" "$IMAGE_TAR"
|
||||
echo "SHARDOK_IMAGE_TAR=$IMAGE_TAR" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Copy Shardok image tarball to Hetzner
|
||||
run: |
|
||||
set -ex
|
||||
ssh -i ~/.ssh/hetzner_deploy -o StrictHostKeyChecking=accept-new deploy@${{ secrets.HETZNER_IP }} \
|
||||
"cat > /tmp/shardok-arm64-image.tar" < "$SHARDOK_IMAGE_TAR"
|
||||
|
||||
- name: Deploy to Hetzner
|
||||
run: |
|
||||
ssh -i ~/.ssh/hetzner_deploy -o StrictHostKeyChecking=accept-new deploy@${{ secrets.HETZNER_IP }} << 'ENDSSH'
|
||||
set -ex
|
||||
cd /opt/eagle0
|
||||
|
||||
SHARDOK_IMAGE="${{ needs.build-shardok-arm64.outputs.image_tag }}"
|
||||
echo "Loading Shardok ARM64 image: $SHARDOK_IMAGE"
|
||||
docker load -i /tmp/shardok-arm64-image.tar
|
||||
rm -f /tmp/shardok-arm64-image.tar
|
||||
docker image inspect "$SHARDOK_IMAGE" > /dev/null
|
||||
echo "Loaded image ID: $(docker image inspect --format '{{.Id}}' "$SHARDOK_IMAGE")"
|
||||
echo "Shardok binary SHA256:"
|
||||
docker run --rm --entrypoint sha256sum "$SHARDOK_IMAGE" /app/shardok-server
|
||||
# Login to registry
|
||||
echo "${{ secrets.DO_REGISTRY_TOKEN }}" | docker login registry.digitalocean.com -u "${{ secrets.DO_REGISTRY_TOKEN }}" --password-stdin
|
||||
|
||||
echo "Deploying Shardok ARM64: ${{ needs.build-shardok-arm64.outputs.image_tag }}"
|
||||
|
||||
# Pre-warm NAT64 path to DigitalOcean registry (IPv6-only Hetzner → IPv4 registry)
|
||||
curl -sf --max-time 10 https://registry.digitalocean.com/v2/ > /dev/null 2>&1 || true
|
||||
|
||||
# Pull the new image (retry up to 3 times for NAT64 connectivity flakiness)
|
||||
for attempt in 1 2 3; do
|
||||
echo "Pull attempt $attempt..."
|
||||
if docker pull "${{ needs.build-shardok-arm64.outputs.image_tag }}"; then
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -eq 3 ]; then
|
||||
echo "ERROR: docker pull failed after 3 attempts"
|
||||
exit 1
|
||||
fi
|
||||
echo "Pull failed, retrying in 10s..."
|
||||
sleep 10
|
||||
done
|
||||
|
||||
# Stop and remove any container using port 40042 or named shardok*
|
||||
docker ps -q --filter "publish=40042" | xargs -r docker stop
|
||||
@@ -279,12 +224,11 @@ jobs:
|
||||
-v /usr/local/share/eagle0:/usr/local/share/eagle0:ro \
|
||||
-e SHARDOK_RESOURCES_PATH=/app/resources \
|
||||
-e SHARDOK_MAPS_PATH=/app/resources/maps \
|
||||
"$SHARDOK_IMAGE"
|
||||
"${{ needs.build-shardok-arm64.outputs.image_tag }}"
|
||||
|
||||
# Wait and verify
|
||||
sleep 5
|
||||
docker ps | grep shardok-ai
|
||||
echo "Running container image ID: $(docker inspect --format '{{.Image}}' shardok-ai)"
|
||||
|
||||
# Cleanup old images
|
||||
docker image prune -f
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
name: Shardok Build
|
||||
|
||||
on:
|
||||
# Main pushes are covered by shardok_arm64_build.yml which builds the same target
|
||||
pull_request:
|
||||
paths:
|
||||
- 'src/main/cpp/**'
|
||||
- 'src/main/proto/net/eagle0/shardok/**'
|
||||
- 'src/main/proto/net/eagle0/common/**'
|
||||
- 'src/main/go/net/eagle0/build/**'
|
||||
- 'WORKSPACE'
|
||||
- 'MODULE.bazel'
|
||||
- 'BUILD.bazel'
|
||||
- '.bazelrc'
|
||||
- '.github/workflows/shardok_build.yml'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, bazel]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
- name: Build Shardok server
|
||||
run: bazel build -c opt //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
@@ -1,74 +0,0 @@
|
||||
name: Storage Cleanup
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# Blob cleanup: run daily at 04:00 UTC
|
||||
- cron: '0 4 * * *'
|
||||
# S3 archive cleanup: run weekly on Sunday at 05:00 UTC
|
||||
- cron: '0 5 * * 0'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
target:
|
||||
description: 'Cleanup target'
|
||||
required: true
|
||||
default: 'all'
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- blob
|
||||
- archive
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: [self-hosted, bazel]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'all' && '{"include":[{"name":"Blob cleanup","command":"bazel run //src/main/go/net/eagle0/build/blob_cleanup:blob_cleanup -- --min-age=1h"},{"name":"S3 archive cleanup","command":"bazel run //src/main/go/net/eagle0/build/s3_archive_cleanup:s3_archive_cleanup -- --min-age=720h"}]}' || github.event_name == 'workflow_dispatch' && github.event.inputs.target == 'archive' && '{"include":[{"name":"S3 archive cleanup","command":"bazel run //src/main/go/net/eagle0/build/s3_archive_cleanup:s3_archive_cleanup -- --min-age=720h"}]}' || github.event_name == 'schedule' && github.event.schedule == '0 5 * * 0' && '{"include":[{"name":"S3 archive cleanup","command":"bazel run //src/main/go/net/eagle0/build/s3_archive_cleanup:s3_archive_cleanup -- --min-age=720h"}]}' || '{"include":[{"name":"Blob cleanup","command":"bazel run //src/main/go/net/eagle0/build/blob_cleanup:blob_cleanup -- --min-age=1h"}]}') }}
|
||||
|
||||
steps:
|
||||
- name: Prepare non-LFS checkout
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
run: |
|
||||
git config --global --unset-all filter.lfs.process || true
|
||||
git config --global filter.lfs.smudge "cat"
|
||||
git config --global filter.lfs.clean "cat"
|
||||
git config --global filter.lfs.required false
|
||||
if [ -d ".git" ]; then
|
||||
git config --local --unset-all filter.lfs.process || true
|
||||
git config --local filter.lfs.smudge "cat"
|
||||
git config --local filter.lfs.clean "cat"
|
||||
git config --local filter.lfs.required false
|
||||
fi
|
||||
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
|
||||
GIT_CONFIG_NOSYSTEM: "1"
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false
|
||||
clean: false
|
||||
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Run blob cleanup
|
||||
if: matrix.name == 'Blob cleanup'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ${{ matrix.command }}
|
||||
|
||||
- name: Run S3 archive cleanup
|
||||
if: matrix.name == 'S3 archive cleanup'
|
||||
env:
|
||||
DO_SPACES_ACCESS_KEY: ${{ secrets.DO_SPACES_ACCESS_KEY }}
|
||||
DO_SPACES_SECRET_KEY: ${{ secrets.DO_SPACES_SECRET_KEY }}
|
||||
run: ${{ matrix.command }}
|
||||
@@ -15,16 +15,10 @@ on:
|
||||
- "scripts/build_plugins.sh"
|
||||
- "scripts/build_windows_plugin.sh"
|
||||
- "ci/github_actions/build_unity.sh"
|
||||
- "ci/github_actions/test_unity_editmode.sh"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- "ci/github_actions/detect_addressables_changes.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- "MODULE.bazel"
|
||||
- "MODULE.bazel.lock"
|
||||
- ".bazelrc"
|
||||
- "WORKSPACE"
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
@@ -40,147 +34,28 @@ on:
|
||||
- "scripts/build_plugins.sh"
|
||||
- "scripts/build_windows_plugin.sh"
|
||||
- "ci/github_actions/build_unity.sh"
|
||||
- "ci/github_actions/test_unity_editmode.sh"
|
||||
- "ci/github_actions/ensure_bazel_installed.sh"
|
||||
- ".github/actions/setup-bazel/**"
|
||||
- "ci/github_actions/detect_addressables_changes.sh"
|
||||
- "ci/github_actions/upload_addressables.sh"
|
||||
- "ci/github_actions/ensure_unity_installed.sh"
|
||||
- "src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
- "MODULE.bazel"
|
||||
- "MODULE.bazel.lock"
|
||||
- ".bazelrc"
|
||||
- "WORKSPACE"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
actions: write
|
||||
contents: read
|
||||
|
||||
env:
|
||||
# Runner-specific build directory to allow parallel builds on multiple runners
|
||||
EAGLE0_BUILD_DIR: /tmp/eagle0-${{ github.run_id }}-${{ github.job }}
|
||||
EAGLE0_BUILD_DIR: /tmp/eagle0-${{ github.run_id }}
|
||||
|
||||
jobs:
|
||||
unity-editmode-tests:
|
||||
runs-on: [self-hosted, macOS, unity-windows]
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-tests-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
run: |
|
||||
# Self-hosted runners persist .git between runs. When a PR is updated,
|
||||
# old local refs (refs/remotes/pull/*/merge) may point to commits whose
|
||||
# objects were never fetched or have been pruned. Remove these stale refs
|
||||
# before checkout to prevent "missing object" errors.
|
||||
if [ -d ".git" ]; then
|
||||
echo "Pruning stale PR refs..."
|
||||
git for-each-ref --format='%(refname)' refs/remotes/pull/ 2>/dev/null | \
|
||||
xargs -r git update-ref -d 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
fi
|
||||
done
|
||||
if ! command -v git-lfs >/dev/null 2>&1; then
|
||||
brew install git-lfs
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
fetch-depth: 2
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
UNITY_ROOT="src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
# These ignored proto outputs are mutually exclusive across proto-generation
|
||||
# layouts. Clean both before regenerating to avoid stale DLL/source duplicates
|
||||
# when a self-hosted runner switches between main and PR branches.
|
||||
rm -rf "$UNITY_ROOT/Assets/GeneratedProtos"
|
||||
rm -rf "$UNITY_ROOT/Assets/Plugins/Eagle0Protos"
|
||||
LIBRARY_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library"
|
||||
VERSION_CACHE="$LIBRARY_DIR/.last_unity_version"
|
||||
PROJECT_VERSION_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
CURRENT_VERSION=$(grep "m_EditorVersion:" "$PROJECT_VERSION_FILE" | head -1 | sed 's/m_EditorVersion: //')
|
||||
|
||||
# Nuke Library/ when Unity version changes to avoid import loops
|
||||
if [ -f "$VERSION_CACHE" ]; then
|
||||
CACHED_VERSION=$(cat "$VERSION_CACHE")
|
||||
if [ "$CACHED_VERSION" != "$CURRENT_VERSION" ]; then
|
||||
echo "Unity version changed ($CACHED_VERSION -> $CURRENT_VERSION) -- clearing Library/"
|
||||
rm -rf "$LIBRARY_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Only clear Bee/ when C# files were added/deleted/renamed (structural
|
||||
# changes that stale the DAG). Content-only modifications are handled by
|
||||
# Bee's incremental compilation. See persist_library.sh for background.
|
||||
BEE_DIR="$LIBRARY_DIR/Bee"
|
||||
SHA_FILE="$LIBRARY_DIR/.last_built_sha"
|
||||
if [ -f "$SHA_FILE" ] && [ -d "$BEE_DIR" ]; then
|
||||
LAST_SHA=$(cat "$SHA_FILE")
|
||||
if git diff --diff-filter=ADR --name-only "$LAST_SHA" HEAD -- '*.cs' '*.csproj' '*.asmdef' 2>/dev/null | grep -q .; then
|
||||
echo "C# files added/deleted/renamed since $LAST_SHA -- clearing Bee/"
|
||||
rm -rf "$BEE_DIR"
|
||||
else
|
||||
echo "No structural C# changes since $LAST_SHA -- keeping Bee/"
|
||||
fi
|
||||
else
|
||||
echo "No previous build SHA or no Bee/ -- clearing Bee/ as safe default"
|
||||
rm -rf "$BEE_DIR"
|
||||
fi
|
||||
|
||||
- name: Fetch LFS files
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: ./ci/github_actions/fetch_lfs.sh
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh windows
|
||||
- name: Ensure .NET SDK installed
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Run Unity EditMode tests
|
||||
run: ./ci/github_actions/test_unity_editmode.sh
|
||||
|
||||
- name: Archive EditMode test artifacts
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unity-editmode-tests
|
||||
path: |
|
||||
${{ env.EAGLE0_BUILD_DIR }}/editor_editmode_tests.log
|
||||
${{ env.EAGLE0_BUILD_DIR }}/editmode-test-results.xml
|
||||
retention-days: 3
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
|
||||
windows-unity:
|
||||
runs-on: [self-hosted, macOS, unity-windows]
|
||||
outputs:
|
||||
addressables_changed: ${{ steps.addressables.outputs.changed }}
|
||||
deployed_version: ${{ steps.get-version.outputs.deployed_version }}
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-build-${{ github.head_ref || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
steps:
|
||||
- name: Prune stale PR refs
|
||||
@@ -194,196 +69,60 @@ jobs:
|
||||
git for-each-ref --format='%(refname)' refs/remotes/pull/ 2>/dev/null | \
|
||||
xargs -r git update-ref -d 2>/dev/null || true
|
||||
fi
|
||||
|
||||
- name: Ensure Git LFS available for checkout
|
||||
run: |
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
fi
|
||||
done
|
||||
if ! command -v git-lfs >/dev/null 2>&1; then
|
||||
brew install git-lfs
|
||||
fi
|
||||
git-lfs --version
|
||||
|
||||
- uses: actions/checkout@v6
|
||||
env:
|
||||
GIT_LFS_SKIP_SMUDGE: 1
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
lfs: false # Fetch LFS after checkout to avoid stale ref issues
|
||||
clean: false # Library/ persists between runs on self-hosted runners
|
||||
fetch-depth: 2 # Keep the previous main commit available for Addressables change detection
|
||||
|
||||
- name: Clean stale files
|
||||
run: |
|
||||
git clean -ffd
|
||||
UNITY_ROOT="src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
# These ignored proto outputs are mutually exclusive across proto-generation
|
||||
# layouts. Clean both before regenerating to avoid stale DLL/source duplicates
|
||||
# when a self-hosted runner switches between main and PR branches.
|
||||
rm -rf "$UNITY_ROOT/Assets/GeneratedProtos"
|
||||
rm -rf "$UNITY_ROOT/Assets/Plugins/Eagle0Protos"
|
||||
LIBRARY_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library"
|
||||
VERSION_CACHE="$LIBRARY_DIR/.last_unity_version"
|
||||
PROJECT_VERSION_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt"
|
||||
CURRENT_VERSION=$(grep "m_EditorVersion:" "$PROJECT_VERSION_FILE" | head -1 | sed 's/m_EditorVersion: //')
|
||||
|
||||
# Nuke Library/ when Unity version changes to avoid import loops
|
||||
if [ -f "$VERSION_CACHE" ]; then
|
||||
CACHED_VERSION=$(cat "$VERSION_CACHE")
|
||||
if [ "$CACHED_VERSION" != "$CURRENT_VERSION" ]; then
|
||||
echo "Unity version changed ($CACHED_VERSION -> $CURRENT_VERSION) -- clearing Library/"
|
||||
rm -rf "$LIBRARY_DIR"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Only clear Bee/ when C# files were added/deleted/renamed (structural
|
||||
# changes that stale the DAG). Content-only modifications are handled by
|
||||
# Bee's incremental compilation. See persist_library.sh for background.
|
||||
BEE_DIR="$LIBRARY_DIR/Bee"
|
||||
SHA_FILE="$LIBRARY_DIR/.last_built_sha"
|
||||
BEE_DIR="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/Bee"
|
||||
SHA_FILE="src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha"
|
||||
if [ -f "$SHA_FILE" ] && [ -d "$BEE_DIR" ]; then
|
||||
LAST_SHA=$(cat "$SHA_FILE")
|
||||
if git diff --diff-filter=ADR --name-only "$LAST_SHA" HEAD -- '*.cs' '*.csproj' '*.asmdef' 2>/dev/null | grep -q .; then
|
||||
echo "C# files added/deleted/renamed since $LAST_SHA -- clearing Bee/"
|
||||
echo "C# files added/deleted/renamed since $LAST_SHA — clearing Bee/"
|
||||
rm -rf "$BEE_DIR"
|
||||
else
|
||||
echo "No structural C# changes since $LAST_SHA -- keeping Bee/"
|
||||
echo "No structural C# changes since $LAST_SHA — keeping Bee/"
|
||||
fi
|
||||
else
|
||||
echo "No previous build SHA or no Bee/ -- clearing Bee/ as safe default"
|
||||
echo "No previous build SHA or no Bee/ — clearing Bee/ as safe default"
|
||||
rm -rf "$BEE_DIR"
|
||||
fi
|
||||
|
||||
- name: Fetch LFS files
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: ./ci/github_actions/fetch_lfs.sh
|
||||
run: |
|
||||
git lfs install
|
||||
git lfs pull
|
||||
- name: Ensure Unity version installed
|
||||
run: ./ci/github_actions/ensure_unity_installed.sh windows
|
||||
- name: Ensure .NET SDK installed
|
||||
uses: actions/setup-dotnet@v5
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
- name: Ensure Bazel installed
|
||||
uses: ./.github/actions/setup-bazel
|
||||
|
||||
- name: Detect Addressables changes
|
||||
id: addressables
|
||||
if: success()
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
BASE_SHA="${{ github.event.pull_request.base.sha }}"
|
||||
else
|
||||
BASE_SHA="${{ github.event.before }}"
|
||||
fi
|
||||
|
||||
./ci/github_actions/detect_addressables_changes.sh "$BASE_SHA" "${{ github.sha }}"
|
||||
- name: Build Windows unity
|
||||
run: ./ci/github_actions/build_unity.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN" true "${{ steps.addressables.outputs.changed }}"
|
||||
run: ./ci/github_actions/build_unity.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN"
|
||||
|
||||
- name: Save build SHA for Bee/ cache invalidation
|
||||
if: success()
|
||||
run: |
|
||||
mkdir -p src/main/csharp/net/eagle0/clients/unity/eagle0/Library
|
||||
git rev-parse HEAD > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha
|
||||
run: git rev-parse HEAD > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha
|
||||
|
||||
- name: Save Unity version for Library/ cache invalidation
|
||||
if: success()
|
||||
run: |
|
||||
mkdir -p src/main/csharp/net/eagle0/clients/unity/eagle0/Library
|
||||
grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //' > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_unity_version
|
||||
|
||||
- name: Check if run is still latest main for Addressables
|
||||
id: check-addressables-latest
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && steps.addressables.outputs.changed == 'true'
|
||||
run: |
|
||||
LATEST_SHA=$(curl -fsSL \
|
||||
-H "Authorization: Bearer ${{ github.token }}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/commits/main" \
|
||||
| python3 -c 'import json, sys; print(json.load(sys.stdin)["sha"])')
|
||||
if [[ "$LATEST_SHA" != "${{ github.sha }}" ]]; then
|
||||
echo "Skipping Addressables upload because ${{ github.sha }} is no longer latest main ($LATEST_SHA is latest)"
|
||||
echo "should_upload=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "should_upload=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Wait for EditMode tests before publishing
|
||||
- name: Upload Addressables to CDN
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
deadline=$((SECONDS + 600))
|
||||
while [ "$SECONDS" -lt "$deadline" ]; do
|
||||
result=$(curl -fsSL \
|
||||
-H "Authorization: Bearer $GH_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/jobs" \
|
||||
| python3 -c 'import json, sys; job = next(j for j in json.load(sys.stdin)["jobs"] if j["name"] == "unity-editmode-tests"); print("{} {}".format(job["status"], job["conclusion"] or ""))')
|
||||
status="${result%% *}"
|
||||
conclusion="${result#* }"
|
||||
if [ "$status" = "completed" ]; then
|
||||
if [ "$conclusion" = "success" ]; then
|
||||
echo "EditMode tests passed; publishing may continue."
|
||||
exit 0
|
||||
fi
|
||||
echo "EditMode tests completed with conclusion: $conclusion"
|
||||
exit 1
|
||||
fi
|
||||
echo "EditMode tests are still $status; waiting..."
|
||||
sleep 5
|
||||
done
|
||||
echo "Timed out waiting for EditMode tests."
|
||||
exit 1
|
||||
|
||||
- name: Check if run is still latest main for publish
|
||||
id: check-publish-latest
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
|
||||
run: |
|
||||
LATEST_SHA=$(curl -fsSL \
|
||||
-H "Authorization: Bearer ${{ github.token }}" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/commits/main" \
|
||||
| python3 -c 'import json, sys; print(json.load(sys.stdin)["sha"])')
|
||||
if [[ "$LATEST_SHA" != "${{ github.sha }}" ]]; then
|
||||
echo "Skipping publish because ${{ github.sha }} is no longer latest main ($LATEST_SHA is latest)"
|
||||
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Stage Windows blobs for publish
|
||||
if: success() && steps.check-publish-latest.outputs.should_publish == 'true'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: bazel run //src/main/go/net/eagle0/build/unity3d_windows_build_handler:unity3d_windows_build_handler -- --skip-previous-manifest "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN" "${{ env.EAGLE0_BUILD_DIR }}/unity_manifest.txt"
|
||||
|
||||
- name: Upload Windows Addressables to CDN
|
||||
if: success() && steps.check-publish-latest.outputs.should_publish == 'true' && steps.check-addressables-latest.outputs.should_upload == 'true'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: ./ci/github_actions/upload_addressables.sh StandaloneWindows64
|
||||
|
||||
- name: Publish previous Windows manifest
|
||||
if: success() && steps.check-publish-latest.outputs.should_publish == 'true'
|
||||
- name: Deploy Windows unity
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
run: bazel run //src/main/go/net/eagle0/build/unity3d_windows_build_handler:unity3d_windows_build_handler -- --publish-previous-manifest "${{ env.EAGLE0_BUILD_DIR }}/unity_manifest.txt"
|
||||
run: bazel run //src/main/go/net/eagle0/build/unity3d_windows_build_handler:unity3d_windows_build_handler -- "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN" "/tmp/unity_manifest.txt"
|
||||
|
||||
- name: Update unified manifest
|
||||
if: success() && steps.check-publish-latest.outputs.should_publish == 'true'
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
env:
|
||||
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
|
||||
SECRET_KEY: ${{ secrets.SECRET_KEY }}
|
||||
@@ -401,33 +140,25 @@ jobs:
|
||||
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 "${{ env.EAGLE0_BUILD_DIR }}/unity_manifest.txt" $SIGNING_ARGS
|
||||
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
|
||||
|
||||
- name: Export deployed version
|
||||
id: get-version
|
||||
if: success() && steps.check-publish-latest.outputs.should_publish == 'true'
|
||||
if: success() && github.ref == 'refs/heads/main' && github.event_name == 'push'
|
||||
run: |
|
||||
VERSION=$(grep "^version=" "${{ env.EAGLE0_BUILD_DIR }}/unity_manifest.txt" | cut -d= -f2 || date +%Y.%m.%d)
|
||||
VERSION=$(grep "^version=" /tmp/unity_manifest.txt | cut -d= -f2 || date +%Y.%m.%d)
|
||||
echo "deployed_version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Archive build log
|
||||
if: success() || failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: editor_win.log
|
||||
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_win.log
|
||||
retention-days: 3
|
||||
- name: Archive Addressables build reports
|
||||
if: (success() || failure()) && steps.addressables.outputs.changed == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: windows-addressables-build-reports
|
||||
path: src/main/csharp/net/eagle0/clients/unity/eagle0/Library/com.unity.addressables/BuildReports
|
||||
if-no-files-found: ignore
|
||||
retention-days: 3
|
||||
retention-days: 5
|
||||
- name: Cleanup build directory
|
||||
if: always()
|
||||
run: rm -rf "${{ env.EAGLE0_BUILD_DIR }}"
|
||||
@@ -437,10 +168,18 @@ jobs:
|
||||
if: needs.windows-unity.outputs.deployed_version != ''
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Notify clients of update
|
||||
- name: Purge CDN cache and notify clients
|
||||
env:
|
||||
NOTIFY_SECRET: ${{ secrets.EAGLE_NOTIFY_SECRET }}
|
||||
DO_CDN_PAT: ${{ secrets.DO_CDN_PAT }}
|
||||
run: |
|
||||
# Purge CDN cache so clients get the latest files immediately
|
||||
curl -s -X DELETE "https://api.digitalocean.com/v2/cdn/endpoints/8c98df29-6f0c-4704-8e82-ca40a2b81aa8/cache" \
|
||||
-H "Authorization: Bearer $DO_CDN_PAT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"files": ["windows/*", "addressables/StandaloneWindows64/*"]}' \
|
||||
--fail || echo "Warning: CDN purge failed (non-fatal)"
|
||||
|
||||
curl -X POST "https://admin.eagle0.net/notify-update?platform=windows&version=${{ needs.windows-unity.outputs.deployed_version }}&required=false" \
|
||||
-H "X-Notify-Secret: $NOTIFY_SECRET" \
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
|
||||
@@ -23,8 +23,6 @@ bazel-bin
|
||||
bazel-eagle0*
|
||||
bazel-out
|
||||
bazel-testlogs
|
||||
.bazelrc.local
|
||||
.bazelrc.xcode
|
||||
.ijwb
|
||||
.clwb
|
||||
buildWin.sh
|
||||
@@ -40,10 +38,7 @@ scripts/refresh_name_layers/refresh_name_layers.zip
|
||||
api_keys.txt
|
||||
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/GeneratedProtos/
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/Eagle0Protos.meta
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/Eagle0Protos/
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/Packages/com.unity.dedicated-server/
|
||||
node_modules/
|
||||
tools/map_generator/output/
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/docs/generated/unity_asset_usage_audit_files.csv
|
||||
src/main/csharp/net/eagle0/clients/unity/eagle0/docs/generated/unity_lfs_asset_audit.csv
|
||||
|
||||
+3
-12
@@ -14,16 +14,7 @@ repos:
|
||||
- id: clang-format
|
||||
args: [-i, --no-diff]
|
||||
types_or: ["c++", "c#"]
|
||||
exclude: >-
|
||||
(?x)^src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/(
|
||||
Plugins|
|
||||
DungeonMonsters2D|
|
||||
Dragon|
|
||||
ithappy|
|
||||
Polytope\ Studio|
|
||||
RRFreelance-Characters|
|
||||
Raccoon
|
||||
)/
|
||||
exclude: ^src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins
|
||||
- repo: https://github.com/yoheimuta/protolint
|
||||
rev: v0.42.2
|
||||
hooks:
|
||||
@@ -34,8 +25,8 @@ repos:
|
||||
hooks:
|
||||
- id: scalafmt
|
||||
name: scalafmt
|
||||
language: system
|
||||
entry: ./scripts/pre-commit-scalafmt.sh
|
||||
language: system
|
||||
entry: scalafmt -i -f
|
||||
types_or: ["scala"]
|
||||
- repo: local
|
||||
hooks:
|
||||
|
||||
@@ -1,440 +0,0 @@
|
||||
# AGENTS.md
|
||||
|
||||
## CRITICAL BASH RULES (NEVER VIOLATE)
|
||||
|
||||
**NEVER chain bash commands.** Do not use `&&`, `||`, or `;` to combine commands. Each command must be a separate Bash tool call. Use parallel tool calls when commands are independent.
|
||||
|
||||
**NEVER prefix a command with `cd`.** Run `git`, `gh`, `bazel`, etc. directly from whatever the cwd already is. The cwd is a subdirectory of the worktree; `git`/`gh` find the repo via `.git` discovery and `bazel` finds the workspace via MODULE.bazel discovery — walking up from a subdir works fine. The shell resets cwd after every command anyway, so a `cd` never persists. (And `cd <dir> && <cmd>` violates the no-chaining rule above and forces an approval prompt every time.) Only `cd` in the rare case a tool genuinely cannot locate its root, and even then as its own Bash call, never a chain.
|
||||
|
||||
## CRITICAL UNITY PROCESS RULES (NEVER VIOLATE)
|
||||
|
||||
**NEVER kill Unity without asking the user first.** The user might be actively using the Unity Editor. Do not kill,
|
||||
force-quit, terminate, or otherwise stop Unity processes unless the user explicitly approves that specific action.
|
||||
|
||||
## CRITICAL GIT RULES (NEVER VIOLATE)
|
||||
|
||||
**NEVER use `git -C`.** Just run `git` directly from the cwd — it finds the repo via `.git` discovery. Do not `cd` to the repo root either (see the no-`cd` bash rule above).
|
||||
|
||||
**NEVER push directly to main/master.** No exceptions. Not for "small changes." Not for docs. Not ever.
|
||||
|
||||
**NEVER merge PRs.** You create PRs. The user merges them. No exceptions.
|
||||
|
||||
**ALWAYS use this workflow:**
|
||||
1. Create a feature branch from origin/main
|
||||
2. Commit to that branch
|
||||
3. Create a PR with `gh pr create`
|
||||
4. Wait for user to merge (DO NOT run `gh pr merge`)
|
||||
|
||||
If you catch yourself about to run `git push origin main` or `git push origin <branch>:main`, STOP. You are about to violate a critical rule. Create a PR instead.
|
||||
|
||||
If you catch yourself about to run `gh pr merge`, STOP. Only the user merges PRs.
|
||||
|
||||
## Commit History During Review
|
||||
|
||||
Prefer new follow-up commits over amending existing commits once a PR is open and the user is actively reviewing or
|
||||
testing it. This preserves working interim states so the user can inspect, compare, or return to a known-good point
|
||||
while improvements continue.
|
||||
|
||||
Use `git commit --amend` and force-push only when the user explicitly asks for history cleanup, when fixing metadata
|
||||
before review has started, or when repairing a local commit that has not been pushed. If you believe squashing is the
|
||||
right choice despite an open review, explain why and ask first.
|
||||
|
||||
## Worktree Usage
|
||||
|
||||
Prefer using the current worktree for small requested fixes. Create a separate worktree only when the user asks for one,
|
||||
when the current branch/state would make the change unsafe, or when isolating a large/risky change is clearly beneficial.
|
||||
|
||||
**NEVER leave the current worktree without explicit user approval.** Do not create, switch to, edit, commit in, or run
|
||||
git commands from another worktree unless the user has specifically asked for that worktree or approved the move. If the
|
||||
current worktree is unsafe because of dirty state, report that and ask before using a different worktree.
|
||||
|
||||
Codex may edit code and run git commands in the current worktree, even when unrelated files are dirty, as long as it:
|
||||
- does not revert, reset, clean, or overwrite unrelated changes
|
||||
- stages only the files relevant to the requested change
|
||||
- verifies the staged diff before committing
|
||||
- creates feature branches and PRs as usual
|
||||
- does not push to main/master or merge PRs
|
||||
|
||||
When the user says a Codex-created PR has been merged, delete the corresponding local branch and temporary worktree
|
||||
automatically, unless there is uncommitted work that would make cleanup unsafe.
|
||||
|
||||
## PR Timing
|
||||
|
||||
When code changes appear correct locally, create the PR promptly even if long-running builds or tests are still running.
|
||||
Opening the PR starts CI on the build machines, uploads results to the Bazel remote cache, and can speed up subsequent
|
||||
local validation. Continue monitoring any already-started local and CI validation after opening the PR.
|
||||
|
||||
For PRs expected to trigger substantial Bazel work in CI, run the relevant Bazel builds and tests locally too. This
|
||||
workstation is fast, and local Bazel runs can help hydrate the remote cache for the CI builders while still giving
|
||||
earlier signal on failures.
|
||||
|
||||
## Cleanup PR Batching
|
||||
|
||||
When making mechanical cleanup changes, group 3-5 files per PR when the files are receiving the same kind of change
|
||||
and can be reviewed as one pattern. Do not open one PR per file for nearly identical few-line cleanups, such as replacing
|
||||
the same unsafe accessor pattern with the same contextual failure pattern across multiple Scala files.
|
||||
|
||||
Keep separate PRs for changes that involve different semantics, materially different risk, unrelated subsystems, or
|
||||
files whose tests/validation strategy makes them better reviewed independently.
|
||||
|
||||
## GitHub CLI PR Bodies
|
||||
|
||||
When creating or editing PR descriptions with multiline bodies, write the body to a temporary markdown file and pass it
|
||||
with `gh pr create --body-file <path>` or `gh pr edit <number> --body-file <path>`. Do not use multiline `$'...'`
|
||||
shell quoting for PR bodies; Codex's permission matching may treat that as a complex shell command instead of a clean
|
||||
`gh pr` invocation, causing unnecessary approval prompts.
|
||||
|
||||
Write these temporary body files under `/private/tmp/eagle0-pr-bodies/`, creating that directory first when needed. This
|
||||
directory is the canonical writable scratch location for PR descriptions. Do not write PR body files to protected paths,
|
||||
repo-tracked paths, or ad hoc locations that might require additional user permission.
|
||||
|
||||
Use ordinary Bash file-writing commands for these temporary PR body files. First run a standalone command to create the
|
||||
directory. Then run a standalone `printf` command that writes to `/private/tmp/eagle0-pr-bodies/<name>.md`. **Do not use
|
||||
`apply_patch` for temporary PR body files**, because patch tools are for tracked workspace edits and may incorrectly
|
||||
request user approval for scratch paths outside the repository.
|
||||
|
||||
Write other scratch files needed for commands under `/private/tmp/eagle0-scratch/`, creating that directory first when
|
||||
needed. Do not use protected paths that require additional user permission just to create or edit temporary files.
|
||||
|
||||
---
|
||||
|
||||
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Eagle0 is a multi-language gaming system combining strategic turn-based gameplay (Eagle) with tactical hex-based
|
||||
combat (Shardok). The system integrates LLM-based narrative generation and supports both human and AI players.
|
||||
|
||||
## Architecture
|
||||
|
||||
**Three-Tier Game System:**
|
||||
|
||||
- **Unity Client (C#)**: Real-time strategy game client with integrated tactical combat UI
|
||||
- **Eagle (Scala)**: Strategic layer managing turn-based gameplay, diplomacy, hero progression, and province control
|
||||
- **Shardok (C++)**: Tactical layer handling real-time hex-based combat simulation with performance-critical battle
|
||||
resolution
|
||||
|
||||
**Communication Flow:**
|
||||
|
||||
```
|
||||
Unity Client ↔ Eagle (gRPC streaming) ↔ Shardok (internal gRPC)
|
||||
```
|
||||
|
||||
**Key Entry Points:**
|
||||
|
||||
- `/src/main/csharp/net/eagle0/clients/unity/eagle0/` - Unity C# game client
|
||||
- `/src/main/scala/net/eagle0/eagle/Main.scala` - Eagle strategic game server
|
||||
- `/src/main/cpp/net/eagle0/shardok/shardok_server_main.cpp` - Shardok tactical server
|
||||
|
||||
**Protocol Buffer Architecture:**
|
||||
|
||||
- Extensive use of protobuf for type-safe communication
|
||||
- Separate packages: `api/` (client-facing), `internal/` (server state), `views/` (client projections)
|
||||
- Event sourcing pattern with immutable action history
|
||||
|
||||
## Essential Commands
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
# Build Eagle server using the same target as GitHub Actions
|
||||
./scripts/build_eagle_ci.sh
|
||||
|
||||
# Build Shardok server using the same target and flags as GitHub Actions
|
||||
./scripts/build_shardok_ci.sh
|
||||
|
||||
# Warm the remote cache for the main GitHub Actions Bazel builds
|
||||
./scripts/hydrate_bazel_remote_cache.sh
|
||||
|
||||
# Build Unity/C# client
|
||||
./scripts/build_protos.sh # Protocol buffer generation for Unity
|
||||
./scripts/build_plugins.sh # Native plugins for all platforms
|
||||
./scripts/build_windows_plugin.sh # Windows-specific plugin build
|
||||
# Unity builds via CI: ci/github_actions/build_unity.sh
|
||||
```
|
||||
|
||||
### Running Services
|
||||
|
||||
```bash
|
||||
# Eagle server (port 40032)
|
||||
bazel run //src/main/scala/net/eagle0/eagle:eagle_server -- --eagle-grpc-port 40032
|
||||
# Or: ./scripts/eagle_run.sh
|
||||
|
||||
# Shardok server
|
||||
bazel run //src/main/cpp/net/eagle0/shardok:shardok-server --compilation_mode=opt
|
||||
# Or: ./scripts/shardok_run.sh
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
bazel test //src/test/... //src/main/go/...
|
||||
|
||||
# Component-specific tests
|
||||
bazel test //src/test/scala/... # Scala Eagle tests
|
||||
bazel test //src/test/cpp/... # C++ Shardok tests
|
||||
```
|
||||
|
||||
### Code Generation
|
||||
|
||||
```bash
|
||||
bazel run gazelle # Update Go build files
|
||||
./scripts/updateActionResultTypes.sh # Update protocol buffer mappings
|
||||
```
|
||||
|
||||
### Pre-Commit Checklist
|
||||
|
||||
**MANDATORY: Before running `git commit`, verify:**
|
||||
|
||||
1. **If you modified any BUILD.bazel file:** Run `bazel run gazelle` and stage any changes it makes
|
||||
2. **If you modified C++ or C# files:** Run `clang-format -i` on the modified files
|
||||
3. **If you modified Scala files:** scalafmt will run automatically via pre-commit hook
|
||||
4. **ALWAYS run the full test suite for your language changes before pushing:**
|
||||
- For Scala changes: `bazel test //src/test/scala/...`
|
||||
- For C++ changes: `bazel test //src/test/cpp/...`
|
||||
- This catches exhaustive pattern match errors and other compile-time failures that single-target builds miss
|
||||
|
||||
The pre-commit hook runs gazelle but only checks if it succeeds - it does NOT verify the BUILD files are in canonical format. The `gazelle_test` will fail if deps are not alphabetically sorted. **Always run gazelle manually after BUILD file changes.**
|
||||
|
||||
### Bazel Cache Hydration
|
||||
|
||||
When making changes that will trigger GitHub Actions Bazel builds, run the matching CI script instead of hand-writing
|
||||
`bazel build` commands. The scripts keep local cache hydration aligned with CI targets, compilation modes, and flags:
|
||||
|
||||
```bash
|
||||
./scripts/build_eagle_ci.sh
|
||||
./scripts/build_shardok_ci.sh
|
||||
./scripts/hydrate_bazel_remote_cache.sh
|
||||
```
|
||||
|
||||
Use direct `bazel build` commands only for targeted debugging where CI cache hydration is not the goal.
|
||||
|
||||
### Code Formatting
|
||||
|
||||
```bash
|
||||
# ALWAYS run clang-format after making any C++ or C# code changes
|
||||
clang-format -i <modified_files>
|
||||
|
||||
# Format all C++ files in a directory:
|
||||
find . -name "*.cpp" -o -name "*.hpp" | xargs clang-format -i
|
||||
|
||||
# Format all C# files in a directory:
|
||||
find . -name "*.cs" | xargs clang-format -i
|
||||
```
|
||||
|
||||
### Static Analysis
|
||||
|
||||
```bash
|
||||
# Run clang-tidy static analysis on C++ files
|
||||
# Note: This may show some header include errors but will still analyze the main file
|
||||
bazel run @llvm_toolchain//:clang-tidy -- --checks='readability-*,bugprone-*,clang-analyzer-*' <file_path> -- -I/Users/dancrosby/CodingProjects/github/eagle0 -std=c++23
|
||||
|
||||
# Example for AI files:
|
||||
bazel run @llvm_toolchain//:clang-tidy -- --checks='readability-*,bugprone-*,clang-analyzer-*' /Users/dancrosby/CodingProjects/github/eagle0/src/main/cpp/net/eagle0/shardok/ai/AIUnitScoreCalculator.cpp -- -I/Users/dancrosby/CodingProjects/github/eagle0 -std=c++23
|
||||
```
|
||||
|
||||
## AI Algorithm Selection
|
||||
|
||||
Eagle0 supports two AI algorithms for tactical combat decision-making:
|
||||
|
||||
### Iterative Deepening AI (Default)
|
||||
|
||||
The original minimax-based AI with sophisticated randomness handling:
|
||||
|
||||
- **Advantages**: Proven, sophisticated randomness evaluation, comprehensive lookahead
|
||||
- **Use cases**: Production builds, scenarios requiring precise evaluation
|
||||
- **Performance**: Single-threaded, thorough evaluation
|
||||
|
||||
### Monte Carlo Tree Search AI (MCTS)
|
||||
|
||||
Modern MCTS-based AI with multithreading support:
|
||||
|
||||
- **Advantages**: Multithreaded, better performance on modern CPUs, anytime algorithm
|
||||
- **Use cases**: Performance testing, scenarios requiring fast decisions
|
||||
- **Performance**: Multithreaded, adaptive depth based on time budget
|
||||
|
||||
### Switching Between Algorithms
|
||||
|
||||
The algorithm is selected at **runtime** via the ShardokAIClient constructor:
|
||||
|
||||
```cpp
|
||||
// Using Iterative Deepening AI (default)
|
||||
ShardokAIClient client(playerId, isDefender, hexMap, settings);
|
||||
// OR explicitly:
|
||||
ShardokAIClient client(playerId, isDefender, hexMap, settings, AIAlgorithmType::ITERATIVE_DEEPENING);
|
||||
|
||||
// Using MCTS AI
|
||||
ShardokAIClient client(playerId, isDefender, hexMap, settings, AIAlgorithmType::MCTS);
|
||||
```
|
||||
|
||||
```bash
|
||||
# Build the server (includes both AI algorithms)
|
||||
./scripts/build_shardok_ci.sh
|
||||
|
||||
# Test both algorithms
|
||||
bazel test //src/test/cpp/net/eagle0/shardok/ai:ai_iterative_deepening_test
|
||||
bazel test //src/test/cpp/net/eagle0/shardok/ai:ai_mcts_test # If available
|
||||
|
||||
# Performance tests
|
||||
./scripts/ai_perf_test.sh # Uses whatever algorithm the server is configured to use
|
||||
```
|
||||
|
||||
Both implementations are compatible with all existing interfaces and produce the same `SearchResult` structure.
|
||||
|
||||
**Note**: Both implementations are documented in `src/main/cpp/net/eagle0/shardok/ai/AI_SCORING_SYSTEM.md`, including
|
||||
recommendations for improving MCTS randomness handling.
|
||||
|
||||
The AI algorithm selection is made at runtime when creating ShardokAIClient instances, allowing different AI strategies
|
||||
to be used for different players or game situations within the same server process.
|
||||
|
||||
## Language-Specific Patterns
|
||||
|
||||
**Scala (Strategic Layer):**
|
||||
|
||||
- Use `EngineImpl.scala` for core game logic modifications
|
||||
- Follow event sourcing pattern - all changes through immutable actions
|
||||
- gRPC streaming for real-time client updates via `EagleServiceImpl.scala`
|
||||
- LLM integration in `/common/llm_integration/` for narrative generation
|
||||
|
||||
**C++ (Tactical Layer):**
|
||||
|
||||
- Performance-critical combat in `ShardokEngine.hpp/.cpp`
|
||||
- FlatBuffers for efficient serialization in `/flatbuffer/` directory
|
||||
- AI systems in `/ai/` subdirectory with pluggable strategy selectors
|
||||
- Extensive unit testing with Google Test framework
|
||||
|
||||
**Protocol Buffers:**
|
||||
|
||||
- Three-layer structure: `api/` (client), `internal/` (server), `views/` (projections)
|
||||
- Use `shardok_internal_interface.proto` for Eagle-Shardok communication
|
||||
- Maintain backward compatibility when modifying existing messages
|
||||
|
||||
**C# (Unity Client):**
|
||||
|
||||
- Located in `/src/main/csharp/net/eagle0/clients/unity/eagle0/`
|
||||
- Uses Unity 6.5 (6000.5.0f1) with comprehensive protobuf integration (100+ .proto files)
|
||||
- Key components: `EagleConnection.cs` (gRPC client), `EagleGameController.cs` (main game logic)
|
||||
- Real-time bidirectional streaming with server via `PersistentClientConnection.cs`
|
||||
- Strategic map UI in `Assets/Eagle/`, tactical battle UI in `Assets/Shardok/`
|
||||
- Seamless transition between strategic gameplay and hex-based tactical combat
|
||||
- **NEVER add defensive null checks on Unity Inspector fields** - these hide configuration bugs. If a field isn't
|
||||
linked in the editor, it should throw a NullReferenceException so the problem is immediately obvious. Silently
|
||||
skipping code when a required field is null makes bugs harder to find.
|
||||
|
||||
**Go (Build Tools):**
|
||||
|
||||
- Build automation and code generation utilities
|
||||
- AWS S3 integration for deployment artifacts
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
- Comprehensive unit tests for both Scala and C++ components
|
||||
- Integration tests for Eagle-Shardok communication
|
||||
- Map validation tests ensure game content integrity
|
||||
- Use `GameSettings_test_utils.cpp` and `ShardokEngineBasedTestData.cpp` for C++ test helpers
|
||||
|
||||
### Scala Testing Patterns
|
||||
|
||||
**Use `inside()` instead of `asInstanceOf` for type matching in tests:**
|
||||
|
||||
Never use `asInstanceOf` in tests. Instead, use ScalaTest's `inside()` pattern for safe type matching:
|
||||
|
||||
```scala
|
||||
// BAD - don't do this
|
||||
val changedHero = result.changedHeroes.head.asInstanceOf[ChangedHeroC]
|
||||
changedHero.heroId shouldBe 19
|
||||
|
||||
// GOOD - use inside() pattern
|
||||
import org.scalatest.Inside.inside
|
||||
|
||||
inside(result.changedHeroes.head) { case changedHero: ChangedHeroC =>
|
||||
changedHero.heroId shouldBe 19
|
||||
changedHero.vigorChange shouldBe StatDelta(17.2)
|
||||
}
|
||||
```
|
||||
|
||||
The `inside()` pattern:
|
||||
- Provides better error messages when the type doesn't match
|
||||
- Is idiomatic ScalaTest
|
||||
- Works with pattern matching for more complex assertions
|
||||
|
||||
## Performance Testing
|
||||
|
||||
When making performance-related changes to the AI or engine:
|
||||
|
||||
```bash
|
||||
# 1. Commit your changes to a feature branch
|
||||
git checkout -b performance-improvement-feature
|
||||
git add . && git commit -m "Implement performance improvement"
|
||||
|
||||
# 2. Run performance tests multiple times on your branch to reduce noise
|
||||
for i in 1 2 3; do
|
||||
echo "=== Run $i ==="
|
||||
./scripts/ai_perf_test.sh 2>&1 | grep -A 20 "AI Search Performance Summary"
|
||||
done
|
||||
# Save or note the results
|
||||
|
||||
# 3. Switch to main branch and run the same tests
|
||||
git checkout main
|
||||
for i in 1 2 3; do
|
||||
echo "=== Run $i ==="
|
||||
./scripts/ai_perf_test.sh 2>&1 | grep -A 20 "AI Search Performance Summary"
|
||||
done
|
||||
|
||||
# 4. Compare the results between your branch and main
|
||||
# Key metrics to compare:
|
||||
# - Commands evaluated at each depth (e.g., "Depth 3: 169/523 commands")
|
||||
# - Average search depth achieved
|
||||
# - Completion rates at each depth
|
||||
```
|
||||
|
||||
**Important notes:**
|
||||
|
||||
- Run tests multiple times (3-5) to account for performance variance
|
||||
- Focus on commands evaluated at each depth rather than total commands
|
||||
- Commands at different depths aren't directly comparable (depth 3 is more valuable than depth 2)
|
||||
- **Always test performance changes** - what seems like an optimization may sometimes have unexpected overhead or
|
||||
behavior changes.
|
||||
|
||||
## Troubleshooting Scala Build Errors
|
||||
|
||||
### MissingType Errors
|
||||
|
||||
When you see errors like:
|
||||
```
|
||||
dotty.tools.dotc.core.MissingType: Cannot resolve reference to type net.eagle0.eagle.internal.game_state.type.GameState
|
||||
```
|
||||
|
||||
**This is NOT a Scala compiler crash.** This is a missing dependency in BUILD.bazel.
|
||||
|
||||
**How to fix:**
|
||||
1. Identify the missing type from the error message (e.g., `game_state.GameState`)
|
||||
2. Find the Bazel target that provides this type (e.g., `//src/main/protobuf/net/eagle0/eagle/internal:game_state_scala_proto`)
|
||||
3. Add it to the `deps` of the failing target
|
||||
4. If the type appears in a public method signature, also add it to `exports` so downstream targets can see it
|
||||
|
||||
**Common pattern:** When adding a method to a class that takes or returns a proto type, the proto dependency often needs to be added to both `deps` AND `exports`.
|
||||
|
||||
### Bazel Clean
|
||||
|
||||
**NEVER run `bazel clean` without asking first.** It rarely fixes actual issues and wastes significant rebuild time. The issues that seem like they need `bazel clean` are usually:
|
||||
- Missing imports in Scala code
|
||||
- Missing dependencies in BUILD.bazel
|
||||
- Missing exports for types used in public signatures
|
||||
|
||||
## Game Content
|
||||
|
||||
**Maps:** `.e0mj` files in `/src/main/resources/net/eagle0/shardok/maps/`
|
||||
**Configuration:** Game parameters in `/src/main/resources/net/eagle0/eagle/game_parameters.json`
|
||||
**Data Files:** TSV format for battalions, heroes, and other game data
|
||||
|
||||
**NEVER modify hero names.** Do not change names in `heroes.tsv`, `generated_heroes`, or any other hero data files. The names are carefully chosen and are not to be altered.
|
||||
|
||||
## Deployment
|
||||
|
||||
- Bazel handles multi-language builds and dependencies
|
||||
- CI/CD via GitHub Actions with platform-specific build scripts in `/ci/github_actions/`
|
||||
- Docker containerization available via `ci/eagle_run.Dockerfile`
|
||||
- Always run "bazel run //:gazelle" after editing any BUILD.bazel files
|
||||
- *ALWAYS ALWAYS* run "bazel run gazelle" after any change that modifies a BUILD.bazel file
|
||||
@@ -1,6 +1,5 @@
|
||||
load("@bazel_gazelle//:def.bzl", "gazelle")
|
||||
load("@io_bazel_rules_go//go:def.bzl", "nogo")
|
||||
load("@rules_shell//shell:sh_test.bzl", "sh_test")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
@@ -11,7 +10,6 @@ platform(
|
||||
"@platforms//os:linux",
|
||||
"@platforms//cpu:x86_64",
|
||||
],
|
||||
missing_toolchain_error = "No Linux x86_64 toolchain matched. Docker/Eagle image builds should pass --platforms=//:linux_x86_64 and --extra_toolchains=@llvm_toolchain_linux//:cc-toolchain-x86_64-linux when compiling C++ code.",
|
||||
)
|
||||
|
||||
# Platform for cross-compiling to Linux ARM64
|
||||
@@ -21,7 +19,6 @@ platform(
|
||||
"@platforms//os:linux",
|
||||
"@platforms//cpu:aarch64",
|
||||
],
|
||||
missing_toolchain_error = "No Linux ARM64 toolchain matched. Shardok ARM64 builds should pass --platforms=//:linux_arm64 and --extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux.",
|
||||
)
|
||||
|
||||
gazelle(name = "gazelle")
|
||||
|
||||
@@ -1,15 +1,7 @@
|
||||
# CLAUDE.md
|
||||
|
||||
## CRITICAL BASH RULES (NEVER VIOLATE)
|
||||
|
||||
**NEVER chain bash commands.** Do not use `&&`, `||`, or `;` to combine commands. Each command must be a separate Bash tool call. Use parallel tool calls when commands are independent.
|
||||
|
||||
**NEVER prefix a command with `cd`.** Run `git`, `gh`, `bazel`, etc. directly from whatever the cwd already is. The cwd is a subdirectory of the worktree; `git`/`gh` find the repo via `.git` discovery and `bazel` finds the workspace via MODULE.bazel discovery — walking up from a subdir works fine. The shell resets cwd after every command anyway, so a `cd` never persists. (And `cd <dir> && <cmd>` violates the no-chaining rule above and forces an approval prompt every time.) Only `cd` in the rare case a tool genuinely cannot locate its root, and even then as its own Bash call, never a chain.
|
||||
|
||||
## CRITICAL GIT RULES (NEVER VIOLATE)
|
||||
|
||||
**NEVER use `git -C`.** Just run `git` directly from the cwd — it finds the repo via `.git` discovery. Do not `cd` to the repo root either (see the no-`cd` bash rule above).
|
||||
|
||||
**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.
|
||||
@@ -228,7 +220,7 @@ to be used for different players or game situations within the same server proce
|
||||
**C# (Unity Client):**
|
||||
|
||||
- Located in `/src/main/csharp/net/eagle0/clients/unity/eagle0/`
|
||||
- Uses Unity 6 (6000.4.10f1) with comprehensive protobuf integration (100+ .proto files)
|
||||
- Uses Unity 6 (6000.0.32f1) with comprehensive protobuf integration (100+ .proto files)
|
||||
- Key components: `EagleConnection.cs` (gRPC client), `EagleGameController.cs` (main game logic)
|
||||
- Real-time bidirectional streaming with server via `PersistentClientConnection.cs`
|
||||
- Strategic map UI in `Assets/Eagle/`, tactical battle UI in `Assets/Shardok/`
|
||||
@@ -352,4 +344,4 @@ dotty.tools.dotc.core.MissingType: Cannot resolve reference to type net.eagle0.e
|
||||
- CI/CD via GitHub Actions with platform-specific build scripts in `/ci/github_actions/`
|
||||
- Docker containerization available via `ci/eagle_run.Dockerfile`
|
||||
- Always run "bazel run //:gazelle" after editing any BUILD.bazel files
|
||||
- *ALWAYS ALWAYS* run "bazel run gazelle" after any change that modifies a BUILD.bazel file
|
||||
- *ALWAYS ALWAYS* run "bazel run gazelle" after any change that modifies a BUILD.bazel file
|
||||
+65
-147
@@ -3,37 +3,25 @@ module(name = "net_eagle0")
|
||||
# Version constants
|
||||
SCALA_VERSION = "3.7.4"
|
||||
|
||||
NETTY_VERSION = "4.1.135.Final"
|
||||
NETTY_VERSION = "4.1.127.Final"
|
||||
|
||||
SCALAPB_VERSION = "1.0.0-alpha.5"
|
||||
SCALAPB_VERSION = "1.0.0-alpha.1"
|
||||
|
||||
SCALAPB_JSON4S_VERSION = "1.0.0-alpha.1"
|
||||
|
||||
AWS_SDK_VERSION = "2.46.9"
|
||||
|
||||
SLF4J_VERSION = "2.0.18"
|
||||
AWS_SDK_VERSION = "2.41.18"
|
||||
|
||||
#
|
||||
# Core Build Tools
|
||||
#
|
||||
|
||||
bazel_dep(name = "bazel_skylib", version = "1.9.0")
|
||||
bazel_dep(name = "aspect_rules_esbuild", version = "0.26.0")
|
||||
bazel_dep(name = "aspect_rules_js", version = "3.2.2")
|
||||
bazel_dep(name = "bazel_jar_jar", version = "0.1.15", repo_name = "bazel_jar_jar")
|
||||
bazel_dep(name = "platforms", version = "1.1.0")
|
||||
bazel_dep(name = "rules_dotnet", version = "0.21.5")
|
||||
bazel_dep(name = "rules_foreign_cc", version = "0.15.1")
|
||||
bazel_dep(name = "rules_android", version = "0.7.3")
|
||||
bazel_dep(name = "rules_nodejs", version = "6.7.4")
|
||||
bazel_dep(name = "platforms", version = "1.0.0")
|
||||
bazel_dep(name = "rules_pkg", version = "1.2.0")
|
||||
bazel_dep(name = "rules_shell", version = "0.8.0")
|
||||
|
||||
#
|
||||
# Language Support - Scala
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_scala", version = "7.2.6")
|
||||
bazel_dep(name = "rules_scala", version = "7.2.1")
|
||||
|
||||
scala_config = use_extension(
|
||||
"@rules_scala//scala/extensions:config.bzl",
|
||||
@@ -53,31 +41,21 @@ scala_deps.scala_proto()
|
||||
# Language Support - C++
|
||||
#
|
||||
|
||||
bazel_dep(name = "toolchains_llvm", version = "1.8.0")
|
||||
bazel_dep(name = "toolchains_llvm", version = "1.6.0")
|
||||
|
||||
llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm")
|
||||
|
||||
LLVM_VERSION = "22.1.7"
|
||||
|
||||
LLVM_DISTRIBUTIONS = {
|
||||
"LLVM-22.1.7-Linux-ARM64.tar.xz": "118ca2d3ad9da34367e05735317854e7977db45dc4c02a32af58da64c23b8789",
|
||||
"LLVM-22.1.7-Linux-X64.tar.xz": "edb0522b41e261819c06ea437d249f9b8acfa413d3805bc9920eec6fb76ff830",
|
||||
"LLVM-22.1.7-macOS-ARM64.tar.xz": "4177245188b0a30a6539c96b361dea56f253485756bfd8927a6a59e7301e7806",
|
||||
}
|
||||
|
||||
# Native toolchain (macOS -> macOS, Linux -> Linux)
|
||||
llvm.toolchain(
|
||||
name = "llvm_toolchain",
|
||||
extra_llvm_distributions = LLVM_DISTRIBUTIONS,
|
||||
llvm_version = LLVM_VERSION,
|
||||
llvm_version = "20.1.4",
|
||||
)
|
||||
|
||||
# Cross-compilation toolchain (macOS -> Linux x86_64)
|
||||
# Uses the same LLVM distribution but with a Linux sysroot
|
||||
llvm.toolchain(
|
||||
name = "llvm_toolchain_linux",
|
||||
extra_llvm_distributions = LLVM_DISTRIBUTIONS,
|
||||
llvm_version = LLVM_VERSION,
|
||||
llvm_version = "20.1.4",
|
||||
)
|
||||
|
||||
# Linux x86_64 sysroot for cross-compilation
|
||||
@@ -90,8 +68,7 @@ llvm.sysroot(
|
||||
# Cross-compilation toolchain (macOS -> Linux ARM64)
|
||||
llvm.toolchain(
|
||||
name = "llvm_toolchain_linux_arm64",
|
||||
extra_llvm_distributions = LLVM_DISTRIBUTIONS,
|
||||
llvm_version = LLVM_VERSION,
|
||||
llvm_version = "20.1.4",
|
||||
)
|
||||
|
||||
# Linux ARM64 sysroot for cross-compilation
|
||||
@@ -100,6 +77,7 @@ llvm.sysroot(
|
||||
label = "@linux_sysroot_arm64//sysroot",
|
||||
targets = ["linux-aarch64"],
|
||||
)
|
||||
|
||||
use_repo(llvm, "llvm_toolchain", "llvm_toolchain_linux", "llvm_toolchain_linux_arm64")
|
||||
|
||||
# Download the Linux sysroots (Ubuntu 24.04 Noble for C++23 support)
|
||||
@@ -125,11 +103,11 @@ sysroot(
|
||||
# Language Support - Go
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_go", version = "0.61.1", repo_name = "io_bazel_rules_go")
|
||||
bazel_dep(name = "gazelle", version = "0.51.3", repo_name = "bazel_gazelle")
|
||||
bazel_dep(name = "rules_go", version = "0.59.0", repo_name = "io_bazel_rules_go")
|
||||
bazel_dep(name = "gazelle", version = "0.47.0", repo_name = "bazel_gazelle")
|
||||
|
||||
go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
|
||||
go_sdk.download(version = "1.25.11")
|
||||
go_sdk.download(version = "1.23.3")
|
||||
use_repo(go_sdk, "go_default_sdk")
|
||||
|
||||
go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps")
|
||||
@@ -152,7 +130,7 @@ use_repo(
|
||||
# Language Support - Rust
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_rust", version = "0.70.0")
|
||||
bazel_dep(name = "rules_rust", version = "0.68.1")
|
||||
|
||||
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
|
||||
rust.toolchain(edition = "2021")
|
||||
@@ -171,16 +149,21 @@ use_repo(crate, "map_generator_crates")
|
||||
#
|
||||
# Platform Support - Apple/iOS
|
||||
#
|
||||
# Note: rules_apple is NOT included in the main workspace due to rules_swift
|
||||
# version conflicts with grpc. SparklePlugin (which needs rules_apple) is built
|
||||
# in a separate workspace at sparkle_workspace/ to isolate the conflict.
|
||||
#
|
||||
|
||||
bazel_dep(name = "apple_support", version = "2.6.1", repo_name = "build_bazel_apple_support")
|
||||
bazel_dep(name = "rules_apple", version = "4.5.3", repo_name = "build_bazel_rules_apple")
|
||||
bazel_dep(name = "apple_support", version = "1.24.1", repo_name = "build_bazel_apple_support")
|
||||
|
||||
multiple_version_override(
|
||||
# rules_swift is a transitive dep of grpc and flatbuffers with conflicting versions:
|
||||
# - grpc 1.76.0.bcr.1 requires rules_swift 3.x (compatibility level 3)
|
||||
# - flatbuffers requires rules_swift 2.x (compatibility level 2)
|
||||
# We don't use Swift directly, but we need to force a single version.
|
||||
# Force 3.x since grpc is more complex and harder to downgrade.
|
||||
single_version_override(
|
||||
module_name = "rules_swift",
|
||||
versions = [
|
||||
"2.1.1",
|
||||
"3.5.0",
|
||||
],
|
||||
version = "3.1.2",
|
||||
)
|
||||
|
||||
# Register Apple CC toolchain for Objective-C compilation
|
||||
@@ -188,47 +171,17 @@ apple_cc_configure = use_extension(
|
||||
"@build_bazel_apple_support//crosstool:setup.bzl",
|
||||
"apple_cc_configure_extension",
|
||||
)
|
||||
use_repo(apple_cc_configure, "local_config_apple_cc", "local_config_apple_cc_toolchains")
|
||||
use_repo(apple_cc_configure, "local_config_apple_cc")
|
||||
|
||||
#
|
||||
# Protocol Buffers & RPC
|
||||
#
|
||||
|
||||
bazel_dep(name = "protobuf", version = "35.1", repo_name = "com_google_protobuf")
|
||||
single_version_override(
|
||||
module_name = "protobuf",
|
||||
version = "35.1",
|
||||
)
|
||||
|
||||
# Use pre-built protoc binaries instead of compiling from source.
|
||||
# See: https://protobuf.dev/reference/cpp/cpp-generated/#invocation
|
||||
prebuilt_protoc = use_extension("@com_google_protobuf//bazel/private/oss/toolchains/prebuilt:protoc_extension.bzl", "protoc")
|
||||
use_repo(
|
||||
prebuilt_protoc,
|
||||
"prebuilt_protoc.linux_aarch_64",
|
||||
"prebuilt_protoc.linux_ppcle_64",
|
||||
"prebuilt_protoc.linux_s390_64",
|
||||
"prebuilt_protoc.linux_x86_32",
|
||||
"prebuilt_protoc.linux_x86_64",
|
||||
"prebuilt_protoc.osx_aarch_64",
|
||||
"prebuilt_protoc.osx_x86_64",
|
||||
"prebuilt_protoc.win32",
|
||||
"prebuilt_protoc.win64",
|
||||
)
|
||||
|
||||
bazel_dep(name = "protobuf", version = "33.5", repo_name = "com_google_protobuf")
|
||||
bazel_dep(name = "rules_proto", version = "7.1.0")
|
||||
bazel_dep(name = "rules_cc", version = "0.2.19")
|
||||
bazel_dep(name = "grpc", version = "1.81.1")
|
||||
|
||||
# gRPC 1.81.1's generated upb module map trips Clang's layering check in the
|
||||
# Linux ARM64 cross-toolchain even though the normal Bazel deps are present.
|
||||
single_version_override(
|
||||
module_name = "grpc",
|
||||
patch_strip = 1,
|
||||
patches = ["//third_party/grpc/patches:disable_xds_client_layering_check.patch"],
|
||||
)
|
||||
|
||||
bazel_dep(name = "grpc-java", version = "1.78.0.bcr.1")
|
||||
bazel_dep(name = "rules_cc", version = "0.2.14")
|
||||
bazel_dep(name = "grpc", version = "1.74.1")
|
||||
bazel_dep(name = "grpc-java", version = "1.78.0")
|
||||
bazel_dep(name = "rules_proto_grpc_csharp", version = "5.8.0")
|
||||
bazel_dep(name = "flatbuffers", version = "25.12.19")
|
||||
|
||||
@@ -236,13 +189,13 @@ bazel_dep(name = "flatbuffers", version = "25.12.19")
|
||||
# Testing
|
||||
#
|
||||
|
||||
bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
|
||||
bazel_dep(name = "googletest", version = "1.17.0")
|
||||
|
||||
#
|
||||
# Container Images (OCI)
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_oci", version = "2.3.0")
|
||||
bazel_dep(name = "rules_oci", version = "2.2.7")
|
||||
bazel_dep(name = "aspect_bazel_lib", version = "2.22.5")
|
||||
|
||||
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
|
||||
@@ -253,19 +206,19 @@ oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
|
||||
# curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json" "https://registry-1.docker.io/v2/library/eclipse-temurin/manifests/25-jre" | python3 -c "import json,sys; m=json.load(sys.stdin); [print(p['digest']) for p in m.get('manifests',[]) if p['platform']['architecture']=='amd64']"
|
||||
oci.pull(
|
||||
name = "eclipse_temurin_25_jre",
|
||||
digest = "sha256:0beb258e10ad28d2bae0f74349a170bdfa67da0ef87c028cbf3d95c3e1170120",
|
||||
digest = "sha256:98236ffdfb61cff3fc4f8e40e5b80460a5a4c35a0f56a5b4182a926844a7db49",
|
||||
image = "docker.io/library/eclipse-temurin",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "26-jre",
|
||||
tag = "25-jre",
|
||||
)
|
||||
|
||||
# Base image for JFR sidecar (Java 25 JDK - includes jcmd for JFR dumps)
|
||||
oci.pull(
|
||||
name = "eclipse_temurin_25_jdk",
|
||||
digest = "sha256:41c3a5b8a607d226c73e81538cd94d0e66f746f41f057561447eb47b40a91a65",
|
||||
digest = "sha256:bc2d562355813350f47bc472b9721b0b84761866671ad95d9a9f1151fa69da6e",
|
||||
image = "docker.io/library/eclipse-temurin",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "26-jdk",
|
||||
tag = "25-jdk",
|
||||
)
|
||||
|
||||
# Base image for Shardok (Ubuntu 24.04 for C++ runtime)
|
||||
@@ -276,16 +229,15 @@ oci.pull(
|
||||
"linux/amd64",
|
||||
"linux/arm64/v8",
|
||||
],
|
||||
tag = "26.04",
|
||||
tag = "24.04",
|
||||
)
|
||||
|
||||
# Base image for Admin Server (Alpine for lightweight Go binary)
|
||||
oci.pull(
|
||||
name = "alpine_linux",
|
||||
digest = "sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8b",
|
||||
image = "docker.io/library/alpine",
|
||||
platforms = ["linux/amd64"],
|
||||
tag = "3.24",
|
||||
tag = "3.21",
|
||||
)
|
||||
use_repo(oci, "alpine_linux", "alpine_linux_linux_amd64", "eclipse_temurin_25_jdk", "eclipse_temurin_25_jdk_linux_amd64", "eclipse_temurin_25_jre", "eclipse_temurin_25_jre_linux_amd64", "ubuntu_24_04", "ubuntu_24_04_linux_amd64", "ubuntu_24_04_linux_arm64_v8")
|
||||
|
||||
@@ -293,8 +245,8 @@ use_repo(oci, "alpine_linux", "alpine_linux_linux_amd64", "eclipse_temurin_25_jd
|
||||
# Java/Scala Dependencies
|
||||
#
|
||||
|
||||
bazel_dep(name = "rules_java", version = "9.6.1")
|
||||
bazel_dep(name = "rules_jvm_external", version = "7.0")
|
||||
bazel_dep(name = "rules_java", version = "9.3.0")
|
||||
bazel_dep(name = "rules_jvm_external", version = "6.10")
|
||||
|
||||
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
|
||||
maven.install(
|
||||
@@ -313,7 +265,7 @@ maven.install(
|
||||
|
||||
# ScalaPB
|
||||
"com.thesamet.scalapb:lenses_3:%s" % SCALAPB_VERSION,
|
||||
"com.thesamet.scalapb:scalapb-json4s_3:%s" % SCALAPB_JSON4S_VERSION,
|
||||
"com.thesamet.scalapb:scalapb-json4s_3:%s" % SCALAPB_VERSION,
|
||||
"com.thesamet.scalapb:scalapb-runtime_3:%s" % SCALAPB_VERSION,
|
||||
"com.thesamet.scalapb:scalapb-runtime-grpc_3:%s" % SCALAPB_VERSION,
|
||||
"com.thesamet.scalapb:compilerplugin_3:%s" % SCALAPB_VERSION,
|
||||
@@ -325,10 +277,7 @@ maven.install(
|
||||
"org.json4s:json4s-native_3:4.1.0-M8",
|
||||
|
||||
# Testing
|
||||
"org.scalamock:scalamock_3:7.5.5",
|
||||
|
||||
# Developer tools
|
||||
"org.scalameta:scalafmt-cli_2.13:3.11.1",
|
||||
"org.scalamock:scalamock_3:7.4.1",
|
||||
|
||||
# AWS SDK
|
||||
"software.amazon.awssdk:s3-transfer-manager:%s" % AWS_SDK_VERSION,
|
||||
@@ -340,73 +289,58 @@ maven.install(
|
||||
"software.amazon.awssdk:http-client-spi:%s" % AWS_SDK_VERSION,
|
||||
|
||||
# AWS Lambda
|
||||
"com.amazonaws:aws-lambda-java-core:1.4.0",
|
||||
"com.amazonaws:aws-lambda-java-events:3.16.1",
|
||||
"com.amazonaws:aws-lambda-java-core:1.2.3",
|
||||
"com.amazonaws:aws-lambda-java-events:3.13.0",
|
||||
|
||||
# Logging
|
||||
"org.slf4j:slf4j-api:%s" % SLF4J_VERSION,
|
||||
"org.slf4j:slf4j-simple:%s" % SLF4J_VERSION,
|
||||
"org.slf4j:slf4j-api:2.0.16",
|
||||
"org.slf4j:slf4j-simple:2.0.16",
|
||||
|
||||
# Other
|
||||
"org.reactivestreams:reactive-streams:1.0.4",
|
||||
"javax.xml.bind:jaxb-api:2.3.1",
|
||||
|
||||
# OkHttp (for SSE with read timeout support, OAuth HTTP calls)
|
||||
"com.squareup.okhttp3:okhttp-jvm:5.4.0",
|
||||
"com.squareup.okhttp3:okhttp-sse:5.4.0",
|
||||
"com.squareup.okhttp3:okhttp:4.12.0",
|
||||
"com.squareup.okhttp3:okhttp-sse:4.12.0",
|
||||
|
||||
# JWT (for OAuth token handling)
|
||||
"com.nimbusds:nimbus-jose-jwt:10.9.1",
|
||||
"com.nimbusds:nimbus-jose-jwt:9.37.3",
|
||||
|
||||
# Error tracking
|
||||
"io.sentry:sentry:8.43.2",
|
||||
"io.sentry:sentry:8.31.0",
|
||||
|
||||
# SQLite for client text storage
|
||||
"org.xerial:sqlite-jdbc:3.53.2.0",
|
||||
|
||||
# Postgres for production history benchmarking and migration work
|
||||
"org.postgresql:postgresql:42.7.11",
|
||||
"org.xerial:sqlite-jdbc:3.46.1.0",
|
||||
],
|
||||
duplicate_version_warning = "error",
|
||||
fail_if_repin_required = True,
|
||||
known_contributing_modules = [
|
||||
"grpc-java",
|
||||
"protobuf",
|
||||
],
|
||||
lock_file = "//:maven_install.json",
|
||||
repositories = [
|
||||
"https://repo.maven.apache.org/maven2",
|
||||
"https://repo1.maven.org/maven2",
|
||||
],
|
||||
)
|
||||
|
||||
# json4s' Scala 3 reflection path can load scala.quoted.staging at runtime, but
|
||||
# its Maven metadata does not bring the staging jar transitively.
|
||||
maven.artifact(
|
||||
artifact = "scala3-staging_3",
|
||||
exclusions = ["org.scala-lang:scala3-compiler_3"],
|
||||
group = "org.scala-lang",
|
||||
version = SCALA_VERSION,
|
||||
)
|
||||
|
||||
# Force specific versions for dependencies with conflicts between grpc-java and protobuf
|
||||
maven.artifact(
|
||||
artifact = "gson",
|
||||
force_version = True,
|
||||
group = "com.google.code.gson",
|
||||
version = "2.14.0",
|
||||
version = "2.11.0",
|
||||
)
|
||||
maven.artifact(
|
||||
artifact = "error_prone_annotations",
|
||||
force_version = True,
|
||||
group = "com.google.errorprone",
|
||||
version = "2.50.0",
|
||||
version = "2.30.0",
|
||||
)
|
||||
maven.artifact(
|
||||
artifact = "guava",
|
||||
force_version = True,
|
||||
group = "com.google.guava",
|
||||
version = "33.6.0-jre",
|
||||
version = "33.3.1-android",
|
||||
)
|
||||
|
||||
use_repo(maven, "maven", "unpinned_maven")
|
||||
|
||||
#
|
||||
@@ -414,7 +348,6 @@ use_repo(maven, "maven", "unpinned_maven")
|
||||
#
|
||||
|
||||
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
|
||||
|
||||
# GTL (for parallel_hashmap)
|
||||
@@ -425,8 +358,6 @@ GTL_SHA = "1969c45dd76eac0dd87e9e2b65cffe358617f4fe1bcd203f72f427742537913a"
|
||||
http_archive(
|
||||
name = "gtl",
|
||||
build_file_content = """
|
||||
load("@rules_cc//cc:defs.bzl", "cc_library")
|
||||
|
||||
cc_library(
|
||||
name = "gtl",
|
||||
hdrs = glob(["include/gtl/*.hpp"]),
|
||||
@@ -453,40 +384,32 @@ http_archive(
|
||||
],
|
||||
)
|
||||
|
||||
# Sparkle framework for macOS auto-updates
|
||||
SPARKLE_VERSION = "2.9.3"
|
||||
|
||||
http_archive(
|
||||
name = "sparkle",
|
||||
build_file = "//src/main/objc/net/eagle0/clients/unity/sparkle/external:BUILD.sparkle",
|
||||
sha256 = "74a07da821f92b79310009954c0e15f350173374a3abe39095b4fc5096916be6",
|
||||
strip_prefix = "",
|
||||
url = "https://github.com/sparkle-project/Sparkle/releases/download/%s/Sparkle-%s.tar.xz" % (SPARKLE_VERSION, SPARKLE_VERSION),
|
||||
)
|
||||
# Note: Sparkle framework is defined in sparkle_workspace/MODULE.bazel
|
||||
# (not in main workspace due to rules_swift version conflicts)
|
||||
|
||||
# Busybox static binary for Docker health checks (provides nc, wget, etc.)
|
||||
# Primary: DigitalOcean Spaces (public, reliable)
|
||||
# Fallback: busybox.net (can be unreliable/slow)
|
||||
http_file(
|
||||
name = "busybox_x86_64",
|
||||
downloaded_file_path = "busybox",
|
||||
executable = True,
|
||||
sha256 = "6e123e7f3202a8c1e9b1f94d8941580a25135382b99e8d3e34fb858bba311348",
|
||||
urls = [
|
||||
"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/busybox/busybox-1.35.0-x86_64-linux-musl",
|
||||
"https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox",
|
||||
],
|
||||
downloaded_file_path = "busybox",
|
||||
executable = True,
|
||||
)
|
||||
|
||||
http_file(
|
||||
name = "busybox_aarch64",
|
||||
downloaded_file_path = "busybox",
|
||||
executable = True,
|
||||
sha256 = "141adb1b625a6f44c4b114f76b4387b4ea4f7ab802b88eb40e0d2f6adcccb1c3",
|
||||
urls = [
|
||||
# TODO: Upload aarch64 binary to GitHub release when needed
|
||||
"https://busybox.net/downloads/binaries/1.35.0-aarch64-linux-musl/busybox",
|
||||
],
|
||||
downloaded_file_path = "busybox",
|
||||
executable = True,
|
||||
)
|
||||
|
||||
# LLVM MinGW toolchain for Windows cross-compilation from macOS
|
||||
@@ -552,15 +475,10 @@ register_toolchains(
|
||||
"@rules_scala//testing:scalatest_toolchain",
|
||||
)
|
||||
|
||||
# Apple CC toolchain for Objective-C compilation (SparklePlugin).
|
||||
# Registered before LLVM so it has higher priority; Apple constraint matching
|
||||
# ensures it's only used for Apple-platform targets (objc_library, macos_bundle).
|
||||
register_toolchains("@local_config_apple_cc_toolchains//:all")
|
||||
|
||||
# Set dev_dependency so we can turn this off for swift MacOS builds
|
||||
register_toolchains(
|
||||
"@llvm_toolchain//:all",
|
||||
"@llvm_toolchain_linux//:cc-toolchain-x86_64-linux",
|
||||
"@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux",
|
||||
"@llvm_toolchain_linux//:all",
|
||||
"@llvm_toolchain_linux_arm64//:all",
|
||||
dev_dependency = True,
|
||||
)
|
||||
|
||||
Generated
+2488
-1917
File diff suppressed because one or more lines are too long
+9
-40
@@ -1,6 +1,5 @@
|
||||
load("@rules_oci//oci:defs.bzl", "oci_image", "oci_load", "oci_push")
|
||||
load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
|
||||
load("//ci:jar_split.bzl", "jar_split")
|
||||
|
||||
#
|
||||
# Deployment artifacts (tools needed on the host, not in containers)
|
||||
@@ -51,37 +50,11 @@ pkg_tar(
|
||||
# Push: bazel run //ci:eagle_server_push
|
||||
#
|
||||
|
||||
# Split the Eagle server runtime classpath into a stable third-party layer
|
||||
# (Scala stdlib, gRPC, Netty, ScalaPB, AWS SDK, ...) and a small first-party
|
||||
# layer that changes every commit. Most pushes then only re-upload the small
|
||||
# app layer instead of the whole ~150-300MB fat JAR.
|
||||
jar_split(
|
||||
name = "eagle_server_jars",
|
||||
binary = "//src/main/scala/net/eagle0/eagle:eagle_server",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "eagle_server_deps_jars",
|
||||
srcs = [":eagle_server_jars"],
|
||||
output_group = "deps",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "eagle_server_app_jars",
|
||||
srcs = [":eagle_server_jars"],
|
||||
output_group = "app",
|
||||
)
|
||||
|
||||
# Package the deploy JAR
|
||||
pkg_tar(
|
||||
name = "eagle_server_deps_layer",
|
||||
srcs = [":eagle_server_deps_jars"],
|
||||
package_dir = "/app/lib/deps",
|
||||
)
|
||||
|
||||
pkg_tar(
|
||||
name = "eagle_server_app_layer",
|
||||
srcs = [":eagle_server_app_jars"],
|
||||
package_dir = "/app/lib/app",
|
||||
name = "eagle_server_jar_layer",
|
||||
srcs = ["//src/main/scala/net/eagle0/eagle:eagle_server_deploy.jar"],
|
||||
package_dir = "/app",
|
||||
)
|
||||
|
||||
# Package the game resources needed at runtime
|
||||
@@ -109,11 +82,8 @@ oci_image(
|
||||
"-XX:+UnlockDiagnosticVMOptions",
|
||||
"-XX:+DebugNonSafepoints", # Required for JFR to see through inlined methods
|
||||
"-XX:FlightRecorderOptions=stackdepth=256",
|
||||
# Classpath glob is expanded by the JVM itself (exec-form, no shell).
|
||||
# Deps dir first keeps third-party precedence; app last shadows nothing.
|
||||
"-cp",
|
||||
"/app/lib/deps/*:/app/lib/app/*",
|
||||
"net.eagle0.eagle.Main",
|
||||
"-jar",
|
||||
"/app/eagle_server_deploy.jar",
|
||||
],
|
||||
env = {
|
||||
"JAVA_OPTS": "-Xmx2g -XX:+UseG1GC",
|
||||
@@ -121,8 +91,7 @@ oci_image(
|
||||
exposed_ports = ["40032/tcp"],
|
||||
tars = [
|
||||
":busybox_layer",
|
||||
":eagle_server_deps_layer",
|
||||
":eagle_server_app_layer",
|
||||
":eagle_server_jar_layer",
|
||||
":eagle_resources_layer",
|
||||
],
|
||||
workdir = "/app",
|
||||
@@ -212,9 +181,9 @@ oci_push(
|
||||
)
|
||||
|
||||
#
|
||||
# Shardok Server ARM64 Docker Image (for the Hetzner deployment host)
|
||||
# Shardok Server ARM64 Docker Image (for Hetzner on-demand compute)
|
||||
#
|
||||
# Build: bazel build //ci:shardok_server_image_arm64 --platforms=//:linux_arm64 --extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux
|
||||
# Build: bazel build //ci:shardok_server_image_arm64 --platforms=//:linux_arm64 --extra_toolchains=@llvm_toolchain_linux_arm64//:all
|
||||
# Load: bazel run //ci:shardok_server_load_arm64
|
||||
# Push: bazel run //ci:shardok_server_push_arm64
|
||||
#
|
||||
|
||||
@@ -5,7 +5,8 @@ set -x
|
||||
|
||||
COMMAND_LOG="$(bazel info command_log)"
|
||||
bazel build \
|
||||
//src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
//src/main/cpp/net/eagle0/shardok:shardok-server \
|
||||
//src/main/cpp/net/eagle0/shardok:shardok-inspector
|
||||
STATUS="$?"
|
||||
|
||||
if [ $STATUS -eq 0 ]
|
||||
@@ -25,6 +26,7 @@ echo "Setting up Shardok files..."
|
||||
/bin/cp -R ./bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server ./shardok/
|
||||
/bin/mkdir -p ./shardok/shardok-server.runfiles/net_eagle0/src/main/
|
||||
/bin/cp -R ./bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-server.runfiles/net_eagle0/src/main/resources ./shardok/shardok-server.runfiles/net_eagle0/src/main/
|
||||
/bin/cp -R ./bazel-bin/src/main/cpp/net/eagle0/shardok/shardok-inspector ./shardok/
|
||||
|
||||
echo "tar & zip shardok files..."
|
||||
/bin/tar cfh - shardok | /usr/bin/pigz > ./deploy/shardok.tar.gz
|
||||
|
||||
@@ -88,16 +88,6 @@ EOF
|
||||
|
||||
echo "Exporting IPA..."
|
||||
|
||||
# Restore keychain search list before export.
|
||||
# xcodebuild archive can reset the user keychain search list during long
|
||||
# builds, dropping the temporary CI keychain that holds the signing cert.
|
||||
# Re-add it here so xcodebuild -exportArchive can find the certificate.
|
||||
if [ -n "${KEYCHAIN_NAME:-}" ]; then
|
||||
echo "Restoring keychain search list (adding $KEYCHAIN_NAME)"
|
||||
security list-keychains -d user -s "$KEYCHAIN_NAME" login.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
|
||||
fi
|
||||
|
||||
# Debug: List available keychains and signing identities
|
||||
echo "=== Debug: Available keychains ==="
|
||||
security list-keychains -d user
|
||||
@@ -107,24 +97,11 @@ echo "=== Debug: Export options plist ==="
|
||||
cat "$EXPORT_OPTIONS_PLIST"
|
||||
echo "=== End debug ==="
|
||||
|
||||
# Export IPA with App Store Connect API key authentication.
|
||||
# Without the API key, xcodebuild falls back to the Xcode-Token in the
|
||||
# keychain, which expires periodically and breaks headless CI builds.
|
||||
EXPORT_AUTH_ARGS=()
|
||||
if [ -n "${APP_STORE_CONNECT_API_KEY_PATH:-}" ] && [ -n "${APP_STORE_CONNECT_API_KEY_ID:-}" ] && [ -n "${APP_STORE_CONNECT_API_ISSUER_ID:-}" ]; then
|
||||
echo "Using App Store Connect API key for export authentication"
|
||||
EXPORT_AUTH_ARGS=(
|
||||
-authenticationKeyPath "$APP_STORE_CONNECT_API_KEY_PATH"
|
||||
-authenticationKeyID "$APP_STORE_CONNECT_API_KEY_ID"
|
||||
-authenticationKeyIssuerID "$APP_STORE_CONNECT_API_ISSUER_ID"
|
||||
)
|
||||
fi
|
||||
|
||||
# Export IPA (removed -allowProvisioningUpdates as we're using manual signing)
|
||||
xcodebuild -exportArchive \
|
||||
-archivePath "$ARCHIVE_PATH" \
|
||||
-exportPath "$EXPORT_PATH" \
|
||||
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST" \
|
||||
"${EXPORT_AUTH_ARGS[@]}"
|
||||
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST"
|
||||
|
||||
# Find and rename the IPA to a consistent name
|
||||
IPA_FILE=$(find "$EXPORT_PATH" -name "*.ipa" | head -1)
|
||||
@@ -134,9 +111,3 @@ fi
|
||||
|
||||
echo "Export complete: $EXPORT_PATH/eagle0.ipa"
|
||||
ls -la "$EXPORT_PATH"
|
||||
|
||||
# Clean up DerivedData for Unity-iPhone builds.
|
||||
# Each build creates a new ~3.6GB folder (Unity-iPhone-<random>) that
|
||||
# accumulates and wastes disk space on the build runner.
|
||||
echo "Cleaning up Unity-iPhone DerivedData..."
|
||||
find ~/Library/Developer/Xcode/DerivedData -maxdepth 1 -name 'Unity-iPhone-*' -type d -exec rm -rf {} + 2>/dev/null || true
|
||||
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Build iOS Addressables only (no player build)
|
||||
# This switches Unity to iOS target and builds addressables for CDN upload
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
# Read Unity version from project file
|
||||
UNITY_VERSION=$(grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //')
|
||||
|
||||
# Use runner-specific build directory if EAGLE0_BUILD_DIR is set, otherwise default
|
||||
BUILD_BASE="${EAGLE0_BUILD_DIR:-/tmp/eagle0}"
|
||||
|
||||
WORKSPACE=$(pwd)
|
||||
|
||||
echo "Building protos"
|
||||
./scripts/build_protos.sh
|
||||
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
|
||||
LOG_PATH=${1:-"${BUILD_BASE}/editor_ios_addressables.log"}
|
||||
|
||||
echo "Building iOS Addressables"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_PATH")"
|
||||
|
||||
# Build Addressables for iOS target
|
||||
# Uses BuildiOSAddressables which explicitly switches build target
|
||||
# Capture exit code to show log on failure
|
||||
set +e
|
||||
${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
|
||||
-nographics \
|
||||
-batchmode \
|
||||
-quit \
|
||||
-executeMethod BuildScript.BuildiOSAddressables \
|
||||
-logFile "$LOG_PATH" \
|
||||
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
UNITY_EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ $UNITY_EXIT_CODE -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Unity build failed with exit code $UNITY_EXIT_CODE"
|
||||
echo "=== Unity Editor Log (last 200 lines) ==="
|
||||
tail -200 "$LOG_PATH" || echo "Could not read log file at $LOG_PATH"
|
||||
echo "=== End of Unity Editor Log ==="
|
||||
exit $UNITY_EXIT_CODE
|
||||
fi
|
||||
|
||||
echo "iOS Addressables build complete"
|
||||
echo "Bundles should be in: $WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0/ServerData/iOS/"
|
||||
@@ -9,23 +9,13 @@ WORKSPACE=$(pwd)
|
||||
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
|
||||
BUILD_DIR=$1
|
||||
LOG_PATH=$2
|
||||
BUILD_ADDRESSABLES=${3:-true}
|
||||
ADDRESSABLES_BUILD_LAYOUT=${4:-$BUILD_ADDRESSABLES}
|
||||
|
||||
echo "Building Mac in $BUILD_DIR"
|
||||
echo "Build Addressables: $BUILD_ADDRESSABLES"
|
||||
echo "Generate Addressables build layout: $ADDRESSABLES_BUILD_LAYOUT"
|
||||
|
||||
echo "Cleaning up $BUILD_DIR"
|
||||
/bin/rm -rf "$BUILD_DIR"
|
||||
/bin/mkdir -p "$BUILD_DIR"
|
||||
|
||||
if [ -n "${DOTNET_ROOT:-}" ]; then
|
||||
export PATH="$DOTNET_ROOT:$PATH"
|
||||
export DOTNET_HOST_PATH="$DOTNET_ROOT/dotnet"
|
||||
fi
|
||||
dotnet --list-sdks
|
||||
|
||||
# Use custom build script that builds Addressables before the player
|
||||
# Capture exit code to show log on failure
|
||||
set +e
|
||||
@@ -35,8 +25,6 @@ ${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
|
||||
-quit \
|
||||
-executeMethod BuildScript.BuildMacPlayer \
|
||||
-buildPath "$BUILD_DIR/eagle0.app" \
|
||||
-buildAddressables "$BUILD_ADDRESSABLES" \
|
||||
-addressablesBuildLayout "$ADDRESSABLES_BUILD_LAYOUT" \
|
||||
-logFile "$LOG_PATH" \
|
||||
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
UNITY_EXIT_CODE=$?
|
||||
@@ -50,25 +38,3 @@ if [ $UNITY_EXIT_CODE -ne 0 ]; then
|
||||
echo "=== End of Unity Editor Log ==="
|
||||
exit $UNITY_EXIT_CODE
|
||||
fi
|
||||
|
||||
if [ ! -f "$LOG_PATH" ]; then
|
||||
echo ""
|
||||
echo "ERROR: Unity exited successfully but did not write an editor log at $LOG_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fail the build if any prefab references are broken — this produces a player
|
||||
# that launches but has null Inspector fields, which is hard to debug.
|
||||
if grep -q "Missing Prefab" "$LOG_PATH"; then
|
||||
echo ""
|
||||
echo "ERROR: Build has missing prefab references (likely stale Library cache):"
|
||||
grep "Missing Prefab" "$LOG_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -q "Build asset version error" "$LOG_PATH"; then
|
||||
echo ""
|
||||
echo "ERROR: Build has asset version mismatches (likely stale Library cache):"
|
||||
grep "Build asset version error" "$LOG_PATH" | head -5
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -15,7 +15,6 @@ git log -3
|
||||
/bin/echo "build Windows"
|
||||
LOG_PATH="${BUILD_BASE}/editor_win.log"
|
||||
BUILD_DIR=$1
|
||||
BUILD_ADDRESSABLES=${2:-true}
|
||||
ADDRESSABLES_BUILD_LAYOUT=${3:-$BUILD_ADDRESSABLES}
|
||||
|
||||
./ci/github_actions/build_windows.sh "$BUILD_DIR" "$LOG_PATH" "$BUILD_ADDRESSABLES" "$ADDRESSABLES_BUILD_LAYOUT"
|
||||
./ci/github_actions/build_windows.sh $BUILD_DIR $LOG_PATH
|
||||
|
||||
|
||||
@@ -12,8 +12,6 @@ BUILD_BASE="${EAGLE0_BUILD_DIR:-/tmp/eagle0}"
|
||||
|
||||
WORKSPACE=$(pwd)
|
||||
BUILD_PATH=${1:-"${BUILD_BASE}/eagle0iOS"}
|
||||
BUILD_ADDRESSABLES=${2:-true}
|
||||
ADDRESSABLES_BUILD_LAYOUT=${3:-$BUILD_ADDRESSABLES}
|
||||
LOG_PATH="${BUILD_BASE}/editor_ios.log"
|
||||
|
||||
# Generate unique build number from git commit count
|
||||
@@ -27,8 +25,6 @@ echo "Building protos"
|
||||
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
|
||||
|
||||
echo "Building iOS Unity player to: $BUILD_PATH"
|
||||
echo "Build Addressables: $BUILD_ADDRESSABLES"
|
||||
echo "Generate Addressables build layout: $ADDRESSABLES_BUILD_LAYOUT"
|
||||
|
||||
mkdir -p "$(dirname "$LOG_PATH")"
|
||||
mkdir -p "$BUILD_PATH"
|
||||
@@ -43,8 +39,6 @@ ${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
|
||||
-executeMethod BuildScript.BuildiOSPlayer \
|
||||
-buildPath "$BUILD_PATH" \
|
||||
-buildNumber "$BUILD_NUMBER" \
|
||||
-buildAddressables "$BUILD_ADDRESSABLES" \
|
||||
-addressablesBuildLayout "$ADDRESSABLES_BUILD_LAYOUT" \
|
||||
-logFile "$LOG_PATH" \
|
||||
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
UNITY_EXIT_CODE=$?
|
||||
@@ -59,36 +53,6 @@ if [ $UNITY_EXIT_CODE -ne 0 ]; then
|
||||
exit $UNITY_EXIT_CODE
|
||||
fi
|
||||
|
||||
if [ ! -f "$LOG_PATH" ]; then
|
||||
echo ""
|
||||
echo "ERROR: Unity exited successfully but did not write an editor log at $LOG_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$BUILD_PATH/Data/Raw/aa/settings.json" ]; then
|
||||
echo ""
|
||||
echo "ERROR: iOS player is missing Addressables runtime data at:"
|
||||
echo " $BUILD_PATH/Data/Raw/aa/settings.json"
|
||||
echo "Build Addressables before packaging the player so Addressables can initialize at runtime."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fail the build if any prefab references are broken — this produces a player
|
||||
# that launches but has null Inspector fields, which is hard to debug.
|
||||
if grep -q "Missing Prefab" "$LOG_PATH"; then
|
||||
echo ""
|
||||
echo "ERROR: Build has missing prefab references (likely stale Library cache):"
|
||||
grep "Missing Prefab" "$LOG_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -q "Build asset version error" "$LOG_PATH"; then
|
||||
echo ""
|
||||
echo "ERROR: Build has asset version mismatches (likely stale Library cache):"
|
||||
grep "Build asset version error" "$LOG_PATH" | head -5
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "iOS Unity build complete"
|
||||
echo "Xcode project generated at: $BUILD_PATH"
|
||||
ls -la "$BUILD_PATH"
|
||||
|
||||
@@ -18,7 +18,5 @@ git log -3
|
||||
/bin/echo "build Mac"
|
||||
LOG_PATH="${BUILD_BASE}/editor_mac.log"
|
||||
BUILD_DIR=$1
|
||||
BUILD_ADDRESSABLES=${2:-true}
|
||||
ADDRESSABLES_BUILD_LAYOUT=${3:-$BUILD_ADDRESSABLES}
|
||||
|
||||
./ci/github_actions/build_mac.sh "$BUILD_DIR" "$LOG_PATH" "$BUILD_ADDRESSABLES" "$ADDRESSABLES_BUILD_LAYOUT"
|
||||
./ci/github_actions/build_mac.sh "$BUILD_DIR" "$LOG_PATH"
|
||||
|
||||
@@ -9,23 +9,13 @@ WORKSPACE=`pwd`
|
||||
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
|
||||
BUILD_DIR=$1
|
||||
LOG_PATH=$2
|
||||
BUILD_ADDRESSABLES=${3:-true}
|
||||
ADDRESSABLES_BUILD_LAYOUT=${4:-$BUILD_ADDRESSABLES}
|
||||
|
||||
echo "Building in $1"
|
||||
echo "Build Addressables: $BUILD_ADDRESSABLES"
|
||||
echo "Generate Addressables build layout: $ADDRESSABLES_BUILD_LAYOUT"
|
||||
|
||||
echo "Cleaning up $1"
|
||||
/bin/rm -rf $1
|
||||
/bin/mkdir -p $1
|
||||
|
||||
if [ -n "${DOTNET_ROOT:-}" ]; then
|
||||
export PATH="$DOTNET_ROOT:$PATH"
|
||||
export DOTNET_HOST_PATH="$DOTNET_ROOT/dotnet"
|
||||
fi
|
||||
dotnet --list-sdks
|
||||
|
||||
# Use custom build script that builds Addressables before the player
|
||||
# Capture exit code to show log on failure
|
||||
set +e
|
||||
@@ -35,8 +25,6 @@ ${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity \
|
||||
-quit \
|
||||
-executeMethod BuildScript.BuildWindowsPlayer \
|
||||
-buildPath "$BUILD_DIR/eagle0.exe" \
|
||||
-buildAddressables "$BUILD_ADDRESSABLES" \
|
||||
-addressablesBuildLayout "$ADDRESSABLES_BUILD_LAYOUT" \
|
||||
-logFile $LOG_PATH \
|
||||
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
UNITY_EXIT_CODE=$?
|
||||
@@ -50,33 +38,3 @@ if [ $UNITY_EXIT_CODE -ne 0 ]; then
|
||||
echo "=== End of Unity Editor Log ==="
|
||||
exit $UNITY_EXIT_CODE
|
||||
fi
|
||||
|
||||
if [ ! -f "$LOG_PATH" ]; then
|
||||
echo ""
|
||||
echo "ERROR: Unity exited successfully but did not write an editor log at $LOG_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$BUILD_DIR/eagle0_Data/StreamingAssets/aa/settings.json" ]; then
|
||||
echo ""
|
||||
echo "ERROR: Windows player is missing Addressables runtime data at:"
|
||||
echo " $BUILD_DIR/eagle0_Data/StreamingAssets/aa/settings.json"
|
||||
echo "Build Addressables before packaging the player so Addressables can initialize at runtime."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Fail the build if any prefab references are broken — this produces a player
|
||||
# that launches but has null Inspector fields, which is hard to debug.
|
||||
if grep -q "Missing Prefab" "$LOG_PATH"; then
|
||||
echo ""
|
||||
echo "ERROR: Build has missing prefab references (likely stale Library cache):"
|
||||
grep "Missing Prefab" "$LOG_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if grep -q "Build asset version error" "$LOG_PATH"; then
|
||||
echo ""
|
||||
echo "ERROR: Build has asset version mismatches (likely stale Library cache):"
|
||||
grep "Build asset version error" "$LOG_PATH" | head -5
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
BASE_SHA=${1:-}
|
||||
HEAD_SHA=${2:-HEAD}
|
||||
OUTPUT_FILE=${GITHUB_OUTPUT:-}
|
||||
|
||||
if [ -z "$BASE_SHA" ] || [[ "$BASE_SHA" =~ ^0+$ ]]; then
|
||||
echo "No usable base SHA for this run; building Addressables as a safe default."
|
||||
changed=true
|
||||
elif ! git cat-file -e "$BASE_SHA^{commit}"; then
|
||||
echo "Base SHA $BASE_SHA is not available locally; building Addressables as a safe default."
|
||||
changed=true
|
||||
else
|
||||
UNITY_PROJECT="src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
ADDRESSABLE_PREFIXES=(
|
||||
"$UNITY_PROJECT/Assets/AddressableAssetsData/"
|
||||
"$UNITY_PROJECT/Assets/Eagle/Effects/"
|
||||
"$UNITY_PROJECT/Assets/Hex Tiles/"
|
||||
"$UNITY_PROJECT/Assets/TileableBridgePack/"
|
||||
"$UNITY_PROJECT/Assets/Shardok/LargeFlames.prefab"
|
||||
"$UNITY_PROJECT/Assets/Shardok/soundEffects/"
|
||||
"$UNITY_PROJECT/Assets/Shardok/Sounds/"
|
||||
"$UNITY_PROJECT/Assets/Medieval Combat Sounds/"
|
||||
"$UNITY_PROJECT/Assets/Magic Spells Sound Effects LITE/"
|
||||
"$UNITY_PROJECT/Assets/Fantasy Interface Sounds/"
|
||||
"$UNITY_PROJECT/Assets/AustraliaAnimalsPackv1/"
|
||||
"$UNITY_PROJECT/Assets/Music/"
|
||||
"$UNITY_PROJECT/Assets/Editor/BuildScript.cs"
|
||||
)
|
||||
|
||||
is_addressables_input() {
|
||||
local file="$1"
|
||||
for prefix in "${ADDRESSABLE_PREFIXES[@]}"; do
|
||||
if [[ "$file" == "$prefix"* ]]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
changed=false
|
||||
while IFS= read -r file; do
|
||||
if is_addressables_input "$file"; then
|
||||
echo "Addressables-impacting change: $file"
|
||||
changed=true
|
||||
break
|
||||
fi
|
||||
done < <(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
|
||||
fi
|
||||
|
||||
if [ "$changed" = "true" ]; then
|
||||
echo "Building Addressables because their inputs changed."
|
||||
else
|
||||
echo "Skipping Addressables build; no Addressables inputs changed."
|
||||
fi
|
||||
|
||||
if [ -n "$OUTPUT_FILE" ]; then
|
||||
echo "changed=$changed" >> "$OUTPUT_FILE"
|
||||
else
|
||||
echo "changed=$changed"
|
||||
fi
|
||||
@@ -1,102 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
|
||||
repo_contents_cache_dir() {
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
printf '%s\n' "${HOME}/Library/Caches/eagle0-bazel-repo-contents"
|
||||
else
|
||||
printf '%s\n' "${XDG_CACHE_HOME:-${HOME}/.cache}/eagle0-bazel-repo-contents"
|
||||
fi
|
||||
}
|
||||
|
||||
write_bazel_local_config() {
|
||||
local repo_contents_cache
|
||||
repo_contents_cache="$(repo_contents_cache_dir)"
|
||||
|
||||
if [ "$(uname -s)" != "Darwin" ]; then
|
||||
if [ -n "${CI:-}" ]; then
|
||||
mkdir -p "$repo_contents_cache"
|
||||
{
|
||||
echo "# Generated by ci/github_actions/ensure_bazel_installed.sh; do not edit."
|
||||
printf 'common --repo_contents_cache=%s\n' "$repo_contents_cache"
|
||||
} > .bazelrc.local
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
local developer_dir=""
|
||||
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
|
||||
developer_dir="/Applications/Xcode.app/Contents/Developer"
|
||||
elif [ -d "/Library/Developer/CommandLineTools" ]; then
|
||||
developer_dir="/Library/Developer/CommandLineTools"
|
||||
else
|
||||
developer_dir="$(xcode-select -p 2>/dev/null || true)"
|
||||
fi
|
||||
|
||||
if [ -z "$developer_dir" ] || [ ! -d "$developer_dir" ]; then
|
||||
echo "ERROR: no usable Apple developer directory found"
|
||||
echo "Install Command Line Tools with: xcode-select --install"
|
||||
echo "Full Xcode is only required for workflows that build/sign Apple apps."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export DEVELOPER_DIR="$developer_dir"
|
||||
if [ -f .bazelrc.local ] && [ -z "${CI:-}" ]; then
|
||||
echo "Keeping existing .bazelrc.local outside CI"
|
||||
else
|
||||
mkdir -p "$repo_contents_cache"
|
||||
{
|
||||
echo "# Generated by ci/github_actions/ensure_bazel_installed.sh; do not edit."
|
||||
printf 'common --repo_contents_cache=%s\n' "$repo_contents_cache"
|
||||
printf 'common:macos --repo_env=DEVELOPER_DIR=%s\n' "$developer_dir"
|
||||
} > .bazelrc.local
|
||||
fi
|
||||
|
||||
if [ -n "${GITHUB_ENV:-}" ]; then
|
||||
echo "DEVELOPER_DIR=$developer_dir" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
echo "Using Apple developer directory at $developer_dir"
|
||||
}
|
||||
|
||||
write_bazel_local_config
|
||||
|
||||
if [ -n "${GITHUB_PATH:-}" ]; then
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if command -v bazel >/dev/null 2>&1; then
|
||||
echo "Using bazel at $(command -v bazel)"
|
||||
bazel --version
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v bazelisk >/dev/null 2>&1; then
|
||||
if ! command -v brew >/dev/null 2>&1; then
|
||||
echo "ERROR: neither bazel nor bazelisk is on PATH, and Homebrew is unavailable"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing bazelisk with Homebrew"
|
||||
brew install bazelisk
|
||||
fi
|
||||
|
||||
BAZEL_BIN_DIR="${RUNNER_TEMP:-/tmp}/bazel-bin"
|
||||
mkdir -p "$BAZEL_BIN_DIR"
|
||||
ln -sf "$(command -v bazelisk)" "$BAZEL_BIN_DIR/bazel"
|
||||
|
||||
if [ -n "${GITHUB_PATH:-}" ]; then
|
||||
echo "$BAZEL_BIN_DIR" >> "$GITHUB_PATH"
|
||||
fi
|
||||
|
||||
export PATH="$BAZEL_BIN_DIR:$PATH"
|
||||
echo "Using bazelisk as bazel at $BAZEL_BIN_DIR/bazel"
|
||||
bazel --version
|
||||
@@ -34,7 +34,6 @@ echo "Platform: ${PLATFORM}"
|
||||
# Check if Unity is installed and has required modules
|
||||
check_modules_installed() {
|
||||
local unity_path="${UNITY_INSTALL_PATH}/${UNITY_VERSION}"
|
||||
local unity_app_contents_path="${unity_path}/Unity.app/Contents"
|
||||
|
||||
if [ ! -d "$unity_path" ]; then
|
||||
return 1
|
||||
@@ -49,12 +48,15 @@ check_modules_installed() {
|
||||
fi
|
||||
;;
|
||||
mac)
|
||||
if ! has_mac_il2cpp_support "$unity_path" "$unity_app_contents_path"; then
|
||||
# Mac IL2CPP module
|
||||
if [ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport/Variations/macos_development_il2cpp" ] && \
|
||||
[ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport" ]; then
|
||||
echo "✗ Mac IL2CPP module not installed for Unity ${UNITY_VERSION}"
|
||||
return 1
|
||||
fi
|
||||
;;
|
||||
windows)
|
||||
# Windows mono module
|
||||
if [ ! -d "${unity_path}/PlaybackEngines/WindowsStandaloneSupport" ]; then
|
||||
echo "✗ Windows module not installed for Unity ${UNITY_VERSION}"
|
||||
return 1
|
||||
@@ -67,8 +69,8 @@ check_modules_installed() {
|
||||
echo "✗ iOS module missing"
|
||||
missing=1
|
||||
fi
|
||||
if ! has_mac_il2cpp_support "$unity_path" "$unity_app_contents_path"; then
|
||||
echo "✗ Mac IL2CPP module missing"
|
||||
if [ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport" ]; then
|
||||
echo "✗ Mac module missing"
|
||||
missing=1
|
||||
fi
|
||||
if [ ! -d "${unity_path}/PlaybackEngines/WindowsStandaloneSupport" ]; then
|
||||
@@ -84,27 +86,6 @@ check_modules_installed() {
|
||||
return 0
|
||||
}
|
||||
|
||||
has_mac_il2cpp_support() {
|
||||
local unity_path="$1"
|
||||
local unity_app_contents_path="$2"
|
||||
|
||||
has_il2cpp_variation "${unity_app_contents_path}/PlaybackEngines/MacStandaloneSupport/Variations" ||
|
||||
has_il2cpp_variation "${unity_path}/PlaybackEngines/MacStandaloneSupport/Variations"
|
||||
}
|
||||
|
||||
has_il2cpp_variation() {
|
||||
local variations_path="$1"
|
||||
local variation
|
||||
|
||||
for variation in "${variations_path}"/*il2cpp*; do
|
||||
if [ -d "$variation" ]; then
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# Check if already installed with required modules
|
||||
if check_modules_installed; then
|
||||
echo "✓ Unity ${UNITY_VERSION} is already installed with ${PLATFORM} support"
|
||||
@@ -210,10 +191,8 @@ echo "Running: ${INSTALL_CMD[*]}"
|
||||
echo ""
|
||||
|
||||
# Capture output to check for "already installed" messages
|
||||
set +e
|
||||
OUTPUT=$("${INSTALL_CMD[@]}" 2>&1)
|
||||
OUTPUT=$("${INSTALL_CMD[@]}" 2>&1) || true
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
echo "$OUTPUT"
|
||||
|
||||
# Check if modules are already installed (Unity Hub returns error but modules are present)
|
||||
@@ -240,13 +219,13 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify installation and requested modules
|
||||
# Verify installation
|
||||
echo ""
|
||||
if check_modules_installed; then
|
||||
echo "✓ Verified: Unity ${UNITY_VERSION} is now installed with ${PLATFORM} support"
|
||||
if [ -d "${UNITY_INSTALL_PATH}/${UNITY_VERSION}" ]; then
|
||||
echo "✓ Verified: Unity ${UNITY_VERSION} is now installed"
|
||||
exit 0
|
||||
else
|
||||
echo "✗ Unity ${UNITY_VERSION} installation with ${PLATFORM} support could not be verified"
|
||||
echo "✗ Unity ${UNITY_VERSION} installation could not be verified"
|
||||
echo " Expected path: ${UNITY_INSTALL_PATH}/${UNITY_VERSION}"
|
||||
echo ""
|
||||
echo "The installation may still be in progress, or may require manual intervention."
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Fetch LFS files from Gitea mirror with retry.
|
||||
#
|
||||
# Gitea's mirror-sync API is async — the webhook fires on push but the
|
||||
# actual sync may still be in progress when CI starts. Retry with backoff
|
||||
# to bridge the gap.
|
||||
#
|
||||
# Usage:
|
||||
# ./ci/github_actions/fetch_lfs.sh # full pull
|
||||
# ./ci/github_actions/fetch_lfs.sh --include="path/to/*" # selective pull
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
|
||||
MAX_ATTEMPTS=5
|
||||
RETRY_DELAY=15
|
||||
|
||||
if [ -n "${GITHUB_PATH:-}" ]; then
|
||||
for path in /opt/homebrew/bin /usr/local/bin; do
|
||||
if [ -d "$path" ]; then
|
||||
echo "$path" >> "$GITHUB_PATH"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
|
||||
if ! git lfs version >/dev/null 2>&1; then
|
||||
if ! command -v brew >/dev/null 2>&1; then
|
||||
echo "ERROR: git-lfs is unavailable, and Homebrew is not on PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing git-lfs with Homebrew"
|
||||
brew install git-lfs
|
||||
fi
|
||||
|
||||
echo "Using git-lfs at $(command -v git-lfs)"
|
||||
git lfs version
|
||||
|
||||
git lfs install --local --force
|
||||
|
||||
echo "LFS objects before pull:"
|
||||
git lfs ls-files | wc -l
|
||||
|
||||
GIT_LFS_PULL=(git lfs pull)
|
||||
FALLBACK_GITHUB_LFS_PULL=()
|
||||
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
||||
BASIC_AUTH=$(printf 'x-access-token:%s' "$GITHUB_TOKEN" | base64 | tr -d '\n')
|
||||
GIT_LFS_PULL=(git -c "http.https://github.com/.extraheader=AUTHORIZATION: basic $BASIC_AUTH" lfs pull)
|
||||
FALLBACK_GITHUB_LFS_PULL=(
|
||||
git
|
||||
-c "http.https://github.com/.extraheader=AUTHORIZATION: basic $BASIC_AUTH"
|
||||
-c "lfs.url=https://github.com/nolen777/eagle0.git/info/lfs"
|
||||
lfs
|
||||
pull
|
||||
)
|
||||
fi
|
||||
|
||||
for i in $(seq 1 $MAX_ATTEMPTS); do
|
||||
if "${GIT_LFS_PULL[@]}" "$@"; then
|
||||
echo "LFS objects after pull:"
|
||||
git lfs ls-files | wc -l
|
||||
exit 0
|
||||
fi
|
||||
if [ "$i" -lt "$MAX_ATTEMPTS" ]; then
|
||||
echo "LFS pull attempt $i/$MAX_ATTEMPTS failed, retrying in ${RETRY_DELAY}s..."
|
||||
sleep $RETRY_DELAY
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${#FALLBACK_GITHUB_LFS_PULL[@]}" -gt 0 ]; then
|
||||
echo "LFS mirror pull failed after $MAX_ATTEMPTS attempts; trying GitHub LFS fallback"
|
||||
if "${FALLBACK_GITHUB_LFS_PULL[@]}" "$@"; then
|
||||
echo "LFS objects after GitHub fallback pull:"
|
||||
git lfs ls-files | wc -l
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "LFS pull failed after $MAX_ATTEMPTS attempts"
|
||||
exit 1
|
||||
@@ -1,235 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Print a compact timing summary from a Bazel build event JSON file."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from collections import Counter, defaultdict
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def duration_to_ms(value: Any) -> float | None:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
if isinstance(value, str):
|
||||
if value.endswith("s"):
|
||||
try:
|
||||
return float(value[:-1]) * 1000
|
||||
except ValueError:
|
||||
return None
|
||||
try:
|
||||
return float(value)
|
||||
except ValueError:
|
||||
return None
|
||||
if isinstance(value, dict):
|
||||
seconds = value.get("seconds", 0)
|
||||
nanos = value.get("nanos", 0)
|
||||
try:
|
||||
return float(seconds) * 1000 + float(nanos) / 1_000_000
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
return None
|
||||
|
||||
|
||||
def format_ms(milliseconds: float | None) -> str | None:
|
||||
if milliseconds is None:
|
||||
return None
|
||||
if milliseconds >= 1000:
|
||||
return f"{milliseconds / 1000:.2f}s"
|
||||
return f"{milliseconds:.0f}ms"
|
||||
|
||||
|
||||
def nested_get(data: dict[str, Any], *keys: str) -> Any:
|
||||
current: Any = data
|
||||
for key in keys:
|
||||
if not isinstance(current, dict):
|
||||
return None
|
||||
current = current.get(key)
|
||||
return current
|
||||
|
||||
|
||||
def read_events(path: Path) -> list[dict[str, Any]]:
|
||||
events = []
|
||||
with path.open() as file:
|
||||
for line_number, line in enumerate(file, start=1):
|
||||
stripped = line.strip()
|
||||
if not stripped:
|
||||
continue
|
||||
try:
|
||||
event = json.loads(stripped)
|
||||
except json.JSONDecodeError as error:
|
||||
print(f"Ignoring malformed BEP line {line_number}: {error}")
|
||||
continue
|
||||
if isinstance(event, dict):
|
||||
events.append(event)
|
||||
return events
|
||||
|
||||
|
||||
def collect_metric_events(events: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
metric_events = []
|
||||
for event in events:
|
||||
if isinstance(event.get("buildMetrics"), dict):
|
||||
metric_events.append(event["buildMetrics"])
|
||||
return metric_events
|
||||
|
||||
|
||||
def first_present(data: dict[str, Any], keys: list[str]) -> Any:
|
||||
for key in keys:
|
||||
value = data.get(key)
|
||||
if value is not None:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def summarize_timing(build_metrics: dict[str, Any]) -> list[str]:
|
||||
timing = build_metrics.get("timingMetrics")
|
||||
if not isinstance(timing, dict):
|
||||
return []
|
||||
|
||||
labels = [
|
||||
("Wall time", ["wallTimeInMs", "wallTime", "elapsedTime"]),
|
||||
("Loading phase", ["loadingPhaseTimeInMs", "loadingPhaseTime"]),
|
||||
("Analysis phase", ["analysisPhaseTimeInMs", "analysisPhaseTime"]),
|
||||
("Execution phase", ["executionPhaseTimeInMs", "executionPhaseTime"]),
|
||||
("Critical path", ["criticalPathTimeInMs", "criticalPathTime"]),
|
||||
]
|
||||
|
||||
lines = []
|
||||
for label, keys in labels:
|
||||
value = format_ms(duration_to_ms(first_present(timing, keys)))
|
||||
if value is not None:
|
||||
lines.append(f" {label}: {value}")
|
||||
return lines
|
||||
|
||||
|
||||
def summarize_counts(build_metrics: dict[str, Any]) -> list[str]:
|
||||
summaries: list[tuple[str, Any]] = []
|
||||
|
||||
action_summary = build_metrics.get("actionSummary")
|
||||
if isinstance(action_summary, dict):
|
||||
summaries.extend(
|
||||
[
|
||||
("Actions created", action_summary.get("actionsCreated")),
|
||||
("Actions executed", action_summary.get("actionsExecuted")),
|
||||
("Action cache hits", action_summary.get("actionCacheHits")),
|
||||
("Remote cache hits", action_summary.get("remoteCacheHits")),
|
||||
]
|
||||
)
|
||||
|
||||
target_metrics = build_metrics.get("targetMetrics")
|
||||
if isinstance(target_metrics, dict):
|
||||
summaries.extend(
|
||||
[
|
||||
("Targets configured", target_metrics.get("targetsConfigured")),
|
||||
("Targets loaded", target_metrics.get("targetsLoaded")),
|
||||
]
|
||||
)
|
||||
|
||||
package_metrics = build_metrics.get("packageMetrics")
|
||||
if isinstance(package_metrics, dict):
|
||||
summaries.extend(
|
||||
[
|
||||
("Packages loaded", package_metrics.get("packagesLoaded")),
|
||||
("Packages successfully loaded", package_metrics.get("packagesSuccessfullyLoaded")),
|
||||
]
|
||||
)
|
||||
|
||||
return [f" {label}: {value}" for label, value in summaries if value is not None]
|
||||
|
||||
|
||||
def worker_mnemonic(metric: dict[str, Any]) -> str:
|
||||
for path in [
|
||||
("mnemonic",),
|
||||
("workerKey", "mnemonic"),
|
||||
("workerKeyInfo", "mnemonic"),
|
||||
]:
|
||||
value = nested_get(metric, *path)
|
||||
if value:
|
||||
return str(value)
|
||||
return "unknown"
|
||||
|
||||
|
||||
def worker_actions(metric: dict[str, Any]) -> int | None:
|
||||
for key in ["actionsExecuted", "executedActionCount", "actionCount", "actions"]:
|
||||
value = metric.get(key)
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def summarize_workers(build_metrics: dict[str, Any]) -> list[str]:
|
||||
worker_metrics = build_metrics.get("workerMetrics")
|
||||
if not isinstance(worker_metrics, list) or not worker_metrics:
|
||||
return []
|
||||
|
||||
counts = Counter()
|
||||
actions_by_mnemonic: defaultdict[str, int] = defaultdict(int)
|
||||
saw_action_counts = False
|
||||
|
||||
for metric in worker_metrics:
|
||||
if not isinstance(metric, dict):
|
||||
continue
|
||||
mnemonic = worker_mnemonic(metric)
|
||||
counts[mnemonic] += 1
|
||||
actions = worker_actions(metric)
|
||||
if actions is not None:
|
||||
saw_action_counts = True
|
||||
actions_by_mnemonic[mnemonic] += actions
|
||||
|
||||
if not counts:
|
||||
return []
|
||||
|
||||
lines = [" Worker metric entries:"]
|
||||
for mnemonic, count in counts.most_common():
|
||||
suffix = ""
|
||||
if saw_action_counts:
|
||||
suffix = f", actions={actions_by_mnemonic[mnemonic]}"
|
||||
lines.append(f" {mnemonic}: workers={count}{suffix}")
|
||||
return lines
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if len(sys.argv) != 2:
|
||||
print("Usage: summarize_bazel_bep.py <build_event_json_file>")
|
||||
return 0
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
if not path.exists():
|
||||
print(f"Bazel BEP summary: {path} does not exist")
|
||||
return 0
|
||||
|
||||
events = read_events(path)
|
||||
metric_events = collect_metric_events(events)
|
||||
if not metric_events:
|
||||
print("Bazel BEP summary: no buildMetrics event found")
|
||||
return 0
|
||||
|
||||
build_metrics = metric_events[-1]
|
||||
sections = [
|
||||
summarize_timing(build_metrics),
|
||||
summarize_counts(build_metrics),
|
||||
summarize_workers(build_metrics),
|
||||
]
|
||||
lines = [line for section in sections for line in section]
|
||||
|
||||
if not lines:
|
||||
print("Bazel BEP summary: buildMetrics event had no recognized metrics")
|
||||
return 0
|
||||
|
||||
print("Bazel BEP summary:")
|
||||
for line in lines:
|
||||
print(line)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,56 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
# Read Unity version from project file
|
||||
UNITY_VERSION=$(grep "m_EditorVersion:" src/main/csharp/net/eagle0/clients/unity/eagle0/ProjectSettings/ProjectVersion.txt | head -1 | sed 's/m_EditorVersion: //')
|
||||
|
||||
# Use runner-specific build directory if EAGLE0_BUILD_DIR is set, otherwise default
|
||||
BUILD_BASE="${EAGLE0_BUILD_DIR:-/tmp/eagle0}"
|
||||
|
||||
WORKSPACE=$(pwd)
|
||||
UNITY_INSTALL_PATH="/Applications/Unity/Hub/Editor"
|
||||
LOG_PATH="${BUILD_BASE}/editor_editmode_tests.log"
|
||||
RESULTS_PATH="${BUILD_BASE}/editmode-test-results.xml"
|
||||
|
||||
mkdir -p "$BUILD_BASE"
|
||||
|
||||
echo "Building protos"
|
||||
./scripts/build_protos.sh
|
||||
|
||||
echo "Running Unity EditMode tests"
|
||||
|
||||
set +e
|
||||
"${UNITY_INSTALL_PATH}/${UNITY_VERSION}/Unity.app/Contents/MacOS/Unity" \
|
||||
-nographics \
|
||||
-batchmode \
|
||||
-quit \
|
||||
-executeMethod eagle0.Tests.DynamicTextBindingTestRunner.Run \
|
||||
-testResults "$RESULTS_PATH" \
|
||||
-logFile "$LOG_PATH" \
|
||||
-projectPath "$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
UNITY_EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ $UNITY_EXIT_CODE -ne 0 ]; then
|
||||
echo ""
|
||||
echo "Unity EditMode tests failed with exit code $UNITY_EXIT_CODE"
|
||||
echo "=== Unity EditMode Test Log (last 200 lines) ==="
|
||||
tail -200 "$LOG_PATH" || echo "Could not read log file at $LOG_PATH"
|
||||
echo "=== End of Unity EditMode Test Log ==="
|
||||
exit $UNITY_EXIT_CODE
|
||||
fi
|
||||
|
||||
if [ ! -f "$LOG_PATH" ]; then
|
||||
echo ""
|
||||
echo "ERROR: Unity exited successfully but did not write an editor log at $LOG_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$RESULTS_PATH" ]; then
|
||||
echo ""
|
||||
echo "ERROR: Unity exited successfully but did not write test results at $RESULTS_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Unity EditMode tests complete"
|
||||
@@ -13,166 +13,11 @@ set -euxo pipefail
|
||||
BUILD_TARGET=$1
|
||||
WORKSPACE=$(pwd)
|
||||
UNITY_PROJECT="$WORKSPACE/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
UNITY_VERSION=$(grep "m_EditorVersion:" "$UNITY_PROJECT/ProjectSettings/ProjectVersion.txt" | head -1 | sed 's/m_EditorVersion: //')
|
||||
UNITY_MAJOR_MINOR=$(echo "$UNITY_VERSION" | sed -E 's/^([0-9]+)\.([0-9]+).*/\1.\2/')
|
||||
ADDRESSABLES_PREFIX="addressables/$BUILD_TARGET/$UNITY_MAJOR_MINOR"
|
||||
SERVER_DATA="$UNITY_PROJECT/ServerData/$BUILD_TARGET/$UNITY_MAJOR_MINOR"
|
||||
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"
|
||||
DEFAULT_MAX_UPLOAD_BYTES=$((1024 * 1024 * 1024))
|
||||
DEFAULT_MAX_SINGLE_FILE_BYTES=$((512 * 1024 * 1024))
|
||||
MAX_UPLOAD_BYTES=${ADDRESSABLES_MAX_UPLOAD_BYTES:-$DEFAULT_MAX_UPLOAD_BYTES}
|
||||
MAX_SINGLE_FILE_BYTES=${ADDRESSABLES_MAX_SINGLE_FILE_BYTES:-$DEFAULT_MAX_SINGLE_FILE_BYTES}
|
||||
TOP_UPLOAD_COUNT=${ADDRESSABLES_TOP_UPLOAD_COUNT:-20}
|
||||
|
||||
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
export PATH="${COMMON_PATHS}:${PATH}"
|
||||
|
||||
format_bytes() {
|
||||
local bytes=$1
|
||||
local unit="B"
|
||||
local value=$bytes
|
||||
|
||||
if [ "$bytes" -ge $((1024 * 1024 * 1024)) ]; then
|
||||
unit="GiB"
|
||||
value=$(awk "BEGIN { printf \"%.2f\", $bytes / 1024 / 1024 / 1024 }")
|
||||
elif [ "$bytes" -ge $((1024 * 1024)) ]; then
|
||||
unit="MiB"
|
||||
value=$(awk "BEGIN { printf \"%.2f\", $bytes / 1024 / 1024 }")
|
||||
elif [ "$bytes" -ge 1024 ]; then
|
||||
unit="KiB"
|
||||
value=$(awk "BEGIN { printf \"%.2f\", $bytes / 1024 }")
|
||||
fi
|
||||
|
||||
echo "$value $unit"
|
||||
}
|
||||
|
||||
file_size() {
|
||||
local path=$1
|
||||
if stat -f%z "$path" >/dev/null 2>&1; then
|
||||
stat -f%z "$path"
|
||||
else
|
||||
stat -c%s "$path"
|
||||
fi
|
||||
}
|
||||
|
||||
append_summary() {
|
||||
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then
|
||||
echo "$1" >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
}
|
||||
|
||||
budget_enabled() {
|
||||
local value=$1
|
||||
[ "$value" -gt 0 ]
|
||||
}
|
||||
|
||||
summarize_pending_uploads() {
|
||||
local sync_args_file
|
||||
sync_args_file=$(mktemp)
|
||||
local uploads_file
|
||||
uploads_file=$(mktemp)
|
||||
local sorted_uploads_file
|
||||
sorted_uploads_file=$(mktemp)
|
||||
trap 'rm -f "$sync_args_file" "$uploads_file" "$sorted_uploads_file"' RETURN
|
||||
|
||||
aws s3 sync "$SERVER_DATA" "s3://$DO_BUCKET/$ADDRESSABLES_PREFIX/" \
|
||||
--endpoint-url "$DO_ENDPOINT" \
|
||||
--acl public-read \
|
||||
--size-only \
|
||||
--delete \
|
||||
--dryrun > "$sync_args_file"
|
||||
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
*upload:*)
|
||||
local source_path=${line#*upload: }
|
||||
source_path=${source_path% to s3://*}
|
||||
if [ -f "$source_path" ]; then
|
||||
local size
|
||||
size=$(file_size "$source_path")
|
||||
printf "%s\t%s\n" "$size" "$source_path" >> "$uploads_file"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done < "$sync_args_file"
|
||||
|
||||
local upload_count=0
|
||||
local upload_bytes=0
|
||||
if [ -s "$uploads_file" ]; then
|
||||
while IFS=$'\t' read -r size _path; do
|
||||
upload_count=$((upload_count + 1))
|
||||
upload_bytes=$((upload_bytes + size))
|
||||
done < "$uploads_file"
|
||||
fi
|
||||
|
||||
echo "Addressables upload preflight: $upload_count changed files, $(format_bytes "$upload_bytes") to upload."
|
||||
append_summary "## Addressables upload preflight"
|
||||
append_summary ""
|
||||
append_summary "- Target: \`$BUILD_TARGET\`"
|
||||
append_summary "- Changed files: \`$upload_count\`"
|
||||
append_summary "- Changed upload bytes: \`$(format_bytes "$upload_bytes")\`"
|
||||
append_summary "- Upload byte budget: \`$(format_bytes "$MAX_UPLOAD_BYTES")\`"
|
||||
append_summary "- Single-file budget: \`$(format_bytes "$MAX_SINGLE_FILE_BYTES")\`"
|
||||
|
||||
local guardrail_failed=0
|
||||
|
||||
if [ -s "$uploads_file" ]; then
|
||||
echo "Largest pending Addressables uploads:"
|
||||
append_summary ""
|
||||
append_summary "### Largest pending uploads"
|
||||
append_summary ""
|
||||
append_summary "| Size | Path |"
|
||||
append_summary "| ---: | --- |"
|
||||
sort -nr "$uploads_file" > "$sorted_uploads_file"
|
||||
local listed_uploads=0
|
||||
while IFS=$'\t' read -r size path; do
|
||||
if [ "$listed_uploads" -ge "$TOP_UPLOAD_COUNT" ]; then
|
||||
break
|
||||
fi
|
||||
local relative_path=${path#"$SERVER_DATA"/}
|
||||
echo " $(format_bytes "$size") $relative_path"
|
||||
append_summary "| $(format_bytes "$size") | \`$relative_path\` |"
|
||||
listed_uploads=$((listed_uploads + 1))
|
||||
done < "$sorted_uploads_file"
|
||||
|
||||
if budget_enabled "$MAX_SINGLE_FILE_BYTES"; then
|
||||
while IFS=$'\t' read -r size path; do
|
||||
if [ "$size" -gt "$MAX_SINGLE_FILE_BYTES" ]; then
|
||||
local relative_path=${path#"$SERVER_DATA"/}
|
||||
echo "ERROR: Pending Addressables upload '$relative_path' is $(format_bytes "$size"), exceeding single-file budget $(format_bytes "$MAX_SINGLE_FILE_BYTES")."
|
||||
append_summary ""
|
||||
append_summary ":x: \`$relative_path\` exceeds the single-file budget at \`$(format_bytes "$size")\`."
|
||||
guardrail_failed=1
|
||||
fi
|
||||
done < "$uploads_file"
|
||||
fi
|
||||
fi
|
||||
|
||||
if budget_enabled "$MAX_UPLOAD_BYTES" && [ "$upload_bytes" -gt "$MAX_UPLOAD_BYTES" ]; then
|
||||
echo "ERROR: Addressables upload is expected to send $(format_bytes "$upload_bytes"), exceeding budget $(format_bytes "$MAX_UPLOAD_BYTES")."
|
||||
echo "A small asset change may have invalidated an oversized bundle; inspect the largest pending uploads above."
|
||||
append_summary ""
|
||||
append_summary ":x: Changed upload bytes exceed budget: \`$(format_bytes "$upload_bytes")\` > \`$(format_bytes "$MAX_UPLOAD_BYTES")\`."
|
||||
guardrail_failed=1
|
||||
fi
|
||||
|
||||
if [ "$guardrail_failed" -ne 0 ]; then
|
||||
echo "Refusing to upload Addressables. Raise ADDRESSABLES_MAX_UPLOAD_BYTES or ADDRESSABLES_MAX_SINGLE_FILE_BYTES only after confirming the upload is intentional."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
if ! command -v aws >/dev/null 2>&1; then
|
||||
if command -v brew >/dev/null 2>&1; then
|
||||
brew install awscli
|
||||
else
|
||||
echo "ERROR: aws CLI is unavailable, and Homebrew is not on PATH"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ ! -d "$SERVER_DATA" ]; then
|
||||
echo "No Addressables bundles found at $SERVER_DATA"
|
||||
@@ -181,34 +26,19 @@ if [ ! -d "$SERVER_DATA" ]; then
|
||||
fi
|
||||
|
||||
echo "Uploading Addressables bundles from $SERVER_DATA"
|
||||
echo "Target: s3://$DO_BUCKET/$ADDRESSABLES_PREFIX/"
|
||||
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"
|
||||
|
||||
summarize_pending_uploads
|
||||
|
||||
# Sync bundles to Spaces. Addressable bundle filenames include content hashes, so a
|
||||
# same-sized existing bundle is already the same content. Use --size-only to avoid
|
||||
# re-uploading every rebuilt bundle just because Unity gave it a fresh local mtime.
|
||||
# Metadata files keep stable names, so sync them again without --size-only below.
|
||||
#
|
||||
# --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_PREFIX/" \
|
||||
# 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 \
|
||||
--size-only \
|
||||
--delete
|
||||
|
||||
aws s3 sync "$SERVER_DATA" "s3://$DO_BUCKET/$ADDRESSABLES_PREFIX/" \
|
||||
--endpoint-url "$DO_ENDPOINT" \
|
||||
--acl public-read \
|
||||
--exclude "*" \
|
||||
--include "*.bin" \
|
||||
--include "*.hash" \
|
||||
--include "*.json"
|
||||
|
||||
echo "Addressables upload complete"
|
||||
echo "Files available at: https://assets.eagle0.net/$ADDRESSABLES_PREFIX/"
|
||||
echo "Files available at: https://assets.eagle0.net/addressables/$BUILD_TARGET/"
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
"""Split a JVM binary's runtime classpath into first-party and third-party jar sets.
|
||||
|
||||
This exists so the Docker image can place rarely-changing third-party jars in a
|
||||
lower OCI layer and frequently-changing first-party jars in a small top layer,
|
||||
so most pushes only re-upload the small layer.
|
||||
"""
|
||||
|
||||
load("@rules_java//java/common:java_info.bzl", "JavaInfo")
|
||||
|
||||
def _unique_name(jar):
|
||||
# short_path is unique per jar and stable across commits (it depends only on
|
||||
# the jar's own package/coordinate, not on unrelated targets), so the deps
|
||||
# layer's tar entries stay byte-identical and its blob digest stays cached.
|
||||
path = jar.short_path
|
||||
if path.startswith("../"):
|
||||
path = path[3:]
|
||||
return path.replace("+", "_").replace("/", "_").replace("~", "_")
|
||||
|
||||
def _impl(ctx):
|
||||
info = ctx.attr.binary[JavaInfo]
|
||||
|
||||
app = []
|
||||
deps = []
|
||||
seen = {}
|
||||
for jar in sorted(info.transitive_runtime_jars.to_list(), key = lambda f: f.path):
|
||||
workspace = jar.owner.workspace_name if jar.owner else ""
|
||||
bucket = "app" if workspace == "" else "deps"
|
||||
out_name = _unique_name(jar)
|
||||
|
||||
key = bucket + "/" + out_name
|
||||
if key in seen:
|
||||
fail("jar_split: duplicate output name %r for %s and %s" % (
|
||||
key,
|
||||
seen[key],
|
||||
jar.path,
|
||||
))
|
||||
seen[key] = jar.path
|
||||
|
||||
link = ctx.actions.declare_file(ctx.label.name + "/" + key)
|
||||
ctx.actions.symlink(output = link, target_file = jar)
|
||||
(app if bucket == "app" else deps).append(link)
|
||||
|
||||
return [
|
||||
DefaultInfo(files = depset(app + deps)),
|
||||
OutputGroupInfo(app = depset(app), deps = depset(deps)),
|
||||
]
|
||||
|
||||
jar_split = rule(
|
||||
implementation = _impl,
|
||||
doc = "Partitions a JVM binary's transitive runtime jars into 'app' " +
|
||||
"(first-party, empty workspace) and 'deps' (third-party) output groups.",
|
||||
attrs = {
|
||||
"binary": attr.label(
|
||||
mandatory = True,
|
||||
providers = [[JavaInfo]],
|
||||
doc = "A jvm binary/library target whose runtime classpath to split.",
|
||||
),
|
||||
},
|
||||
)
|
||||
@@ -1,3 +1,5 @@
|
||||
load("@bazel_tools//tools/build_defs/pkg:pkg.bzl", "pkg_tar")
|
||||
|
||||
#pkg_tar(
|
||||
# name = "eagle_servers",
|
||||
# strip_prefix = "/src/main/scala/net/eagle0",
|
||||
|
||||
@@ -32,13 +32,6 @@ services:
|
||||
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:-}"
|
||||
EAGLE_HISTORY_BACKEND: "${EAGLE_HISTORY_BACKEND:-postgres}"
|
||||
EAGLE_POSTGRES_HOST: "${EAGLE_POSTGRES_HOST:-}"
|
||||
EAGLE_POSTGRES_PORT: "${EAGLE_POSTGRES_PORT:-}"
|
||||
EAGLE_POSTGRES_DATABASE: "${EAGLE_POSTGRES_DATABASE:-}"
|
||||
EAGLE_POSTGRES_USER: "${EAGLE_POSTGRES_USER:-}"
|
||||
EAGLE_POSTGRES_PASSWORD: "${EAGLE_POSTGRES_PASSWORD:-}"
|
||||
EAGLE_POSTGRES_SSLMODE: "${EAGLE_POSTGRES_SSLMODE:-require}"
|
||||
# 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)
|
||||
@@ -88,13 +81,6 @@ services:
|
||||
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:-}"
|
||||
EAGLE_HISTORY_BACKEND: "${EAGLE_HISTORY_BACKEND:-postgres}"
|
||||
EAGLE_POSTGRES_HOST: "${EAGLE_POSTGRES_HOST:-}"
|
||||
EAGLE_POSTGRES_PORT: "${EAGLE_POSTGRES_PORT:-}"
|
||||
EAGLE_POSTGRES_DATABASE: "${EAGLE_POSTGRES_DATABASE:-}"
|
||||
EAGLE_POSTGRES_USER: "${EAGLE_POSTGRES_USER:-}"
|
||||
EAGLE_POSTGRES_PASSWORD: "${EAGLE_POSTGRES_PASSWORD:-}"
|
||||
EAGLE_POSTGRES_SSLMODE: "${EAGLE_POSTGRES_SSLMODE:-require}"
|
||||
# JWT public key for token validation (auth service handles signing)
|
||||
# Reads from /etc/eagle0/keys/public.pem via shared volume
|
||||
SHARDOK_AUTH_TOKEN: "${SHARDOK_AUTH_TOKEN:-}"
|
||||
|
||||
@@ -1,281 +0,0 @@
|
||||
# Allied Victory: Battle Resolution Flow
|
||||
|
||||
How allied victories work end-to-end, from Shardok tactical resolution through Eagle strategic
|
||||
processing to player-facing aftermath decisions.
|
||||
|
||||
## 1. Battle Setup (Eagle -> Shardok)
|
||||
|
||||
When Eagle sends a battle to Shardok via `RequestBattlesAction`, each player gets specific victory
|
||||
conditions:
|
||||
|
||||
| Player | Victory Conditions |
|
||||
|-------------|-----------------------------------------------------------------------|
|
||||
| Attacker(s) | `HoldsCriticalTiles`, `LastPlayerStanding`, `LastAllianceStanding` |
|
||||
| Defender | `LastPlayerStanding`, `WinAfterMaxRounds` |
|
||||
|
||||
This is set in `RequestBattlesAction.shardokPlayers`.
|
||||
|
||||
Alliance information is sent separately in `PlayerSetupInfo.allies`. Eagle reads each player's
|
||||
`factionRelationships` and includes all non-HOSTILE factions as allies
|
||||
(`ShardokInterfaceGrpcClient.scala:75-80`).
|
||||
|
||||
## 2. Victory Condition Evaluation (Shardok)
|
||||
|
||||
Victory conditions are checked in `UpdateGameStatusAction.cpp` in priority order:
|
||||
|
||||
### Check 1: LastPlayerStanding (any time)
|
||||
|
||||
If exactly **one** player has surviving units and has `LastPlayerStanding`:
|
||||
- That player is the sole winner.
|
||||
- This applies to solo victories only (1 survivor).
|
||||
|
||||
### Check 2: LastAllianceStanding (any time)
|
||||
|
||||
If **multiple** players survive, check if they form a winning alliance:
|
||||
- ALL survivors must have `LastAllianceStanding` condition
|
||||
- ALL survivors must be **mutually** allied (every pair checks both directions)
|
||||
- If yes: all survivors are winners
|
||||
|
||||
This triggers for AssaultProvince battles when allied attackers eliminate all defenders. Since the
|
||||
defender does **not** have `LastAllianceStanding`, the check correctly requires that only the
|
||||
allied attackers remain alive.
|
||||
|
||||
### Check 3: WinAfterMaxRounds (end-of-round only)
|
||||
|
||||
If the round counter exceeds `max_rounds`:
|
||||
- Find the player with `WinAfterMaxRounds` (always the defender)
|
||||
- That player wins, even if they have no surviving units
|
||||
- Eagle validates exactly 1 player has this condition (`internalRequire` in `RequestBattlesAction`)
|
||||
|
||||
### Check 4: HoldsCriticalTiles (end-of-round only)
|
||||
|
||||
**Single-player castle control**: If one player with `HoldsCriticalTiles` occupies ALL critical
|
||||
tiles with hero-bearing units, that player wins. Additionally, any other player who:
|
||||
- Has `HoldsCriticalTiles`
|
||||
- Is **mutually** allied with the castle holder
|
||||
|
||||
is also added to `winning_shardok_ids` as a co-winner.
|
||||
|
||||
**Allied castle control**: If multiple players collectively occupy all critical tiles with
|
||||
hero-bearing units, AND:
|
||||
- All occupants have `HoldsCriticalTiles`
|
||||
- All occupants are mutually allied
|
||||
|
||||
Then all occupants are winners together.
|
||||
|
||||
## 3. EndGameCondition Assignment Per Player (Shardok -> Eagle)
|
||||
|
||||
Shardok assigns each player a `Victory` or `Loss`. All winners receive `Victory`; there is no
|
||||
`AllyVictory` distinction. The `EndGameCondition` proto reserves fields 2 (`ally_victory`) and
|
||||
3 (`draw`) for backwards compatibility but Eagle rejects both.
|
||||
|
||||
`GameOverResponsePopulator` validates that any player allied to a winner is also in
|
||||
`winning_shardok_ids`. If an ally is missing from the winners list, it throws an internal error.
|
||||
|
||||
**Known issue**: This validation is too strict for `LastPlayerStanding`. When one allied attacker
|
||||
is the sole survivor, their dead co-attacker is allied to the winner but legitimately not in
|
||||
`winning_shardok_ids` (they're dead). The throw should be changed to `Loss` for dead allies, or
|
||||
the check should only apply when the ally has surviving units.
|
||||
|
||||
| Condition | Criterion |
|
||||
|-----------|-----------|
|
||||
| `Victory(type)` | Player is in `winning_shardok_ids` |
|
||||
| `Loss(type)` | Player is not in `winning_shardok_ids` |
|
||||
|
||||
The `type` is the VictoryCondition that caused the game to end (e.g., `HoldsCriticalTiles`).
|
||||
|
||||
### When do multiple players get Victory?
|
||||
|
||||
In standard AssaultProvince battles:
|
||||
|
||||
1. **Allied attackers jointly hold all castles** -> both get `Victory(HoldsCriticalTiles)`.
|
||||
|
||||
2. **One attacker holds all castles alone, ally has `HoldsCriticalTiles` and is mutually allied**
|
||||
-> both get `Victory(HoldsCriticalTiles)`. The ally is added to `winning_shardok_ids` by
|
||||
`UpdateGameStatusAction`.
|
||||
|
||||
3. **Allied attackers eliminate all defenders** -> both get `Victory(LastAllianceStanding)`.
|
||||
This triggers because both have `LastAllianceStanding` and are mutually allied.
|
||||
|
||||
4. **One attacker holds all castles, co-attacker is NOT allied** -> only the castle holder
|
||||
gets `Victory(HoldsCriticalTiles)`; the non-allied co-attacker gets `Loss`.
|
||||
|
||||
## 4. Eagle Processes Battle Results
|
||||
|
||||
`ResolveBattleAction.scala` receives `BattleResolution` containing each player's
|
||||
`EndGameCondition` and resolved units.
|
||||
|
||||
### Winner/Loser Partition
|
||||
|
||||
Players are split by `isVictory` (line 383-386):
|
||||
- `winningResolvedPlayers`: Victory
|
||||
- `losingResolvedPlayers`: Loss
|
||||
|
||||
### The `unitReturned` Function (line 765-788)
|
||||
|
||||
Determines which units go home vs stay at the battle province:
|
||||
|
||||
| Unit Status | Returned? | Notes |
|
||||
|---------------|-----------|-------|
|
||||
| `Fled` | Always | Sent to their army's flee province |
|
||||
| `NeverEntered`| Conditional | Only if: attacker AND has flee province AND did NOT win (`!isVictory`) |
|
||||
| `Captured` | Never | |
|
||||
| `Normal` | Never | |
|
||||
| `Retreated` | Never | |
|
||||
| `Outlawed` | Never | Becomes unaffiliated hero |
|
||||
|
||||
Key point: `NeverEntered` units from **winning** factions stay at the battle province rather than
|
||||
being sent home. This is important for bounced co-attackers who are allied with the actual winner.
|
||||
|
||||
### Withdrawn/Fled Unit Routing (line 690-706)
|
||||
|
||||
For units that ARE returned, `armyFromResolvedArmy` builds a returning army with
|
||||
`fleeProvinceId = None`. If the army has no `fleeProvinceId`, it becomes "shattered" (units
|
||||
become outlaws in the battle province). If the original army had a `fleeProvinceId` set, returned
|
||||
units are sent there as incoming armies.
|
||||
|
||||
### Battle Resolution Branching (line 397-552)
|
||||
|
||||
Three branches based on who won:
|
||||
|
||||
#### Branch A: Defender Won
|
||||
|
||||
Triggered when: `winningShardokPlayers.exists(_.isDefender)`
|
||||
|
||||
- Executes `ProvinceHeldAction`
|
||||
- All non-defender, non-returned, non-outlawed units are **captured**
|
||||
- Province stays with the defending faction
|
||||
|
||||
#### Branch B: Single Attacker Won
|
||||
|
||||
Triggered when: exactly 1 attacking winner
|
||||
|
||||
- Executes `ProvinceConqueredAction` immediately
|
||||
- Winning attacker's non-fled units occupy the province
|
||||
- Losing defenders' non-fled units are captured
|
||||
- Province transfers to the attacker
|
||||
|
||||
#### Branch C: Multiple Allied Attackers Won
|
||||
|
||||
Triggered when: 2+ attacking winners
|
||||
|
||||
- Executes `MultiVictorBattleSetupAction`
|
||||
- Creates `PendingConquestInfo` with aftermath claimants
|
||||
- Province enters the **Battle Aftermath** phase
|
||||
|
||||
## 5. Battle Aftermath (Multi-Victor Only)
|
||||
|
||||
When multiple allied attackers win, the province enters an aftermath decision phase where players
|
||||
decide who keeps it.
|
||||
|
||||
### Claimant Setup
|
||||
|
||||
Each attacking winner becomes an `AftermathClaimant` with:
|
||||
- `units`: their non-fled, non-outlawed units still at the battle province
|
||||
- `armySize`: total troop count of those units
|
||||
- `broughtGold` / `broughtFood`: supplies their armies carried
|
||||
|
||||
Claimants are **sorted by army size descending** (largest army decides first).
|
||||
|
||||
### Decision Phase
|
||||
|
||||
Claimants are presented with a choice **one at a time**, in order:
|
||||
|
||||
**If no one has chosen "Keep" yet:**
|
||||
- **Keep Province**: claim the province for your faction
|
||||
- **Withdraw To**: pick an adjacent province to march your army to, optionally carrying up to
|
||||
the gold/food you brought
|
||||
|
||||
**If someone already chose "Keep":**
|
||||
- **Withdraw To** is the only option (can't have two keepers)
|
||||
|
||||
**Last undecided claimant:**
|
||||
- If no one has chosen "Keep" yet, the last claimant **automatically keeps** (no command shown,
|
||||
resolved by `AutoResolveBattleAftermathAction`)
|
||||
- This guarantees someone always claims the province
|
||||
|
||||
### Finalization
|
||||
|
||||
Once all claimants have decided (`FinalizeAftermathAction`):
|
||||
|
||||
1. The keeper's units run through `ProvinceConqueredAction` -- province transfers to their faction
|
||||
2. Withdrawing claimants' units become incoming armies to their chosen adjacent province,
|
||||
arriving next round, carrying the specified supplies
|
||||
3. `PendingConquestInfo` is cleared from the province
|
||||
|
||||
## 6. Summary: Who Gets What
|
||||
|
||||
### Two allied attackers jointly hold all castles
|
||||
- Shardok: Both get `Victory(HoldsCriticalTiles)` (both in `winning_shardok_ids`)
|
||||
- Eagle: Both are attacking winners -> MultiVictorBattleSetupAction
|
||||
- Players: Largest army picks first: Keep or Withdraw. Last player auto-keeps if no one chose Keep.
|
||||
|
||||
### One attacker holds all castles, allied attacker doesn't
|
||||
- Shardok: Castle-holder gets `Victory(HoldsCriticalTiles)`. Mutually-allied co-attacker with
|
||||
`HoldsCriticalTiles` is added to `winning_shardok_ids` -> also gets `Victory(HoldsCriticalTiles)`.
|
||||
- Eagle: Both are attacking winners -> MultiVictorBattleSetupAction -> Battle Aftermath.
|
||||
- Players: Largest army picks first: Keep or Withdraw.
|
||||
|
||||
### Allied attackers eliminate all defenders
|
||||
- Shardok: Both attackers have `LastAllianceStanding` and are mutually allied -> both get
|
||||
`Victory(LastAllianceStanding)`.
|
||||
- Eagle: Both are attacking winners -> MultiVictorBattleSetupAction -> Battle Aftermath.
|
||||
- Players: Largest army picks first: Keep or Withdraw.
|
||||
|
||||
### Non-allied co-attackers: one holds all castles
|
||||
- Shardok: Castle-holder gets `Victory(HoldsCriticalTiles)`, non-allied co-attacker gets
|
||||
`Loss(HoldsCriticalTiles)`.
|
||||
- Eagle: Single attacker won -> ProvinceConqueredAction. Non-allied co-attacker's units are
|
||||
captured along with the defender's.
|
||||
|
||||
### One allied attacker is sole survivor (LastPlayerStanding)
|
||||
- Shardok: Sole survivor gets `Victory(LastPlayerStanding)`. Dead ally should get `Loss`.
|
||||
- **BUG**: `GameOverResponsePopulator` currently throws because dead ally is allied to winner
|
||||
but not in `winning_shardok_ids`. Needs fix — see known issue in Section 3.
|
||||
- Eagle (once fixed): Single attacker won -> ProvinceConqueredAction. Dead ally's units were
|
||||
already destroyed in battle.
|
||||
|
||||
### Single attacker wins (any condition, no co-attackers)
|
||||
- No alliance considerations. ProvinceConqueredAction immediately.
|
||||
|
||||
### Defender wins (WinAfterMaxRounds or LastPlayerStanding)
|
||||
- ProvinceHeldAction. All non-returned attacker units are captured.
|
||||
|
||||
## 7. Edge Cases and Known Issues
|
||||
|
||||
### BUG: Dead ally causes crash via LastPlayerStanding
|
||||
|
||||
When one allied attacker is the sole survivor (both the co-attacker and defender are dead),
|
||||
`LastPlayerStanding` fires and only the survivor is in `winning_shardok_ids`. The
|
||||
`GameOverResponsePopulator` then sees the dead co-attacker is allied to the winner but not in
|
||||
`winning_shardok_ids`, and throws `ShardokInternalErrorException`. Fix: dead allies should
|
||||
receive `Loss`, not trigger a validation error.
|
||||
|
||||
### Non-allied co-attackers eliminate all defenders
|
||||
|
||||
Two non-allied attackers who both survive after eliminating the defender cannot win via
|
||||
`LastAllianceStanding` (they aren't mutually allied). The game continues until max rounds,
|
||||
at which point the defender wins via `WinAfterMaxRounds`. They must capture all critical tiles.
|
||||
|
||||
### Non-allied co-attackers: one holds all castles
|
||||
|
||||
Only the castle holder wins. The non-allied co-attacker gets `Loss` and their units are captured
|
||||
along with the defender's. The mutual-alliance check in `UpdateGameStatusAction` prevents
|
||||
non-allied players from being added to `winning_shardok_ids`.
|
||||
|
||||
## Key File References
|
||||
|
||||
| Component | File |
|
||||
|-----------|------|
|
||||
| Victory condition checking | `src/main/cpp/net/eagle0/shardok/library/actions/UpdateGameStatusAction.cpp` |
|
||||
| Per-player EndGameCondition | `src/main/cpp/net/eagle0/shardok/server/GameOverResponsePopulator.cpp` |
|
||||
| Battle setup (victory conditions) | `src/main/scala/.../library/actions/impl/action/RequestBattlesAction.scala` |
|
||||
| Alliance communication | `src/main/scala/.../shardok_interface/ShardokInterfaceGrpcClient.scala:75-80` |
|
||||
| Battle result processing | `src/main/scala/.../library/actions/impl/action/ResolveBattleAction.scala` |
|
||||
| Unit return logic | `ResolveBattleAction.scala:765-788` (`unitReturned`) |
|
||||
| Multi-victor setup | `src/main/scala/.../library/actions/impl/action/MultiVictorBattleSetupAction.scala` |
|
||||
| Aftermath availability | `src/main/scala/.../library/actions/availability/AvailableBattleAftermathDecisionCommandFactory.scala` |
|
||||
| Aftermath command | `src/main/scala/.../library/actions/impl/command/BattleAftermathDecisionCommand.scala` |
|
||||
| Auto-resolve last claimant | `src/main/scala/.../library/actions/impl/action/AutoResolveBattleAftermathAction.scala` |
|
||||
| Finalize aftermath | `src/main/scala/.../library/actions/impl/action/FinalizeAftermathAction.scala` |
|
||||
| EndGameCondition enum | `src/main/scala/.../model/state/shardok_battle/EndGameCondition.scala` |
|
||||
+1
-25
@@ -12,7 +12,7 @@ This document catalogs all media assets in the Unity project for licensing revie
|
||||
|----------|-------|-------|
|
||||
| Images | 10,637 | Mostly PNG icons and UI sprites |
|
||||
| Audio | 1,778 | 26 music tracks + 1,752 sound effects |
|
||||
| 3D Models | 66+ | Bridge pack, Animal pack deluxe, Honey Badger |
|
||||
| 3D Models | 52 | Bridge pack only |
|
||||
| Fonts | 16 | TTF files |
|
||||
|
||||
---
|
||||
@@ -55,30 +55,6 @@ These are commercial Unity Asset Store purchases tied to your account:
|
||||
- **Contents:** Bridge construction pieces
|
||||
- **License:** Unity Asset Store
|
||||
|
||||
### Animal pack deluxe
|
||||
- **Location:** `Assets/Animal pack deluxe/`
|
||||
- **Publisher:** janpec
|
||||
- **Asset Store Link:** https://assetstore.unity.com/packages/3d/characters/animals/animal-pack-deluxe-99702
|
||||
- **Contents:** 26 rigged and animated 3D animal models (bear, boar, crocodile, wolf, frog, crab, rabbit, scorpion, snake, stag, deer, rat, goat, pig, etc.) with idle, walk, run, eat, attack, and die animations
|
||||
- **Used for:** Beast province effects on the strategic map (AnimalEffect)
|
||||
- **License:** Unity Asset Store
|
||||
|
||||
### 2D Monster Pack: Basic Bundle (+PSB)
|
||||
- **Location:** `Assets/DungeonMonsters2D/`
|
||||
- **Publisher:** SP1
|
||||
- **Asset Store Link:** https://assetstore.unity.com/packages/2d/characters/2d-monster-pack-basic-bundle-psb-328637
|
||||
- **Contents:** 2D animated monster sprites with PSB (Photoshop) source files. Characters: SkeletonWarrior, SkeletonArcher, SkeletonMage, Zombie, ZombieWarrior, Demon, Succubus, Imp, Spider, Rat, Vampire, DragonRed, Butcher
|
||||
- **Used for:** Beast province effects (MonsterEffect, DragonEffect) and human-type beast effects on the strategic map
|
||||
- **License:** Unity Asset Store
|
||||
|
||||
### Honey Badger 3D Model
|
||||
- **Location:** `Assets/HoneyBadger/`
|
||||
- **Creator:** WildMesh 3D (@WildMesh_3D)
|
||||
- **Source:** https://sketchfab.com/3d-models/realistic-honey-badger-3d-model-09751ed5383c4afb924cfb6485d313f1
|
||||
- **Contents:** Rigged 3D honey badger model (FBX) with 63 animations and PBR texture
|
||||
- **Used for:** Honey badger beast province effect (AnimalEffect)
|
||||
- **License:** Purchased via Sketchfab/Patreon
|
||||
|
||||
### Fantasy Interface Sounds
|
||||
- **Location:** `Assets/Fantasy Interface Sounds/`
|
||||
- **Count:** 320 WAV files
|
||||
|
||||
@@ -1,246 +0,0 @@
|
||||
# Adding Beast-Type-Specific Effects
|
||||
|
||||
When a province has a `BeastsEvent`, `ProvinceBeastsController` spawns a visual effect at the province centroid. By default this is the generic `BeastsEffect` (circling vultures/crows). Beast-specific effects can override this based on `BeastInfo.SingularName`.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
ProvinceBeastsController
|
||||
├── GetBeastName(province) → reads BeastsEvent.BeastInfo.SingularName
|
||||
├── GetEffectPrefab(beastName) → switch on name → returns prefab
|
||||
└── SpawnBeastsEffect(provinceId, beastName) → instantiates at centroid
|
||||
```
|
||||
|
||||
**Key files:**
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Assets/Eagle/ProvinceBeastsController.cs` | Routes beast names to prefabs, positions effects |
|
||||
| `Assets/Eagle/BeastsEffect.cs` | Generic effect (circling birds via particle system) |
|
||||
| `Assets/Eagle/DragonEffect.cs` | Dragon-specific effect (3D model with flying/ground modes) |
|
||||
| `Assets/Eagle/MonsterEffect.cs` | Wandering 2D monster group (DungeonMonsters2D prefabs) |
|
||||
| `Assets/Eagle/AnimalEffect.cs` | Wandering 3D animal group (Animal Pack Deluxe prefabs) |
|
||||
| `Assets/Eagle/Effects/` | All effect prefabs |
|
||||
|
||||
## Adding a new beast type
|
||||
|
||||
### 1. Create the effect MonoBehaviour (or reuse an existing one)
|
||||
|
||||
Three effect scripts exist for different asset types:
|
||||
|
||||
**MonsterEffect** (2D sprites, DungeonMonsters2D): Spawns a group of wandering 2D monsters. Uses PascalCase animator states (`Idle`, `Move`) and triggers (`AttackTrigger`, `SpecialATrigger`). Flips sprite X scale for facing direction. Best for DungeonMonsters2D prefabs.
|
||||
|
||||
**AnimalEffect** (3D models, Animal Pack Deluxe): Spawns a group of wandering 3D animals. Uses lowercase animator states (`idle`, `walk`, `eat`, `attack`) with `Animator.Play()`. Rotates model on Y axis for facing. Sets `sortingOrder=103`, `renderQueue=4000`, and `ZTest=Always` to render above the map. Best for Animal Pack Deluxe prefabs.
|
||||
|
||||
**DragonEffect** (single animated 3D creature): Spawns a 3D dragon that alternates between flying (circling at altitude with fly actions) and grounded (wandering with ground actions) modes. Uses `Animator.Play()` with DragonMapAnims controller. Best for large creatures with flight capabilities.
|
||||
|
||||
**Particle-based** (like `BeastsEffect.cs`): Good for simple effects like swarms or atmospheric particles.
|
||||
|
||||
To create a new effect script, use `[RequireComponent(typeof(RectTransform))]` for UI canvas positioning and accept tuning parameters as public fields.
|
||||
|
||||
### 2. Create the prefab
|
||||
|
||||
1. Create an empty GameObject in `Assets/Eagle/Effects/`
|
||||
2. Add a `RectTransform` component
|
||||
3. Add your effect MonoBehaviour
|
||||
4. Wire up any references (animated prefab, textures, materials)
|
||||
5. Save as prefab
|
||||
|
||||
### 3. Register in ProvinceBeastsController
|
||||
|
||||
Add a prefab field and a case in `GetEffectPrefab()`:
|
||||
|
||||
```csharp
|
||||
// In ProvinceBeastsController.cs
|
||||
|
||||
// Add inspector field alongside existing ones:
|
||||
public GameObject wolfEffectPrefab;
|
||||
|
||||
// Add case in GetEffectPrefab(), keyed by singularName from beasts.tsv:
|
||||
private GameObject GetEffectPrefab(string beastName) {
|
||||
switch (beastName) {
|
||||
case "dragon": return dragonEffectPrefab;
|
||||
case "wolf": return wolfEffectPrefab;
|
||||
default: return beastsEffectPrefab;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Wire up in the scene
|
||||
|
||||
In `Assets/Scenes/Eagle.unity`, find the `ProvinceBeastsController` component on the Eagle map object and assign your new prefab to the new field.
|
||||
|
||||
### 5. Test
|
||||
|
||||
Use `[ContextMenu]` methods on `ProvinceBeastsController` to test:
|
||||
- "Test Dragon in Motcia (ID 2)" spawns a dragon effect
|
||||
- Add similar methods for your beast type
|
||||
- "Clear All Effects" removes all active effects
|
||||
|
||||
## Beast name matching
|
||||
|
||||
Beast names come from `BeastInfo.SingularName` in the proto, which uses the canonical `singularName` from `src/main/resources/net/eagle0/eagle/beasts.tsv`. The switch in `GetEffectPrefab` uses exact string matching on this canonical name (e.g. `"dragon"`, `"wolf"`, `"skeleton"`).
|
||||
|
||||
Human-type beasts (pirates, bandits, etc.) are matched via a `HumanBeastNames` HashSet in `ProvinceBeastsController` rather than individual switch cases.
|
||||
|
||||
## Beast animation coverage
|
||||
|
||||
### Custom animations
|
||||
|
||||
| Beast | Effect Type | Asset Pack | Prefabs Used |
|
||||
|---|---|---|---|
|
||||
| dragon | DragonEffect | 3dFoin Dragon | dragon_skin.FBX (w/ DragonMapAnims controller) |
|
||||
| skeleton | MonsterEffect | DungeonMonsters2D | SkeletonWarrior, SkeletonArcher, SkeletonMage |
|
||||
| zombie | MonsterEffect | DungeonMonsters2D | Zombie, ZombieWarrior |
|
||||
| rat | MonsterEffect + AnimalEffect | DungeonMonsters2D + Animal Pack Deluxe | Rat (2D), Rat (3D) |
|
||||
| spider | MonsterEffect | DungeonMonsters2D | Spider |
|
||||
| demon | MonsterEffect | DungeonMonsters2D | Demon, Succubus |
|
||||
| orc | MonsterEffect | DungeonMonsters2D | Imp |
|
||||
| vampire | MonsterEffect | DungeonMonsters2D | Vampire |
|
||||
| knight types (6 names) | AnimalEffect | Polytope Studio | PT_Male_Knight_01/02, PT_Female_Knight_01/02 (w/ HumanMapAnims controller) |
|
||||
| soldier types (8 names) | AnimalEffect | Polytope Studio | PT_Male_Soldier_01/02, PT_Female_Soldier_01/02 (w/ HumanMapAnims controller) |
|
||||
| archer types (3 names) | AnimalEffect | Polytope Studio | PT_Male_Archer_01/02, PT_Female_Archer_01/02 (w/ HumanMapAnims controller) |
|
||||
| militia types (8 names) | AnimalEffect | Polytope Studio | PT_Male_Militia_01/02, PT_Female_Militia_01/02 (w/ HumanMapAnims controller) |
|
||||
| peasant types (9 names) | AnimalEffect | Polytope Studio | PT_Male_Peasant_01, PT_Female_Peasant_01_a (w/ HumanMapAnims controller) |
|
||||
| clown | AnimalEffect | Clown Pack | Clown, Fat Clown (w/ per-prefab controller overrides) |
|
||||
| giant | AnimalEffect | Polytope Studio | PT_Male/Female_Peasant at 3x scale (w/ HumanMapAnims controller) |
|
||||
| ogre, troll | AnimalEffect | Orc-Ogre Pack | OrcOgre_Animated (w/ OgreMapAnims controller) |
|
||||
| bear | AnimalEffect | Animal Pack Deluxe | Brown_bear |
|
||||
| boar | AnimalEffect | Animal Pack Deluxe | Wild_boar |
|
||||
| crocodile | AnimalEffect | Animal Pack Deluxe | Crocodile |
|
||||
| snake | AnimalEffect | Animal Pack Deluxe | Viper |
|
||||
| crab | AnimalEffect | Animal Pack Deluxe | Crab |
|
||||
| frog | AnimalEffect | Animal Pack Deluxe | Common_frog, Common_frog_v2, Common_frog_v3 |
|
||||
| rabbit | AnimalEffect | Animal Pack Deluxe | Wild_rabbit, Wild_rabbit_v2 |
|
||||
| salamander | AnimalEffect | Animal Pack Deluxe | Fire_salamander |
|
||||
| scorpion | AnimalEffect | Animal Pack Deluxe | Scorpion, Yellow_fattail_scorpion |
|
||||
| stag | AnimalEffect | Animal Pack Deluxe | Deer, Deer_male |
|
||||
| wild goat | AnimalEffect | Animal Pack Deluxe | Goat, Ibex |
|
||||
| wild pig | AnimalEffect | Animal Pack Deluxe | Iron_age_pig, Iron_age_pig_v2 |
|
||||
| wolf, wolverine | AnimalEffect | Animal Pack Deluxe | Wolf |
|
||||
| honey badger | AnimalEffect | HoneyBadger (Sketchfab) | HoneyBadger (w/ HoneyBadgerMapAnims controller) |
|
||||
| raccoon | AnimalEffect | Raccoon (Sketchfab) | Raccoon (w/ RaccoonMapAnims controller) |
|
||||
| tiger | AnimalEffect | ithappy Animals FREE | Tiger_001 (w/ TigerMapAnims controller) |
|
||||
| elephant | AnimalEffect | Animal Pack Deluxe v2 | African_elephant, Indian_elephant (w/ ElephantMapAnims controller) |
|
||||
| mammoth | AnimalEffect | Animal Pack Deluxe v2 | African_elephant, Indian_elephant (w/ ElephantMapAnims controller) |
|
||||
| hippopotamus | AnimalEffect | Africa Animals Pack Low Poly V2 | Hippopotamus (w/ HippopotamusMapAnims controller) |
|
||||
| lion | AnimalEffect | Africa Animals Pack Low Poly V1 | Lion, Lioness (w/ LionMapAnims controller) |
|
||||
| velociraptor | AnimalEffect | Dino Pack Low Poly V1 | VelociraptorColor1, VelociraptorColor2 (w/ VelociraptorMapAnims controller) |
|
||||
| black panther | AnimalEffect | Africa Animals Pack Low Poly V1 | BlackPanther (w/ BlackPantherMapAnims controller) |
|
||||
| rhinoceros | AnimalEffect | Africa Animals Pack Low Poly V1 | Rhinoceros (w/ RhinocerosMapAnims controller) |
|
||||
| gorilla | AnimalEffect | Africa Animals Pack Low Poly V2 | Gorilla (w/ GorillaMapAnims controller) |
|
||||
| hyena | AnimalEffect | Africa Animals Pack Low Poly V2 | Hyena (w/ HyenaMapAnims controller) |
|
||||
| leopard | AnimalEffect | Africa Animals Pack Low Poly V2 | Leopard (w/ LeopardMapAnims controller) |
|
||||
| warthog | AnimalEffect | Africa Animals Pack Low Poly V2 | Phacochoerus (w/ WarthogMapAnims controller) |
|
||||
| emu | AnimalEffect | Australia Animals Pack V1 | Emu (w/ EmuMapAnims controller) |
|
||||
| kangaroo | AnimalEffect | Australia Animals Pack V1 | Kangaroo (w/ KangarooMapAnims controller) |
|
||||
| tasmanian devil | AnimalEffect | Australia Animals Pack V1 | TasmanianDevil (w/ TasmanianDevilMapAnims controller) |
|
||||
| hippogryph | DragonEffect | Heroic Fantasy Creatures Full Pack Vol 2 | Hippogriff_PBR (w/ HippogryphMapAnims controller) |
|
||||
|
||||
### Human-type beasts
|
||||
|
||||
34 human-type beast names use 3D Polytope Studio characters grouped into 5 archetypes. Each archetype uses a shared `HumanMapAnims.controller` that retargets Clown animation clips (idle, walk, attack, damage) via Unity's Humanoid system. One additional human type (psychopath) uses the 2D Butcher sprite from DungeonMonsters2D.
|
||||
|
||||
| Archetype | Polytope Type | Beast Names (count) |
|
||||
|---|---|---|
|
||||
| **Knight** (heavy armor) | Knight M/F x2 | cultist, heretic, cannibal, terrorist, cossack, desperado (6) |
|
||||
| **Soldier** (medium armor) | Soldier M/F x2 | bandit, brigand, robber, highwayman, marauder, dacoit, freebooter, mobster (8) |
|
||||
| **Archer** (light armor, ranged) | Archer M/F x2 | thief, pirate, buccaneer (3) |
|
||||
| **Militia** (light armed) | Militia M/F x2 | hooligan, street tough, ruffian, scalawag, ne'er-do-well, crook, miscreant, asshole (8) |
|
||||
| **Peasant** (unarmed) | Peasant M/F | agitator, instigator, rabble-rouser, separatist, particularist, nihilist, proud boy, rebel, traitor (9) |
|
||||
|
||||
Asset packs: Polytope Studio - Lowpoly Medieval Characters (Soldiers, Knights, Militia, Archers, Peasants)
|
||||
|
||||
| **Butcher** (armed loner) | DungeonMonsters2D Butcher | psychopath (1) |
|
||||
|
||||
### No custom animation (falls back to generic vultures)
|
||||
|
||||
Ordered by spawn likelihood (most common first):
|
||||
|
||||
| Beast | Likelihood | Notes |
|
||||
|---|---|---|
|
||||
| chimpanzee | 0.04 | Primate |
|
||||
| unknown | 0.00 | Intentional fallback |
|
||||
|
||||
## Using low-poly animal packs (Africa Animals, Dino Pack)
|
||||
|
||||
Assets from these packs (by the same author) require extra setup because they ship with **Legacy** animation import and include physics components that conflict with AnimalEffect. The workflow below applies to all of them:
|
||||
- Africa Animals Pack Low Poly V1 (Lion, Lioness, Black Panther, Crocodile, Elephant, Giraffe, Rhinoceros, Tiger, Zebra)
|
||||
- Africa Animals Pack Low Poly V2 (Hippopotamus, Antelope, Gorilla, Hyena, Leopard, Phacochoerus/Warthog)
|
||||
- Dino Pack Low Poly V1 (Velociraptor, Brontosaurus, Pteranodon, Stegosaurus, T-Rex, Triceratops)
|
||||
- Australia Animals Pack V1 (Chlamydosaurus/Frilled Lizard, Echidna, Emu, Kangaroo, Koala, Platypus, Tasmanian Devil)
|
||||
|
||||
### Per-animal setup steps
|
||||
|
||||
1. **Switch FBX import to Generic animation type.**
|
||||
In the `.fbx.meta` file (location varies by pack):
|
||||
- Change `animationType: 1` to `animationType: 2`
|
||||
- Change `avatarSetup: 0` to `avatarSetup: 1` (newer format) — older metas without `avatarSetup` will auto-create an avatar at runtime
|
||||
- Set `loopTime: 1` on looping clips (idle, walk, eat, run) but NOT one-shot clips (attack, death, hurt)
|
||||
Unity will re-import the FBX with Mecanim-compatible clips when it next opens.
|
||||
|
||||
2. **Create an AnimatorController** in `Assets/Eagle/Effects/` named `<Animal>MapAnims.controller`.
|
||||
Use the same 4-state pattern (idle, walk, eat, attack) as ElephantMapAnims. Reference the animation clips by their fileIDs from the FBX meta's `internalIDToNameTable` (type 74 entries) or `fileIDToRecycleName` (older format, type 7400000+). These IDs are stable across import type changes. Note: if the FBX names its idle clip `idle1`, create the controller state as `idle` but reference the `idle1` clip by fileID.
|
||||
|
||||
3. **Create an AnimalEffect prefab** in `Assets/Eagle/Effects/` named `<Animal>Effect.prefab`.
|
||||
Reference the pack's prefab as the animal prefab, and wire up the new AnimatorController. Multiple color/sex variants from the same or similar FBX can share one controller (e.g., Lion and Lioness). AnimalEffect will automatically strip the pack's legacy Animation, Rigidbody, and Collider components at spawn time, and add an Animator if the prefab doesn't already have one.
|
||||
|
||||
4. **Register in ProvinceBeastsController** with a case in `GetEffectPrefab()` and a test ContextMenu method.
|
||||
|
||||
5. **Mark the effect prefab as Addressable** with the `beast-effects` label in the Unity editor (Window > Asset Management > Addressables > Groups).
|
||||
|
||||
### Notes
|
||||
|
||||
- The packs include both SingleTexture and TextureAtlas variants; use SingleTexture for best visual quality.
|
||||
- Hippos have water-specific animations (idlewater, deathwater) that could be used for future water-province effects.
|
||||
- The packs' prefabs include Rigidbody/BoxCollider components intended for gameplay use. AnimalEffect strips these automatically since it controls position directly.
|
||||
|
||||
### Available animals not yet set up as beasts
|
||||
|
||||
These animals exist in the imported packs and could be set up as in-game beasts using the workflow above:
|
||||
|
||||
**Africa Animals Pack V1** (already imported):
|
||||
| Animal | Good fit for | Notes |
|
||||
|---|---|---|
|
||||
| Giraffe (x2 variants) | giraffe beast | Tall model, may need larger scale |
|
||||
| Zebra | zebra beast | Herd animal; good with higher animalCount |
|
||||
|
||||
(Black Panther, Rhinoceros, Crocodile, Elephant, and Tiger already have effects.)
|
||||
|
||||
**Africa Animals Pack V2** (already imported):
|
||||
| Animal | Good fit for | Notes |
|
||||
|---|---|---|
|
||||
| Antelope | antelope/gazelle beast | Fast-moving; good with higher moveSpeed |
|
||||
|
||||
(Gorilla, Hyena, Leopard, Warthog, and Hippopotamus already have effects.)
|
||||
|
||||
**Dino Pack Low Poly V1** (already imported):
|
||||
| Animal | Good fit for | Notes |
|
||||
|---|---|---|
|
||||
| Brontosaurus | brontosaurus/sauropod beast | Very large; needs big scale, low count |
|
||||
| Pteranodon | pteranodon/flying beast | Has fly animation; could use DragonEffect-style flight |
|
||||
| Stegosaurus | stegosaurus beast | Large herbivore |
|
||||
| T-Rex | tyrannosaurus beast | Iconic predator; good as a solo beast |
|
||||
| Triceratops | triceratops beast | Large herbivore; good for tough beast |
|
||||
|
||||
(Velociraptor already has an effect.)
|
||||
|
||||
**Australia Animals Pack V1** (already imported):
|
||||
| Animal | Good fit for | Notes |
|
||||
|---|---|---|
|
||||
| Koala | koala beast | Peaceful; good for low-threat province events |
|
||||
| Echidna | echidna beast | Small, spiny |
|
||||
| Chlamydosaurus (Frilled Lizard) | lizard beast | Has threat display animation |
|
||||
| Platypus (Ornitorinco) | platypus beast | Semi-aquatic; unique |
|
||||
|
||||
(Emu, Kangaroo, and Tasmanian Devil already have effects.)
|
||||
|
||||
None of the remaining animals above are currently in `beasts.tsv`.
|
||||
|
||||
## Tips
|
||||
|
||||
- Keep effects lightweight; multiple provinces may have beasts simultaneously
|
||||
- Use `ParticleSystemScalingMode.Hierarchy` so effects scale with the map zoom
|
||||
- Set appropriate `sortingOrder` on renderers (existing effects use 102-104)
|
||||
- For animated prefabs, set `AnimatorCullingMode.AlwaysAnimate` since UI elements may be considered offscreen by the culling system
|
||||
- For 3D models (AnimalEffect), set `renderQueue=4000` and `ZTest=Always` on materials to prevent clipping behind the map
|
||||
@@ -0,0 +1,159 @@
|
||||
# Hetzner Setup Guide
|
||||
|
||||
This guide walks through setting up Hetzner Cloud infrastructure for running Shardok on-demand compute.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- All code PRs merged (#4990, #4996, #4998, #5001, #5009)
|
||||
- Access to DigitalOcean Container Registry (for pulling Shardok ARM64 image)
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Hetzner Cloud Account
|
||||
|
||||
1. Go to https://console.hetzner.cloud/
|
||||
2. Sign up and add payment method
|
||||
3. Create a new project (e.g., "eagle0")
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Generate Hetzner API Token
|
||||
|
||||
1. In Hetzner Console → Security → API Tokens
|
||||
2. Click "Generate API Token"
|
||||
3. Give it **Read & Write** permissions
|
||||
4. Copy the token (you'll only see it once)
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Generate Shardok Auth Token
|
||||
|
||||
Generate a 256-bit random token for Eagle-Shardok authentication:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
Save this output - it's the shared secret between Eagle and Shardok.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Store Secrets in GitHub Actions
|
||||
|
||||
Add these secrets in GitHub → Settings → Secrets and variables → Actions:
|
||||
|
||||
| Secret Name | Description |
|
||||
|-------------|-------------|
|
||||
| `HETZNER_API_TOKEN` | From Step 2 - for Hetzner API calls |
|
||||
| `SHARDOK_AUTH_TOKEN` | From Step 3 - shared secret for gRPC auth |
|
||||
|
||||
Note: `DO_REGISTRY_TOKEN` already exists and will be used for Hetzner to pull container images.
|
||||
|
||||
These secrets will be passed to Eagle at runtime via `docker_build.yml`, similar to how `OPENAI_API_KEY` and other secrets are handled.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: DNS Setup (for Let's Encrypt)
|
||||
|
||||
You need a domain pointing to the Shardok instance for TLS certificates.
|
||||
|
||||
### Option A: Floating IP (Recommended)
|
||||
|
||||
1. In Hetzner Console → Networking → Floating IPs
|
||||
2. Create a **Floating IPv6** in **Hillsboro, Oregon (hil)** region
|
||||
- IPv6 costs €1/month vs €3/month for IPv4
|
||||
- Hillsboro has better latency to DigitalOcean SFO than Ashburn
|
||||
- Server-to-server communication works fine with IPv6-only
|
||||
3. Point `shardok.prod.eagle0.net` to this IP via AAAA record
|
||||
4. The ShardokInstanceManager will attach this IP to instances on spin-up
|
||||
|
||||
**Location choice**: Hillsboro, OR (`hil`) is recommended for US West Coast. Same pricing as Ashburn (`ash`).
|
||||
|
||||
### Option B: Dynamic DNS
|
||||
|
||||
Update DNS programmatically when instance spins up. More complex but avoids floating IP cost.
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Upload SSH Key to Hetzner
|
||||
|
||||
For debugging access to instances:
|
||||
|
||||
1. In Hetzner Console → Security → SSH Keys
|
||||
2. Click "Add SSH Key"
|
||||
3. Paste your public key (e.g., `~/.ssh/id_rsa.pub`)
|
||||
4. Give it a name (e.g., "eagle-deploy")
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Wire Security Config into Eagle
|
||||
|
||||
Update Eagle's startup code to use the security config when connecting to remote Shardok:
|
||||
|
||||
```scala
|
||||
val securityConfig = ShardokSecurityConfig(
|
||||
useTls = true,
|
||||
authToken = Some(sys.env("SHARDOK_AUTH_TOKEN"))
|
||||
)
|
||||
|
||||
val channel = ServerSetupHelpers.newChannel(
|
||||
"shardok.prod.eagle0.net",
|
||||
50051,
|
||||
securityConfig
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Instance Spin-up
|
||||
|
||||
Test the Hetzner integration by triggering instance creation:
|
||||
|
||||
```scala
|
||||
val manager = new ShardokInstanceManager(
|
||||
hetznerApiToken = sys.env("HETZNER_API_TOKEN"),
|
||||
// ... other config
|
||||
)
|
||||
|
||||
manager.ensureInstanceRunning()
|
||||
```
|
||||
|
||||
### Verify TLS and Auth
|
||||
|
||||
1. Instance spins up and gets Let's Encrypt certificate
|
||||
2. Eagle connects via TLS
|
||||
3. Auth token is validated on each request
|
||||
|
||||
---
|
||||
|
||||
## Cost Estimate
|
||||
|
||||
| Component | Cost |
|
||||
|-----------|------|
|
||||
| CAX41 (16 ARM cores) | ~$0.04/hour |
|
||||
| Floating IP | ~$4/month |
|
||||
| Typical usage (20 hrs/week) | ~$3.50/month compute |
|
||||
|
||||
**Total: ~$7-8/month** for typical usage.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Instance won't start
|
||||
- Check Hetzner API token has Read & Write permissions
|
||||
- Verify you're using the correct region (`hil` for Hillsboro OR, or `ash` for Ashburn VA)
|
||||
|
||||
### TLS certificate fails
|
||||
- Ensure DNS points to the instance IP before certbot runs
|
||||
- Check port 80 is open for Let's Encrypt HTTP-01 challenge
|
||||
|
||||
### Auth failures
|
||||
- Verify `SHARDOK_AUTH_TOKEN` matches on both Eagle and Shardok
|
||||
- Check the token file is readable by Shardok container
|
||||
|
||||
### Can't pull container image
|
||||
- Ensure `DO_REGISTRY_TOKEN` is passed to cloud-init
|
||||
- Verify the ARM64 image exists: `registry.digitalocean.com/eagle0/shardok-server:arm64-latest`
|
||||
@@ -42,17 +42,16 @@ Time-to-first-token (TTFT) was measured from request initiation to the first tex
|
||||
|
||||
### For Narrative Text Generation (Default)
|
||||
|
||||
**Gemini 3.1 Flash-Lite** is recommended as the default:
|
||||
- Priced at $0.25/$1.50 per 1M input/output tokens
|
||||
- Significantly faster than 2.5 Flash-Lite on throughput and TTFT
|
||||
- Meaningfully smarter than 2.5 Flash-Lite, while remaining one of the cheapest options
|
||||
- Quality is more than sufficient for short narrative snippets
|
||||
**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 3.1 Flash-Lite | Default for most use cases |
|
||||
| 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 |
|
||||
@@ -71,7 +70,7 @@ 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-3.1-flash-lite-preview)
|
||||
- `GeminiModelName` (default: gemini-2.5-flash-lite)
|
||||
- `OpenAiModelName` (default: gpt-4.1-mini)
|
||||
- `ClaudeModelName` (default: claude-3-5-haiku-20241022)
|
||||
|
||||
|
||||
@@ -1,121 +1,23 @@
|
||||
# New Profession Proposals
|
||||
|
||||
This document proposes new hero professions for Eagle0. Each profession should matter in both the Eagle strategic layer
|
||||
and the Shardok tactical layer, and should be earned through a prime stat in the same way existing professions are.
|
||||
This document proposes 5 new hero professions for Eagle0. Each profession has abilities for both the Eagle (strategic) and Shardok (tactical) game layers.
|
||||
|
||||
## Current Professions Reference
|
||||
|
||||
Professions are currently gained when a no-profession hero crosses the profession stat threshold and wins a profession
|
||||
roll. The current stat mapping is:
|
||||
|
||||
| Prime Stat | Professions |
|
||||
|------------|-------------|
|
||||
| **Strength** | Champion |
|
||||
| **Agility** | Engineer, Ranger |
|
||||
| **Wisdom** | Mage |
|
||||
| **Charisma** | Necromancer, Paladin |
|
||||
| **Constitution** | None |
|
||||
|
||||
The current profession capabilities are:
|
||||
|
||||
| Profession | Eagle Ability | Shardok Abilities | Current Niche |
|
||||
|------------|---------------|-------------------|---------------|
|
||||
| **Mage** | Control Weather: start/end blizzards and droughts in the current or neighboring province | Lightning Bolt, Meteor, Freeze Water, Start Fire access/enhancement | Strategic weather control and high-impact elemental battlefield effects |
|
||||
| **Necromancer** | Start Epidemic in the current or neighboring province | Raise Dead, Fear | Attrition pressure, undead creation, morale attack |
|
||||
| **Engineer** | Improve command recommendation and bonus improvement output | Repair, Fortify, Build Bridge, Reduce siege | Infrastructure, battlefield construction, mechanical support |
|
||||
| **Paladin** | Alms priority and increased support from food | Holy Wave | Public support, holy area effect, anti-undead flavor |
|
||||
| **Ranger** | Recon against enemy provinces | Scout, Hide access/enhancement, Brave Water enhancement | Information, stealth, terrain crossing |
|
||||
| **Champion** | Train command recommendation and training bonus | Challenge Duel | Martial excellence, battalion training, direct hero confrontation |
|
||||
|
||||
## Recommendation
|
||||
|
||||
Add **Warden** first.
|
||||
|
||||
Warden is the cleanest next profession because Constitution is the only hero stat that does not currently unlock any
|
||||
profession. A Constitution profession also creates a distinctive tactical identity: not another caster, scout, or damage
|
||||
dealer, but a durable hero who keeps important units alive. That fills a real mechanical gap without forcing a large
|
||||
rewrite of the profession system.
|
||||
|
||||
The first version should be intentionally simple:
|
||||
|
||||
- **Prime stat:** Constitution
|
||||
- **Eagle ability:** Garrison, a defensive province action or passive defense bonus when a Warden is present with enough
|
||||
vigor; or Custody, a prisoner-control ability for captured heroes
|
||||
- **Shardok ability:** Guard, a command that marks an adjacent friendly unit until the Warden's next turn; the first
|
||||
attack against that unit is redirected to the Warden's unit or reduced by a fixed amount
|
||||
- **Battalion suitability:** Optimal with durable melee infantry; suboptimal with fragile archers or stealth-focused
|
||||
units if we want a stricter identity
|
||||
- **Balance direction:** Spend action points and/or vigor to protect another unit, rather than adding free passive
|
||||
prevention every round
|
||||
|
||||
This keeps the profession legible: high-Constitution heroes become the people who can hold formation, anchor a province,
|
||||
and protect fragile specialists.
|
||||
| Profession | Eagle Ability | Shardok Abilities |
|
||||
|------------|---------------|-------------------|
|
||||
| **Mage** | Control Weather | Lightning Bolt, Meteor, Freeze Water, Start Fire (enhanced) |
|
||||
| **Necromancer** | Start Epidemic | Raise Dead, Fear |
|
||||
| **Engineer** | (general) | Repair, Fortify, Build Bridge, Reduce (siege) |
|
||||
| **Paladin** | Alms (prioritized) | Holy Wave |
|
||||
| **Ranger** | Recon | Scout, Hide (enhanced), Brave Water (enhanced) |
|
||||
| **Champion** | (general) | Challenge Duel |
|
||||
|
||||
---
|
||||
|
||||
## Proposed New Professions
|
||||
|
||||
### 1. WARDEN (Defense & Protection Specialist)
|
||||
|
||||
**Fantasy:** The stalwart defender who holds the line and protects allies.
|
||||
|
||||
**Why it fits now:**
|
||||
- Gives Constitution a profession path
|
||||
- Adds a defensive/tank identity that no current profession owns
|
||||
- Creates counterplay to burst damage, duel pressure, and fragile high-value units
|
||||
|
||||
**Eagle Ability: "Garrison"**
|
||||
- A province with a Warden-led unit gets a defense bonus when attacked
|
||||
- Alternative active version: spend vigor to fortify the province until next round
|
||||
- Synergy: works well in border provinces, choke points, and provinces recovering after losses
|
||||
|
||||
**Alternative Eagle Ability: "Custody"**
|
||||
- A Warden can handle captured heroes with extra authority: execute them immediately or move them to a neighboring ruled
|
||||
province before normal prisoner management
|
||||
- Passive custody version: each new round, prisoners in a Warden-guarded province become more likely to join the ruling
|
||||
faction
|
||||
- Prefer implementing this as a small positive addition to the prisoner's existing `factionBiases` entry for the ruling
|
||||
faction, rather than changing the prisoner `roundsInType` multiplier from `+5/round` to `+10/round`
|
||||
- Rationale: changing the multiplier would be retroactive. A prisoner held for ten rounds would immediately receive a
|
||||
large odds jump the moment a Warden arrived. Accumulating a Warden-specific bias only rewards rounds actually spent
|
||||
under Warden custody, and the existing faction-bias decay makes the effect fade naturally if the Warden leaves
|
||||
- Stronger version: if the Warden's side captures enemy heroes during battle but still loses the province, the Warden can
|
||||
evacuate one captured hero to a neighboring ruled province instead of losing custody
|
||||
- This should be limited to one captured hero per battle or require a high-vigor Warden, because keeping prisoners after
|
||||
losing the battle is a major strategic swing
|
||||
- Best flavor: Wardens are not just defenders of walls, but keepers of oaths, chains, and battlefield custody
|
||||
|
||||
**Shardok Ability: "Guard"**
|
||||
- Target adjacent friendly unit is protected until the Warden's next turn
|
||||
- First incoming attack against the protected unit is redirected to the Warden's unit or reduced
|
||||
- Costs action points and cannot target self
|
||||
- Creates a clear positioning puzzle without introducing another long-range damage button
|
||||
|
||||
**Implementation Notes:**
|
||||
- Add `Warden` to the Scala profession enum and common/client profession enums
|
||||
- Map Constitution to `Vector(Profession.Warden)` in `HeroStatGainAction`
|
||||
- Add display names, profession-gained notification copy, tutorial trigger mapping, and headshot bucket support
|
||||
- Add strategic availability and command handling only after deciding whether Warden uses Garrison, Custody, or both
|
||||
- For Custody, prefer extending the existing captured-hero/prisoner-management flow instead of creating a parallel
|
||||
prisoner system
|
||||
- For passive prisoner conversion, update `NewRoundAction` near the existing unaffiliated-hero `roundsInType` and
|
||||
`factionBiases` maintenance. If the province is ruled, contains a ruling-faction Warden, and the unaffiliated hero is
|
||||
a prisoner, add the Warden custody bonus to that prisoner's `factionBiases(rulingFactionId)`.
|
||||
- For Shardok, start with a single-turn guard status before attempting more complex interception chains
|
||||
|
||||
**Current Follow-Up Priorities:**
|
||||
- Teach the Shardok AI when to use the Warden evacuation command. The command is valuable when the Warden's side has
|
||||
captured enemy heroes and can secure them by leaving the battle, especially when the battle outcome is uncertain or
|
||||
trending against that side.
|
||||
- Add the strategic passive custody bonus: each round, prisoners in a province guarded by a ruling-faction Warden should
|
||||
get a small accumulated positive bias toward joining that faction. Prefer storing this as an addition to the existing
|
||||
`factionBiases` entry rather than changing the global `roundsInType` multiplier, so the bonus only reflects rounds
|
||||
actually spent under Warden custody.
|
||||
- Make tactical AI more protective of heroes at risk of capture when a Warden may be present. Warden evacuation makes
|
||||
captured heroes harder to recover, so the AI should treat exposing high-value heroes to capture as more dangerous than
|
||||
before. This matters even when the AI does not have perfect knowledge that a Warden is in the battle.
|
||||
|
||||
### 2. HERALD (Morale & Communication Specialist)
|
||||
### 1. HERALD (Morale & Communication Specialist)
|
||||
|
||||
**Fantasy:** The inspiring leader who rallies troops and carries messages across the battlefield.
|
||||
|
||||
@@ -130,7 +32,7 @@ and protect fragile specialists.
|
||||
|
||||
---
|
||||
|
||||
### 3. ALCHEMIST (Fire & Transformation Specialist)
|
||||
### 2. ALCHEMIST (Fire & Transformation Specialist)
|
||||
|
||||
**Fantasy:** The mad scientist who manipulates the elements through science, not magic.
|
||||
|
||||
@@ -145,6 +47,21 @@ and protect fragile specialists.
|
||||
|
||||
---
|
||||
|
||||
### 3. WARDEN (Defensive Specialist)
|
||||
|
||||
**Fantasy:** The stalwart defender who holds the line and protects allies.
|
||||
|
||||
**Eagle Ability: "Garrison"**
|
||||
- A province with a Warden-led unit gets +1 to defense when attacked
|
||||
- Encourages strategic placement of defensive heroes
|
||||
|
||||
**Shardok Ability: "Intercept"**
|
||||
- Once per turn, when an adjacent friendly unit is attacked, the Warden's unit can take the hit instead
|
||||
- Creates a "bodyguard" mechanic that protects valuable units
|
||||
- Costs action points to maintain readiness
|
||||
|
||||
---
|
||||
|
||||
### 4. INQUISITOR (Anti-Magic & Intelligence)
|
||||
|
||||
**Fantasy:** The witch-hunter who counters supernatural threats and uncovers secrets.
|
||||
@@ -179,174 +96,8 @@ and protect fragile specialists.
|
||||
|
||||
These professions were designed to:
|
||||
|
||||
1. **Fill mechanical gaps**: Warden gives Constitution a profession and adds defensive depth
|
||||
2. **Create counterplay**: Inquisitor vs Mage/Necromancer, Warden vs burst damage, Beastmaster vs beast events
|
||||
3. **Avoid overlap**: Each profession should own a tactical and strategic niche not covered by existing professions
|
||||
4. **Support both layers**: Each ability should be meaningful in Eagle and Shardok
|
||||
5. **Enable interesting decisions**: Guard creates positioning choices, Inspire creates action economy choices, Transmute
|
||||
creates resource tradeoffs
|
||||
|
||||
## Unified Candidate Ranking
|
||||
|
||||
This ranking considers every candidate together, regardless of where the idea came from. The main criteria are mechanical
|
||||
fit, overlap with existing professions, implementation clarity, strategic-layer value, Shardok value, and narrative
|
||||
payoff.
|
||||
|
||||
1. **Warden**: Best immediate fit. It gives Constitution a profession path and creates a distinctive custody/protection
|
||||
identity.
|
||||
2. **Quartermaster**: Best strategic depth after Warden. Logistics touches resources, armies, travel, and attrition
|
||||
without adding another damage specialist.
|
||||
3. **Envoy**: Best diplomacy/prisoner-system extension. Strong narrative payoff and clear Eagle-side value.
|
||||
4. **Surgeon**: Best loss-mitigation role. Makes battle aftermath less binary and gives non-magical healing a place.
|
||||
5. **Herald**: Strong morale and action-economy support. Distinct from Paladin if kept secular and battlefield-command
|
||||
focused.
|
||||
6. **Marshal**: Strong army identity, especially for formation play and troop organization, but needs careful separation
|
||||
from Champion.
|
||||
7. **Inquisitor**: Useful counterplay against Mage and Necromancer, though anti-magic should not become too narrow.
|
||||
8. **Pathfinder**: Good campaign movement role, but should focus on routes and terrain rather than replacing Ranger
|
||||
scouting.
|
||||
9. **Alchemist**: Good resource and terrain manipulation fantasy, but risks crowding Mage and Engineer unless scoped
|
||||
around risky transformations.
|
||||
10. **Spymaster**: Strong flavor and intelligence value, but needs slow strategic effects to avoid replacing Recon.
|
||||
11. **Artificer**: Fun temporary-device fantasy, but overlaps Engineer unless it owns one-shot preparation rather than
|
||||
construction.
|
||||
12. **Beastmaster**: Connects nicely to beast events, but overlaps Ranger unless focused on province events and wild
|
||||
allies.
|
||||
13. **Harbinger**: Excellent flavor, but probably later because fear/morale already touches Necromancer and Paladin
|
||||
space.
|
||||
|
||||
The best near-term sequence is likely **Warden**, then **Quartermaster** or **Envoy**. Avoid adding another pure damage
|
||||
caster until defensive, logistical, diplomatic, morale, and aftermath niches are better represented.
|
||||
|
||||
## Additional Candidate Details
|
||||
|
||||
These ideas come from looking at open mechanical space in Eagle and Shardok: logistics, loyalty, prisoners, province
|
||||
events, diplomacy, battlefield control, and non-damage support.
|
||||
|
||||
### Marshal
|
||||
|
||||
**Fantasy:** The army organizer who turns a pile of units into an actual campaign force.
|
||||
|
||||
**Best stat fit:** Strength or Charisma
|
||||
|
||||
**Eagle Ability: "Muster"**
|
||||
- Reorganize, reinforce, or prepare battalions more efficiently than a normal hero
|
||||
- Could reduce vigor cost for Organize Troops or improve training/armament transfer efficiency
|
||||
|
||||
**Shardok Ability: "Command Formation"**
|
||||
- Adjacent friendly units gain a small defensive or morale bonus while holding formation
|
||||
- Creates a tactical identity around positioning several units together
|
||||
|
||||
**Why it is interesting:** Champion currently owns heroic combat and training, but not army-level coordination. Marshal
|
||||
would be about disciplined formations rather than duels.
|
||||
|
||||
### Quartermaster
|
||||
|
||||
**Fantasy:** The logistics expert who keeps armies fed, paid, armed, and moving.
|
||||
|
||||
**Best stat fit:** Constitution or Wisdom
|
||||
|
||||
**Eagle Ability: "Provision"**
|
||||
- Move food/gold/supplies with less waste or farther than normal
|
||||
- Reduce attrition or readiness loss for armies operating away from strong provinces
|
||||
|
||||
**Shardok Ability: "Resupply"**
|
||||
- Restore limited ammunition, repair light damage, or grant a one-turn readiness buff to a nearby unit
|
||||
|
||||
**Why it is interesting:** The strategic layer already has meaningful resources. A profession that manipulates logistics
|
||||
would create strong choices without simply adding another combat spell.
|
||||
|
||||
### Envoy
|
||||
|
||||
**Fantasy:** The negotiator, hostage-broker, and oath-maker.
|
||||
|
||||
**Best stat fit:** Charisma
|
||||
|
||||
**Eagle Ability: "Parley"**
|
||||
- Improve diplomacy outcomes, ransom terms, prisoner returns, or truce/alliance offer odds
|
||||
- Could reduce the risk of ambassadors being imprisoned or create better return-prisoner rewards
|
||||
|
||||
**Shardok Ability: "Demand Surrender"**
|
||||
- Attempt to force a damaged or isolated enemy unit to flee, with odds based on charisma and battlefield state
|
||||
|
||||
**Why it is interesting:** Eagle has relationships, ransoms, truces, alliances, and prisoner choices. Envoy would make
|
||||
diplomacy feel like a profession rather than only a menu action.
|
||||
|
||||
### Spymaster
|
||||
|
||||
**Fantasy:** The patient handler of informants, rumors, sabotage, and false trails.
|
||||
|
||||
**Best stat fit:** Wisdom or Charisma
|
||||
|
||||
**Eagle Ability: "Infiltrate"**
|
||||
- Plant a delayed intelligence effect in an enemy province, revealing troop movements or weakening support
|
||||
- Could counter or complement Ranger recon without duplicating it
|
||||
|
||||
**Shardok Ability: "Sabotage"**
|
||||
- Before or during battle, reduce one enemy unit's readiness, movement, or first action effectiveness
|
||||
|
||||
**Why it is interesting:** Ranger is field reconnaissance. Spymaster can be slower, political, and province-focused.
|
||||
|
||||
### Surgeon
|
||||
|
||||
**Fantasy:** The healer who saves lives after the dramatic part of the story is over.
|
||||
|
||||
**Best stat fit:** Wisdom or Constitution
|
||||
|
||||
**Eagle Ability: "Triage"**
|
||||
- Reduce hero vigor loss, casualty severity, or prisoner death/execution fallout after battles
|
||||
- Could improve recovery in provinces with many wounded heroes or battered battalions
|
||||
|
||||
**Shardok Ability: "Stabilize"**
|
||||
- Prevent a nearby friendly hero unit from being captured or destroyed once per battle, leaving it routed or exhausted
|
||||
instead
|
||||
|
||||
**Why it is interesting:** Paladin has holy support, but not mundane medical recovery. Surgeon creates a grounded support
|
||||
role that can make losses less binary.
|
||||
|
||||
### Artificer
|
||||
|
||||
**Fantasy:** The maker of rare devices, siege tools, lenses, traps, and battlefield instruments.
|
||||
|
||||
**Best stat fit:** Wisdom or Agility
|
||||
|
||||
**Eagle Ability: "Prototype"**
|
||||
- Invest gold and vigor to create a temporary province or battalion enhancement
|
||||
- Examples: better siege readiness, scouting lenses, defensive traps, or weatherproof stores
|
||||
|
||||
**Shardok Ability: "Deploy Device"**
|
||||
- Place a one-use trap, barricade, signal flare, or field tool on a nearby hex
|
||||
|
||||
**Why it is interesting:** Engineer currently owns building and repair. Artificer can own temporary inventions and
|
||||
one-shot tactical preparation.
|
||||
|
||||
### Harbinger
|
||||
|
||||
**Fantasy:** The terrifying omen-bearer whose arrival changes morale before the first blow lands.
|
||||
|
||||
**Best stat fit:** Charisma or Wisdom
|
||||
|
||||
**Eagle Ability: "Portent"**
|
||||
- Lower enemy support, increase unrest, or amplify the psychological effect of victories and executions
|
||||
- Could be risky: fear-based rule damages diplomacy or loyalty if overused
|
||||
|
||||
**Shardok Ability: "Dread Standard"**
|
||||
- Enemies near the Harbinger suffer morale penalties or worse flee odds
|
||||
|
||||
**Why it is interesting:** Necromancer has supernatural fear, but Harbinger could be political and symbolic rather than
|
||||
undead-focused.
|
||||
|
||||
### Pathfinder
|
||||
|
||||
**Fantasy:** The guide who knows hidden passes, river crossings, and winter roads.
|
||||
|
||||
**Best stat fit:** Agility or Constitution
|
||||
|
||||
**Eagle Ability: "Find Passage"**
|
||||
- Move armies or heroes through difficult terrain, winter, blizzards, or river-heavy borders with lower penalties
|
||||
- Could create one-turn temporary travel links between neighboring provinces under specific conditions
|
||||
|
||||
**Shardok Ability: "Open Route"**
|
||||
- Let a nearby unit ignore one terrain penalty or cross a difficult hex safely this turn
|
||||
|
||||
**Why it is interesting:** Ranger owns stealth and scouting. Pathfinder owns movement and campaign geography.
|
||||
1. **Fill mechanical gaps**: Warden provides defensive depth, Inquisitor counters magic-heavy strategies
|
||||
2. **Create counterplay**: Inquisitor vs Mage/Necromancer, Beastmaster vs Rangers (nature vs nature)
|
||||
3. **Avoid overlap**: Each has a unique niche not covered by existing professions
|
||||
4. **Support both layers**: Each ability is meaningful in its respective game mode
|
||||
5. **Enable interesting decisions**: Intercept creates bodyguard tactics, Inspire creates action economy choices
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
# Postgres History Migration Plan
|
||||
|
||||
## Goal
|
||||
|
||||
Evaluate and, if the measurements support it, migrate production game history persistence from local SQLite plus S3 base
|
||||
and delta files to a managed Postgres database.
|
||||
|
||||
This is not just a mechanical port. The important product requirement is that battle commands, especially Shardok map
|
||||
clicks, show results to the player as quickly as possible while still keeping the chance of lost results acceptably low.
|
||||
|
||||
## Proposed Direction
|
||||
|
||||
Use Postgres as the authoritative production store. Local SQLite should remain useful for development, tests, and rollback
|
||||
during rollout, but production should not have two long-lived authoritative stores for the same game.
|
||||
|
||||
Start with a small DigitalOcean Managed Postgres instance in the same region and VPC as Eagle. The $15 single-node plan is
|
||||
reasonable for early measurement and staging. If the design works, production can move to a more reliable plan when the
|
||||
availability requirements justify it.
|
||||
|
||||
## Phase 1: Measure First
|
||||
|
||||
Before porting the full history implementation, add a benchmark or admin utility that runs from the Eagle deployment
|
||||
environment against the candidate Postgres database.
|
||||
|
||||
Measure at least:
|
||||
|
||||
- single action-result append transaction latency
|
||||
- batch append latency for 25 and 100 action results
|
||||
- latest snapshot lookup
|
||||
- replay from a snapshot to a target index
|
||||
- `since` and stream-update query latency
|
||||
- Shardok battle result append latency
|
||||
- p50, p95, p99, and max latency
|
||||
- async queue lag, if an async persistence prototype is included
|
||||
|
||||
Useful initial exit criteria:
|
||||
|
||||
- Strategic action-result writes should be comfortably below 50 ms p95.
|
||||
- Shardok synchronous persistence should be below roughly 20-30 ms p95 to stay on the click-to-animation path.
|
||||
- If Shardok sync writes are above that, use an async Shardok persistence design rather than blocking player feedback.
|
||||
|
||||
## Phase 2: Schema Prototype
|
||||
|
||||
Add Postgres configuration and a prototype schema that maps the current SQLite history model.
|
||||
|
||||
Expected tables:
|
||||
|
||||
- `games`, for game-level metadata and backend bookkeeping
|
||||
- `action_results`, for ordered strategic action results
|
||||
- per-entity decomposition tables currently written by the SQLite dispatchers
|
||||
- `state_snapshots`, for replay anchors
|
||||
- Shardok battle result tables, including battle id, sequence number, command/result payloads, and player metadata
|
||||
- `schema_migrations`, unless the migration mechanism is handled outside the app
|
||||
|
||||
Prefer batched inserts and batched reads. The current SQLite implementation can afford some local-file patterns that will
|
||||
be too chatty over a network.
|
||||
|
||||
## Phase 3: Implement Postgres History
|
||||
|
||||
Implement a `FullGameHistory` backend backed by Postgres. Keep the existing SQLite backend available behind a config flag
|
||||
during rollout.
|
||||
|
||||
Core methods to support:
|
||||
|
||||
- `count`
|
||||
- `last`
|
||||
- `withNewResults`
|
||||
- `stateAfter`
|
||||
- `since`
|
||||
- `sinceDate`
|
||||
- round-local recent-result queries
|
||||
- Shardok result writes and reads
|
||||
|
||||
The implementation should preserve action ordering and idempotency. If retries are possible, writes should include stable
|
||||
keys or sequence ranges so replaying a write request cannot duplicate results.
|
||||
|
||||
## Phase 4: Decide Shardok Durability Mode
|
||||
|
||||
Shardok latency is the place where this migration can most visibly hurt the player. There are two viable modes.
|
||||
|
||||
### Option A: Synchronous Shardok Writes
|
||||
|
||||
Use this if measured Postgres latency is low enough. The command path writes results to Postgres before telling the client
|
||||
about them.
|
||||
|
||||
This is the simplest durability model, but only acceptable if it keeps click-to-animation latency low.
|
||||
|
||||
### Option B: Async Shardok Writes
|
||||
|
||||
Use this if synchronous Postgres writes add noticeable latency.
|
||||
|
||||
The model:
|
||||
|
||||
- apply battle results to authoritative in-memory game state immediately
|
||||
- send client updates immediately after the ordered in-memory apply
|
||||
- enqueue ordered persistence batches in the same order they were applied
|
||||
- persist batches with `battle_id`, sequence range, payload hash, and payload
|
||||
- track `lastAppliedSeq` and `lastPersistedSeq`
|
||||
- retry failed writes until they succeed
|
||||
- alert when persistence lag exceeds a small threshold
|
||||
- block or degrade new commands if the queue grows too large
|
||||
- force a synchronous checkpoint before battle end reconciliation or strategic-layer handoff
|
||||
- drain the queue during graceful shutdown
|
||||
|
||||
This creates a small crash-loss window, but only for rare crashes that happen after the player sees a result and before
|
||||
the async write completes. That tradeoff may be better than adding network persistence latency to every tactical command.
|
||||
|
||||
## Phase 5: Rollout
|
||||
|
||||
Add a backend flag such as:
|
||||
|
||||
```text
|
||||
EAGLE_HISTORY_BACKEND=sqlite|postgres
|
||||
```
|
||||
|
||||
Suggested rollout:
|
||||
|
||||
1. Run benchmarks from the deployed environment.
|
||||
2. Enable Postgres in staging for new games.
|
||||
3. Run warmup games and Shardok battle smoke tests.
|
||||
4. Enable Postgres for new production games.
|
||||
5. Migrate old games either lazily on first open or through an admin-triggered migration job.
|
||||
6. Keep SQLite/S3 rollback available until enough new-game production time has passed.
|
||||
|
||||
If Postgres is unavailable, commands should not be acknowledged as durable. Either hold/retry them before applying, or
|
||||
reject them clearly. Do not show a result to the client while pretending it has already been durably stored unless that is
|
||||
an explicit async-durability path with lag monitoring.
|
||||
|
||||
## Phase 6: Cleanup
|
||||
|
||||
After production confidence:
|
||||
|
||||
- remove production dependence on S3 base/delta history files
|
||||
- keep object storage for backups, exports, or emergency archives
|
||||
- keep SQLite for local development and tests if it remains useful
|
||||
- document restore, migration, and rollback procedures
|
||||
|
||||
## Open Questions
|
||||
|
||||
- What exact durability guarantee do we want for "the client already saw this result"?
|
||||
- What Shardok persistence latency is acceptable in real play?
|
||||
- Should old games migrate lazily or through a bulk/admin process?
|
||||
- When should the production database move from single-node to HA?
|
||||
- What should the player experience be if Postgres is reachable but slow?
|
||||
|
||||
## Recommended Next PR
|
||||
|
||||
The next implementation PR should add a Postgres benchmark/admin utility and configuration plumbing, not the full port.
|
||||
That gives us real latency numbers from the Eagle deployment environment before committing to the larger migration.
|
||||
@@ -486,7 +486,7 @@ jobs:
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
@@ -517,7 +517,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: []
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
lfs: false
|
||||
|
||||
@@ -532,7 +532,7 @@ jobs:
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: ~/.cache/bazel
|
||||
key: bazel-linux-${{ hashFiles('MODULE.bazel', 'MODULE.bazel.lock', 'WORKSPACE') }}
|
||||
key: bazel-linux-${{ hashFiles('MODULE.bazel', 'WORKSPACE') }}
|
||||
|
||||
- name: Build Shardok server
|
||||
run: bazel build -c opt //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
@@ -555,7 +555,7 @@ jobs:
|
||||
needs: [build-eagle, build-shardok]
|
||||
environment: production
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Deploy to DigitalOcean
|
||||
uses: appleboy/ssh-action@v1.0.0
|
||||
|
||||
@@ -136,7 +136,7 @@ Create a tool to programmatically regenerate the Eagle strategic map images with
|
||||
| `Assets/Eagle/Materials/*.png` | Replaced province masks |
|
||||
| `Assets/Eagle/map_bw_labels.png` | Either: regenerated, or removed if using dynamic labels |
|
||||
| `province_map.tsv` | Add centroid columns |
|
||||
| `Assets/Scenes/Eagle.unity` | Add ProvinceLabelsController, label prefab instances |
|
||||
| `Assets/Gameplay.unity` | Add ProvinceLabelsController, label prefab instances |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,77 +0,0 @@
|
||||
# Shardok AI Experiment Tooling Plan
|
||||
|
||||
## Summary
|
||||
|
||||
Build a reusable workflow for Shardok AI battle baselines and scoring experiments. The goal is to
|
||||
turn the current one-off benchmark process into repeatable commands that extract real battles, run
|
||||
baseline and candidate configurations, capture detailed per-command traces, and write comparable
|
||||
reports.
|
||||
|
||||
This work starts with the target-aware archery scoring experiment: attacker `EXPERIMENTAL` against
|
||||
defender `STANDARD`.
|
||||
|
||||
## Key Changes
|
||||
|
||||
- Add simulator trace support to `ai_battle_simulator_main`.
|
||||
- New flag: `--trace-jsonl=<path>`.
|
||||
- Emit one JSONL row per AI-selected command.
|
||||
- Include run label, config path, state file, phase, sequence, round, player, side, command type,
|
||||
actor unit, target coords, chosen index, available command count, search depth, commands
|
||||
evaluated, completion reason, forced commands posted afterward, and pre/post side troop totals.
|
||||
- Keep existing summary output unchanged.
|
||||
|
||||
- Add reusable benchmark tooling under
|
||||
`src/main/cpp/net/eagle0/shardok/ai_battle_simulator/benchmarks/`.
|
||||
- `real_battle_suite.py`: shared helpers for reading `game.db`, materializing battle/state/result
|
||||
blobs, extracting configs, running simulator binaries, parsing summaries, and collecting
|
||||
real-game metadata.
|
||||
- `run_real_battle_experiment.py`: CLI orchestration for baseline/candidate runs.
|
||||
- Outputs: `summary.csv`, `comparison.csv`, `report.md`, generated configs, and per-run trace
|
||||
JSONL files.
|
||||
|
||||
- Add a clean EXPERIMENTAL switch for target-aware archery value.
|
||||
- Use a new experiment id.
|
||||
- Change only archery-availability scoring.
|
||||
- Replace the flat archery-possible value with a target-sensitive value based on the best enemy
|
||||
unit value.
|
||||
- Do not include the previous fear-value change from experiment `27`.
|
||||
|
||||
## Execution Plan
|
||||
|
||||
- Create a feature branch from fetched `origin/main`.
|
||||
- Commit this plan document first.
|
||||
- Implement simulator trace output and reusable benchmark tooling.
|
||||
- Add the target-aware archery-only EXPERIMENTAL switch.
|
||||
- Run the new tool first on battle `6248`, then on the all-40 saved-battle suite if local runtime is
|
||||
acceptable.
|
||||
- Compare baseline attacker `STANDARD` / defender `STANDARD` with candidate attacker
|
||||
`EXPERIMENTAL` / defender `STANDARD`.
|
||||
- Record the results under the existing benchmark results area and call out battle `6248` in the
|
||||
generated report.
|
||||
- Push the branch and create a PR after local validation.
|
||||
|
||||
## Tests
|
||||
|
||||
- Build:
|
||||
- `bazel build //src/main/cpp/net/eagle0/shardok/ai_battle_simulator:ai_battle_simulator_main`
|
||||
- `bazel build //src/main/cpp/net/eagle0/shardok/ai_battle_simulator:real_battle_config_extractor`
|
||||
- `bazel build //src/main/cpp/net/eagle0/shardok/ai_battle_simulator:real_battle_smoke_metadata`
|
||||
- Unit/integration:
|
||||
- `bazel test //src/test/cpp/net/eagle0/shardok/ai_battle_simulator:ai_battle_simulator_test`
|
||||
- Add or extend a simulator test that verifies trace output is created and contains required
|
||||
command fields.
|
||||
- `python3 -m py_compile` on the new benchmark scripts.
|
||||
- Validation:
|
||||
- Run the new tool on battle `6248`.
|
||||
- Run the new tool on all 40 battles if feasible locally.
|
||||
- Run `bazel test //src/test/cpp/...` before push if local time allows; otherwise open the PR
|
||||
promptly and continue validation while CI starts.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- "Experiment 3" means target-aware archery value from the previous recommendation list.
|
||||
- The first experiment compares attacker-only candidate behavior against unchanged defender
|
||||
`STANDARD`.
|
||||
- JSONL is the trace format because command diagnostics will evolve over time.
|
||||
- Python benchmark scripts follow the existing executable-script pattern rather than adding Python
|
||||
Bazel targets.
|
||||
@@ -1,82 +0,0 @@
|
||||
# Shardok AI Scoring Benchmarks
|
||||
|
||||
The current STANDARD scoring experiment results are recorded in
|
||||
`src/main/cpp/net/eagle0/shardok/ai_battle_simulator/benchmarks/experimental_results/`.
|
||||
They use 40 saved Shardok battles extracted from `game_389ffb1901903118.zip`.
|
||||
|
||||
The corrected all-40 run uses:
|
||||
|
||||
- `max_rounds = 31`
|
||||
- `random_seed = 1`
|
||||
- iterative-deepening timeout propagation fix from PR #6923
|
||||
- AI setup placement
|
||||
- forced single-option commands
|
||||
- defender scoring fixed to `STANDARD` for every row
|
||||
- archery/start-fire capability derived like production setup
|
||||
- per-unit flee capability preserved by real-battle extraction
|
||||
- zero-troop combat heroes preserved as live units
|
||||
- `NO_PROFESSION` treated as a valid hero profession state, not as "no hero"
|
||||
|
||||
Shardok siege battles do not have a draw result. If the attacker has not won after 31 rounds, the
|
||||
defender wins. The simulator records those cutoffs as `max_rounds_reached` defender wins.
|
||||
|
||||
All 40 saved battle payloads now produce valid simulator configs. A battle side can have zero
|
||||
remaining troops as long as it has a hero; this is common for `NO_PROFESSION` heroes with no
|
||||
commanded battalion.
|
||||
|
||||
An all-40 real-vs-sim comparison records whether the saved-game attacker/defender were human or AI
|
||||
players. Factions `3` and `4` are human players; all other factions in this save are AI players.
|
||||
The comparison uses broad winner side from the saved Shardok result payload, because exact replay
|
||||
is not available from the compact result rows. Real end-of-battle troop totals come from the
|
||||
all-factions Shardok `ActionResultView` stream's final `changed_player_totals`.
|
||||
|
||||
An AI-vs-AI smoke check also reruns a subset where neither side used faction `3` or `4`.
|
||||
|
||||
## Current Results
|
||||
|
||||
These artifacts are the current baseline for future scoring comparisons.
|
||||
|
||||
| Dataset | Valid battles | Invalid rows | Attacker wins | Defender wins | Notes |
|
||||
| --- | ---: | ---: | ---: | ---: | --- |
|
||||
| All-40 timeout-fix STANDARD baseline | 40 | 0 | 18 | 22 | `standard_scoring_all40_post_bugfix_baseline.*`; regenerated after fixing iterative-deepening timeout score propagation |
|
||||
| All-40 real saved game | 40 known | 0 unknown | 19 | 21 | `standard_scoring_all40_real_vs_sim.*`; sim matches 39/40 known winners |
|
||||
| AI-vs-AI smoke | 31 | 0 | 10 | 21 | 31/31 winner-side matches |
|
||||
|
||||
The remaining real-vs-sim winner-side miss is `11754` (real attacker, sim defender), which had an
|
||||
AI attacker but a human defender in the saved game. The real attacker won with `2119` troops
|
||||
remaining; the STANDARD-vs-STANDARD simulation replaces the human defender with AI and the defender
|
||||
wins with `1000` troops remaining, so this row is not a comparable AI-vs-AI smoke miss. Battle
|
||||
`6248`, previously the remaining AI-vs-AI miss, now matches as an attacker win after expected-impact
|
||||
archery scoring was promoted to STANDARD.
|
||||
|
||||
The closest six valid baseline rows are used for the scalar and behavior experiment batches. The
|
||||
single-run harness still has some nondeterminism even with `random_seed = 1`, so near-margin changes
|
||||
should be repeated before being promoted.
|
||||
|
||||
## Experiment Tooling
|
||||
|
||||
`src/main/cpp/net/eagle0/shardok/ai_battle_simulator/benchmarks/run_real_battle_experiment.py`
|
||||
is the reusable real-battle experiment runner. It can read a saved-game zip or extracted `game.db`,
|
||||
generate baseline/candidate simulator configs, run both sides, and write `summary.csv`,
|
||||
`comparison.csv`, `report.md`, generated configs, and per-command trace JSONL files.
|
||||
|
||||
The repository keeps the current STANDARD baseline artifacts, but omits generated configs/traces
|
||||
for abandoned experiment batches. Regenerate those locally with the runner when a new comparison
|
||||
needs command-level traces.
|
||||
|
||||
`src/main/cpp/net/eagle0/shardok/ai_battle_simulator/benchmarks/run_standard_baselines.py`
|
||||
regenerates the tracked STANDARD-vs-STANDARD baseline artifacts from a saved-game zip or extracted
|
||||
`game.db`, including the all-40 baseline, real-vs-sim comparison, and AI-vs-AI smoke files.
|
||||
|
||||
The simulator supports `--trace-jsonl=<path>` and `--run-label=<label>` for detailed command traces.
|
||||
Each row records the selected command, search depth, forced command count, and pre/post side troop
|
||||
totals.
|
||||
|
||||
Expected-impact archery replaced the old flat archery-position value in STANDARD. A unit that can
|
||||
shoot now gets half of the expected volley impact; a unit that is merely positioned for a future
|
||||
volley gets one quarter. Promoting that behavior flips `6248` to match the real AI-vs-AI attacker
|
||||
win without introducing an AI-vs-AI winner-side miss in the current saved-battle baseline.
|
||||
|
||||
The latest baseline refresh was generated after the iterative-deepening evaluator stopped letting
|
||||
timeout fallback scores compete with completed command evaluations. Future STANDARD-vs-EXPERIMENTAL
|
||||
comparisons should compare against these checked-in timeout-fix baseline artifacts.
|
||||
@@ -6,16 +6,6 @@ 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.
|
||||
|
||||
## Current priority order
|
||||
|
||||
1. Finish tutorial & first-session onboarding, especially the clear first-session goal and guided first scenario.
|
||||
2. Fix the basic Shardok AI issues that can make tactical battles feel broken or unfair.
|
||||
3. Publish a known issues doc so alpha testers do not repeatedly report the same rough edges.
|
||||
4. Verify the existing game-ended flow, then mark the win condition done if "all other factions defeated" works end-to-end.
|
||||
5. Add mid-game progression events where the King recognizes the player as they gain power.
|
||||
6. Add account-linking when provider switching or account recovery becomes important for alpha support.
|
||||
7. Leave terms of service and Windows code signing deferred until they matter for public release or broader distribution.
|
||||
|
||||
## Required
|
||||
|
||||
### Gameplay Productionization
|
||||
@@ -27,7 +17,7 @@ Larger set of goals in [The Big Eagle TODO](https://docs.google.com/document/d/1
|
||||
- [x] ~~Fix long disconnects on deployments~~
|
||||
- [x] Fix the Mac installer
|
||||
- [x] ~~Still not reconnecting after deployments~~
|
||||
- [x] Notify about client updates, button to come directly back
|
||||
- [ ] Notify about client updates, button to come directly back
|
||||
- [x] Generatedtext healing
|
||||
- [x] Kill outstanding shardok requests when game is deleted
|
||||
|
||||
@@ -61,7 +51,7 @@ Larger set of goals in [The Big Eagle TODO](https://docs.google.com/document/d/1
|
||||
|
||||
### Basic Gameplay
|
||||
|
||||
- [ ] Tutorial & first-session onboarding
|
||||
- [ ] Tutorial
|
||||
- [x] 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~~
|
||||
@@ -72,16 +62,17 @@ Larger set of goals in [The Big Eagle TODO](https://docs.google.com/document/d/1
|
||||
- [x] ~~Time to swear brotherhood~~
|
||||
- [x] ~~When you get large, or~~
|
||||
- [x] ~~When you get a good candidate~~
|
||||
- [x] ~~Shardok tutorial!~~
|
||||
- [x] ~~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)
|
||||
- [x] ~~Early small victory to build momentum~~
|
||||
- [ ] Guided first scenario vs. overwhelming sandbox?
|
||||
- [ ] Shardok tutorial!
|
||||
- [ ] Basic Shardok AI stuff fixed
|
||||
- [x] ~~Lobby fixes~~
|
||||
- [ ] Lobby fixes
|
||||
- [ ] Have goals / ending
|
||||
- [ ] Win condition: all other factions defeated
|
||||
- [ ] Mid-game progression: King recognizes you as you gain power (generated events)
|
||||
- [ ] 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
|
||||
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
# SQLite Action Result Decomposition
|
||||
|
||||
Follow-up to [SQLITE_HISTORY_DESIGN.md](SQLITE_HISTORY_DESIGN.md). This document plans the second wave of the SQLite migration: decomposing the `ActionResult` proto blob in `action_results.payload` into native relational tables, deleting the proto representation entirely for everything in scope.
|
||||
|
||||
## Status — option B adopted (updated 2026-05-17)
|
||||
|
||||
**This section supersedes the original plan below. Where the body conflicts with this section, this section wins.** The full-relational design is kept below for historical context and as the migration target if a structured-query need on deep aggregates ever materializes.
|
||||
|
||||
The original plan (decompose every field into relational tables; "decision locked") was implemented for `ChangedProvince` first — ~37 child tables, ~1500 lines (PR #6721) — and then **rejected on cost/benefit**. We adopted **option B**: apply the "stopping rule" (originally carved out only for `Quest`) *consistently*. Shipped and merged as Phase 4.5b in **PR #6725**:
|
||||
|
||||
- Keep the per-entity **top-level table with scalar / entity-reference columns + indexes** (the 4.5a tables). That is the part that earns its keep — "which actions changed province/hero/faction X, by how much" is one-line SQL.
|
||||
- Store the **rest of the aggregate as one `<entity>_proto BLOB` column** on that row: lossless round-trip / replay, no child tables for the deep nesting.
|
||||
- Scalar columns are pure query denormalization; **the proto blob is authoritative**. A later fully-relational pass (if ever needed) is a pure read-blob → write-columns migration — which is why the blob must survive any future proto deletion.
|
||||
|
||||
Rationale: the deep army/unit/hero/claimant nesting is never analytics-queried — the exact property that justified blobbing `Quest`. Exploding it into ~37 tables was code volume without query value. The blob also means deep-aggregate field changes need **no SQL migration** (only promoted scalar columns do), which is the right shape for the volatile parts of the model. This rescinds "Decisions locked #1" and revises principle 7, the Scope, the Sequencing table, and "Deleting the proto" (all updated in place below).
|
||||
|
||||
## Refinement — "refined option B" for shallow entities (updated 2026-05-19)
|
||||
|
||||
**This section further supersedes the body and the Status section above where they conflict; it wins.** It records the pattern actually shipped for `ChangedHero` in Phase 4.5c.1 (#6727), which is a refinement of option B, not the whole-entity-blob shape the body still describes for that entity.
|
||||
|
||||
Option B as originally stated keeps **one `<entity>_proto BLOB`** per row for the entire non-scalar residual. For a *shallow* entity that is more blob than it needs. Refined option B decomposes the residual into three tiers instead of one blob:
|
||||
|
||||
- **Tier 1 — scalar / entity-reference columns + indexes.** The queryable surface (deltas, touched entities, enum ordinals). Always present; unchanged from 4.5a.
|
||||
- **Tier 2 — typed columns for sealed value-oneofs.** A sealed choice over simple values (e.g. `ChangedHeroC`'s `loyalty` / `vigor` `StatChange`, subtypes `StatDelta`/`StatAbsolute`/`StatNoChange`) becomes a **pair of nullable columns** (`loyalty_delta` / `loyalty_absolute`, …). The *column name is the discriminator*; at most one of each pair is non-NULL; both NULL ⇒ the no-change case. No `kind` column.
|
||||
- **Tier 3 — per-row leaf-proto blobs in a child table.** A repeated/optional nested aggregate becomes a child table keyed by `(action_seq, entity_id, n)` storing the **leaf** proto's bytes one row per element (e.g. `action_changed_heroes_new_backstory_events` holding `EventForHeroBackstory` blobs) — *not* a whole-entity blob.
|
||||
|
||||
**Consequence (changes Decisions #1–#3 and 4.5i):** an entity fully covered by Tiers 1–3 has **no whole-entity blob**. It round-trips losslessly from columns + per-leaf blobs, so its per-entity proto and converter (`changed_hero.proto` / `ChangedHeroConverter`) become genuinely dead code, deletable in 4.5i. Only the **leaf** protos used as Tier-3 blob payloads (`event_for_hero_backstory.proto`) are retained as a wire format. The whole-entity `<entity>_proto BLOB` is now reserved for **deep** entities where Tiers 2–3 would be more work than value (`ChangedProvince`, the "new entity" snapshots).
|
||||
|
||||
Per-entity decision (supersedes Decisions #2): classify each entity as **all-scalar** (Tier 1 only, no blob — e.g. likely `ChangedFaction`), **refined** (Tiers 1–3, no whole-entity blob, proto deletable — `ChangedHero`), or **deep** (Tier 1 + whole-entity `<entity>_proto BLOB`, proto retained — `ChangedProvince`, new-entity snapshots). Default to **refined** for shallow entities; fall back to **deep** only when the residual is too varied to be worth Tiers 2–3. Wherever the body or Status says "the proto blob is authoritative," for a *refined* entity read "the columns + per-leaf-proto blobs are authoritative."
|
||||
|
||||
## Why
|
||||
|
||||
The first wave (Phases 1–4) stores each action as a single proto blob in `action_results.payload`. That blob is opaque to SQL — any query about action contents (changed provinces, heroes affected, etc.) requires deserializing every row. The `action_result_type`, `round_id`, and date columns we denormalized are the only handles SQL has into actions.
|
||||
|
||||
Two observations make full decomposition more attractive than the original design admitted:
|
||||
|
||||
1. **The proto exists for storage only.** 92 Scala files reference `ChangedProvince`, but exactly one imports the proto type — `ChangedProvinceConverter.scala`. Everything else (action handlers, AI, utils) operates on `ChangedProvinceC`. The proto is **not** in any client-facing RPC contract — it lives under `eagle.internal`. Same is true for `ChangedHero`, `ChangedFaction`, `ChangedBattalion`, `Notification`, `GeneratedTextRequest`, and `ActionResult` itself. Replacing proto serialization with SQL writes does **not** lose forward/backward compatibility, because there are no external consumers depending on the proto wire format.
|
||||
|
||||
2. **`ActionResultC` has no polymorphism at the top level.** It's a 34-field flat struct — `Option`s and `Vector`s, but no `sealed trait` discriminator at the action level. Every action has the same shape; the `actionResultType` enum tells you *which fields* are populated. This means the entire structure is relationalizable without per-action-type tables.
|
||||
|
||||
The work is bounded, the proto becomes dead code at the end of it, and it has to happen before alpha — once we have users, every schema change costs a real migration. Doing it pre-alpha lets us nuke save dirs as part of deployment.
|
||||
|
||||
## Scope
|
||||
|
||||
**In:** `action_results.payload` (the whole-`ActionResult` blob) is gone by the end. Per-entity scalar / entity-reference columns make the *queryable* surface (deltas, touched entities) native SQL. **Under refined option B (see Status):** `internal/action_result.proto` and *deep* entities' sub-message protos (`changed_province.proto`, …) are retained as blob payloads; *refined* entities' protos (`changed_hero.proto`) are decomposed away and deleted, with only their Tier-3 leaf protos (`event_for_hero_backstory.proto`) kept. (The original plan deleted all of them, then a draft of option B kept all of them; neither holds — it's now per-entity.)
|
||||
|
||||
**Out:**
|
||||
- `state_snapshots.payload` — stays as proto-blob `GameState`. These are caches for replay, not authoritative data; querying inside a snapshot adds nothing over querying the action deltas that produced it. Cost/benefit doesn't justify decomposing them.
|
||||
- Shardok tables — already designed in Phase 3, no changes here. The shardok protos (`ShardokActionResult`, etc.) are owned by the Shardok subsystem and are part of an actual RPC contract; they stay.
|
||||
- Client text — already SQLite-backed (`SqliteClientTextStore`).
|
||||
|
||||
## Principles
|
||||
|
||||
1. **One table per non-trivial aggregate type.** Every `Vector[X]` or `Option[X]` where `X` is a Scala case class with multiple fields gets its own table.
|
||||
2. **Vectors of primitives → join tables.** `Vector[Int]` becomes a two-column table: `(parent_key, value)`. No JSON arrays, no comma-separated strings — those defeat the point.
|
||||
3. **Optional aggregates → presence by row existence.** `Option[X]` for aggregate `X` is a row that may or may not exist with the matching parent key. The natural primary key on `(action_seq)` enforces 0-or-1-row semantics for `Option[X]` aggregates and `(action_seq, n)` for vectors.
|
||||
4. **Sealed traits get a discriminator column.** When a Scala sealed trait has a few subtypes with diverging fields, prefer one wide table with `kind TEXT NOT NULL` + nullable per-subtype columns over per-subtype tables. When subtypes diverge significantly (>5 unrelated fields each), use per-subtype tables. Decide per case.
|
||||
5. **`ON DELETE CASCADE` everywhere.** Every child table has `FOREIGN KEY (action_seq) REFERENCES action_results(action_seq) ON DELETE CASCADE`. `truncateTo` becomes a single `DELETE FROM action_results WHERE action_seq >= ?` and the cascades do the rest.
|
||||
6. **Schema migrations are now real.** Once we have a relational schema, evolving it means SQL migrations. We introduce a real migration runner (see "Schema evolution" below). The `metadata` table's `schema_version` becomes load-bearing.
|
||||
7. **(Revised under option B — see Status.) The proto blob is the retained authoritative store for each entity's deep aggregates; scalar / entity-reference columns are denormalized projections of it.** The original principle assumed full decomposition and proto deletion. Instead each entity keeps one proto-blob source of truth and denormalizes only the queryable columns from the same write — so there is no dual *relational* representation to keep in sync.
|
||||
|
||||
## Top-level: `action_results`
|
||||
|
||||
The `payload BLOB` column goes away. Scalar fields become columns; aggregate fields move to related tables.
|
||||
|
||||
```sql
|
||||
CREATE TABLE action_results (
|
||||
action_seq INTEGER PRIMARY KEY,
|
||||
action_result_type INTEGER NOT NULL,
|
||||
-- existing scalar denormalizations
|
||||
round_id INTEGER NOT NULL,
|
||||
date_year INTEGER,
|
||||
date_month INTEGER,
|
||||
-- new scalar columns (formerly inside payload)
|
||||
acting_hero_id INTEGER,
|
||||
acting_faction_id INTEGER,
|
||||
province_id INTEGER,
|
||||
province_id_acted INTEGER,
|
||||
new_round_phase INTEGER,
|
||||
new_round_id INTEGER,
|
||||
last_command_type_for_acting_province INTEGER,
|
||||
resolved_battle TEXT,
|
||||
new_victor_faction_id INTEGER,
|
||||
game_ended INTEGER, -- 0/1/NULL
|
||||
new_random_seed INTEGER,
|
||||
new_game_type INTEGER
|
||||
);
|
||||
|
||||
-- Vectors of bare IDs become join tables:
|
||||
CREATE TABLE action_destroyed_battalion_ids (
|
||||
action_seq INTEGER NOT NULL,
|
||||
battalion_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (action_seq, battalion_id),
|
||||
FOREIGN KEY (action_seq) REFERENCES action_results(action_seq) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE action_removed_hero_ids (
|
||||
action_seq INTEGER NOT NULL,
|
||||
hero_id INTEGER NOT NULL,
|
||||
PRIMARY KEY (action_seq, hero_id),
|
||||
FOREIGN KEY (action_seq) REFERENCES action_results(action_seq) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE action_removed_faction_ids (...);
|
||||
CREATE TABLE action_affected_faction_ids (...);
|
||||
|
||||
-- Singleton optional aggregates: a row exists if and only if Option is Some.
|
||||
CREATE TABLE action_new_battles (
|
||||
action_seq INTEGER PRIMARY KEY,
|
||||
-- ShardokBattle fields here, columns or sub-tables as needed
|
||||
FOREIGN KEY (action_seq) REFERENCES action_results(action_seq) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE action_new_chronicle_entries (action_seq INTEGER PRIMARY KEY, ...);
|
||||
CREATE TABLE action_new_eagle_map_info (action_seq INTEGER PRIMARY KEY, ...);
|
||||
```
|
||||
|
||||
Each `Vector[Aggregate]` field on `ActionResultC` gets its own table (covered below).
|
||||
|
||||
### Indexes
|
||||
|
||||
We keep the existing indexes (`round_id`, `(date_year, date_month)`) and add a few that obvious queries need:
|
||||
|
||||
- `idx_action_results_acting_hero_id`
|
||||
- `idx_action_results_acting_faction_id`
|
||||
- `idx_action_results_province_id`
|
||||
|
||||
Plus indexes on the per-entity join tables on the entity-id side, so cross-game queries like "all actions touching hero X" become single-index lookups:
|
||||
|
||||
- `idx_action_changed_provinces_province_id`
|
||||
- `idx_action_changed_heroes_hero_id`
|
||||
- `idx_action_changed_faction_faction_id`
|
||||
- `idx_action_changed_battalions_battalion_id`
|
||||
|
||||
## Changed-entity tables (four parallel structures)
|
||||
|
||||
### `action_changed_provinces`
|
||||
|
||||
`ChangedProvinceC` has ~50 fields: scalar deltas, nested aggregates, ID vectors. The scalar fields become columns on `action_changed_provinces`; aggregate fields become child tables.
|
||||
|
||||
```sql
|
||||
CREATE TABLE action_changed_provinces (
|
||||
action_seq INTEGER NOT NULL,
|
||||
province_id INTEGER NOT NULL,
|
||||
-- resource changes
|
||||
gold_delta INTEGER,
|
||||
food_delta INTEGER,
|
||||
-- stat changes
|
||||
new_price_index REAL,
|
||||
economy_delta REAL,
|
||||
agriculture_delta REAL,
|
||||
infrastructure_delta REAL,
|
||||
economy_devastation_delta REAL,
|
||||
agriculture_devastation_delta REAL,
|
||||
infrastructure_devastation_delta REAL,
|
||||
support_delta REAL,
|
||||
-- round properties
|
||||
set_has_acted INTEGER, -- 0/1/NULL
|
||||
set_ruler_is_traveling INTEGER,
|
||||
-- ruling faction changes
|
||||
clear_ruling_faction_id INTEGER NOT NULL DEFAULT 0,
|
||||
new_ruling_faction_id INTEGER,
|
||||
-- misc scalar fields
|
||||
new_locked_improvement_kind TEXT, -- 'none', 'new'
|
||||
new_locked_improvement_value INTEGER, -- ImprovementType if kind='new'
|
||||
new_province_orders INTEGER, -- ProvinceOrderType enum
|
||||
clear_defending_army INTEGER NOT NULL DEFAULT 0,
|
||||
cleared_pending_conquest_info INTEGER NOT NULL DEFAULT 0,
|
||||
removed_deferred_change_index INTEGER,
|
||||
PRIMARY KEY (action_seq, province_id),
|
||||
FOREIGN KEY (action_seq) REFERENCES action_results(action_seq) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_action_changed_provinces_province_id
|
||||
ON action_changed_provinces(province_id);
|
||||
```
|
||||
|
||||
The vector and optional-aggregate fields of `ChangedProvinceC` become per-aggregate child tables, all keyed by `(action_seq, province_id)`:
|
||||
|
||||
- `action_changed_provinces_new_unaffiliated_heroes(action_seq, province_id, n, ...)` — one row per added hero, with `UnaffiliatedHero` fields as columns
|
||||
- `action_changed_provinces_changed_unaffiliated_heroes(...)` — same shape
|
||||
- `action_changed_provinces_removed_unaffiliated_hero_ids(action_seq, province_id, hero_id)` — join table for ID vector
|
||||
- `action_changed_provinces_new_captured_heroes(...)` — `CapturedHero` fields
|
||||
- `action_changed_provinces_recruitment_attempted_captured_hero_ids(...)` — join table
|
||||
- `action_changed_provinces_removed_captured_hero_ids(...)` — join table
|
||||
- `action_changed_provinces_new_ruling_faction_hero_ids(...)` — join table
|
||||
- `action_changed_provinces_removed_ruling_faction_hero_ids(...)` — join table
|
||||
- `action_changed_provinces_new_battalion_ids(...)` — join table
|
||||
- `action_changed_provinces_removed_battalion_ids(...)` — join table
|
||||
- `action_changed_provinces_new_incoming_armies(...)` — `MovingArmy` decomposition
|
||||
- `action_changed_provinces_removed_incoming_army_ids(...)` — join table
|
||||
- `action_changed_provinces_new_withdrawing_armies(...)` — `MovingArmy` decomposition
|
||||
- `action_changed_provinces_removed_withdrawing_army_ids(...)` — join table
|
||||
- `action_changed_provinces_new_hostile_armies(...)` — `HostileArmyGroup` decomposition
|
||||
- `action_changed_provinces_removed_hostile_army_faction_ids(...)` — join table
|
||||
- `action_changed_provinces_hostile_army_status_changes(action_seq, province_id, faction_id, new_status)` — small flat aggregate
|
||||
- `action_changed_provinces_new_defending_army` — `Option[Army]` (singleton row if present)
|
||||
- `action_changed_provinces_new_incoming_shipments(...)` — `MovingSupplies` decomposition
|
||||
- `action_changed_provinces_removed_incoming_shipment_ids(...)` — join table
|
||||
- `action_changed_provinces_new_incoming_end_turn_actions(...)` — `IncomingEndTurnAction` decomposition
|
||||
- `action_changed_provinces_removed_incoming_end_turn_actions(...)` — same shape (these are values, not IDs)
|
||||
- `action_changed_provinces_new_battle_revelations(...)` — `BattleRevelation` decomposition
|
||||
- `action_changed_provinces_removed_battle_revelations(...)` — same shape
|
||||
- `action_changed_provinces_new_pending_conquest_info` — `Option[PendingConquestInfo]` (singleton row)
|
||||
- `action_changed_provinces_new_province_events` — `Vector[ProvinceEvent]`
|
||||
- `action_changed_provinces_new_deferred_change` — `Option[DeferredChange]` (singleton, with discriminator since `DeferredChangeT` is sealed)
|
||||
|
||||
That's a lot of tables — ~25 just under `ChangedProvince`. The schema is mechanical once the principles are set; what makes it tractable is that every table follows the same shape: parent FK, `n` (vector index) where ordering matters, the aggregate's fields as columns, child aggregates as their own tables.
|
||||
|
||||
### `action_changed_heroes`, `action_changed_factions`, `action_changed_battalions`
|
||||
|
||||
Same pattern. Each gets its top-level table keyed by `(action_seq, entity_id)`, plus child tables for nested aggregates and ID vectors. Indexes on the entity id (`hero_id`, `faction_id`, `battalion_id`) so cross-game queries are fast.
|
||||
|
||||
`ChangedHeroC` is a *refined* entity (see "Refinement" in Status): no whole-entity blob. Its sealed `StatChange` (`StatDelta`/`StatAbsolute`/`StatNoChange`) `loyalty` and `vigor` oneofs are Tier-2 column pairs (`loyalty_delta`/`loyalty_absolute`, `vigor_delta`/`vigor_absolute`; column name discriminates, both-NULL ⇒ `StatNoChange`). Its `newBackstoryEvents: Vector[EventForHeroBackstory]` is Tier-3: per-event `EventForHeroBackstory` blobs, one row each, in `action_changed_heroes_new_backstory_events`. Columns + per-event blobs round-trip losslessly, so `changed_hero.proto`/`ChangedHeroConverter` become deletable (4.5i) and only `event_for_hero_backstory.proto` is retained as a blob format.
|
||||
|
||||
## "New" entity tables
|
||||
|
||||
`ActionResultC.newProvinces: Vector[ProvinceT]`, `newHeroes`, `newFactions`, `newBattalions`. These are *full snapshots* of new entities at creation.
|
||||
|
||||
These are bigger and recursive: `ProvinceT` itself has fields including its armies, heroes, battalions, events, etc. Decomposing fully duplicates the structure already covered by the existing `state_snapshots` blob (which we're explicitly keeping). The decomposition still needs to happen because we're dropping the proto entirely — and these vectors live in `ActionResultC`, which we're decomposing.
|
||||
|
||||
**Decision**: go fully relational here too. Each "new entity" gets its top-level table (`action_new_provinces` etc.) keyed by `(action_seq, entity_id)`, with the same per-aggregate-field decomposition pattern as the changed-entity tables. Where a "new entity" type contains its own nested aggregates, those aggregates either share child tables with the `changed_*` family (preferred where the field shape is identical, e.g., `MovingArmy`, `UnaffiliatedHero`) or get their own (where they diverge).
|
||||
|
||||
This is the most expensive single chunk of the decomposition — explicitly called out so we know what we're committing to in PR 4.5f.
|
||||
|
||||
## Notifications and generated text requests
|
||||
|
||||
```sql
|
||||
CREATE TABLE action_new_notifications (action_seq INTEGER NOT NULL, n INTEGER NOT NULL, ...);
|
||||
CREATE TABLE action_removed_notifications (action_seq INTEGER NOT NULL, n INTEGER NOT NULL, ...);
|
||||
CREATE TABLE action_new_generated_text_requests (...);
|
||||
CREATE TABLE action_client_text_visibility_extensions (...);
|
||||
```
|
||||
|
||||
`NotificationT` and `GeneratedTextRequestT` are both sealed traits. Use the discriminator-column pattern: `kind TEXT NOT NULL` plus nullable per-subtype columns, unless a subtype's fields are too divergent to share a row.
|
||||
|
||||
## Schema evolution
|
||||
|
||||
We can't punt on this any more. With this many tables, the next field addition can't be "edit the proto." It needs a real migration mechanism.
|
||||
|
||||
Conventions:
|
||||
- `metadata['schema_version']` is a small integer, currently `1`.
|
||||
- A `Migrations` object holds an ordered `Vector[(Int, Connection => Unit)]` of migration steps.
|
||||
- On `SqliteHistory.loaded`, the schema version is read; any pending migrations are applied in a single transaction; the version is bumped.
|
||||
- New games start at the latest version.
|
||||
- Migration steps are append-only — never edit an existing step.
|
||||
|
||||
This is the standard Rails / Flyway / etc. pattern, scaled down to one file.
|
||||
|
||||
## Read path: reconstructing `ActionResultC`
|
||||
|
||||
The current pattern:
|
||||
```scala
|
||||
val proto = ActionResult.parseFrom(payloadBlob)
|
||||
val scala = ActionResultProtoConverter.fromProto(proto)
|
||||
```
|
||||
|
||||
The new pattern:
|
||||
```scala
|
||||
val scala = ActionResultDbReader.read(connection, actionSeq) // assembles from action_results + child tables
|
||||
```
|
||||
|
||||
`ActionResultDbReader.read` is one query per child table (or a single multi-result query with carefully ordered joins, though that has aggregation pitfalls). Per-table queries are simpler to write and SQLite query planning handles them well.
|
||||
|
||||
For bulk reads (`since`, `all`, `sinceDate`, etc.), the existing `replayFrom` helper iterates action seqs in order, and currently parses one proto per action. The new equivalent runs one batched query per child table for the relevant action_seq range, materializing rows into a map keyed by action_seq, then walks the range emitting `ActionResultC` instances assembled from the maps. This is more code than the proto version but doesn't change the algorithmic shape.
|
||||
|
||||
## Write path
|
||||
|
||||
`withNewResults` becomes:
|
||||
1. INSERT into `action_results` (scalar fields).
|
||||
2. For each non-empty aggregate: INSERT N rows into the corresponding child table.
|
||||
|
||||
All inside a single transaction. Per-action insert count is bounded by what the action does — most actions touch 0–3 provinces, 0–5 heroes, 0–10 changed entities total, plus 0–2 notifications. So the typical insert is ~5–20 rows. With WAL and a single per-batch transaction, this is cheap.
|
||||
|
||||
## Replay performance
|
||||
|
||||
Today's `replayFrom` parses one proto per action. Decomposed, each action requires per-child-table queries. For a 25-action replay between snapshots, that's still small absolute work — maybe 100–500 rows fetched from a handful of indexed tables. Should be faster than proto parsing once the JIT warms up, but won't matter either way at typical sizes.
|
||||
|
||||
## Deleting the proto
|
||||
|
||||
> **Superseded by refined option B (see Status).** Whole-`ActionResult` `payload` is removed in 4.5h. After that it depends on per-entity classification: *refined* entities' per-entity sub-message protos + converters (e.g. `changed_hero.proto`) **do** become dead code and are deleted in 4.5i; *deep* entities' whole-entity blob protos, the leaf protos used as Tier-3 blob payloads (e.g. `event_for_hero_backstory.proto`), and `Quest` are *retained*. The original full-deletion plan below is kept for historical context.
|
||||
|
||||
Once `SqliteHistory` writes and reads exclusively from the new tables:
|
||||
|
||||
1. Delete `ActionResultProtoConverter.scala`, the `Changed{Province,Hero,Faction,Battalion}Converter.scala` files, the `Notification`/`GeneratedTextRequest`/etc. converters.
|
||||
2. Delete the proto definitions: `action_result.proto`, `changed_province.proto`, `changed_hero.proto`, `changed_faction.proto`, `notification.proto`, `generated_text_request.proto`, anything else in this scope.
|
||||
3. Delete the corresponding `_scala_proto` Bazel targets.
|
||||
4. Gazelle should drop the unused deps from downstream targets.
|
||||
|
||||
This is satisfying but should land *last*, after everything's stable. The proto files are dead code at that point — keeping them around for a PR or two while we verify migration is fine.
|
||||
|
||||
## Sequencing
|
||||
|
||||
**Phase 4** (cutover) lands as planned: `GamesManager` swaps to `SqliteHistory`, save dirs get nuked, action data is the single proto-blob `payload`. Days of work, validates the lifecycle integration without conflating with schema design.
|
||||
|
||||
**Phase 4.5** (this design) starts after Phase 4 is verified stable. Subdivided into small PRs. **Re-planned for option B (see Status):** each entity gets the 4.5a scalar/entity-ref columns + a `<entity>_proto BLOB`; no child-table explosion. Each decomposition PR splits into `.1` (schema + backfill over existing `payload`) and `.2` (live `withNewResults` write path + read path), per the precedent set by 4.5b.
|
||||
|
||||
| PR | Work | State |
|
||||
|---|---|---|
|
||||
| 4.5a | Schema-migration scaffolding + four changed-entity top-level tables (scalar columns) | ✅ merged |
|
||||
| 4.5b | `ChangedProvince`: scalar columns + `changed_province_proto BLOB` + v3 backfill (option B, #6725) | ✅ merged |
|
||||
| 4.5c.1 | `ChangedHero` (**refined option B**, see Status): 4.5a scalars + loyalty/vigor Tier-2 column pairs + `newBackstoryEvents` as per-event `EventForHeroBackstory` Tier-3 blobs in `action_changed_heroes_new_backstory_events`; **no whole-entity blob** — `changed_hero.proto`/`ChangedHeroConverter` become deletable, `event_for_hero_backstory.proto` retained. Schema v4 + backfill over existing `payload` (#6727) | ✅ merged |
|
||||
| 4.5c.2 | `ChangedHero`: live `withNewResults` write path + read path | planned |
|
||||
| 4.5d | `ChangedFaction`: same pattern (mostly scalar already; small/empty blob — see Decisions #2) | planned |
|
||||
| 4.5e | `ChangedBattalion`: `changed_battalion_proto BLOB` (sole field is the contained `BattalionT`) + battalion_id index from 4.5a | planned |
|
||||
| 4.5f | "New entity" vectors (`newProvinces`/`newHeroes`/`newFactions`/`newBattalions`): one table per type keyed by `(action_seq, entity_id)` + a couple of queryable handles + `<entity>_proto BLOB`. Biggest win for B — these are the deepest snapshots | planned |
|
||||
| 4.5g | Notifications + generated text requests: `(action_seq, n, kind, <entity>_proto BLOB)` — `kind` discriminator queryable, payload blobbed (quest-hybrid shape) | planned |
|
||||
| 4.5h | Top-level `ActionResultC` scalar/entity-ref columns on `action_results` (acting_hero_id, acting_faction_id, province_id, …) + **drop `payload BLOB`**. Read path = scalar columns + per-entity proto blobs (close to today's proto parse; simpler than the original per-table-join plan) | planned |
|
||||
| 4.5i | Delete proto files/targets that became dead code. *Refined* entities' per-entity protos + converters (e.g. `changed_hero.proto`/`ChangedHeroConverter`) **are deleted** here. *Deep* entities' protos + leaf Tier-3 protos (e.g. `event_for_hero_backstory.proto`) + `Quest` are **retained** as blob payloads. No longer near-nothing — scope scales with how many entities went *refined* | planned |
|
||||
|
||||
Each PR nukes save dirs as part of its rollout (still pre-alpha). Each PR is independently reviewable and revertable. Under option B the per-PR cost is far lower than the original estimate (~110-line writer per entity, no child-table schema), so the remaining sequence is roughly **1 week**, not 3–4.
|
||||
|
||||
After 4.5: Phase 5 (idle-game eviction) and Phase 6 (final cleanup) proceed as the parent design doc lays out.
|
||||
|
||||
## What we gain at the end
|
||||
|
||||
- **Single source of truth.** Action data lives in tables; no proto representation for the things in scope.
|
||||
- **Native SQL analytics.** Cross-game queries like "actions touching province X" or "all riots in year Y" are one-line SELECTs. No backfill code needed.
|
||||
- **Inspectable saves.** `sqlite3 game.db` shows readable data. Debugging is normal SQL work, not "parse this binary blob."
|
||||
- **Cleaner replay path.** No proto parsing during replay. The `ActionResultProtoConverter` (currently called per replayed action in `replayFrom`) goes away entirely.
|
||||
- **Less code.** ~7 proto files deleted; their generated Scala targets deleted; their converter classes deleted; the dual proto/Scala representation collapses to one.
|
||||
- **Schema migrations as a first-class concept.** Future field additions are SQL migrations, which is the right shape for a system that owns its storage.
|
||||
|
||||
## What we lose
|
||||
|
||||
- **The "edit one proto" workflow** for adding fields. Adding `newFooDelta: Double` to `ChangedProvinceC` is now a Scala edit + SQL migration step + converter update. More steps. Pre-alpha this is cheap; post-alpha (when migrations apply to real data) it's a careful PR but still bounded.
|
||||
- **A safety net for unrecognized fields.** Proto silently keeps unknown fields on parse; SQL doesn't. We accept this because we control both ends.
|
||||
|
||||
## Decisions locked
|
||||
|
||||
1. **Refined option B is the pattern (see "Refinement" in Status; rescinds both "fully relational, no blob fallback" and the original whole-entity-blob-per-entity rule).** Per entity, three tiers: scalar/entity-ref columns (Tier 1) + typed columns for sealed value-oneofs (Tier 2) + per-row leaf-proto blobs in child tables (Tier 3). A whole-entity `<entity>_proto BLOB` is used **only for deep entities** where Tiers 2–3 cost more than they're worth. No child-table explosion for deep aggregates.
|
||||
2. **Classify each entity: all-scalar / refined / deep (supersedes the old "is a blob needed").** *All-scalar* → Tier 1 only, no blob (likely `ChangedFaction`). *Refined* → Tiers 1–3, no whole-entity blob, per-entity proto deletable (`ChangedHero`). *Deep* → Tier 1 + whole-entity `<entity>_proto BLOB`, proto retained (`ChangedProvince`, new-entity snapshots). Default to *refined* for shallow entities; fall back to *deep* only when the residual is too varied for Tiers 2–3.
|
||||
3. **What's retained vs. deleted depends on the classification.** `action_results.payload` (the whole-`ActionResult` blob) still goes away in 4.5h. *Refined* entities' per-entity sub-message protos + converters become dead code, deleted in 4.5i; only the **leaf** protos they use as Tier-3 blob payloads (e.g. `event_for_hero_backstory.proto`) are retained. *Deep* entities' protos + `Quest` stay as whole-entity blob payloads. So 4.5i is no longer near-nothing.
|
||||
4. **Sub-phase ordering** as in the Sequencing table; each decomposition PR splits into `.1` (schema + backfill) and `.2` (live write + read).
|
||||
5. **Phase 4 landed separately, before 4.5a** (historical, unchanged). Cutover validated the lifecycle integration with the simpler schema before decomposition began.
|
||||
@@ -1,291 +0,0 @@
|
||||
# SQLite Game History Design
|
||||
|
||||
## Why
|
||||
|
||||
The current persistence layer is file-based: action results are chunked into `.e0a` files, individual results spill to `.e0i` files for crash recovery, state snapshots live in `.e0s` files, and a `directory.e0i` index tracks chunks. This shape has produced three workarounds in recent history:
|
||||
|
||||
1. A `games.e0es` cache (PR #6706) to avoid re-reading the running-games list.
|
||||
2. Dual-storage in `PersistedActionResult` (proto bytes + parsed proto) to amortize serialization across save flushes.
|
||||
3. Lazy `gameState` (PR #6692) to avoid eager proto conversion on every replayed step, after PR #6691 fixed the `stateAfter` OOM that materialized ~4500 `PersistedActionResult` instances during a stale-LLM-replay path.
|
||||
|
||||
Each is a workaround for the same root cause: file-based random access is expensive, so we cache aggressively, and the cache layers are fragile (one misplaced `.map(_.gameState)` reintroduces the OOM).
|
||||
|
||||
The alternative — SQLite as a per-game container, with proto bytes stored as BLOB columns indexed by action sequence number — gives us random access by construction, removes the need for all three workarounds, and makes idle-game eviction cheap (rehydrate from the DB instead of reparsing files). The pattern is already in production here: `SqliteClientTextStore` does exactly this for the client text cache. We extend the same pattern to game history.
|
||||
|
||||
## Goal & non-goals
|
||||
|
||||
**In scope:**
|
||||
- Per-game SQLite container (`game.db`) holding action history, state snapshots, and shardok results.
|
||||
- A new `SqliteHistory` class implementing the existing `FullGameHistory` trait, drop-in replacement for `PersistedHistory` at the call site.
|
||||
|
||||
**Out of scope (deferred or rejected):**
|
||||
- **Migration of existing saves.** Pre-alpha there are only two users and a handful of test games. Cheaper to nuke the existing save directories at cutover than to write and verify a migration importer. New games created post-cutover land in `game.db` directly; existing games are gone.
|
||||
- The top-level `games.e0es` running-games registry. Stays as-is; its cache (PR #6706) already works.
|
||||
- Idle-game eviction. Enabled by this work but lands in a follow-up phase after `SqliteHistory` is the authoritative path.
|
||||
- A shared cross-game analytics DB. Per-game design supports ad-hoc cross-game queries via iterate-and-aggregate; build the centralized analytics DB only if/when those queries become hot.
|
||||
|
||||
## File layout
|
||||
|
||||
`game.db` lives in the existing per-game save directory, alongside `text_store.db`:
|
||||
|
||||
```
|
||||
${EAGLE_SAVE_DIR}/${gameIdHex}/
|
||||
├── game.db # NEW — action history, snapshots, shardok results
|
||||
└── text_store.db # existing — client text (SqliteClientTextStore)
|
||||
```
|
||||
|
||||
Existing `.e0a` / `.e0s` / `.e0i` / `directory.e0i` files do not coexist with `game.db`: at cutover we nuke save directories one time. There is no migration path and no legacy-file fallback.
|
||||
|
||||
### Why `game.db` and `text_store.db` are separate files (not tables in one DB)
|
||||
|
||||
It's tempting to combine them — one file per game, one connection per game, one cloud-upload story. The reason not to: **writer-lock contention.** SQLite serializes writers per-database file. `SqliteClientTextStore` writes frequently during LLM streaming (one `UPDATE` per token append on `texts.text`). `SqliteHistory` writes per-action during turn commits. Combining them means an in-flight LLM token append can block a turn commit, or vice versa. Separate DBs have separate writer locks and never contend. Subsystem ownership (each package owning its own schema, independent migration paths if the schemas evolve) is a bonus.
|
||||
|
||||
`Persister` integration follows the `SqliteClientTextStore` pattern: `game.db` is uploaded to / downloaded from cloud storage as a single opaque blob, keyed by the filename. On first load, if `game.db` is missing locally, try `persister.retrieveAsStream("game.db")`. If both are missing, this is a new game and we create an empty DB.
|
||||
|
||||
## Schema
|
||||
|
||||
```sql
|
||||
-- Action results, one row per action_result_index.
|
||||
CREATE TABLE action_results (
|
||||
action_seq INTEGER PRIMARY KEY, -- 0-based, dense, never sparse
|
||||
action_result_type INTEGER NOT NULL, -- denormalized for filtering
|
||||
round_id INTEGER NOT NULL, -- denormalized for recentResultsForRound
|
||||
date_year INTEGER, -- denormalized for sinceDate; NULL pre-game-start
|
||||
date_month INTEGER, -- denormalized for sinceDate; NULL pre-game-start
|
||||
payload BLOB NOT NULL -- ActionResult proto bytes
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE INDEX idx_action_results_round ON action_results(round_id);
|
||||
CREATE INDEX idx_action_results_date ON action_results(date_year, date_month);
|
||||
-- action_seq is the PK so no index needed there.
|
||||
|
||||
-- State snapshots at chunk boundaries (and the starting state at seq 0).
|
||||
-- boundary_action_seq is the action_seq AFTER which the snapshot reflects state.
|
||||
-- The starting state lives at boundary_action_seq = 0 (state before any actions).
|
||||
CREATE TABLE state_snapshots (
|
||||
boundary_action_seq INTEGER PRIMARY KEY,
|
||||
payload BLOB NOT NULL -- GameState proto bytes
|
||||
) WITHOUT ROWID;
|
||||
|
||||
-- Shardok per-battle results.
|
||||
CREATE TABLE shardok_results (
|
||||
shardok_game_id TEXT NOT NULL,
|
||||
action_seq INTEGER NOT NULL, -- 0-based within this shardok game
|
||||
payload BLOB NOT NULL, -- ShardokActionResult proto bytes
|
||||
PRIMARY KEY (shardok_game_id, action_seq)
|
||||
) WITHOUT ROWID;
|
||||
|
||||
-- Shardok per-game state (one row per shardok_game_id).
|
||||
CREATE TABLE shardok_state (
|
||||
shardok_game_id TEXT PRIMARY KEY,
|
||||
game_state BLOB NOT NULL, -- ShardokGameState proto bytes
|
||||
last_eagle_round_id INTEGER NOT NULL
|
||||
) WITHOUT ROWID;
|
||||
|
||||
-- Shardok per-player results (filtered ActionResultView per faction).
|
||||
CREATE TABLE shardok_player_results (
|
||||
shardok_game_id TEXT NOT NULL,
|
||||
faction_id INTEGER NOT NULL,
|
||||
seq INTEGER NOT NULL,
|
||||
payload BLOB NOT NULL, -- ShardokActionResultView proto bytes
|
||||
PRIMARY KEY (shardok_game_id, faction_id, seq)
|
||||
) WITHOUT ROWID;
|
||||
|
||||
-- Shardok per-player available commands (latest only, keyed by game + faction).
|
||||
CREATE TABLE shardok_player_commands (
|
||||
shardok_game_id TEXT NOT NULL,
|
||||
faction_id INTEGER NOT NULL,
|
||||
payload BLOB, -- ShardokAvailableCommands proto bytes; NULL = no commands
|
||||
PRIMARY KEY (shardok_game_id, faction_id)
|
||||
) WITHOUT ROWID;
|
||||
|
||||
-- Schema version and starting state.
|
||||
CREATE TABLE metadata (
|
||||
key TEXT PRIMARY KEY,
|
||||
value BLOB NOT NULL
|
||||
);
|
||||
-- Seeded with: schema_version=1
|
||||
```
|
||||
|
||||
### Column rationale
|
||||
|
||||
- **`action_seq`** as `INTEGER PRIMARY KEY` (the SQLite rowid alias) — densest possible storage and no separate index; range scans for `since(start)` are O(log n + result count).
|
||||
- **`WITHOUT ROWID`** on tables with synthetic keys to skip the implicit rowid column.
|
||||
- **Denormalized `action_result_type`, `round_id`, `date_year`, `date_month`** — these are the predicates the existing read paths use (`recentResultsForRound`, `sinceDate`). Keeping them as columns avoids parsing the proto blob to filter.
|
||||
- **No `created_at` timestamp** — wall-clock time isn't queried by the History trait, and game date (year/month) is what matters semantically.
|
||||
- **`payload BLOB`** — the proto bytes are the source of truth for the structured action result. Polymorphic action types make a relational schema painful for limited gain; the denormalized columns above cover the queries we need.
|
||||
- **Snapshots keyed by `boundary_action_seq`** — `stateAfter(N)` finds `MAX(boundary_action_seq) WHERE boundary_action_seq <= N`, returns that snapshot, replays forward `N - boundary_action_seq` actions. Snapshot at `0` is the game's starting state.
|
||||
|
||||
### Snapshot strategy
|
||||
|
||||
Match the existing `resultsPerSaveFile = 25` boundary: write a `state_snapshots` row every 25 actions. That gives the same replay-window cost as the current chunk-file design (`stateAfter(N)` replays at most 25 actions to reach an arbitrary point), and matches the cadence developers are already calibrated to.
|
||||
|
||||
Snapshots are GameState proto bytes, identical in shape to today's chunk-file `startingState`. The migration importer derives them directly from the chunk files. New games write a snapshot after every 25th `withNewResults` action.
|
||||
|
||||
## Connection lifecycle
|
||||
|
||||
Mirror `SqliteClientTextStore`:
|
||||
|
||||
- One `Connection` per loaded game, opened when `GamesManager` loads the game, closed when the game is evicted (future eviction work) or the server shuts down.
|
||||
- `Class.forName("org.sqlite.JDBC")` + `DriverManager.getConnection("jdbc:sqlite:${path}")` on open.
|
||||
- `PRAGMA journal_mode = WAL` on every connection open. WAL gives us crash-safe writes and lets readers proceed concurrently with the single writer — important because gRPC stream readers (humanPlayerClientConnectionState) query history mid-turn.
|
||||
- `PRAGMA synchronous = NORMAL` (the WAL-recommended setting; durability is preserved through WAL checkpoint).
|
||||
- `PRAGMA foreign_keys = ON` (defensive; we have no FKs today but cheap to enable).
|
||||
- Auto-commit on by default. Transactions explicitly opened for `withNewResults` (batch of action inserts + optional snapshot) and `truncateTo` (deletes across all tables).
|
||||
|
||||
### Threading
|
||||
|
||||
The current `PersistedHistory` is an immutable case class; `withNewResults` returns a new instance. `SqliteHistory` cannot be pure-immutable (the DB is mutable state) but should present the same interface: methods that "change" the history return `this` after a successful write. The underlying `Connection` is shared.
|
||||
|
||||
JDBC `Connection` is not thread-safe in general; SQLite's JDBC driver serializes operations per-connection. Existing call sites already serialize writes through the `EngineApplier` flow, so single-threaded write access is preserved. Reads from gRPC stream readers can use the same connection — SQLite serializes them transparently, and WAL prevents read-write blocking.
|
||||
|
||||
## Read paths
|
||||
|
||||
How each `FullGameHistory` method maps to SQL:
|
||||
|
||||
| Method | Query |
|
||||
|---|---|
|
||||
| `count` | `SELECT COALESCE(MAX(action_seq), -1) + 1 FROM action_results` (cached as a counter after the first read) |
|
||||
| `last` | `SELECT payload FROM action_results ORDER BY action_seq DESC LIMIT 1` + state from `stateAfter(count)` |
|
||||
| `all` | `SELECT payload FROM action_results ORDER BY action_seq` — used by `GameAdminServiceImpl.getActionDetail` to fetch one action by index. See follow-up note below. |
|
||||
| `since(start)` | `SELECT payload FROM action_results WHERE action_seq >= ? ORDER BY action_seq` |
|
||||
| `sinceDate(date)` | `SELECT payload FROM action_results WHERE (date_year, date_month) >= (?, ?) ORDER BY action_seq` |
|
||||
| `recentResultsForRound(round, pred)` | `SELECT payload FROM action_results WHERE round_id = ? AND action_seq > ? ORDER BY action_seq` where `?` is the cutoff matching current `recentHistory` semantics (last N actions, or all actions for the current round) |
|
||||
| `stateAfter(N)` | Find latest snapshot ≤ N, replay forward via `replayApplier` (same logic as `replayScalaOnlyToState`) |
|
||||
|
||||
For methods that return `Vector[ActionResultWithResultingState]` (with resulting state per row): we **do not** materialize per-row gameStates. Instead, fold the actions through `replayApplier` starting from the latest snapshot ≤ start, producing the states on the fly. This matches what `formAwrs` does today, with one critical difference: no `PersistedActionResult` wrapper is allocated, and no proto-conversion is performed per step. This is the same shape as `replayScalaOnlyToState`, generalized to produce intermediate states.
|
||||
|
||||
### `all()` follow-up
|
||||
|
||||
`GameAdminServiceImpl.getActionDetail` is the one production caller. It fetches a single action by index from the full vector. Either of these is cheaper than materializing the whole history:
|
||||
|
||||
- Replace the call site with `history.since(index).headOption`.
|
||||
- Add a new `actionAt(index): Option[ActionResultWithResultingState]` method and drop `all()` from the trait entirely.
|
||||
|
||||
`SqliteHistory.all` will work — it's just `SELECT * ORDER BY action_seq` — but it's expensive (materializes the full history into memory), so we should switch the admin caller in a small follow-up PR. Not blocking the SQLite work.
|
||||
|
||||
### `recentResultsForRound` semantics
|
||||
|
||||
Today this filters `recentHistory` (the in-memory tail) by `roundId`. In SQLite there's no in-memory/persisted split — all results live in the DB. The semantics shift slightly: return all results matching `round_id` after a configurable cutoff. The cutoff should match today's behavior (results since the start of the current round, or some bounded recent window). Default to "all results with the given `round_id`," which is correct as long as `round_id` uniquely identifies a round across the game's history (it does, per the current `RoundId` model).
|
||||
|
||||
## Write paths
|
||||
|
||||
### `withNewResults(newResults)`
|
||||
|
||||
In a transaction:
|
||||
1. `INSERT INTO action_results (action_seq, action_result_type, round_id, date_year, date_month, payload) VALUES ...` — one row per new result. Use `addBatch()` for multiple.
|
||||
2. For each new result whose `action_seq % 25 == 0` (snapshot boundary), `INSERT INTO state_snapshots(boundary_action_seq, payload) VALUES (?, ?)` with the GameState proto bytes.
|
||||
3. Commit.
|
||||
|
||||
The denormalized columns (`action_result_type`, `round_id`, `date_year`, `date_month`) are extracted from the action's resulting state at insert time. They are immutable once written; if the schema interpretation changes, a migration is required.
|
||||
|
||||
The "individual result for crash recovery" pattern (`.e0i` files) is replaced by: the action result is durably written when the transaction commits. WAL gives us atomicity per transaction. No separate crash-recovery file is needed.
|
||||
|
||||
### `saveNow`
|
||||
|
||||
Becomes a no-op in normal operation — writes are already durable per `withNewResults` commit. We keep the method on the trait for API compatibility but the implementation just returns `this`. (We could call `PRAGMA wal_checkpoint(TRUNCATE)` here to roll the WAL into the main DB file, useful before cloud upload; defer this until we measure WAL growth.)
|
||||
|
||||
### `truncateTo(targetActionCount)`
|
||||
|
||||
In a transaction:
|
||||
1. `DELETE FROM action_results WHERE action_seq >= ?`
|
||||
2. `DELETE FROM state_snapshots WHERE boundary_action_seq > ?`
|
||||
3. Shardok cleanup: `DELETE FROM shardok_results / shardok_state / shardok_player_results / shardok_player_commands WHERE shardok_game_id NOT IN (...)` (the set of battles still outstanding at the truncate point; mirrors `deleteOrphanedShardokFiles`).
|
||||
4. Commit.
|
||||
|
||||
`truncateTo(0)` resets the game; everything after the starting-state snapshot is deleted.
|
||||
|
||||
## Shardok results
|
||||
|
||||
The shardok subsystem currently lives in `.e0s` files (one per battle, full per-battle state). It's loaded selectively at game-load time, only for outstanding battles (see `PersistedHistory.apply` line 248-256).
|
||||
|
||||
Moving to SQLite, the four shardok tables above capture:
|
||||
- `shardok_results` — the per-battle result stream
|
||||
- `shardok_state` — current per-battle state (one row per battle)
|
||||
- `shardok_player_results` — per-faction filtered views
|
||||
- `shardok_player_commands` — current available commands per faction
|
||||
|
||||
`withNewShardokResults` writes to all four in a transaction. `shardokCount`, `shardokGameState`, etc., become single-row indexed lookups.
|
||||
|
||||
The selective-load optimization disappears with SQLite: we don't proactively read anything; queries hit the DB on demand. The "load only outstanding battles" logic is replaced by "query by `shardok_game_id` when needed."
|
||||
|
||||
## Crash recovery
|
||||
|
||||
WAL replaces the `.e0i` individual-result-file mechanism. On startup:
|
||||
- WAL is automatically replayed by SQLite if the previous shutdown was unclean. No application code needed.
|
||||
- A successful commit means durable; an interrupted commit means rolled back. No half-written state visible.
|
||||
|
||||
The current "orphaned individual results on load" path (`loadIndividualResults` in `PersistedHistory.apply`) is gone.
|
||||
|
||||
## Cutover
|
||||
|
||||
No migration path. At cutover:
|
||||
|
||||
1. Stop the server.
|
||||
2. Nuke the contents of `${EAGLE_SAVE_DIR}` (and `${EAGLE_ARCHIVE_DIR}` if there's anything there).
|
||||
3. Deploy.
|
||||
4. New games created post-deploy land in `game.db` directly.
|
||||
|
||||
Pre-alpha there are only two users and a handful of test games; a one-time nuke is cheaper than a verified migration importer. Trade-off accepted by the user explicitly.
|
||||
|
||||
### Testing strategy (no migration)
|
||||
|
||||
Without migration, we don't need equivalence-vs-`PersistedHistory` testing. The correctness gate becomes:
|
||||
|
||||
1. **Adapt `PersistedHistoryTest`** to run against `SqliteHistory` instead. The 890-line existing test suite covers the trait surface exhaustively. Same assertions, new implementation under test.
|
||||
2. **Property-style end-to-end test**: create a synthetic game, run N batches of `withNewResults`, verify that `since(start)`, `stateAfter(N)`, `sinceDate(date)`, `recentResultsForRound`, `truncateTo`, etc., all produce expected values. Run with a deterministic random seed.
|
||||
3. **Manual smoke test in dev**: start a fresh game, play several turns, restart the server, verify saves persist and replay correctly.
|
||||
|
||||
## Cloud-storage integration
|
||||
|
||||
`Persister.save(key, bytes)` and `persister.retrieveAsStream(key)` already handle local-vs-S3 transparently. `SqliteClientTextStore` integrates by treating `text_store.db` as a single binary blob: upload after writes, download before reads.
|
||||
|
||||
`SqliteHistory` will do the same with `game.db`:
|
||||
- **On load:** if local `game.db` missing, try to download via `persister.retrieveAsStream("game.db")`. Fall back to migration from `.e0a` if both are missing.
|
||||
- **On save:** after a meaningful change (turn boundary? configurable cadence?), upload the current `game.db` file to S3. Need to decide the upload trigger — too frequent and we burn S3 PUTs; too rare and crash recovery is lossy.
|
||||
|
||||
**Open question:** the right upload cadence. Today, chunk saves trigger cloud upload at chunk boundaries (every 25 actions). The simplest match: keep that cadence, upload `game.db` once per chunk boundary. We could be smarter (only upload if WAL has been checkpointed; only upload deltas) but those are optimizations.
|
||||
|
||||
## What the trait-level cutover looks like
|
||||
|
||||
`GamesManager` currently has roughly:
|
||||
|
||||
```scala
|
||||
val history: FullGameHistory = PersistedHistory(gameId, persister).getOrElse(...)
|
||||
```
|
||||
|
||||
Becomes:
|
||||
|
||||
```scala
|
||||
val history: FullGameHistory = SqliteHistory.loaded(gameId, persister)
|
||||
```
|
||||
|
||||
Every other consumer of `FullGameHistory` is unchanged.
|
||||
|
||||
## Decisions locked
|
||||
|
||||
1. **Per-game `game.db`** (not shared across games).
|
||||
2. **`game.db` and `text_store.db` stay separate files** (not combined into one DB). Writer-lock contention is the deciding factor.
|
||||
3. **No migration** of existing saves. Nuke save directories at cutover. Pre-alpha trade-off accepted by the user.
|
||||
4. **No feature flag.** Cutover is unconditional. Failures are loud.
|
||||
5. **Cloud upload cadence**: match the existing 25-action chunk cadence. Upload `game.db` once per snapshot boundary.
|
||||
6. **`recentResultsForRound`**: return all results for the given round (no recent-window cap). The `round_id` predicate bounds the scan naturally.
|
||||
7. **WAL checkpointing**: rely on SQLite's auto-checkpoint. Add explicit `PRAGMA wal_checkpoint` only if WAL file growth becomes a problem.
|
||||
8. **Schema versioning**: seed `metadata` with `schema_version = 1`. Schema migration mechanism deferred until we need a v2.
|
||||
9. **`history.all`**: keep on the trait for now; admin call site moved to `since(i).headOption` (or a new `actionAt(i)` method) in a small follow-up PR. Not blocking.
|
||||
|
||||
## Phase plan (revised)
|
||||
|
||||
| Phase | Work | Estimate |
|
||||
|---|---|---|
|
||||
| 0 | This design doc | 2-3 days (in flight) |
|
||||
| 1 | `SqliteHistory` schema + write paths + tests adapted from `PersistedHistoryTest` | ~1 week |
|
||||
| 2 | Read paths + full test parity | ~1 week |
|
||||
| 3 | Shardok results integration | 3-5 days |
|
||||
| 4 | Cutover in `GamesManager` (nuke `${EAGLE_SAVE_DIR}` as part of deploy) | 2-3 days |
|
||||
| 5 | Idle-game eviction | 3-5 days |
|
||||
| 6 | Post-alpha cleanup (delete `PersistedHistory`, `PersistedActionResult`, `PartialGameUtils`, the chunk-file save code, `games.e0es` cache if no longer needed) | 1-2 days |
|
||||
|
||||
**Total: ~3-4 weeks** (down from 5-6 — migration and equivalence-testing dropped).
|
||||
|
||||
The follow-up to migrate `GameAdminServiceImpl.getActionDetail` off `history.all` is a small, independent PR that can land any time.
|
||||
+234
-130
@@ -1,163 +1,267 @@
|
||||
# Tutorial Battle System
|
||||
|
||||
This document describes the current opening tutorial battle. It is based on the live implementation in `TutorialGameCreation.scala`, `TutorialBattleController.cpp`, `ShardokGamesManager.cpp`, `ResolveBattleAction.scala`, and the Unity dialogue scripts.
|
||||
|
||||
For the Unity tutorial UI/dialogue architecture, see `TUTORIAL_SYSTEM.md`. For copy and content notes, see `TUTORIAL_CONTENT.md`.
|
||||
This document describes the tutorial battle system for first-time players. The system provides a scripted introductory battle that teaches combat basics while telling a story.
|
||||
|
||||
---
|
||||
|
||||
## Current Flow
|
||||
## Overview
|
||||
|
||||
When a player creates a tutorial game at the default `OpeningBattle` phase:
|
||||
When a new player starts their first game in tutorial mode, they experience:
|
||||
|
||||
1. `TutorialGameCreation.createTutorialGame()` creates a `GameType.Tutorial(TutorialPhase.OpeningBattle)` game starting in `RoundPhase.BattleRequest`.
|
||||
2. Province 14, **Onmaa**, belongs to Sadar Rakon's faction and already has a defending army.
|
||||
3. Bregos Fyar's faction has a hostile attacking army, led by Ikhaan Tarn, already moving from province 31 to Onmaa.
|
||||
4. `RequestBattlesAction` immediately creates the Shardok battle and tags tutorial reinforcement hero IDs `100` and `101`.
|
||||
5. `GamesManager` stores the generated `TutorialBattleConfig` for this game and passes it to Shardok when the battle starts.
|
||||
6. Shardok creates the battle with the configured timed reinforcement events.
|
||||
7. Unity plays the active dialogue scripts from `tutorial_strategic.json` and `tutorial_battle.json`.
|
||||
|
||||
There is also a tutorial phase-start path. Starting after `OpeningBattle` applies precomputed tutorial setup results from `TutorialPhaseResultsLoader`; this is why the Unity post-battle trigger logic handles a "battle was never observed" auto-resolve path.
|
||||
1. **Narrative Intro** - Story screens introducing the scenario
|
||||
2. **Immediate Battle** - Skip strategic map, start directly in combat
|
||||
3. **Scripted Flee** - Enemy flees when player is "on the ropes"
|
||||
4. **Reinforcements** - Allied heroes arrive mid-battle
|
||||
5. **Heroes Join** - Reinforcement heroes join player's faction after battle
|
||||
|
||||
---
|
||||
|
||||
## Battle Setup
|
||||
## Battle Configuration
|
||||
|
||||
### Defender: Sadar Rakon's Faction
|
||||
### Defender (Player - John Ranil)
|
||||
- **Heroes**: John Ranil + 2 random vassals (3 total)
|
||||
- **Units**:
|
||||
- 2x Light Infantry (300 troops each, 60 training/armament)
|
||||
- 1x Longbowmen (200 troops, 60 training/armament)
|
||||
- **Province**: 14 (Onmaa) with 40 support, 500 gold, 3000 food
|
||||
- **Restriction**: Cannot flee (must defend)
|
||||
|
||||
Configured in `src/main/resources/net/eagle0/eagle/tutorial_parameters.json`:
|
||||
### Attacker (Ikhaan Tarn)
|
||||
- **Heroes**: Ikhaan Tarn + 2 sworn brothers (3 total)
|
||||
- **Units**:
|
||||
- 1x Heavy Cavalry (600 troops, 80 training/armament)
|
||||
- 1x Heavy Infantry (500 troops, 80 training/armament)
|
||||
- 1x Longbowmen (300 troops, 80 training/armament)
|
||||
- **Origin Province**: 32
|
||||
|
||||
- **Faction ID:** 3
|
||||
- **Province:** 14, Onmaa
|
||||
- **Starting support:** 23
|
||||
- **Starting resources:** 50 gold, 4000 food
|
||||
- **Heroes:** Sadar Rakon, Old Marek the Learned, Agamemnon
|
||||
- **Battalions:**
|
||||
- Rakon's Loyalists: Light Infantry, 529 size, 80 training, 75 armament
|
||||
- Onmaa Defenders: Light Infantry, 311 size, 80 training, 60 armament
|
||||
- Hunters of the Steppe: Longbowmen, 478 size, 80 training, 60 armament
|
||||
### Flee Trigger
|
||||
Attacker flees when ANY of these conditions are met:
|
||||
- Player loses 1 unit
|
||||
- 5 rounds have passed
|
||||
|
||||
### Attacker: Bregos Fyar's Faction
|
||||
|
||||
The opening attacking army is configured under Bregos Fyar's `attackingArmies`:
|
||||
|
||||
- **Faction ID:** 2
|
||||
- **Origin province:** 31
|
||||
- **Destination province:** 14, Onmaa
|
||||
- **Heroes:** Ikhaan Tarn, Tall Edgtheow, Waylaid Julius, Luke the Prank-tricker
|
||||
- **Battalions:**
|
||||
- Doomriders: Heavy Cavalry, 592 size, 80 training, 80 armament
|
||||
- The Shardok's Guard: Heavy Infantry, 507 size, 80 training, 80 armament
|
||||
- Bowmen of Nikemi: Longbowmen, 291 size, 80 training, 80 armament
|
||||
- Swift Sabres: Light Cavalry, 450 size, 80 training, 80 armament
|
||||
|
||||
Tarn's unit is made visible to the defender from the start through `TutorialBattleConfig.initial_visibilities`.
|
||||
### Reinforcements
|
||||
When Tarn flees, these heroes arrive as player reinforcements:
|
||||
- Elena Fyar
|
||||
- Hedrick
|
||||
- The Boulder
|
||||
|
||||
---
|
||||
|
||||
## Reinforcements
|
||||
## Implementation Architecture
|
||||
|
||||
The current tutorial config has two timed reinforcement events. It does not currently configure a scripted Tarn flee event.
|
||||
### Phase 1: Narrative Intro System
|
||||
|
||||
| Event ID | Trigger | Hero ID | Hero | Battalion |
|
||||
|----------|---------|---------|------|-----------|
|
||||
| `elena_reinforcement_round_5` | End of round 5 or later | 101 | Elena Fyar | Fyar's Vanguard, Heavy Infantry, 535 size, 75 training, 75 armament |
|
||||
| `ranil_reinforcement_round_7` | End of round 7 or later | 100 | John Ranil | Ranil's Riders, Heavy Cavalry, 458 size, 80 training, 80 armament |
|
||||
**Proto Changes:**
|
||||
- `game_state_view.proto`: Added `TutorialNarrativeScreen` message and `pending_narrative_screens` field
|
||||
- `game_parameters.proto`: Added `TutorialNarrativeScreen` and `tutorial_narrative_screens` field
|
||||
|
||||
Shardok creates reinforcement units at battle creation time with `PENDING_REINFORCEMENT` status. `TutorialBattleController` activates them when their event triggers and returns a `TUTORIAL_REINFORCEMENTS_ARRIVED` action result. Unity maps that action to profession-specific dialogue triggers:
|
||||
**Unity Client:**
|
||||
- `NarrativeScreenController.cs`: Modal screen with title, body text, optional image
|
||||
- `EagleGameController.cs`: Checks for pending narrative screens on game start
|
||||
|
||||
- `tutorial_reinforcement_paladin` for Elena Fyar
|
||||
- `tutorial_reinforcement_engineer` for John Ranil
|
||||
**Narrative Content** (defined in `tutorial_game_parameters.json`):
|
||||
```
|
||||
Screen 1: "The Engineer of Onmaa" - Player backstory
|
||||
Screen 2: "The Traitor Arrives" - Tarn's attack
|
||||
Screen 3: "Defend Your Home" - Call to action
|
||||
```
|
||||
|
||||
The reinforcement action includes an `attacker_starting_position_index`; Shardok resolves that to currently open map positions and starts a reinforcement placement phase.
|
||||
### Phase 2: Auto-Start Battle
|
||||
|
||||
**Configuration:**
|
||||
- `tutorial_game_parameters.json`: Contains `tutorialBattle` config with attacker, target, flee conditions
|
||||
- `game_parameters.proto`: `TutorialBattleConfig` message
|
||||
|
||||
**Game Creation:**
|
||||
- `NewGameCreation.scala`: `setupTutorialBattle()` method creates:
|
||||
- `HostileArmyGroup` with `Attacking` status (bypasses decision phase)
|
||||
- `Army` for defender
|
||||
- Both added to target province
|
||||
|
||||
**Battle Creation:**
|
||||
- When round advances to `BattleRequest` phase, `RequestBattlesAction` automatically creates the `ShardokBattle`
|
||||
|
||||
### Phase 3: Scripted Flee Mechanism
|
||||
|
||||
**C++ Component:**
|
||||
- `TutorialBattleController.hpp/cpp`: Controller for scripted battle logic
|
||||
|
||||
**Key Methods:**
|
||||
```cpp
|
||||
// Check all events and execute triggered actions (called at end of each round)
|
||||
std::vector<ActionResult> CheckAndExecuteEvents(
|
||||
const GameStateW& state,
|
||||
const SettingsGetter& settings,
|
||||
const std::shared_ptr<RandomGenerator>& randomGenerator);
|
||||
```
|
||||
|
||||
**Integration:**
|
||||
- `ShardokEngine.cpp`: Calls `CheckAndExecuteEvents()` at end of each round in `HandlePlayerTurnEnd()`
|
||||
- Events fire in config order, each event fires at most once
|
||||
|
||||
### Phase 4: Mid-Battle Reinforcements
|
||||
|
||||
**Action Types** (in `action_type.proto`):
|
||||
- `TUTORIAL_ENEMY_FLED = 78`
|
||||
- `TUTORIAL_REINFORCEMENTS_ARRIVED = 79`
|
||||
|
||||
**Implementation (TODO):**
|
||||
- When scripted flee triggers, create reinforcement units in defender's reserves
|
||||
- Units have `UnitStatus_RESERVE_UNIT` status
|
||||
- Reinforcements enter battle on subsequent turns
|
||||
|
||||
### Phase 5: Tutorial Popups
|
||||
|
||||
**Content Definitions** (in `TutorialContentDefinitions.cs`):
|
||||
- `tutorial_battle_started`: "Defend your province!"
|
||||
- `tutorial_enemy_fled`: "The enemy retreats!"
|
||||
- `tutorial_reinforcements_arrived`: "Allies have arrived!"
|
||||
- `tutorial_battle_victory`: Victory celebration
|
||||
|
||||
**Trigger Integration:**
|
||||
- `TutorialTriggerRegistry.cs`: Handles tutorial action types from `ActionResultView`
|
||||
- `ShardokGameController.cs`: Detects tutorial events and triggers popups
|
||||
|
||||
### Phase 6: Post-Battle Hero Joining
|
||||
|
||||
**Implementation (TODO):**
|
||||
- `ResolveBattleAction.scala`: Check if battle was tutorial and defender won
|
||||
- Create `TutorialHeroJoined` action results adding reinforcement heroes to player faction
|
||||
- Set appropriate loyalty, vigor, location for joined heroes
|
||||
|
||||
### Phase 7: Post-Battle Strategic Dialogue
|
||||
|
||||
After the battle ends and the player returns to the strategic map, narrative dialogues guide them through the next steps.
|
||||
|
||||
**Dialogue 1: Battle Aftermath** (trigger: `tutorial_battle_ended`)
|
||||
- Old Marek reflects on the close call
|
||||
- Tarn has vanished — rumors of sorcery or escape, no one knows
|
||||
- Captured lieutenants: they followed orders, not to blame for Tarn's madness
|
||||
- Player should try to recruit them or hold them
|
||||
- No instruction text needed — Handle Captured Heroes UI is self-explanatory
|
||||
|
||||
**Dialogue 2: Rebuild Support** (trigger: `tutorial_rebuild_support`)
|
||||
- Fires when available commands no longer include HandleCapturedHeroCommand
|
||||
- Marek explains the province needs support rebuilt after the battle
|
||||
- Engineers (John Ranil) can **Improve** the province — build infrastructure, develop economy
|
||||
- Paladins (Elena Fyar) can **Give Alms** — distribute food to win hearts
|
||||
- Instructions highlight **Improve** and **Give Alms** buttons
|
||||
- Goal: get Support to **40** before January for tax revenue
|
||||
|
||||
**Implementation:**
|
||||
- Dialogues defined in `tutorial_strategic.json`
|
||||
- `tutorial_battle_ended`: fire when tutorial battle is removed from `RunningShardokGameModels`
|
||||
- `tutorial_rebuild_support`: fire when available commands no longer include `HandleCapturedHeroCommand`
|
||||
- Register `ImproveButton` and `AlmsButton` as highlight targets in `TutorialTargetRegistry`
|
||||
|
||||
See `TUTORIAL_CONTENT.md` for full dialogue text.
|
||||
|
||||
---
|
||||
|
||||
## Scripted Event Controller
|
||||
|
||||
`TutorialBattleController` is a generic event-driven controller, even though the current tutorial battle only uses timed reinforcements.
|
||||
|
||||
Supported trigger types:
|
||||
|
||||
- `after_round`
|
||||
- `units_lost`
|
||||
- `damage_taken`
|
||||
- `unit_killed`
|
||||
|
||||
Supported action types:
|
||||
|
||||
- `flee`
|
||||
- `reinforcements`
|
||||
|
||||
Events are checked at the end of each round, evaluated in config order, and each `event_id` fires at most once.
|
||||
|
||||
---
|
||||
|
||||
## Battle Resolution
|
||||
|
||||
`RequestBattlesAction` marks tutorial opening battles with `reinforcementHeroIds = Set(100, 101)`. `ResolveBattleAction` uses that metadata so reinforcement heroes are accepted when battle results return, even though they were not part of the original defending army.
|
||||
|
||||
After the opening battle resolves, `ResolveBattleAction` applies `TutorialTarnDisappearsAction` for `GameType.Tutorial(TutorialPhase.OpeningBattle)`. Tarn is removed from captured/unaffiliated province state and from his faction's leaders list, matching the post-battle dialogue that he has vanished.
|
||||
|
||||
`TutorialBattleAutoResolve` provides a synthetic defender victory for tutorial setup paths that skip past the opening battle. In that synthetic result:
|
||||
|
||||
- The defender wins.
|
||||
- Reinforcement heroes 100 and 101 survive.
|
||||
- Tarn is marked outlawed/escaped.
|
||||
- Other attacker heroes are captured.
|
||||
- Defender battalions take about 30% casualties.
|
||||
- Attacker battalions are destroyed.
|
||||
|
||||
---
|
||||
|
||||
## Unity Dialogue Hooks
|
||||
|
||||
Strategic tutorial dialogue:
|
||||
|
||||
- `game_started`: opening Onmaa monologue, ending with `FightButton` highlighted.
|
||||
- `tutorial_battle_ended`: post-battle aftermath.
|
||||
- `tutorial_rebuild_support`: support rebuilding guidance after captured-hero handling is done.
|
||||
|
||||
Battle tutorial dialogue:
|
||||
|
||||
- `shardok_placement_started`: placement and unit overview.
|
||||
- `shardok_battle_running`: first player turn guidance.
|
||||
- Ability/terrain triggers such as `archery_available`, `melee_available`, `ability_charge_available`, `start_fire_available`, `thunderstorm`, `duel_available`, `hide_available`, and `engineer_near_enemy`.
|
||||
- Reinforcement triggers `tutorial_reinforcement_paladin` and `tutorial_reinforcement_engineer`.
|
||||
- Capture triggers `enemy_hero_captured` and `friendly_hero_captured`.
|
||||
- `shardok_battle_reset`: replay dialogue after a battle reset.
|
||||
|
||||
---
|
||||
|
||||
## Files To Check First
|
||||
## File Summary
|
||||
|
||||
### New Files
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/main/resources/net/eagle0/eagle/tutorial_parameters.json` | Tutorial map, factions, starting provinces, armies |
|
||||
| `src/main/scala/net/eagle0/eagle/service/new_game_creation/TutorialGameCreation.scala` | Opening battle setup and `TutorialBattleConfig` creation |
|
||||
| `src/main/protobuf/net/eagle0/common/tutorial_battle_config.proto` | Scripted event config schema |
|
||||
| `src/main/cpp/net/eagle0/shardok/server/ShardokGamesManager.cpp` | Adds pending reinforcement units and applies initial visibility |
|
||||
| `src/main/cpp/net/eagle0/shardok/library/tutorial/TutorialBattleController.cpp` | Evaluates scripted events and creates Shardok action results |
|
||||
| `src/main/scala/net/eagle0/eagle/library/actions/impl/action/RequestBattlesAction.scala` | Creates the opening battle and tags reinforcement hero IDs |
|
||||
| `src/main/scala/net/eagle0/eagle/library/actions/impl/action/ResolveBattleAction.scala` | Resolves reinforcements and applies Tarn disappearance |
|
||||
| `src/main/scala/net/eagle0/eagle/service/tutorial/TutorialBattleAutoResolve.scala` | Synthetic result for phase-start paths after the opening battle |
|
||||
| `src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Tutorial/Triggers/TutorialTriggerRegistry.cs` | Converts strategic and tactical events into dialogue triggers |
|
||||
| `src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Resources/Dialogues/tutorial_battle.json` | Active battle dialogue content |
|
||||
| `TutorialBattleController.hpp/cpp` | C++ scripted flee and reinforcement logic |
|
||||
| `NarrativeScreenController.cs` | Unity narrative screen display |
|
||||
|
||||
### Modified Files
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `tutorial_game_parameters.json` | Battle configuration and narrative content |
|
||||
| `game_state_view.proto` | Narrative screens field |
|
||||
| `game_parameters.proto` | Tutorial battle config |
|
||||
| `player_info.proto` | Tutorial battle flags for Shardok |
|
||||
| `action_type.proto` | Tutorial action types |
|
||||
| `NewGameCreation.scala` | Auto-start battle setup |
|
||||
| `EagleGameController.cs` | Narrative display integration |
|
||||
| `TutorialContentDefinitions.cs` | Battle tutorial content |
|
||||
| `TutorialTriggerRegistry.cs` | Tutorial event triggers |
|
||||
|
||||
---
|
||||
|
||||
## Remaining Implementation
|
||||
|
||||
### Disable Player Flee
|
||||
In `FleeCommandFactory.cpp`:
|
||||
- Check if tutorial mode is enabled
|
||||
- If defender, don't offer flee commands
|
||||
|
||||
### Reinforcement Units
|
||||
In `TutorialBattleController::ExecuteScriptedFlee()`:
|
||||
- Create `Unit` objects for Elena Fyar, Hedrick, The Boulder
|
||||
- Add to defender's reserve units
|
||||
- Generate `TUTORIAL_REINFORCEMENTS_ARRIVED` action
|
||||
|
||||
### Post-Battle Hero Joining
|
||||
In Eagle's battle resolution:
|
||||
- Detect tutorial battle completion
|
||||
- Create heroes in player's faction with proper stats
|
||||
- Generate notification action results
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Tutorial game starts in BattleRequest and creates an Onmaa battle immediately.
|
||||
- [ ] Opening strategic dialogue highlights the Fight button.
|
||||
- [ ] Tarn's unit is visible to the defender from battle start.
|
||||
- [ ] Placement dialogue fires during setup.
|
||||
- [ ] First-turn battle-running dialogue fires on the player's first active turn.
|
||||
- [ ] Elena Fyar arrives from the configured round-5 event.
|
||||
- [ ] John Ranil arrives from the configured round-7 event.
|
||||
- [ ] Reinforcement arrival dialogues fire once per hero.
|
||||
- [ ] Battle reset clears battle dialogue completion for replay.
|
||||
- [ ] Post-battle aftermath fires after the battle leaves `RunningShardokGameModels`.
|
||||
- [ ] Rebuild-support dialogue waits until captured-hero handling and visible notifications are done.
|
||||
- [ ] Starting a tutorial at a later phase uses precomputed setup and still reaches coherent strategic dialogue state.
|
||||
- [ ] New user sees narrative screens before battle
|
||||
- [ ] Battle starts immediately after narratives (no strategic map)
|
||||
- [ ] Player has correct units (3 heroes, 3 battalions)
|
||||
- [ ] Attacker has correct units (3 heroes, 3 battalions)
|
||||
- [ ] Player cannot flee
|
||||
- [ ] Tarn flees after player loses 1 unit
|
||||
- [ ] Tarn flees after 5 rounds (if no units lost)
|
||||
- [ ] Reinforcements appear when Tarn flees
|
||||
- [ ] Tutorial popups appear at correct moments
|
||||
- [ ] Reinforcement heroes join faction after battle victory
|
||||
|
||||
---
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
### tutorial_game_parameters.json
|
||||
```json
|
||||
{
|
||||
"tutorialBattle": {
|
||||
"attackerFactionHead": "Ikhaan Tarn",
|
||||
"targetProvinceId": 14,
|
||||
"fleeAfterDefenderUnitsLost": 1,
|
||||
"fleeAfterRounds": 5,
|
||||
"reinforcements": ["Elena Fyar", "Hedrick", "The Boulder"]
|
||||
},
|
||||
"tutorialNarrativeScreens": [
|
||||
{ "title": "...", "bodyText": "...", "imagePath": "" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### TutorialBattleConfig Proto (Shardok)
|
||||
```protobuf
|
||||
message TutorialBattleConfig {
|
||||
bool enabled = 1;
|
||||
repeated TutorialEvent events = 2;
|
||||
repeated UnitInitialSize initial_sizes = 3; // For damage tracking
|
||||
}
|
||||
|
||||
message TutorialEvent {
|
||||
string event_id = 1;
|
||||
TutorialTrigger trigger = 2;
|
||||
TutorialAction action = 3;
|
||||
}
|
||||
|
||||
message TutorialTrigger {
|
||||
oneof trigger_type {
|
||||
RoundTrigger after_round = 1;
|
||||
UnitsLostTrigger units_lost = 2;
|
||||
DamageTakenTrigger damage_taken = 3;
|
||||
UnitKilledTrigger unit_killed = 4;
|
||||
}
|
||||
}
|
||||
|
||||
message TutorialAction {
|
||||
oneof action_type {
|
||||
FleeAction flee = 1;
|
||||
ReinforcementsAction reinforcements = 2; // Uses CommonUnit
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
+13
-13
@@ -1,8 +1,6 @@
|
||||
# Tutorial Content Guide
|
||||
|
||||
This document describes tutorial content. The active tutorial experience is driven by the narrative dialogue JSON files under `src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Resources/Dialogues/`.
|
||||
|
||||
The older step-based modal/overlay content in `TutorialContentDefinitions.cs` is currently dormant: `RegisterAll()` returns before registering any sequences. Keep that in mind when editing this file; changing `TutorialContentDefinitions.cs` will not affect the live tutorial until the early return is removed and the overlap with dialogue triggers is audited.
|
||||
This document defines all tutorial content. Edit this to refine the text, then update `TutorialContentDefinitions.cs` to match.
|
||||
|
||||
---
|
||||
|
||||
@@ -183,16 +181,17 @@ These fire after the first tutorial battle ends and the player returns to the st
|
||||
>
|
||||
> Any of our heroes can develop the land or give alms to the people — nothing wins hearts faster than a full belly. Engineers like John Ranil are especially effective at improving a province, and paladins like Elena Fyar are the best at winning hearts. Either way, we need the people's support before the tax collectors come round in January.
|
||||
|
||||
**Instructions:** John Ranil's step highlights `ImproveProvinceButton` and explains **Improve**. Elena Fyar's step highlights `GiveAlmsButton` and explains **Give Alms**. You must have at least **40** support in the province by January in order to collect gold and food in taxes.
|
||||
**Instructions:** Use **Improve** and **Give Alms** to raise Support. You must have at least **40** support in the province by January in order to collect gold and food in taxes.
|
||||
|
||||
**Highlight targets:** `ImproveProvinceButton`, `GiveAlmsButton`
|
||||
**Highlight targets:** `SupportField`
|
||||
|
||||
### Implementation Notes
|
||||
|
||||
- Both dialogues are defined in `tutorial_strategic.json`.
|
||||
- `tutorial_battle_ended` fires from `TutorialTriggerRegistry.CheckTutorialBattleEnded()` when the opening battle is gone and strategic commands are available. It also handles the server-side auto-resolve path where the Unity client never observed a running Shardok model.
|
||||
- `tutorial_rebuild_support` fires from `TutorialTriggerRegistry.CheckTutorialRebuildSupport()` when available commands no longer include `HandleCapturedHeroCommand` and the notification panel has no visible info.
|
||||
- `ImproveProvinceButton` and `GiveAlmsButton` are runtime command-button targets registered by `CommandButtonPanelController`.
|
||||
- Both dialogues go in `tutorial_strategic.json`
|
||||
- Need new trigger events: `tutorial_battle_ended` and `tutorial_rebuild_support`
|
||||
- `tutorial_battle_ended` should fire when the tutorial battle is removed from `RunningShardokGameModels` in `EagleGameModel.ApplyGameStateViewDiff`
|
||||
- `tutorial_rebuild_support` should fire when available commands no longer include `HandleCapturedHeroCommand` (i.e. the captured heroes phase is done and the player has regular commands available)
|
||||
- The "rebuild" dialogue should highlight the Improve and Give Alms buttons in the command panel (requires registering them as highlight targets)
|
||||
|
||||
---
|
||||
|
||||
@@ -211,10 +210,11 @@ These fire after the first tutorial battle ends and the player returns to the st
|
||||
## Adding New Tutorials
|
||||
|
||||
1. Add entry to this document
|
||||
2. For active tutorial content, add or edit a script in `Assets/Resources/Dialogues/tutorial_strategic.json` or `tutorial_battle.json`.
|
||||
3. Ensure the trigger event exists in `TutorialTriggerRegistry.cs` or is fired directly by the relevant controller.
|
||||
4. If reviving the dormant step-UI system, remove the early return in `TutorialContentDefinitions.RegisterAll()` only after auditing duplicate trigger coverage with the dialogue JSON.
|
||||
5. Test the flow.
|
||||
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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
# Tutorial System Architecture
|
||||
|
||||
This document describes how the Unity client's tutorial system is wired together: the classes involved, the trigger catalog, the step lifecycle, and the expected first-session flow. It complements two existing docs:
|
||||
|
||||
- **`TUTORIAL_CONTENT.md`** — human-facing copy for tutorial steps and dialogues
|
||||
- **`TUTORIAL_BATTLE_SYSTEM.md`** — the scripted opening battle at Onmaa
|
||||
|
||||
It also overlaps slightly with the in-tree `Assets/Tutorial/TUTORIAL_PLAN.md`, which is an older implementation plan.
|
||||
|
||||
All paths below are relative to `src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/`.
|
||||
|
||||
---
|
||||
|
||||
## Current State (important)
|
||||
|
||||
There are **two parallel subsystems** that share the same trigger plumbing:
|
||||
|
||||
| Subsystem | Status | Lives in |
|
||||
|-----------|--------|----------|
|
||||
| Step-based tutorial UI (modals, overlays, hints) | **Dormant** | `Tutorial/TutorialManager.cs`, `Tutorial/UI/*`, `Tutorial/Content/*` |
|
||||
| Narrative dialogue system | **Live** | `Tutorial/Dialogue/*`, `Resources/Dialogues/*.json` |
|
||||
|
||||
`TutorialContentDefinitions.RegisterAll()` returns early with the comment *"Old tutorial content suppressed — replaced by narrative dialogue system."* As a result:
|
||||
|
||||
- `OnboardingSequence` is never assigned, so `StartOnboarding()` no-ops at the "No onboarding sequence assigned" branch.
|
||||
- No contextual tutorial sequences are registered with the trigger registry.
|
||||
- The trigger registry still fires events normally, but nothing on the step-UI side is listening.
|
||||
- `DialogueManager` consumes those same events and matches them against scripts in `Resources/Dialogues/` (`tutorial_strategic.json`, `tutorial_battle.json`).
|
||||
- Dialogue completion is persisted per game through `TutorialDialogueProgressStore`, so completed scripts do not replay after a reconnect or scene reload for the same game.
|
||||
|
||||
**In practice today, the entire active tutorial experience runs through the dialogue system.** The step-UI machinery is preserved for future use — content definitions still exist below the early return in `TutorialContentDefinitions.cs` for reference.
|
||||
|
||||
---
|
||||
|
||||
## Class Map
|
||||
|
||||
### Orchestration
|
||||
- **`Tutorial/TutorialManager.cs`** — Singleton. Owns `OnboardingSequence`, the active step queue, the trigger registry, and the dialogue manager handle. Initialized by `EagleGameController.SetUpGame()` and `ShardokGameController.SetUpGame()`.
|
||||
- **`Tutorial/TutorialState.cs`** — PlayerPrefs persistence. Tracks `OnboardingCompleted`, `OnboardingStepReached`, completed sequence IDs (HashSet for O(1) lookup), dismissed hints, and a global "tutorials disabled" preference.
|
||||
- **`Tutorial/TutorialTargetRegistry.cs`** — Maps string IDs to `RectTransform`s for highlighting. Static targets are Inspector-assigned (`ProvinceInfoPanel`, `SupportField`, `CommitButton`, etc.); dynamic targets register at runtime (e.g., hero rows from `HeroesAndBattalionsPanelController`).
|
||||
- **`Tutorial/TutorialDialogueCoordinator.cs`** — Recomputes durable dialogue triggers from the current strategic model and sends them directly to `DialogueManager`. This covers reconnects, phase-start tutorial games, and strategic beats that should be recoverable from state.
|
||||
|
||||
### Triggers
|
||||
- **`Tutorial/Triggers/TutorialTriggerRegistry.cs`** (~1100 lines) — Routes game events to both the step UI (when sequences are registered) and `DialogueManager`. Maintains one-shot session flags so the same first-encounter trigger doesn't fire twice.
|
||||
|
||||
### Content (step UI — dormant)
|
||||
- **`Tutorial/Content/TutorialStep.cs`** — Per-step record: `DisplayMode`, `CompletionType`, target path, copy, panel anchor, highlight options.
|
||||
- **`Tutorial/Content/TutorialSequence.cs`** — `ScriptableObject` holding ordered `TutorialStep`s plus `IsOnboarding` flag and lifecycle callbacks.
|
||||
- **`Tutorial/Content/TutorialContentDefinitions.cs`** — Static class that *would* register all sequences. Currently short-circuits before any registration.
|
||||
|
||||
### UI (step UI — dormant)
|
||||
- **`Tutorial/UI/TutorialUIManager.cs`** — Coordinates which presenter renders each step.
|
||||
- **`Tutorial/UI/TutorialCanvasBuilder.cs`** — Builds the Canvas at runtime (no prefab dependency).
|
||||
- **`Tutorial/UI/TutorialModalPanel.cs`** — Full-screen blocking modal.
|
||||
- **`Tutorial/UI/TutorialOverlayController.cs`** + **`TutorialOverlayBuilder.cs`** — Dimmer with highlighted cutout, gold border, pulsing animation, and adjacent tooltip text.
|
||||
- **`Tutorial/UI/TutorialHintIndicator.cs`** — Stub for pulsing-dot mode.
|
||||
|
||||
### Dialogue (live)
|
||||
- **`Tutorial/Dialogue/DialogueManager.cs`** — Loads all `Resources/Dialogues/*.json` scripts at startup, indexes by trigger ID, drives the panel.
|
||||
- **`Tutorial/Dialogue/DialogueScript.cs`** / **`DialogueStep.cs`** — JSON-shaped records for scripts and steps.
|
||||
- **`Tutorial/Dialogue/DialoguePanelController.cs`** — Renders the speaker headshot, body text, instruction line, optional highlight target, and Continue button.
|
||||
- **Scripts:** `Assets/Resources/Dialogues/tutorial_strategic.json`, `tutorial_battle.json`.
|
||||
|
||||
---
|
||||
|
||||
## Step Lifecycle (Step-UI System)
|
||||
|
||||
1. **Triggered** — `TutorialTriggerRegistry` raises an event (e.g., `game_started`, `battle_entered`).
|
||||
2. **Queued or shown** — If no active sequence, show immediately. If an active step is hidden (`DisplayMode.None`), interrupt it. If both new and active are command tutorials, interrupt for responsiveness. Otherwise queue.
|
||||
3. **Rendered** — `TutorialUIManager.ShowCurrentStep()` dispatches to the modal/overlay/hint presenter based on `DisplayMode`.
|
||||
4. **Awaiting completion** — One of:
|
||||
- `ButtonClick` — user dismisses
|
||||
- `GameEvent` — wait for a named event (e.g., `province_selected`)
|
||||
- `Timer` — auto-advance after delay
|
||||
- `UIInteraction` — wait for the highlighted target to be interacted with
|
||||
- `Condition` — custom predicate (rare)
|
||||
5. **Advance** — `AdvanceStep()` moves to the next step or completes the sequence; completion writes to `TutorialState`.
|
||||
|
||||
Hidden steps (`DisplayMode.None`) deliberately don't block — they exist so contextual tutorials can fire while the onboarding sequence is waiting on an async event.
|
||||
|
||||
---
|
||||
|
||||
## Display Modes
|
||||
|
||||
| Mode | Renderer | Notes |
|
||||
|------|----------|-------|
|
||||
| **Modal** | `TutorialModalPanel` | Full blocking dialog with dim background |
|
||||
| **Overlay** | `TutorialOverlayController` | Dimmer with cutout + tooltip near a target |
|
||||
| **Tooltip** | `TutorialOverlayController` | Currently same renderer as overlay |
|
||||
| **Hint** | `TutorialHintIndicator` | Pulsing dot only (stub) |
|
||||
| **None** | (invisible) | Waits for a completion event |
|
||||
|
||||
Panels can be anchored via `TutorialPanelAnchor` (`Center` / `Left` / `Right` / `Top` / `Bottom`).
|
||||
|
||||
---
|
||||
|
||||
## Trigger Catalog
|
||||
|
||||
Most event-style triggers are raised via `TutorialTriggerRegistry`. Durable strategic dialogue beats are also recomputed by `TutorialDialogueCoordinator` on each model update and sent directly to `DialogueManager.TriggerDialogue()`. Both paths match against the dialogue JSON; if step-UI sequences are re-registered, only the registry path will route to them. File:line citations are for the registry unless noted; line numbers may drift.
|
||||
|
||||
### Bootstrap / first-session
|
||||
| Trigger | Fires from | When |
|
||||
|---------|-----------|------|
|
||||
| `game_started` | `TutorialDialogueCoordinator.EligibleTriggers()` | Any tutorial strategic model update; dialogue progress keeps it from replaying |
|
||||
| `first_battle_available` | `OnModelUpdated()` ~L204 | A `RunningShardokGameModel` first appears |
|
||||
| `battle_entered` | `OnBattleEntered()` ~L793 | User clicks **Fight!** |
|
||||
| `tutorial_battle_ended` | `CheckTutorialBattleEnded()` ~L594 | Tutorial battle removed from running models |
|
||||
| `tutorial_rebuild_support` | `CheckTutorialRebuildSupport()` ~L616 | Captured-heroes phase done |
|
||||
| `tutorial_taxes_collected` | `CheckTaxesCollected()` ~L680 | New Year action with positive tax delta |
|
||||
| `tutorial_in_town` | `CheckInTown()` ~L500 | Return command becomes available |
|
||||
|
||||
### Strategic-map contextual
|
||||
| Trigger | Site | When |
|
||||
|---------|------|------|
|
||||
| `province_selected` | `EagleGameController.ProvinceWasSelected()` | Province click |
|
||||
| `command_issued` | `EagleGameController.PostCommittedCommand()` | User commits |
|
||||
| `diplomacy_available` | `CheckStrategicCommandsAvailable()` ~L243 | Diplomacy command appears |
|
||||
| `weather_control_available` | `CheckStrategicCommandsAvailable()` ~L249 | Weather command appears |
|
||||
| `hero_recruitment_available` | `CheckHeroRecruitmentAvailable()` ~L266 | Free heroes detected |
|
||||
| `tutorial_ready_to_join` | `CheckReadyToJoinHero()` ~L479 | Free hero with `WouldJoin` |
|
||||
| `profession_<X>_encountered` | `CheckProfessionTutorial()` ~L726 | First time seeing each profession |
|
||||
|
||||
### Strategic guidance (state-derived, conditions checked each model update)
|
||||
| Trigger | Site | Condition |
|
||||
|---------|------|-----------|
|
||||
| `guidance_loyalty_danger` | `CheckLoyaltyDanger()` ~L339 | November + hero loyalty < 70 |
|
||||
| `guidance_neighbor_danger` | `CheckNeighborDanger()` ~L383 | Hostile faction adjacent |
|
||||
| `guidance_recruit_heroes` | `CheckRecruitHeroesGuidance()` ~L420 | Province with support ≥40 has free heroes |
|
||||
| `guidance_expand` | `CheckExpandGuidance()` ~L449 | One stable province, hasn't expanded |
|
||||
| `guidance_sworn_kinship` | `CheckSwornKinshipGuidance()` ~L549 | Good candidate or 4+ provinces |
|
||||
| `tutorial_loyalty_warning` | `CheckTutorialLoyaltyWarning()` ~L634 | November + hero loyalty < 70 |
|
||||
| `tutorial_support_deadline` | `CheckTutorialSupportDeadline()` ~L654 | December + province support < 40 |
|
||||
|
||||
### Strategic action-result triggers (fired from `OnStrategicActionResult()`)
|
||||
| Trigger | Site | When |
|
||||
|---------|------|------|
|
||||
| `hero_stat_gained` | ~L281 | `HeroStatGained` action result |
|
||||
| `hero_profession_gained` | ~L284 | `ProfessionGained` action result |
|
||||
| `tutorial_faction_appears` | ~L287 | `TutorialFactionAppears` action — the Fracture Covenant landing |
|
||||
| `tutorial_hero_faction_appears` | ~L290 | `TutorialHeroFactionAppears` action; has a four-step strategic dialogue today |
|
||||
| `tutorial_hero_departed` | ~L299 | First `HeroesDeparted` action against another player (King's hero abandons service) |
|
||||
| `tutorial_hero_departed_again` | ~L302 | Second `HeroesDeparted` in a *different* month than the first |
|
||||
|
||||
### Tactical (battle) — abilities and spells
|
||||
Combat triggers are *paced*: `_combatTutorialPending` ensures only one fires per action, with priority `archery > duel > melee > charge > fire`.
|
||||
|
||||
| Trigger | Site | When |
|
||||
|---------|------|------|
|
||||
| `shardok_placement_started` | `OnBattleEntered()` ~L796 | Battle in setup phase |
|
||||
| `shardok_battle_started` | `OnBattleEntered()` ~L793 | Battle begins |
|
||||
| `shardok_battle_running` | `OnTacticalCommandsAvailable()` ~L948 | Player's first turn |
|
||||
| `archery_available` | `OnTacticalCommandsAvailable()` ~L990 | Archery available |
|
||||
| `melee_available` | `OnTacticalCommandsAvailable()` ~L991 | Melee available |
|
||||
| `duel_available` | `OnTacticalCommandsAvailable()` ~L1005 | Duel available (non-Tarn target) |
|
||||
| `ability_charge_available` | `OnTacticalCommandsAvailable()` ~L1001 | Charge for Old Marek |
|
||||
| `start_fire_available` | `OnTacticalCommandsAvailable()` ~L1021 | Start Fire on enemy |
|
||||
| `hide_available` | `OnTacticalCommandsAvailable()` ~L985 | Hedrick can hide |
|
||||
| `thunderstorm` | `OnTacticalCommandsAvailable()` ~L1056 | Weather is thunderstorm |
|
||||
| `spell_lightning_available` | `OnTacticalCommandsAvailable()` ~L960 | Lightning available |
|
||||
| `spell_meteor_available` | `OnTacticalCommandsAvailable()` ~L963 | Meteor available |
|
||||
| `spell_holywave_available` | `OnTacticalCommandsAvailable()` ~L965 | Holy Wave available |
|
||||
| `spell_raisedead_available` | `OnTacticalCommandsAvailable()` ~L969 | Raise Dead available |
|
||||
| `spell_lightning_cast` / `spell_meteor_cast` / `spell_holywave_cast` / `spell_raisedead_cast` | `CheckTacticalActionType()` ~L830-838 | Spell action observed |
|
||||
| `ability_charge_used` | `CheckTacticalActionType()` ~L842 | Charge attack executed |
|
||||
| `terrain_fire_encountered` | `CheckTacticalActionType()` ~L864 | Fire damage / spread |
|
||||
| `terrain_water_encountered` | `CheckTacticalActionType()` ~L867 | Water crossing |
|
||||
| `engineer_near_enemy` | `CheckEngineerNearEnemy()` ~L1105 | John Ranil within 3 hexes of enemy |
|
||||
| `tutorial_reinforcement_engineer` / `tutorial_reinforcement_paladin` | `CheckTacticalActionType()` ~L857 | Reinforcement arrival |
|
||||
| `friendly_hero_captured` | `CheckHeroRemovals()` ~L919 | Friendly hero unit removed |
|
||||
| `enemy_hero_captured` | `CheckHeroRemovals()` ~L928 | Enemy hero unit removed |
|
||||
| `battle_action` | `ShardokGameController.OnBattleAction()` | Any action result |
|
||||
| `turn_ended` | `ShardokGameController.OnTurnEnded()` ~L1137 | Battle turn ends |
|
||||
| `shardok_battle_reset` | `ShardokGameController.cs:627` (fired *directly* to `DialogueManager`, bypassing the registry) | Battle retry; the registry's battle flags and the battle dialogue scripts' completed-set are reset alongside, so per-battle tutorials re-fire |
|
||||
|
||||
### One-shot semantics
|
||||
Most contextual triggers are guarded by booleans on the registry (e.g., a `_diplomacyShown` flag). These are session-local — they don't persist via `TutorialState`, but the registry has `ResetBattleTriggerFlags()` for replays. The strategic completion list *does* persist across sessions.
|
||||
|
||||
---
|
||||
|
||||
## Highlight Targets
|
||||
|
||||
Steps reference UI by string ID through `TutorialTargetRegistry`:
|
||||
|
||||
1. Static targets are wired in the Inspector on the registry.
|
||||
2. Dynamic targets register at runtime (`RegisterTarget(id, rectTransform)`).
|
||||
3. Lookup falls back to `GameObject.Find()` if not registered.
|
||||
|
||||
Step options:
|
||||
- `TargetGameObjectPath` — primary highlight
|
||||
- `AdditionalHighlightTargets[]` — multiple at once
|
||||
- `HighlightBoundsFromChildren` — use children's bounding box
|
||||
- `HighlightPulsing` — strobe animation
|
||||
|
||||
Dialogue scripts use the same registry via `highlightTarget` (and `persistHighlight: true` to keep the highlight after the dialogue closes).
|
||||
|
||||
---
|
||||
|
||||
## Sequences vs. Contextual Dispatch
|
||||
|
||||
- **Onboarding sequence** (`IsOnboarding = true`) — Single linear sequence, started by `StartOnboarding()`. Resumes from `OnboardingStepReached`. Counts only visible steps for progress.
|
||||
- **Contextual** (`IsOnboarding = false`) — Single- or multi-step, dispatched on demand by `TriggerContextualTutorial()`.
|
||||
- Dispatch rules in `TriggerContextualTutorial()`:
|
||||
1. No active sequence → show immediately.
|
||||
2. Active step is hidden (`None`) → interrupt.
|
||||
3. Both are command tutorials → interrupt.
|
||||
4. Otherwise → queue.
|
||||
|
||||
---
|
||||
|
||||
## Dialogue System (Currently Live)
|
||||
|
||||
`DialogueManager` is a parallel system, not a step-UI subclass. It loads every `TextAsset` in `Resources/Dialogues/` at startup, parses them as `DialogueScript` objects, and indexes by `trigger`.
|
||||
|
||||
Script shape (see `tutorial_strategic.json`):
|
||||
```json
|
||||
{
|
||||
"scripts": [{
|
||||
"id": "tutorial_opening",
|
||||
"trigger": "game_started",
|
||||
"panelPosition": null,
|
||||
"steps": [{
|
||||
"speakerName": "Old Marek the Learned",
|
||||
"speakerImagePath": "fixed/old_marek_the_learned.png",
|
||||
"dialogueText": "...",
|
||||
"instructionText": "Click <b>Fight!</b> to enter tactical combat.",
|
||||
"highlightTarget": "FightButton",
|
||||
"persistHighlight": true,
|
||||
"highlightProvince": "Onmaa",
|
||||
"completionEvent": null
|
||||
}]
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
When a registry trigger fires, `OnGameEvent()` calls `DialogueManager.TriggerDialogue(triggerId)`. If a script matches and hasn't completed, it queues (or shows immediately) and the panel renders speaker headshot + body + instruction. Steps can pause for a `completionEvent` (e.g., wait for the player to click the highlighted button), allowing the dialogue to chain across game state changes.
|
||||
|
||||
Dialogue panel position defaults sensibly per scene (`top` during combat) and can be overridden per script.
|
||||
|
||||
---
|
||||
|
||||
## Persistence
|
||||
|
||||
`TutorialState` (PlayerPrefs JSON):
|
||||
- `OnboardingCompleted` (bool)
|
||||
- `OnboardingStepReached` (int)
|
||||
- `CompletedTutorials` (List → HashSet at load)
|
||||
- `DismissedHints` (List → HashSet at load)
|
||||
- `AllTutorialsDisabled` (bool)
|
||||
|
||||
Reset paths:
|
||||
- `TutorialState.Reset()` clears everything.
|
||||
- Entering a tutorial game (`GameType.Tutorial`) resets `TutorialState`; dialogue replay is controlled separately by the per-game dialogue progress key.
|
||||
- Settings → "Reset Tutorials" calls `TutorialManager.ResetAllProgress()`, which also calls `DialogueManager.ResetCompletedScripts()`.
|
||||
|
||||
`TutorialDialogueProgressStore` separately stores completed dialogue script IDs per game under PlayerPrefs keys named `Eagle0_TutorialDialogueProgress_<gameId>`.
|
||||
|
||||
`DialogueManager` keeps the active game's completed script IDs in memory and saves them through `TutorialDialogueProgressStore`. `ResetCompletedScripts()` clears the current game's dialogue progress; `ClearCurrentGameProgress()` deletes the current game's persisted dialogue key.
|
||||
|
||||
---
|
||||
|
||||
## Bootstrap
|
||||
|
||||
1. `TutorialManager` is in the scene as a singleton; `Awake` loads state and creates the trigger registry. `RegisterAll()` is called here but currently no-ops.
|
||||
2. `ConnectionHandler` sets `IsTutorialGame` and `IsMultiplayerGame` when the game is created/joined.
|
||||
3. `EagleGameController.SetUpGame()` calls `TutorialManager.Instance.Initialize(this, null)`. If `IsTutorialGame && !OnboardingCompleted`, it would call `StartOnboarding()` — currently a no-op because `OnboardingSequence` is null.
|
||||
4. The first model update raises `game_started`. `DialogueManager` matches `tutorial_opening` from `tutorial_strategic.json` and the dialogue runs.
|
||||
5. `ShardokGameController.SetUpGame()` calls `TutorialManager.Instance.Initialize(null, this)` when battle starts; raises `battle_entered` and battle-only triggers from there.
|
||||
|
||||
---
|
||||
|
||||
## Expected Tutorial Flow (today)
|
||||
|
||||
Driven by `tutorial_strategic.json` + `tutorial_battle.json`. Old Marek the Learned narrates almost everything.
|
||||
|
||||
### Narrative arc
|
||||
|
||||
You play **Sadar Rakon**, formerly Ikhaan Tarn's most trusted lieutenant. Tarn's behavior grew erratic, the King wouldn't listen, so Sadar broke off and raised the **Reclamation**. He's been pushed back to **Onmaa** for a last stand against Tarn's superior royal army. After surviving, the real twist arrives: a new invasion — **The Fracture Covenant**, led by a mysterious figure called *The Eagle* — lands ships on the western coast and starts taking provinces. Heroes from the King's own service mysteriously begin to desert. The Reclamation's framing pivots from "rebellion" to "the only force that can stop the actual invasion."
|
||||
|
||||
### Strategic-map flow (`tutorial_strategic.json`)
|
||||
|
||||
| # | Trigger | What happens |
|
||||
|---|---------|--------------|
|
||||
| 1 | `game_started` | **Opening monologue** (3 steps): Sadar's backstory, the Reclamation, last stand at Onmaa. Ends highlighting the **Fight!** button (`FightButton`, `persistHighlight`) |
|
||||
| 2 | `tutorial_battle_ended` | **Aftermath**: Tarn has *vanished*; you've captured his lieutenants. Recruit them or hold them |
|
||||
| 3 | `tutorial_rebuild_support` | **Rebuild Onmaa** (3 steps): Marek frames it; **John Ranil** introduces *Improve* (engineer bonus); **Elena Fyar** introduces *Give Alms* (paladin bonus). Goal: 40 support by January |
|
||||
| 4 | `tutorial_loyalty_warning` | November-ish, low-loyalty hero may leave at year end → use *Give Gold* / *Feast* |
|
||||
| 5 | `tutorial_support_deadline` | December nag if support < 40: have Elena give alms now |
|
||||
| 6 | `guidance_expand` (script `tutorial_expansion`) | Once support is stable: keep developing, taxes coming in January |
|
||||
| 7 | `tutorial_taxes_collected` | January: gold/food collected. Now expand — March your warlord + most heroes to a neighbor, leave 1–2 behind |
|
||||
| 8 | `tutorial_ready_to_join` | A free hero in `{provinceName}` is ready to recruit → Travel + Recruit |
|
||||
| 9 | `tutorial_in_town` | When you Travel: explains Trade / Arm Troops / Divine / Recruit / Manage Prisoners; *Return* ends the turn |
|
||||
| 10 | `tutorial_faction_appears` | **The twist**: Fracture Covenant lands at Ingia and Soria. *The Eagle* commands them. Tarn may be with them |
|
||||
| 11 | `tutorial_hero_departed` | First King's hero abandons service — "something deeper is at work" |
|
||||
| 12 | `tutorial_hero_departed_again` | Second one in a different month — "something is wrong, I can feel it" (foreshadowing) |
|
||||
| 13 | `tutorial_hero_faction_appears` | John Ranil and Elena Fyar leave Sadar's service and reappear as independent tutorial factions |
|
||||
|
||||
### Tactical battle flow (`tutorial_battle.json`)
|
||||
|
||||
| # | Trigger | What happens |
|
||||
|---|---------|--------------|
|
||||
| 1 | `shardok_placement_started` | **Placement** (3 steps): identify Tarn's units (knights, heavy infantry, dragoons), color legend (red/blue/black), placement instructions, highlight Commit |
|
||||
| 2 | `shardok_battle_running` | **Turn 1 strategy** (2 steps): victory conditions for both sides, advice to hold castles and End Turn |
|
||||
| 3 | `archery_available` | First archery opportunity → purple outlines, longbows vs. armor |
|
||||
| 4 | `melee_available` | First melee opportunity → right-click adjacent enemy |
|
||||
| 5 | `ability_charge_available` | Charge mechanics (Marek hams it up: "old scholar without a horse") |
|
||||
| 6 | `start_fire_available` | Start Fire on enemy hex, fire spread mechanics |
|
||||
| 7 | `thunderstorm` | Weather: archery disabled, fires extinguished |
|
||||
| 8 | `tutorial_reinforcement_paladin` | **Elena Fyar arrives** mid-battle (2-step exchange) → Paladin profession intro (Holy Wave) |
|
||||
| 9 | `tutorial_reinforcement_engineer` | **John Ranil arrives** (2-step exchange) → Engineer profession intro |
|
||||
| 10 | `engineer_near_enemy` | Once Ranil is close to enemy: Fortify + Reduce (siege bombardment) |
|
||||
| 11 | `duel_available` | Champion duel mechanics (Marek warns *not* to duel Tarn himself) |
|
||||
| 12 | `hide_available` | Hedrick the Hedge-merchant: forest/swamp Hide + ambush |
|
||||
| 13 | `enemy_hero_captured` / `friendly_hero_captured` | Capture mechanics, with `{heroName}` substitution |
|
||||
| 14 | `shardok_battle_reset` | If you lose and replay: Marek "had the strangest sensation… as though we'd already fought this battle, and lost" — fourth-wall-adjacent retry framing |
|
||||
|
||||
Most remaining "Tutorial / first-session onboarding" items in `SMALL_EAGLE_TODO.md` (Shardok tutorial, narrative hook, first-session goal, early small victory, guided first scenario vs. sandbox) map to *adding new dialogue scripts* against existing trigger IDs, or — if the step-UI is revived — to populating `TutorialContentDefinitions.RegisterAll()` and removing the early return.
|
||||
|
||||
---
|
||||
|
||||
## Notable Behaviors and Gotchas
|
||||
|
||||
- **`IsTutorialGame` gates everything.** Outside tutorial games, both subsystems short-circuit early.
|
||||
- **Combat tutorials are deliberately paced.** Don't expect every available-ability trigger to fire on its first eligible turn — the registry intentionally spaces them.
|
||||
- **One-shot flags are session-local.** A trigger that "already fired" will not re-fire even if `TutorialState` is reset, until the registry is recreated.
|
||||
- **`TutorialTargetRegistry` lookups silently fall back to `GameObject.Find`.** Typos in target IDs will render with no highlight rather than throwing.
|
||||
- **`TutorialContentDefinitions.RegisterAll()` returning early is load-bearing.** If the step UI is revived without auditing trigger overlap, dialogue and step-UI tutorials may double-fire on the same trigger.
|
||||
- **`Tutorial/TUTORIAL_PLAN.md`** in the Unity project is an older implementation plan and partially overlaps this doc.
|
||||
@@ -1,19 +0,0 @@
|
||||
# Two-Stage Animation Implementation Pattern
|
||||
|
||||
Actions with server-determined success/failure outcomes use a two-stage system:
|
||||
1. **Attempt phase**: Animation + sound plays immediately when the command is issued
|
||||
2. **Result phase**: A distinct success or failure animation + sound plays when the server responds
|
||||
|
||||
## Adding a New Two-Stage Action
|
||||
|
||||
To add distinct success/failure animations for an action (using `ExtinguishFire` as an example):
|
||||
|
||||
1. **AnimationType enum** (`ShardokGameController.cs`): Add `ExtinguishFireFailed`
|
||||
2. **AnimationTypeForAction()**: Map `ActionType.ExtinguishFireFailed` to new type
|
||||
3. **AttemptAnimationType()**: Map `ExtinguishFireFailed` back to `ExtinguishFire`
|
||||
4. **ActionTypeForSound()**: Map new type to `ActionType.ExtinguishFireFailed`
|
||||
5. **PlayAnimation()**: Add dispatch case calling the new animator method
|
||||
6. **Animator**: Add `AnimateExtinguishFailed()` method
|
||||
7. **SoundManager**: Ensure result sound is mapped (usually already done)
|
||||
|
||||
See FearAnimator and FleeAnimator for reference implementations.
|
||||
@@ -1,290 +0,0 @@
|
||||
# URP Migration Completion Notes
|
||||
|
||||
Last refreshed: 2026-06-14
|
||||
|
||||
## Why This Matters
|
||||
|
||||
Unity is steering projects away from the Built-in Render Pipeline (BiRP) toward the
|
||||
Universal Render Pipeline (URP). Eagle0 has completed the URP migration and now runs
|
||||
on URP:
|
||||
|
||||
- `ProjectSettings/GraphicsSettings.asset` points at
|
||||
`Assets/Settings/URP/Eagle0URPPipeline.asset`.
|
||||
- Every active quality level in `ProjectSettings/QualitySettings.asset` uses the
|
||||
same URP pipeline asset.
|
||||
- Legacy Post Processing Stack v2 has been removed from `Packages/manifest.json`;
|
||||
keep verifying it stays out with the migration inventory.
|
||||
|
||||
The migration is no longer an active compatibility project. Keep this document as the
|
||||
completion record and maintenance checklist for future Unity, URP, shader, material,
|
||||
camera layering, or render-order changes.
|
||||
|
||||
## Current Recommendation
|
||||
|
||||
Keep URP enabled on main. Do not continue speculative URP cleanup. Use the baseline
|
||||
checklist and inventory tooling only when a future change touches shaders, materials,
|
||||
camera layering, render order, pipeline settings, or a visible rendering regression.
|
||||
|
||||
Maintenance rules:
|
||||
|
||||
1. Keep the visual baseline current for Connection, Eagle, Shardok, and Settings.
|
||||
2. Refresh the shader/material inventory after meaningful asset or scene changes.
|
||||
3. Remove or isolate unused rendering dependencies.
|
||||
4. Keep the existing CI guardrails passing.
|
||||
5. Treat future pipeline experiments as separate branches with explicit findings.
|
||||
|
||||
Known URP regressions found during playtesting have been fixed. Normal gameplay work
|
||||
can proceed; future rendering PRs should be driven by observable regressions or
|
||||
intentional rendering changes.
|
||||
|
||||
## Maintenance Workflow
|
||||
|
||||
### 1. Create Visual Baselines
|
||||
|
||||
Capture a small set of known-good views before any future change to URP settings,
|
||||
custom shaders, render ordering, or third-party visual materials. These should be
|
||||
reproducible enough that a human can compare screenshots after each rendering change.
|
||||
|
||||
Minimum baseline scenes and states:
|
||||
|
||||
| Area | Required View |
|
||||
|---|---|
|
||||
| Connection | Connect panel, stored account list, running games list |
|
||||
| Eagle map | Normal province map with borders, faction highlights, selected province tooltip |
|
||||
| Eagle weather | Drought/heat shimmer, flood, blizzard/rain overlays |
|
||||
| Eagle beasts | Bird/animal/monster beast effects on map and notification panel |
|
||||
| Eagle UI | Command panel, notifications, settings panel, generated text |
|
||||
| Shardok | Terrain, grid lines, labels/icons, bridges, fires, overlays, command buttons |
|
||||
| Scene transitions | Connection -> Eagle -> Shardok -> Eagle |
|
||||
|
||||
Keep the manual checklist and editor-only capture tool working so rendering PRs can
|
||||
leave screenshots in an ignored directory.
|
||||
|
||||
Use `docs/URP_VISUAL_BASELINE_CHECKLIST.md` for the current manual baseline capture
|
||||
set.
|
||||
|
||||
### 2. Refresh The Shader And Material Inventory When Needed
|
||||
|
||||
The inventory is useful before significant rendering work. It reports:
|
||||
|
||||
- all shaders under `Assets/`
|
||||
- all materials and the shader each material uses
|
||||
- all scenes/prefabs that reference custom shaders or Post Processing components
|
||||
- all usages of Built-in-only shader features:
|
||||
- `GrabPass`
|
||||
- `#pragma surface`
|
||||
- `UnityCG.cginc`
|
||||
- `UnityUI.cginc`
|
||||
- tessellation
|
||||
|
||||
Run `Eagle0 > Rendering > Generate URP Migration Inventory` in the Unity editor to
|
||||
generate the current report. The tool writes markdown and CSV files under Unity
|
||||
`Temp/urp_migration/`, so running it should not dirty project assets.
|
||||
|
||||
The generated CSVs include both the full material list and production material
|
||||
references, which helps separate game-path shader risk from unused package/demo
|
||||
assets.
|
||||
|
||||
`docs/URP_SHADER_DEBT_INVENTORY.md` records the current production-vs-package
|
||||
classification for remaining Built-in-style shader patterns.
|
||||
|
||||
Current inventory snapshot:
|
||||
|
||||
- Render pipeline settings are URP for both graphics default and active quality.
|
||||
- Post Processing Stack reference hits are still zero.
|
||||
- The all-material inventory is noisy: most material assets are `Standard`, and
|
||||
many Built-in-only shader hits are in third-party or package/demo assets.
|
||||
- The production material-reference report is the better starting point for the
|
||||
probe. Current production shader references are concentrated in TextMeshPro,
|
||||
Eagle custom map/effect shaders, Shardok grid/fire materials, and a small number
|
||||
of standard-material beast assets.
|
||||
- The project-owned Eagle and Shardok production custom shaders have been hardened
|
||||
for URP and passed Windows Unity builds after merge.
|
||||
- Project-owned runtime-created Eagle/Shardok materials now use serialized shader or
|
||||
material references instead of project shader-name fallbacks.
|
||||
- No project-owned production shader under `Assets/Eagle` or `Assets/Shardok`
|
||||
currently contains the scanned Built-in-era shader patterns.
|
||||
- Package/third-party shader debt remains documented but is not on the production
|
||||
path unless a future inventory run finds new references.
|
||||
|
||||
### 3. Verify Legacy Post Processing Usage
|
||||
|
||||
The project no longer depends on `com.unity.postprocessing`. The old references were
|
||||
limited to an unused Orc/Ogre sample scene profile and scene-template type metadata,
|
||||
not production scenes.
|
||||
|
||||
Keep the project free of Post Processing Stack v2 references and use the inventory
|
||||
report to catch regressions.
|
||||
|
||||
If references reappear:
|
||||
|
||||
- document the exact scene/camera/profile usage
|
||||
- either remove the stale dependency or plan the equivalent URP Volume settings
|
||||
|
||||
### 4. Keep Drought Effects URP-Friendly
|
||||
|
||||
The old `Eagle/HeatShimmer` shader used a Built-in-style `GrabPass`, but shimmer was
|
||||
already disabled in `DroughtEffect` because it did not work well with the UI Canvas.
|
||||
Drought now intentionally uses the animated sun overlay as its current visual signal.
|
||||
|
||||
Keep drought visuals free of `GrabPass` unless we intentionally add a new
|
||||
URP-friendly effect.
|
||||
|
||||
If stronger drought visuals are needed later, prefer:
|
||||
|
||||
- an animated transparent texture overlay clipped like the existing province particles
|
||||
- a small particle/sprite effect around the sun icon
|
||||
- a URP renderer feature only if screen-space distortion is truly worth the complexity
|
||||
|
||||
### 5. Leave Third-Party Shader Conversion Demand-Driven
|
||||
|
||||
The riskiest third-party shader set is Polytope Studio. Do not hand-convert package
|
||||
shader sets speculatively.
|
||||
|
||||
`docs/URP_THIRD_PARTY_ASSET_AUDIT.md` records the current production GUID-reference
|
||||
scan. The important result is that production assets use Polytope character prefabs
|
||||
for Eagle beast effects, but the Polytope environment/water assets that still contain
|
||||
`GrabPass` were not found on the production path.
|
||||
|
||||
If a future visible regression or inventory update makes third-party conversion
|
||||
necessary, record:
|
||||
|
||||
- current installed version
|
||||
- whether a URP-compatible version exists
|
||||
- whether upgrading is license/account-accessible
|
||||
- whether the package is still used in production scenes
|
||||
|
||||
### 6. Keep Probe Guidance For Future Pipeline Experiments
|
||||
|
||||
The original disposable probe has served its main purpose: URP is enabled on main and
|
||||
the biggest visual regressions were fixed in focused PRs. Keep the probe playbook as
|
||||
a template for future risky pipeline experiments, not as current migration guidance.
|
||||
|
||||
Use `docs/URP_PROBE_PLAYBOOK.md` when we need to measure a future Unity or render
|
||||
pipeline change cheaply before committing to a mergeable branch.
|
||||
|
||||
## Known Maintenance Risks
|
||||
|
||||
| Risk | Why It Matters | Right-Now Action |
|
||||
|---|---|---|
|
||||
| `UnityUI.cginc` usage in package shaders | URP does not provide the same include path/semantics for hand-authored shaders | Leave package-managed TMP shaders alone unless a visible regression appears. |
|
||||
| `GrabPass` in unused package shaders | No direct URP equivalent | Ignore until production starts referencing those assets. |
|
||||
| Surface shaders in third-party packages | URP does not support Built-in surface shader generation | Covered by beast material compatibility on current production paths; convert only for a visible regression. |
|
||||
| Post Processing Stack v2 | Replaced by URP Volume system | Keep package and runtime/project-setting references from returning. |
|
||||
| Scene lighting | URP lighting/shadow settings differ from BiRP | Baseline and rebake only for visible regressions or intentional lighting work. |
|
||||
| Render ordering | Shardok and Eagle overlays depend on careful layering | Include affected overlays in baseline whenever render order changes. |
|
||||
|
||||
## Follow-Up Strategy
|
||||
|
||||
Main now carries the completed URP switch. Future rendering work should be split by
|
||||
observable behavior:
|
||||
|
||||
- **Compatibility fixes**: small PRs for specific pink/missing/dark assets.
|
||||
- **Render ordering fixes**: small PRs for Shardok/Eagle layer order regressions.
|
||||
- **Inventory/docs updates**: docs-only or editor-tool PRs that do not alter runtime
|
||||
visuals.
|
||||
- **Pipeline experiments**: isolated branches using the probe playbook, with findings
|
||||
copied into a mergeable follow-up.
|
||||
|
||||
This keeps the project from accumulating another broad, hard-to-revert rendering PR.
|
||||
|
||||
## Completed Phases
|
||||
|
||||
### Phase 0: Preparation And Switch
|
||||
|
||||
- Visual baseline checklist/screenshots: complete, keep current
|
||||
- Repeatable shader/material inventory: complete, keep current
|
||||
- Post Processing Stack usage decision: complete, keep removed
|
||||
- Drought visual decision: complete, use non-GrabPass visual signal
|
||||
- Third-party URP availability check: documented
|
||||
- Disposable URP probe findings: superseded by the merged URP switch
|
||||
- CI guardrails for pipeline settings, runtime shader lookup, project shader
|
||||
patterns, and beast-material compatibility: complete
|
||||
|
||||
### Phase 1: Pipeline Setup
|
||||
|
||||
- Install/configure URP: complete
|
||||
- Create URP Pipeline Asset and Renderer Asset: complete
|
||||
- Configure forward rendering, shadows, HDR, and renderer features: complete enough
|
||||
for current gameplay paths
|
||||
- Run Unity's Render Pipeline Converter: complete for the merged switch
|
||||
- Keep a record of every auto-converted material: use the inventory/audit tools
|
||||
|
||||
### Phase 2: Runtime Visual QA And Cleanup
|
||||
|
||||
Project-owned custom shader conversion, shader-name fallback cleanup, and deletion
|
||||
cleanup are complete. Current CI coverage keeps the URP pipeline assigned, required
|
||||
runtime shader references serialized, required player-runtime-only shaders included,
|
||||
project-owned Eagle/Shardok shaders free of scanned Built-in-only patterns, key
|
||||
Addressables labels covered, and major Eagle/Shardok visual wiring guarded.
|
||||
|
||||
Runtime visual QA was completed during playtesting. Known regressions from that pass
|
||||
were fixed before the migration was called done.
|
||||
|
||||
Follow-up notes:
|
||||
|
||||
- Use `docs/URP_VISUAL_BASELINE_CHECKLIST.md` for each future rendering follow-up.
|
||||
- Keep screenshots in an ignored local directory, not in git.
|
||||
- Treat package/third-party shader conversions as response work for visible
|
||||
regressions or new production references, not as speculative cleanup.
|
||||
|
||||
### Phase 3: GrabPass And Special Effects Maintenance
|
||||
|
||||
- Keep drought visuals on the current animated sun overlay unless a new URP-friendly
|
||||
effect is intentionally added.
|
||||
- Reimplement or replace the Polytope `PT_Water_Shader` GrabPass shaders if they are
|
||||
ever become used by production assets.
|
||||
- Avoid leaving screen-space effects until the end; they are likely to drive renderer
|
||||
feature requirements.
|
||||
|
||||
### Phase 4: Third-Party Shaders Maintenance
|
||||
|
||||
- Upgrade packages where vendor URP shaders exist and a production need appears.
|
||||
- Convert remaining PBR/toon/vegetation shaders manually only for production
|
||||
references or visible regressions.
|
||||
- Prefer replacement or deletion for unused demo-only assets.
|
||||
|
||||
### Phase 5: Materials, Lighting, And Post Processing Maintenance
|
||||
|
||||
- Batch-update materials only for production paths or visible regressions.
|
||||
- Replace Post Processing Stack v2 with URP Volumes only if a production need returns.
|
||||
- Rebake lighting for production scenes when scene lighting is intentionally changed.
|
||||
- Tune shadow and HDR settings when visual QA or performance work justifies it.
|
||||
|
||||
### Phase 6: Validation Maintenance
|
||||
|
||||
Required visual QA for future rendering changes:
|
||||
|
||||
- Connection lobby
|
||||
- Eagle map and command flows
|
||||
- Eagle weather/beasts/notifications
|
||||
- Shardok battle map overlays, unit text/icons, bridges, fires
|
||||
- Scene transitions
|
||||
- Mac and Windows builds
|
||||
- Performance sanity check
|
||||
|
||||
## Rough Effort
|
||||
|
||||
| Work | Status |
|
||||
|---|---|
|
||||
| Preparation/probe | Complete |
|
||||
| Pipeline setup | Complete |
|
||||
| Core custom shaders | Complete |
|
||||
| Runtime shader/build guardrails | Complete; project-owned runtime paths use serialized shader/material references |
|
||||
| GrabPass/special effects | Deferred until a production reference or new effect need appears |
|
||||
| Third-party shaders/materials | Covered by runtime compatibility and visual QA unless a visible regression appears |
|
||||
| Lighting/post-processing | No current Post Processing Stack dependency; rebake/tune only for visible regressions |
|
||||
| QA/polish | Completed for the migration; repeat for future rendering changes |
|
||||
|
||||
## Key Reminders
|
||||
|
||||
- URP is already enabled on main; keep follow-up work narrowly scoped.
|
||||
- Project-owned shader-name fallbacks should stay out of runtime-created materials;
|
||||
use serialized shader/material references instead.
|
||||
- `Shader.Find` remains acceptable for Unity/URP-provided shader names inside
|
||||
third-party compatibility shims, where there is no project asset to serialize.
|
||||
- Do not hand-convert package shaders unless runtime visual QA exposes a concrete
|
||||
regression or a new production reference appears.
|
||||
- Keep C# changes minimal; current rendering scripts mostly set material properties.
|
||||
- Preserve recent Shardok render-order behavior while changing pipelines.
|
||||
- Treat screenshots as migration tests; compilation alone is not meaningful here.
|
||||
@@ -1,133 +0,0 @@
|
||||
# URP Probe Playbook
|
||||
|
||||
Last refreshed: 2026-06-14
|
||||
|
||||
This playbook is historical reference for the disposable URP probe described in
|
||||
`docs/URP_MIGRATION_PLAN.md`. The URP migration is complete; keep this only as a
|
||||
template for future risky render-pipeline experiments. A probe branch is not a
|
||||
production branch and should not be merged as-is.
|
||||
|
||||
## Goals
|
||||
|
||||
- Use the already-installed URP package in an isolated branch.
|
||||
- Create the minimum URP pipeline/renderer assets needed to enter Play Mode.
|
||||
- Run Unity's Render Pipeline Converter.
|
||||
- Record compile errors, pink materials, broken shaders, and visual regressions.
|
||||
- Preserve findings in docs, then discard or reset the probe branch.
|
||||
|
||||
## Branch Shape
|
||||
|
||||
Use two branches:
|
||||
|
||||
| Branch | Purpose | Mergeable |
|
||||
|---|---|---|
|
||||
| `codex/urp-probe` | Disposable package/settings/material conversion experiment | No |
|
||||
| `codex/urp-probe-findings` | Small docs-only summary copied from the probe | Yes |
|
||||
|
||||
Do not open a PR from `codex/urp-probe` unless it is clearly marked draft and
|
||||
experimental. Prefer opening the PR from `codex/urp-probe-findings`.
|
||||
|
||||
## Before Starting
|
||||
|
||||
1. Confirm `origin/main` is fresh.
|
||||
2. Confirm the worktree is clean.
|
||||
3. Capture or review the baseline checklist in
|
||||
`docs/URP_VISUAL_BASELINE_CHECKLIST.md`.
|
||||
4. Generate a current inventory with
|
||||
`Eagle0 > Rendering > Generate URP Migration Inventory`.
|
||||
5. Keep the inventory output under Unity `Temp/urp_migration/`; do not commit
|
||||
generated reports unless they are intentionally summarized.
|
||||
|
||||
## Probe Steps
|
||||
|
||||
1. Create `codex/urp-probe` from `origin/main`.
|
||||
2. Confirm `com.unity.render-pipelines.universal` is installed in
|
||||
`Packages/manifest.json`.
|
||||
3. If testing new pipeline settings, create alternate URP Pipeline/Renderer assets
|
||||
under a clearly named temporary folder.
|
||||
4. Assign the alternate URP pipeline asset in:
|
||||
- `ProjectSettings/GraphicsSettings.asset`
|
||||
- `ProjectSettings/QualitySettings.asset`
|
||||
5. Run Unity's Render Pipeline Converter only if the experiment intentionally tests
|
||||
material conversion.
|
||||
6. Enter Play Mode from `Assets/Scenes/Main.unity`.
|
||||
7. Exercise the baseline views:
|
||||
- Connection lobby
|
||||
- Eagle map
|
||||
- Eagle weather and beast effects
|
||||
- Shardok terrain, grid, labels, bridges, fires, overlays
|
||||
- Connection -> Eagle -> Shardok -> Eagle transitions
|
||||
8. Record the exact failure mode for every broken visual:
|
||||
- compile error
|
||||
- pink material
|
||||
- invisible mesh or sprite
|
||||
- wrong render order
|
||||
- lighting/shadow difference
|
||||
- input or scene-load regression
|
||||
|
||||
## Expected High-Risk Areas
|
||||
|
||||
- Eagle map rendering:
|
||||
- province colors, borders, and ocean animation
|
||||
- weather overlays and province particles
|
||||
- beast effects on the map and in notifications
|
||||
- Shardok map rendering:
|
||||
- terrain, bridges, fire overlays, and grid overlays
|
||||
- foreground UI/effect overlays
|
||||
- unit labels/icons over fires, bridges, and one-shot animations
|
||||
- Third-party 3D materials:
|
||||
- Polytope Studio character prefabs used by Eagle human-type beast effects
|
||||
- RRFreelance Orc/Ogre materials used by `OgreEffect`
|
||||
- Render ordering in Shardok:
|
||||
- unit text/icons versus fires
|
||||
- unit text/icons versus bridges
|
||||
- selection/command overlays versus labels
|
||||
|
||||
## Findings Template
|
||||
|
||||
Copy this template into the mergeable findings branch after the probe:
|
||||
|
||||
```markdown
|
||||
# URP Probe Findings
|
||||
|
||||
Date:
|
||||
Unity version:
|
||||
URP package version:
|
||||
Probe branch commit:
|
||||
|
||||
## Summary
|
||||
|
||||
- Entered Play Mode:
|
||||
- Connection usable:
|
||||
- Eagle usable:
|
||||
- Shardok usable:
|
||||
- Build attempted:
|
||||
|
||||
## Converter Results
|
||||
|
||||
- Materials converted:
|
||||
- Materials left pink:
|
||||
- Shader compile errors:
|
||||
|
||||
## Visual Regressions
|
||||
|
||||
| Area | Expected | Actual | Likely Cause | Next Action |
|
||||
|---|---|---|---|---|
|
||||
|
||||
## Required Migration PRs
|
||||
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
|
||||
## Notes
|
||||
|
||||
-
|
||||
```
|
||||
|
||||
## After The Probe
|
||||
|
||||
1. Copy findings into a docs-only branch.
|
||||
2. Delete or abandon `codex/urp-probe`.
|
||||
3. Do not merge probe-generated package/settings/material churn into main until the
|
||||
findings have been reviewed and broken visuals have owner PRs.
|
||||
@@ -1,145 +0,0 @@
|
||||
# URP Shader Debt Inventory
|
||||
|
||||
Last refreshed: 2026-06-14
|
||||
|
||||
This inventory classifies remaining Built-in Render Pipeline shader patterns after
|
||||
the completed URP switch. The project is running URP, but some package shader source
|
||||
files still use Built-in-era helpers or features. That is package/vendor debt, not
|
||||
evidence that the old pipeline is active and not an active migration blocker.
|
||||
|
||||
## Scan Method
|
||||
|
||||
The scan looked for these patterns in Unity shader-like assets:
|
||||
|
||||
- `GrabPass`
|
||||
- `#pragma surface`
|
||||
- `UnityCG.cginc`
|
||||
- `UnityUI.cginc`
|
||||
- `UNITY_MATRIX_`
|
||||
- `sampler2D _GrabTexture`
|
||||
- `tessell`
|
||||
- `Tessellation`
|
||||
|
||||
It then mapped every matching shader asset to its Unity GUID and scanned production
|
||||
serialized assets under:
|
||||
|
||||
- `Assets/Scenes/`
|
||||
- `Assets/Eagle/`
|
||||
- `Assets/Shardok/`
|
||||
- `Assets/ConnectionHandler/`
|
||||
- `Assets/UI/`
|
||||
- `Assets/common/`
|
||||
- `Assets/Tutorial/`
|
||||
- `Assets/Resources/`
|
||||
- `Assets/Materials/`
|
||||
|
||||
This catches serialized production references. It does not prove runtime code never
|
||||
loads an asset by path, but it separates game-path shader debt from package/demo
|
||||
noise well enough to decide whether future rendering work is needed.
|
||||
|
||||
## Summary
|
||||
|
||||
| Bucket | Shader Assets With Built-in Patterns | Serialized Production References |
|
||||
|---|---:|---:|
|
||||
| Project-owned production shaders | 0 | 0 referenced |
|
||||
| Project-owned runtime-loaded shaders | 0 | 0 serialized refs |
|
||||
| Project-owned unreferenced shaders | 0 | 0 referenced |
|
||||
| TextMeshPro package shaders | 17 | 1 referenced |
|
||||
| Third-party/package shaders | 25 | 0 referenced |
|
||||
|
||||
Project-owned runtime-created materials now use serialized shader or material
|
||||
references for Eagle weather, Eagle fireworks, Shardok foreground UI/effects, and
|
||||
Shardok foreground text. Remaining production `Shader.Find` usage is limited to
|
||||
Unity/URP-provided shader names inside `BeastMaterialCompatibility`, plus
|
||||
third-party package code outside our ownership.
|
||||
|
||||
## Completed Project-Owned Shader Hardening
|
||||
|
||||
These production shaders have been converted to URP-compatible HLSL and passed the
|
||||
Windows Unity build after merge.
|
||||
|
||||
| Shader | Production References | Completion Notes |
|
||||
|---|---|---|
|
||||
| `Assets/Eagle/Shaders/ProvinceMapShader.shader` | `Assets/Eagle/Materials/ProvinceMapMaterial.mat` | Converted with province color, border, ocean, and UI clipping behavior preserved. |
|
||||
| `Assets/Eagle/Shaders/ProvinceWeatherMapShader.shader` | `Assets/Eagle/Materials/ProvinceWeatherMapMaterial.mat` | Converted with weather lookup, overlay alignment, clipping, and initialized effect/tint values. |
|
||||
| `Assets/Eagle/Shaders/ProvinceWeatherShader.shader` | `Assets/Eagle/Weather/BlizzardEffect.mat`, `Assets/Eagle/Weather/DroughtEffect.mat`, `Assets/Eagle/Weather/FloodEffect.mat` | Converted for the live drought/flood/blizzard material path. |
|
||||
| `Assets/Eagle/Shaders/ProvinceParticleShader.shader` | `Assets/Eagle/Effects/ProvinceParticleMaterial.mat` | Converted for Eagle map province particles. Weather effect prefabs serialize the shared particle material instead of relying on shader-name fallback. |
|
||||
| `Assets/Eagle/Shaders/ClipRectParticleUnlit.shader` | `Assets/Eagle/Effects/Fireworks.prefab`, `Assets/Eagle/Effects/ParticleAlphaBlendMaterial.mat`, `Assets/Eagle/Effects/ParticleStandardUnlitMaterial.mat` | Converted for Eagle one-shot effects and clipped particle UI paths; Fireworks serializes this shader instead of using shader-name fallback. |
|
||||
| `Assets/Eagle/maskShader.shader` | Formerly `Assets/Eagle/Materials/map_color_nolabels.mat`, `Assets/Eagle/Materials/map_color_whitened.mat` | Deleted after confirming the serialized material path was stale. |
|
||||
| `Assets/Hex Mesh Shader.shader` | None | Deleted after confirming no serialized or code references to the asset, shader name, or GUID. |
|
||||
| `Assets/Shardok/ShardokFireOverlay.shader` | `Assets/Shardok/ShardokFireOverlay.mat` | Converted with the Shardok fire overlay layering path preserved. |
|
||||
| `Assets/Shardok/Shaders/ForegroundUIOverlay.shader` | Serialized Shardok foreground UI/effect shader references | URP pass is serialized on the Shardok scene/container and included in `ProjectSettings/GraphicsSettings.asset`; the stale Built-in fallback pass has been removed. |
|
||||
| `Assets/TextMesh Pro/Resources/Shaders/TMP_SDF Overlay.shader` | Serialized Shardok foreground text shader references | Serialized on the Shardok scene/container and included in `ProjectSettings/GraphicsSettings.asset` for required foreground text overlay material. |
|
||||
|
||||
## CI Guardrails
|
||||
|
||||
- `URPPipelineSettingsTests` keeps Graphics and Quality settings on the same URP
|
||||
pipeline asset.
|
||||
- `RuntimeShaderValidationTests` keeps required runtime shader assets present,
|
||||
serialized Shardok foreground shader references assigned, and player-runtime-only
|
||||
shaders listed in Always Included Shaders.
|
||||
- `WeatherEffectMaterialTests` keeps Eagle flood/blizzard prefabs wired to the
|
||||
shared URP province particle material and prevents shader-name fallbacks from
|
||||
returning to those runtime paths.
|
||||
- `EpidemicEffectMaterialTests` keeps Eagle epidemic particles wired to the shared
|
||||
clipped particle material.
|
||||
- `EagleMapMaterialWiringTests` keeps Eagle map/weather controllers wired to the
|
||||
project map materials and expected URP shaders.
|
||||
- `ProjectShaderPatternTests` fails if project-owned Eagle/Shardok shaders
|
||||
reintroduce the scanned Built-in-only patterns.
|
||||
- `RuntimeShaderFindUsageTests` fails if project-owned Eagle/Shardok runtime
|
||||
scripts reintroduce shader-name lookups outside documented compatibility shims.
|
||||
- `BeastMaterialCompatibilityTests` keeps runtime third-party beast material
|
||||
conversion covered.
|
||||
- `ProvinceBeastsControllerTests` keeps Eagle beast content names routed to
|
||||
specific animated effect prefabs instead of silently falling back to the generic
|
||||
circling-bird effect.
|
||||
- `FireworksEffectTests` keeps the fireworks prefab wired to the project-owned
|
||||
clipped particle shader and prevents shader-name fallback from returning.
|
||||
- `TutorialSpriteAssetTests` keeps tutorial TMP sprite assets and the shared dialogue
|
||||
panel instruction sprite asset renderable.
|
||||
- `ProvinceActionAnimatorSpriteTests` keeps Eagle action animation sprite fields
|
||||
assigned in the Eagle scene.
|
||||
- `PostProcessingStackRemovalTests` keeps legacy Post Processing Stack v2 package
|
||||
and runtime/project-setting references from returning.
|
||||
- `ProductionShaderReferenceTests` keeps shaders with Built-in-only patterns, and
|
||||
materials that use them, off the production serialized asset path except for the
|
||||
package-managed TextMeshPro shader family.
|
||||
|
||||
## Highest Priority: Referenced Project-Owned Shader Debt
|
||||
|
||||
No project-owned production shader under `Assets/Eagle` or `Assets/Shardok`
|
||||
currently contains the scanned Built-in-era shader patterns.
|
||||
|
||||
| Shader | Patterns | Production References | Recommended Action |
|
||||
|---|---|---|---|
|
||||
| None | None | None | No active migration work. Re-run inventory after future rendering changes. |
|
||||
|
||||
## Referenced Package Shader
|
||||
|
||||
| Shader | Patterns | Production References | Recommended Action |
|
||||
|---|---|---|---|
|
||||
| `Assets/TextMesh Pro/Resources/Shaders/TMP_Sprite.shader` | `UnityCG.cginc`, `UnityUI.cginc`, `UNITY_MATRIX_` | 16 tutorial sprite assets under `Assets/Tutorial/Sprite Assets/` | Leave alone unless Unity/URP surfaces a visible TextMeshPro sprite regression. Prefer package/vendor updates over hand-editing TMP package shaders. |
|
||||
|
||||
## Third-Party And Package Hits Without Serialized Production References
|
||||
|
||||
These files still contain Built-in-only shader patterns, but the current GUID scan
|
||||
did not find serialized production references. Treat them as package/demo cleanup,
|
||||
not as urgent game-path blockers.
|
||||
|
||||
| Package Area | Examples | Patterns | Recommended Action |
|
||||
|---|---|---|---|
|
||||
| Polytope Studio characters/weapons/props | Modular NPC, armor, weapons, props shaders | `#pragma surface`, `UnityCG.cginc` | Keep runtime beast material compatibility in place; only import/replace vendor URP materials if we intentionally remove the compatibility shim. |
|
||||
| Polytope Studio environment/water | Water, vegetation, rock shaders | `GrabPass`, `#pragma surface`, `UnityCG.cginc`, tessellation | Ignore unless production starts referencing these assets. Water shaders are the only remaining `GrabPass` hits. |
|
||||
| RRFreelance Orc/Ogre | Ogre, armor, weapon shaders | `#pragma surface`, `UnityCG.cginc` | Current production references are handled by compatibility/material fixes; do not hand-convert unless a visible ogre regression returns. |
|
||||
| TextMeshPro package shaders | TMP SDF/bitmap/surface shaders | `UnityCG.cginc`, `UnityUI.cginc`, `UNITY_MATRIX_`, surface shader variants | Prefer package updates. Avoid editing package shaders unless a concrete TMP visual regression exists. |
|
||||
| Clown.fat and TileableBridgePack | Toony shaders, bridge cutout shader | `#pragma surface`, `UnityCG.cginc`, `UNITY_MATRIX_` | Leave as low priority unless a production reference appears. |
|
||||
|
||||
## Maintenance Guidance
|
||||
|
||||
Runtime visual QA for the URP migration was completed during playtesting and known
|
||||
regressions from that pass were fixed. Leave unreferenced third-party/package shaders
|
||||
alone until they have a visible regression or production reference.
|
||||
|
||||
Future rendering PRs should use `docs/URP_VISUAL_BASELINE_CHECKLIST.md` and include
|
||||
affected screenshots in an ignored local directory, not in git.
|
||||
@@ -1,122 +0,0 @@
|
||||
# URP Third-Party Asset Audit
|
||||
|
||||
Last refreshed: 2026-06-14
|
||||
|
||||
This audit scopes third-party visual packages after the completed URP migration. It
|
||||
is based on a GUID reference scan from production Unity assets under:
|
||||
|
||||
- `Assets/Scenes`
|
||||
- `Assets/Eagle`
|
||||
- `Assets/Shardok`
|
||||
- `Assets/ConnectionHandler`
|
||||
- `Assets/UI`
|
||||
- `Assets/common`
|
||||
- `Assets/Tutorial`
|
||||
- `Assets/Resources`
|
||||
- `Assets/Materials`
|
||||
|
||||
The scan maps GUIDs from each package folder, then finds production scenes,
|
||||
prefabs, materials, controllers, assets, and scripts that reference those GUIDs.
|
||||
It does not prove runtime code never loads assets by path, but it catches the
|
||||
serialized references that matter most for future render-pipeline maintenance.
|
||||
|
||||
## Summary
|
||||
|
||||
| Package | Production Use | URP Risk | Notes |
|
||||
|---|---:|---|---|
|
||||
| Polytope Studio | 20 refs / 18 assets | Medium | Used by Eagle human-type beast effects. Runtime beast-material compatibility converts active renderers to URP materials; production refs are character prefabs, not environment/water assets. |
|
||||
| GUI Pro Kit Fantasy RPG | 138 refs / 58 assets | Low | Production use is UI sprites/icons. No package shader dependency found in production refs. |
|
||||
| Modern UI Pack v4.2.0 | 23 refs / 6 assets | Low | Production use is UI textures/icons. |
|
||||
| RRFreelance Orc/Ogre | 2 refs / 2 assets | Medium | Ogre effect uses custom Built-in surface shaders in source assets, but active renderers are covered by runtime beast-material compatibility under URP. |
|
||||
| DungeonMonsters2D | 12 refs / 12 assets | Medium | 2D monster prefabs for Eagle beast effects. Expected to be mostly SpriteRenderer/Animator; keep covered in beast-effect visual checks. |
|
||||
| Animal pack deluxe v2 | 7 refs / 7 assets | Medium | Elephant effect and controller refs. Verify lit-material conversion. |
|
||||
| Animal pack deluxe | 24 refs / 24 assets | Medium | Multiple Eagle animal beast effects. Verify lit-material conversion. |
|
||||
| AfricaAnimalsPackLowPoly V2 | 10 refs / 10 assets | Medium | Eagle beast effects and controllers. Verify lit-material conversion. |
|
||||
| AfricaAnimalsPackv1 | 8 refs / 8 assets | Medium | Eagle beast effects and controllers. Verify lit-material conversion. |
|
||||
| AustraliaAnimalsPackv1 | 6 refs / 6 assets | Medium | Eagle beast effects and controllers. Verify lit-material conversion. |
|
||||
| DinoPackLowPolyV1 | 3 refs / 3 assets | Medium | Velociraptor effect and controller. Verify lit-material conversion. |
|
||||
| HEROIC FANTASY CREATURES FULL PACK Vol 2 | 12 refs / 12 assets | Medium | Hippogryph effect and controller. Verify lit-material conversion. |
|
||||
| TileableBridgePack | 0 refs | Low | No production serialized refs found. Shardok bridge visuals appear to use project-owned/runtime assets instead. |
|
||||
| Stylize Water Texture | 1 ref / 1 asset | Low | Eagle province map material uses one water texture, not a shader. |
|
||||
| Terrain Hexes | 2 refs / 1 asset | Low | Shardok scene/container use one rock texture. |
|
||||
| Hex Tiles | 0 refs | Low | No production serialized refs found. |
|
||||
| 4000_Fantasy_Icons | 273 refs / 117 assets | Low | Sprite/icon assets only. |
|
||||
| StrategyGameIcons | 21 refs / 12 assets | Low | Sprite/icon assets only. |
|
||||
| Clown.fat | 14 refs / 10 assets | Medium | Clown effects and animation clips; also retargeted by Polytope human effects. |
|
||||
| Dragon | 2 refs / 1 asset | Medium | Dragon effect mesh/animation. Verify material conversion. |
|
||||
| HoneyBadger | 2 refs / 1 asset | Medium | Honey badger effect mesh/animation. Verify material conversion. |
|
||||
| Raccoon | 2 refs / 1 asset | Medium | Raccoon effect mesh/animation. Verify material conversion. |
|
||||
|
||||
## Runtime Beast Findings
|
||||
|
||||
### Polytope Studio
|
||||
|
||||
Production assets reference Polytope character prefabs for Eagle map effects:
|
||||
|
||||
- `Assets/Eagle/Effects/ArcherEffect.prefab`
|
||||
- `Assets/Eagle/Effects/GiantEffect.prefab`
|
||||
- `Assets/Eagle/Effects/KnightEffect.prefab`
|
||||
- `Assets/Eagle/Effects/MilitiaEffect.prefab`
|
||||
- `Assets/Eagle/Effects/PeasantEffect.prefab`
|
||||
- `Assets/Eagle/Effects/SoldierEffect.prefab`
|
||||
|
||||
These prefabs ultimately use `PT_NPC_Mat`, which references
|
||||
`Polytope Studio/Lowpoly_Characters/Sources/Modular_NPC/Shaders/PT_Modular_NPC_Shader_PBR.shader`.
|
||||
That shader uses Built-in surface shader generation and is not URP-ready as-is.
|
||||
At runtime, Eagle beast effects pass their renderers through
|
||||
`BeastMaterialCompatibility.ConfigureRenderer`, which replaces non-URP materials
|
||||
with URP/Lit materials and bakes Polytope palette textures when needed. Treat this
|
||||
as covered by the compatibility path unless a future visual regression appears.
|
||||
|
||||
Polytope environment and water shaders still exist in the project and include
|
||||
Built-in-only features such as `GrabPass`, `UnityCG.cginc`, surface shaders, and
|
||||
tessellation. However, this scan found no production serialized references to the
|
||||
Polytope environment or water assets. Treat them as demo/package risk, not current
|
||||
runtime risk, unless a later inventory run finds production references.
|
||||
|
||||
### RRFreelance Orc/Ogre
|
||||
|
||||
Production assets reference the Orc/Ogre package from:
|
||||
|
||||
- `Assets/Eagle/Effects/OgreEffect.prefab`
|
||||
- `Assets/Eagle/Effects/OgreMapAnims.controller`
|
||||
|
||||
The custom materials reference package shaders that use Built-in surface shader
|
||||
generation in the source assets:
|
||||
|
||||
- `RRFreelance-Characters/Orc-Ogre/Shaders/OgreShader.shader`
|
||||
- `RRFreelance-Characters/Orc-Ogre/Shaders/ArmorShader2sided.shader`
|
||||
- `RRFreelance-Characters/Orc-Ogre/Shaders/WeaponShader.shader`
|
||||
|
||||
At runtime, the Eagle monster-effect path also uses
|
||||
`BeastMaterialCompatibility.ConfigureRenderer`, so these renderers should be
|
||||
left on the compatibility path unless the Ogre effect becomes pink, dark, or
|
||||
otherwise visibly broken.
|
||||
|
||||
## Maintenance Guidance
|
||||
|
||||
The URP migration is complete; do not convert third-party packages speculatively.
|
||||
|
||||
1. Keep the runtime beast-effect compatibility path covered by existing tests and
|
||||
future visual checks when beast rendering changes.
|
||||
2. If compatibility leaves a beast pink, dark, or visibly broken, replace the affected
|
||||
materials with URP/Lit materials before touching unused demo-only shader packs.
|
||||
3. Leave Polytope water/vegetation/environment conversion until a production
|
||||
reference appears. They are noisy in the shader inventory but not currently on
|
||||
the game path.
|
||||
4. Include Eagle beast effects in the visual baseline. They cover most third-party
|
||||
3D package risk in one place.
|
||||
5. Treat UI sprite packs as low pipeline risk. Their risk is texture import/render
|
||||
ordering, not shader conversion.
|
||||
|
||||
## Vendor Version / Access Notes
|
||||
|
||||
Known versions from existing repo docs:
|
||||
|
||||
- Modern UI Pack: `v4.2.0`
|
||||
|
||||
Most other package versions are not recorded in repo-local metadata. Before hand
|
||||
converting large third-party shader sets, check the Unity Asset Store or vendor
|
||||
account for URP-compatible updates. This especially matters for Polytope Studio,
|
||||
because the package contains many Built-in surface shaders even though only a
|
||||
small character subset is currently used by production assets.
|
||||
@@ -1,61 +0,0 @@
|
||||
# URP Visual Baseline Checklist
|
||||
|
||||
Use this checklist before any future branch changes render-pipeline settings, custom
|
||||
shaders, materials, lighting, camera layering, or render order. The URP migration is
|
||||
complete; this checklist is now a maintenance tool. The goal is not exhaustive
|
||||
gameplay QA, but a stable set of visual states that can be compared by eye after
|
||||
rendering changes.
|
||||
|
||||
Save screenshots under:
|
||||
|
||||
`src/main/csharp/net/eagle0/clients/unity/eagle0/Temp/urp-baselines/`
|
||||
|
||||
That directory is ignored by git.
|
||||
|
||||
Use `Eagle0 > Rendering > Capture URP Baseline Screenshot` in the Unity editor to
|
||||
write a timestamped Game view capture to that directory.
|
||||
|
||||
## Capture Setup
|
||||
|
||||
- Use the same Unity version as the branch under test.
|
||||
- Start from `Assets/Scenes/Main.unity`.
|
||||
- Use the same Game view resolution for every capture in a comparison set.
|
||||
- Capture the full Game view, not cropped panels.
|
||||
- Name files with a stable prefix and short state name, for example
|
||||
`main_01_connection_accounts.png` and `branch_01_connection_accounts.png`.
|
||||
- If a view depends on server data or an account, write a short note next to the
|
||||
screenshot naming the game/account/state used.
|
||||
|
||||
## Required Captures
|
||||
|
||||
| ID | Area | State | Must Verify |
|
||||
|---|---|---|---|
|
||||
| 01 | Connection | Connect panel with stored accounts visible | Background color, panel layout, text contrast, profession icons |
|
||||
| 02 | Connection | Running games list visible | Row icons, timestamps, buttons, selected/hover states if practical |
|
||||
| 03 | Eagle map | Normal province map after joining a game | Province colors, borders, ocean animation frame, faction highlights |
|
||||
| 04 | Eagle map | Province hovered/selected | On-map province tooltip, selected outline, side-panel readability |
|
||||
| 05 | Eagle weather | Drought province | Heat shimmer/drought signal remains visible and does not obscure UI |
|
||||
| 06 | Eagle weather | Flood/rain or blizzard province | Weather overlay color, particle ordering, map readability |
|
||||
| 07 | Eagle beasts | Beast effect on map | Beast animation visible in Game view and positioned over province |
|
||||
| 08 | Eagle beasts | Beast notification open | Animated beast visible in notification and not hidden behind panel art |
|
||||
| 09 | Eagle UI | Command panel with generated text | Text glyphs, fallback glyphs, panel clipping, button states |
|
||||
| 10 | Eagle UI | Settings panel open | Top-right settings button, Escape behavior, modal layering |
|
||||
| 11 | Shardok setup | Initial placement | Terrain tiles, grid lines, unit labels/icons, placement overlays |
|
||||
| 12 | Shardok battle | Unit selected with destination overlays | Overlay labels/icons draw above terrain, bridges, fires, and highlights |
|
||||
| 13 | Shardok battle | Bridge and fire visible near units | Bridge/fire render order does not cover unit labels/icons |
|
||||
| 14 | Shardok battle | Command buttons visible | Right-side UX, disabled/enabled button states, tooltip readability |
|
||||
| 15 | Scene transition | Eagle to Shardok to Eagle | No black/blank frame persists, returning Eagle map is visually intact |
|
||||
|
||||
## Acceptance Notes
|
||||
|
||||
For each rendering follow-up, record:
|
||||
|
||||
- Unity version and branch name.
|
||||
- Screenshot directory.
|
||||
- Any pink/missing materials.
|
||||
- Any invisible UI, labels, particle effects, bridges, fires, or weather effects.
|
||||
- Any render-order differences, especially Shardok overlays and Eagle beast effects.
|
||||
- Whether the difference is acceptable, needs shader work, or needs asset/material work.
|
||||
|
||||
Do not merge a rendering branch until the affected Connection, Eagle, Shardok, or
|
||||
Settings baseline captures are visually acceptable.
|
||||
+51
-119
@@ -4,36 +4,17 @@ This document describes which quests the AI attempts to complete proactively to
|
||||
|
||||
## Overview
|
||||
|
||||
The AI attempts to complete most quests via `FulfillQuestsCommandSelector`, which is invoked by `MidGameAIClient.chosenFulfillEasyQuestsCommand`.
|
||||
|
||||
Important scope limits:
|
||||
|
||||
- The selector only considers quests from unaffiliated heroes in provinces owned by the acting faction.
|
||||
- It further filters to provinces ruled by that faction's leader.
|
||||
- It only emits a command when the corresponding command is currently available.
|
||||
- Handler order is fixed. The first handler that produces a valid command wins.
|
||||
The AI attempts to complete quests via `FulfillQuestsCommandSelector`, which is invoked by `MidGameAIClient.chosenFulfillEasyQuestsCommand`. The AI only considers quests from unaffiliated heroes in provinces ruled by a faction leader.
|
||||
|
||||
## Quests the AI Actively Completes
|
||||
|
||||
This section lists quests with direct `QuestCommandChooser` handlers in `FulfillQuestsCommandSelector`.
|
||||
|
||||
### Diplomacy Quests
|
||||
|
||||
| Quest | Handler | Conditions |
|
||||
|-------|---------|------------|
|
||||
| `AllianceQuest` | `AllianceQuestCommandChooser` | Target faction must meet trust conditions for alliance and not already be in an alliance |
|
||||
| `TruceWithFactionQuest` | `TruceWithFactionQuestCommandChooser` | Target faction must meet trust conditions for truce |
|
||||
| `TruceCountQuest` | `TruceCountQuestCommandChooser` | Picks a random faction that meets trust conditions and isn't already in a truce/alliance |
|
||||
| `AllianceQuest` | `AllianceQuestCommandChooser` | Target faction must meet trust conditions for alliance and not already be in an alliance |
|
||||
| `BetrayAllyQuest` | `BetrayAllyQuestCommandChooser` | Break alliance with target faction. Only if factions don't share a border. Sends weakest non-leader hero. |
|
||||
|
||||
### Province Development Quests
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `ImproveAgricultureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Agriculture type |
|
||||
| `ImproveEconomyQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Economy type |
|
||||
| `ImproveInfrastructureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Infrastructure type |
|
||||
| `TotalDevelopmentQuest` | `TotalDevelopmentQuestCommandChooser` | Improve the lowest stat in the target province |
|
||||
|
||||
### Resource Giving Quests
|
||||
|
||||
@@ -43,99 +24,72 @@ This section lists quests with direct `QuestCommandChooser` handlers in `Fulfill
|
||||
| `AlmsAcrossRealmQuest` | `AlmsAcrossRealmQuestCommandChooser` | Gives food from the province with the largest surplus |
|
||||
| `GiveToHeroesInProvinceQuest` | `GiveToHeroesInProvinceQuestCommandChooser` | Gives gold to the hero with the lowest loyalty in the specified province |
|
||||
| `GiveToHeroesAcrossRealmQuest` | `GiveToHeroesAcrossRealmQuestCommandChooser` | Gives gold from the province with the most gold available |
|
||||
| `SpendOnFeastsInProvinceQuest` | `SpendOnFeastsQuestCommandChooser` | Spend gold on feasts in the quest holder's province |
|
||||
| `SpendOnFeastsAcrossRealmQuest` | `SpendOnFeastsQuestCommandChooser` | Spend gold on the most expensive currently available feast |
|
||||
| `SendSuppliesQuest` | `SendSuppliesQuestCommandChooser` | Send food to the target province |
|
||||
|
||||
### Prisoner Quests
|
||||
### Province Development Quests
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `ReleasePrisonerQuest` | `ReleasePrisonerQuestCommandChooser` | Release a specific prisoner |
|
||||
| `ExilePrisonerQuest` | `ExilePrisonerQuestCommandChooser` | Exile a specific prisoner |
|
||||
| `ExecutePrisonerQuest` | `ExecutePrisonerQuestCommandChooser` | Execute a specific prisoner |
|
||||
| `ReturnPrisonerQuest` | `ReturnPrisonerQuestCommandChooser` | Return a prisoner to their faction |
|
||||
| `ReleaseAllPrisonersQuest` | `ReleaseAllPrisonersQuestCommandChooser` | Release all prisoners |
|
||||
|
||||
### Province Order Quests
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `DevelopProvincesQuest` | `DevelopProvincesQuestCommandChooser` | Switches provinces to Develop order. Prefers provinces without hostile neighbors. |
|
||||
| `MobilizeProvincesQuest` | `MobilizeProvincesQuestCommandChooser` | Switches provinces to Mobilize order. Prefers provinces with hostile neighbors. Skipped if a DevelopProvincesQuest also exists (conflict avoidance). |
|
||||
|
||||
### Weather/Epidemic Quests
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `StartEpidemicQuest` | `StartEpidemicQuestCommandChooser` | Start an epidemic in a province. Skipped if the target province belongs to the acting faction. |
|
||||
| `StartBlizzardQuest` | `ControlWeatherQuestCommandChooser` | Start a blizzard via ControlWeather command. Skipped if the target province belongs to the acting faction. |
|
||||
| `StartDroughtQuest` | `ControlWeatherQuestCommandChooser` | Start a drought via ControlWeather command. Skipped if the target province belongs to the acting faction. |
|
||||
|
||||
### Military Quests
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `GrandArmyQuest` | `GrandArmyQuestCommandChooser` | Uses OrganizeTroops to top off existing battalions and hire Light Infantry. Only executes if resources are sufficient to fully meet the troop target. |
|
||||
| `BattalionDiversityQuest` | `BattalionDiversityQuestCommandChooser` | Hires one battalion of a missing type to reach 3+ distinct types. Only attempts if the province already has 2+ existing types, has sufficient development (`meetsRequirements`), and gold/food surplus. |
|
||||
| `UpgradeBattalionQuest` | `UpgradeBattalionQuestCommandChooser` | Walks a priority tree per turn: arm-completes (with optional travel and/or food sale setup) → train-completes → march in a pre-qualified neighbor battalion → organize/top-off of the target type → march in an unqualified neighbor battalion → develop Economy/Agriculture → train progress → develop Infrastructure → arm progress. Only toggles Travel when it directly enables an arm-completes finisher. |
|
||||
|
||||
### Reconnaissance Quests
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `ReconSpecificProvincesQuest` | `ReconSpecificProvincesQuestCommandChooser` | Recon specific target provinces. Prefers not-yet-reconned targets. |
|
||||
| `ReconProvincesQuest` | `ReconProvincesQuestCommandChooser` | Issues a Recon command to reconnoiter a province |
|
||||
|
||||
### Beast Quests
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `FightBeastsAloneQuest` | `FightBeastsAloneQuestCommandChooser` | Send an expendable non-leader hero to fight beasts without a battalion. Hero must have power at most `MaxExpendableHeroPowerRatio` (default 0.75) times the quest-giving hero's power. Picks the weakest eligible hero. |
|
||||
|
||||
### Special Event Quests
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `ApprehendOutlawQuest` | `ApprehendOutlawQuestCommandChooser` | Apprehend a specific outlaw hero when present in the province |
|
||||
| `ImproveAgricultureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Agriculture type |
|
||||
| `ImproveEconomyQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Economy type |
|
||||
| `ImproveInfrastructureQuest` | `ImproveQuestCommandChooser` | Issues an Improve command with Infrastructure type |
|
||||
|
||||
### Other Quests
|
||||
|
||||
| Quest | Handler | Conditions |
|
||||
|-------|---------|------------|
|
||||
| `RestProvinceQuest` | `RestProvinceQuestCommandChooser` | Use the Rest command in a specific province |
|
||||
| `SwearBrotherhoodWithHeroQuest` | `SwearBrotherhoodQuestCommandChooser` | Swear brotherhood with a specific hero |
|
||||
| `DismissSpecificVassalQuest` | `DismissSpecificVassalCommandChooser` | Only if province has more than 2 heroes AND the unaffiliated hero's power >= target hero's power * `RequiredPowerMultiplierForDismiss` |
|
||||
|
||||
## Quests the AI Handles Indirectly
|
||||
## Quests the AI Does Not Attempt to Complete
|
||||
|
||||
These are not in `FulfillQuestsCommandSelector`, but other AI command-selection code can still bias toward completing them.
|
||||
The following quests have no handler in `FulfillQuestsCommandSelector` and must be completed naturally through gameplay:
|
||||
|
||||
| Quest | Handler | Notes |
|
||||
|-------|---------|-------|
|
||||
| `SuppressRiotByForceQuest` | `CommandChoiceHelpers.handleRiotSelectedCommand` | When this quest exists and the faction has battalions, the AI prefers CrackDown over Give when handling riots. |
|
||||
### Combat/Military Quests
|
||||
- `DefeatFactionQuest` - Defeat a specific faction
|
||||
- `GrandArmyQuest` - Accumulate a large number of troops
|
||||
- `UpgradeBattalionQuest` - Upgrade a battalion to minimum armament/training
|
||||
- `WinBattleOutnumberedQuest` - Win a battle while outnumbered
|
||||
- `WinBattlesQuest` - Win a number of battles
|
||||
- `RescueImprisonedLeaderQuest` - Rescue an imprisoned leader from another faction
|
||||
|
||||
## Quests the AI Does Not Yet Attempt to Complete
|
||||
### Expansion Quests
|
||||
- `ExpandToProvincesQuest` - Expand to control a certain number of provinces
|
||||
- `SpecificExpansionQuest` - Conquer a specific province
|
||||
- `BorderSecurityQuest` - Have troops in a border province
|
||||
|
||||
The following quests have no handler and must be completed naturally through gameplay. They are listed in rough priority order for future implementation.
|
||||
### Prisoner Quests
|
||||
- `ExecutePrisonerQuest` - Execute a specific prisoner
|
||||
- `ExilePrisonerQuest` - Exile a specific prisoner
|
||||
- `ReleasePrisonerQuest` - Release a specific prisoner
|
||||
- `ReturnPrisonerQuest` - Return a prisoner to their faction
|
||||
- `ReleaseAllPrisonersQuest` - Release all prisoners
|
||||
|
||||
### Planned for Implementation
|
||||
### Province Order Quests
|
||||
- `DevelopProvincesQuest` - Maintain provinces in Develop order for months
|
||||
- `MobilizeProvincesQuest` - Maintain provinces in Mobilize order for months
|
||||
- `RestProvinceQuest` - Use the Rest command in a specific province
|
||||
|
||||
None currently planned.
|
||||
### Reconnaissance Quests
|
||||
- `ReconProvincesQuest` - Reconnoiter a number of provinces
|
||||
- `ReconSpecificProvincesQuest` - Reconnoiter specific provinces
|
||||
|
||||
### Not Planned
|
||||
### Economic Quests
|
||||
- `TotalDevelopmentQuest` - Achieve total development level in a province
|
||||
- `WealthQuest` - Accumulate gold and food
|
||||
- `SpendOnFeastsQuest` - Spend gold on feasts
|
||||
- `SendSuppliesQuest` - Send food to a specific province
|
||||
- `RepairDevastationQuest` - Repair devastation
|
||||
|
||||
These quests are too situational, passive, or risky to actively pursue. They may be completed naturally through gameplay.
|
||||
### Special Event Quests
|
||||
- `SuppressRiotByForceQuest` - Suppress a riot by force
|
||||
- `FightBeastsAloneQuest` - Fight beasts alone
|
||||
- `StartBlizzardQuest` - Start a blizzard in a province
|
||||
- `StartEpidemicQuest` - Start an epidemic in a province
|
||||
- `ApprehendOutlawQuest` - Apprehend an outlaw hero
|
||||
|
||||
- `BorderSecurityQuest` - Have troops in a border province. Involves taking provinces; better left to organic expansion.
|
||||
- `DefeatFactionQuest` - Defeat a specific faction. Too risky to pursue proactively.
|
||||
- `SpecificExpansionQuest` - Conquer a specific province. Too risky.
|
||||
- `ExpandToProvincesQuest` - Expand to control a certain number of provinces. Too risky.
|
||||
- `WinBattleOutnumberedQuest` - Win a battle while outnumbered. Can't reliably engineer.
|
||||
- `WinBattlesQuest` - Win a number of battles. Passive.
|
||||
- `RescueImprisonedLeaderQuest` - Rescue an imprisoned leader. Complex multi-step.
|
||||
- `WealthQuest` - Accumulate gold and food. Happens passively.
|
||||
- `RepairDevastationQuest` - Repair devastation. Happens passively.
|
||||
### Miscellaneous
|
||||
- `BattalionDiversityQuest` - Have diverse battalion types
|
||||
- `SwearBrotherhoodWithHeroQuest` - Swear brotherhood with a specific hero
|
||||
- `BetrayAllyQuest` - Betray an allied faction
|
||||
|
||||
## Implementation Details
|
||||
|
||||
@@ -148,34 +102,12 @@ Each chooser extends either:
|
||||
- `DeterministicQuestCommandChooser` - For quests with deterministic command selection
|
||||
|
||||
The AI prioritizes quests in the order they appear in `FulfillQuestsCommandSelector.choosers`:
|
||||
1. TruceWithFaction
|
||||
2. Improve (Agriculture/Economy/Infrastructure)
|
||||
3. TotalDevelopment
|
||||
1. Alliance
|
||||
2. TruceWithFaction
|
||||
3. Improve (Agriculture/Economy/Infrastructure)
|
||||
4. AlmsToProvince
|
||||
5. GiveToHeroesInProvince
|
||||
6. AlmsAcrossRealm
|
||||
7. GiveToHeroesAcrossRealm
|
||||
8. TruceCount
|
||||
9. DismissSpecificVassal
|
||||
10. ReleasePrisoner
|
||||
11. ExilePrisoner
|
||||
12. ExecutePrisoner
|
||||
13. ReturnPrisoner
|
||||
14. ReleaseAllPrisoners
|
||||
15. ApprehendOutlaw
|
||||
16. SpendOnFeastsInProvince / SpendOnFeastsAcrossRealm
|
||||
17. RestProvince
|
||||
18. DevelopProvinces
|
||||
19. MobilizeProvinces
|
||||
20. SendSupplies
|
||||
21. StartEpidemic
|
||||
22. ControlWeather (StartBlizzard/StartDrought)
|
||||
23. GrandArmy
|
||||
24. FightBeastsAlone
|
||||
25. BattalionDiversity
|
||||
26. UpgradeBattalion
|
||||
27. ReconSpecificProvinces
|
||||
28. ReconProvinces
|
||||
29. SwearBrotherhood
|
||||
30. Alliance
|
||||
31. BetrayAlly
|
||||
|
||||
@@ -87,7 +87,7 @@ Create these prefabs in `Assets/Eagle/Effects/`:
|
||||
|
||||
### Scene Setup
|
||||
|
||||
Wire up in `Assets/Scenes/Eagle.unity`:
|
||||
Wire up in `Gameplay.unity`:
|
||||
1. Add controller components to the Map GameObject
|
||||
2. Assign `mapContainer`, `centroidsJson` (shared with other controllers)
|
||||
3. Assign the effect prefab for each controller
|
||||
|
||||
Binary file not shown.
@@ -1,40 +1,36 @@
|
||||
module github.com/nolen777/eagle0
|
||||
|
||||
go 1.25.0
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.26.4
|
||||
toolchain go1.23.3
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.25
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.8
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.10
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.51
|
||||
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.46.0
|
||||
google.golang.org/grpc v1.81.1
|
||||
google.golang.org/protobuf v1.36.11
|
||||
golang.org/x/sys v0.28.0
|
||||
google.golang.org/grpc v1.68.0
|
||||
google.golang.org/protobuf v1.36.3
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 // indirect
|
||||
github.com/aws/smithy-go v1.27.2 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 // indirect
|
||||
github.com/aws/smithy-go v1.22.1 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,184 +1,56 @@
|
||||
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
|
||||
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0=
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.8 h1:cZV+NUS/eGxKXMtmyhtYPJ7Z4YLoI/V8bkTdRZfYhGo=
|
||||
github.com/aws/aws-sdk-go-v2 v1.32.8/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U=
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.0 h1:XvXMJTkFQtpBKIWZnmr9ZEOc2InWM2yldjXEJ/bymhA=
|
||||
github.com/aws/aws-sdk-go-v2 v1.42.0/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7 h1:lL7IfaFzngfx0ZwUGOZdsFFnQ5uLvR0hWqqhyE7Q9M8=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.7/go.mod h1:QraP0UcVlQJsmHfioCrveWOC1nbiWUl3ej08h4mXWoc=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13 h1:p1BBrg/Hhp6uK7zpejeI8QFXHJeC/mynzi04Sl03k9g=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.13/go.mod h1:8cIfkE9MDhkRZGpQ22aV6/lkYeYSozpz16Smrs5x4Ls=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.10 h1:fKODZHfqQu06pCzR69KJ3GuttraRJkhlC8g80RZ0Dfg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.28.10/go.mod h1:PvdxRYZ5Um9QMq9PQ0zHHNdtKK+he2NHtFCUFMXWXeg=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.25 h1:ACCejvStYoilgwrfegSt5ZntCbPrk52qfwyNcnl3omM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.25/go.mod h1:LJyU8sDRbXUxFn8xMJIGP+v9QYYwveNLI8a/giAOiAs=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.51 h1:F/9Sm6Y6k4LqDesZDPJCLxQGXNNHd/ZtJiWd0lCZKRk=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.17.51/go.mod h1:TKbzCHm43AoPyA+iLGGcruXd4AFhF8tOmLex2R9jWNQ=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24 h1:2hQqYCV9yqyePQ9o6dCrZc/zO8U3TwPr9mIKlZnPu/I=
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.24/go.mod h1:IDwpACtwqHLISdzfwUUNq4P9DsB/h5BLg4FwJPNfqFY=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23 h1:IBAoD/1d8A8/1aA8g4MBVtTRHhXRiNAgwdbo/xRM2DI=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.23/go.mod h1:vfENuCM7dofkgKpYzuzf1VT1UKkA/YL3qanfBn7HCaA=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29 h1:r6qZHbT+wxgWO/e9vYNUEtg7lv5+UN3pRqKhLXvnArg=
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.29/go.mod h1:QRnaRcTVGKPGRy8w78HMQtKUGRYcnMZAANATkeVA6Mo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27 h1:jSJjSBzw8VDIbWv+mmvBSP8ezsztMYJGH+eKqi9AmNs=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.27/go.mod h1:/DAhLbFRgwhmvJdOfSm+WwikZrCuUJiA4WgJG0fTNSw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29 h1:f3vKqSo13fhTYb+JEcXwXefZQE26I1FB5eTSniU67ko=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.29/go.mod h1:MzoLFUArKGpGD+ukmPiTPG1X5x4o6M2kq4v2dr1FiEc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27 h1:l+X4K77Dui85pIj5foXDhPlnqcNRG2QUyvca300lXh8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.27/go.mod h1:KvZXSFEXm6x84yE8qffKvT3x8J5clWnVFXphpohhzJ8=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29 h1:RdwIf/CuUsvJX3RgJagbOyotl/cxoLY4xviKuE7p2GY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.29/go.mod h1:71wt8W2EgswdZy9Mf9KNnzxZ3TiZlv4caKghPktDOkA=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.27 h1:AmB5QxnD+fBFrg9LcqzkgF/CaYvMyU/BTlejG4t1S7Q=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.27/go.mod h1:Sai7P3xTiyv9ZUYO3IFxMnmiIP759/67iQbU4kdmkyU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30 h1:VTGy885W5DKBxWRUJbym9hytNaYzsyaPkCHGRRMAOhU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.30/go.mod h1:AS0HycUvJRFvTt613AYDOgO2jzw+00cVSMny8XB3yMY=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.8 h1:iwYS40JnrBeA9e9aI5S6KKN4EB2zR4iUVYN0nwVivz4=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.4.8/go.mod h1:Fm9Mi+ApqmFiknZtGpohVcBGvpTu542VC4XO9YudRi0=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22 h1:V51LGlOq/1VsDsHUdoklAQi7rMmx4qQubvFYAlP2254=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.22/go.mod h1:4Pzhyz8hJOm2bepgl+NjvRx8vlUFAIIvJnZ/MkcNPpU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8 h1:cWno7lefSH6Pp+mSznagKCgfDGeZRin66UvYUqAkyeA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.8/go.mod h1:tPD+VjU3ABTBoEJ3nctu5Nyg4P4yjqSH5bJGGkY4+XE=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.8 h1:/Mn7gTedG86nbpjT4QEKsN1D/fThiYe1qvq7WsBGNHg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.8/go.mod h1:Ae3va9LPmvjj231ukHB6UeT8nS7wTPfC3tMZSZMwNYg=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29 h1:hiME6pBzC7OTl9LMtlyTWBuEl1f4QBcUmFDKC7MLXtc=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.29/go.mod h1:G7RP+uhagpKtKhd1BM9N6JQqjCcGEU47K5lBVZQyRQw=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2 h1:a7aQ3RW+ug4IbhoQp29NZdc7vqrzKZZfWZSaQAXOZvQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.72.2/go.mod h1:xMekrnhmJ5aqmyxtmALs7mlvXw5xRh+eYjOjvrIIFJ4=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3 h1:JRseEu/vIDMaWis4bSw0QbXL+cvIGc1XnX076H5ZXLE=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.103.3/go.mod h1:77ZAgynvx1txMvDG8gGWoWkO1augYDxkp9JElWFgjQU=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.2.0 h1:3nXpRcFwRCW8n7HgO2QGy0Dc20eQNfBuUemGQhpF8m8=
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.2.0/go.mod h1:LxYujSTLPRlp2vTtcUO/+1ilrew8ytt6SvQyOgejzFQ=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.9 h1:YqtxripbjWb2QLyzRK9pByfEDvgg95gpC2AyDq4hFE8=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.24.9/go.mod h1:lV8iQpg6OLOfBnqbGMBKYjilBlf633qwHnBEiMSPoHY=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3 h1:ey1XLTYXb9PcLt4535632o5kCGXNXEhNb620Dqwuylo=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.31.3/go.mod h1:Lk7PlmoTYryQmyBG0EXqj5BcUbj3whXdU2s3yGI3EAc=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 h1:6dBT1Lz8fK11m22R+AqfRsFn8320K0T5DTGxxOQBSMw=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8/go.mod h1:/kiBvRQXBc6xeJTYzhSdGvJ5vm1tjaDEjH+MSeRJnlY=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6 h1:yLr03zQE/5Eu5l3QU0Si+xMbLMbSDF2YXsigqXngs6g=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.6/go.mod h1:Q5N6icH+KJZDLh+ESNwzdv6cZ6vLFF/egy3IOxWhmz4=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 h1:VwhTrsTuVn52an4mXx29PqRzs2Dvu921NpGk7y43tAM=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.33.6/go.mod h1:+8h7PZb3yY5ftmVLD7ocEoE98hdc8PoKS0H3wfx1dlc=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3 h1:VrIhKRCSK1umelSgB9RghvA9RTUYeQffyAS5ApXehNI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.43.3/go.mod h1:r8wkDOuLaaMFqFiYAb8dGY2A3gJCOujMc6CFOVC4Zhc=
|
||||
github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro=
|
||||
github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
|
||||
github.com/aws/smithy-go v1.27.1 h1:4T340VFndXtADGF52gYa1POyL7s9E4Z1OeZ1hCscIw8=
|
||||
github.com/aws/smithy-go v1.27.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/aws/smithy-go v1.27.2 h1:y9NPmSE6am6LjEFPfqHqG/jJk7AauQvhCJONKh7kpzk=
|
||||
github.com/aws/smithy-go v1.27.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik=
|
||||
github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
|
||||
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ=
|
||||
github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
|
||||
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds=
|
||||
github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
|
||||
github.com/golang/glog v1.2.5/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo=
|
||||
github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs=
|
||||
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=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ=
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
|
||||
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
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/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad h1:3iLyITS/sySRwbUKoC7ogfj2Yr1Cjs0pfaRKj5U5HEw=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:KdNqO+rCIWgFumrNBSEDlDNrkrQnpkax7Tv1WxNY8V4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad h1:45WmJvIV6C2+O/jjLkPUH+F3aOj/1miDoU2DD0+NWbg=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260610212136-7ab31c22f7ad/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
||||
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
|
||||
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
|
||||
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
|
||||
+463
-1899
File diff suppressed because it is too large
Load Diff
@@ -1,47 +0,0 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:recommended"
|
||||
],
|
||||
"dependencyDashboard": true,
|
||||
"enabledManagers": [
|
||||
"bazel-module",
|
||||
"cargo",
|
||||
"github-actions",
|
||||
"gomod",
|
||||
"swift"
|
||||
],
|
||||
"labels": [
|
||||
"dependencies"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"description": "Bazel module artifact updates run in GitHub Actions so hosted Renovate does not need unsafe bazel mod deps execution",
|
||||
"matchManagers": [
|
||||
"bazel-module"
|
||||
],
|
||||
"skipArtifactsUpdate": true
|
||||
},
|
||||
{
|
||||
"description": "Go packages import Bazel-generated protobuf packages, so Renovate's generic Go artifact updater cannot run ./...",
|
||||
"matchManagers": [
|
||||
"gomod"
|
||||
],
|
||||
"skipArtifactsUpdate": true
|
||||
}
|
||||
],
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
"schedule": [
|
||||
"before 6am on monday"
|
||||
]
|
||||
},
|
||||
"prConcurrentLimit": 3,
|
||||
"prHourlyLimit": 1,
|
||||
"rebaseWhen": "behind-base-branch",
|
||||
"schedule": [
|
||||
"before 6am on monday"
|
||||
],
|
||||
"separateMajorMinor": true,
|
||||
"timezone": "America/Los_Angeles"
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
bazel_cmd=(bazel)
|
||||
if [ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]; then
|
||||
bazel_cmd+=("--output_user_root=${BAZEL_OUTPUT_USER_ROOT}")
|
||||
fi
|
||||
if [ -n "${BAZEL_OUTPUT_BASE:-}" ]; then
|
||||
bazel_cmd+=("--output_base=${BAZEL_OUTPUT_BASE}")
|
||||
fi
|
||||
|
||||
"${bazel_cmd[@]}" build "$@" //src/main/scala/net/eagle0/eagle:eagle_server
|
||||
+53
-15
@@ -1,30 +1,68 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
set -euxo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
UNITY_ROOT="$REPO_ROOT/src/main/csharp/net/eagle0/clients/unity/eagle0"
|
||||
PLUGIN_DIR="$UNITY_ROOT/Assets/Plugins/Eagle0Protos"
|
||||
GENERATED_SOURCES_DIR="$UNITY_ROOT/Assets/GeneratedProtos"
|
||||
PROTO_DLL_TARGET="//src/main/csharp/net/eagle0/clients/unity/eagle0:eagle0_protos"
|
||||
PROTO_DLL_OUTPUT="$REPO_ROOT/bazel-bin/src/main/csharp/net/eagle0/clients/unity/eagle0/eagle0_protos/netstandard2.1"
|
||||
OUTPUT_DIR="$UNITY_ROOT/Assets/GeneratedProtos"
|
||||
|
||||
/bin/echo "Building C# protobuf DLL..."
|
||||
bazel build "$PROTO_DLL_TARGET"
|
||||
# All C# protobuf generation targets
|
||||
TARGETS=(
|
||||
"//src/main/protobuf/net/eagle0/common:common_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/shardok/storage:storage_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/shardok/common:common_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/shardok/api:api_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/common:common_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/views:views_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:api_csharp_proto_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/api:api_csharp_grpc_srcs"
|
||||
"//src/main/protobuf/net/eagle0/eagle/api/command/util:util_csharp_proto_srcs"
|
||||
)
|
||||
|
||||
/bin/echo "Syncing generated protobuf DLL to $PLUGIN_DIR..."
|
||||
# Subdirectory for each target (avoids filename collisions across packages)
|
||||
SUBDIRS=(
|
||||
"net/eagle0/common"
|
||||
"net/eagle0/shardok/storage"
|
||||
"net/eagle0/shardok/common"
|
||||
"net/eagle0/shardok/api"
|
||||
"net/eagle0/eagle/common"
|
||||
"net/eagle0/eagle/views"
|
||||
"net/eagle0/eagle/api"
|
||||
"net/eagle0/eagle/api"
|
||||
"net/eagle0/eagle/api/command/util"
|
||||
)
|
||||
|
||||
/bin/echo "Building C# protobuf sources..."
|
||||
bazel build "${TARGETS[@]}"
|
||||
|
||||
/bin/echo "Syncing generated .cs files to $OUTPUT_DIR..."
|
||||
|
||||
# Stage new files in a temp directory, then rsync to preserve timestamps
|
||||
# on unchanged files. This avoids triggering Unity reimport when protos
|
||||
# haven't changed, saving ~7 minutes of script recompilation.
|
||||
STAGING_DIR=$(mktemp -d)
|
||||
trap "rm -rf '$STAGING_DIR'" EXIT
|
||||
|
||||
cp "$PROTO_DLL_OUTPUT/Eagle0Protos.dll" "$STAGING_DIR/"
|
||||
cp "$PROTO_DLL_OUTPUT/Eagle0Protos.xml" "$STAGING_DIR/"
|
||||
for i in "${!TARGETS[@]}"; do
|
||||
target="${TARGETS[$i]}"
|
||||
subdir="${SUBDIRS[$i]}"
|
||||
|
||||
# Convert target label to bazel-bin path
|
||||
# //src/main/protobuf/net/eagle0/common:common_csharp_proto_srcs
|
||||
# -> bazel-bin/src/main/protobuf/net/eagle0/common/common_csharp_proto_srcs
|
||||
target_path="${target#//}"
|
||||
pkg="${target_path%%:*}"
|
||||
name="${target_path##*:}"
|
||||
bin_dir="$REPO_ROOT/bazel-bin/$pkg/$name"
|
||||
|
||||
dest_dir="$STAGING_DIR/$subdir"
|
||||
mkdir -p "$dest_dir"
|
||||
find "$bin_dir" -name "*.cs" -exec cp {} "$dest_dir/" \;
|
||||
done
|
||||
|
||||
# Sync only changed files and delete removed ones; --checksum compares
|
||||
# content not timestamps so unchanged files keep their original mtime.
|
||||
mkdir -p "$PLUGIN_DIR"
|
||||
rsync -rc --delete --exclude "*.meta" "$STAGING_DIR/" "$PLUGIN_DIR/"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
rsync -rc --delete "$STAGING_DIR/" "$OUTPUT_DIR/"
|
||||
|
||||
rm -rf "$GENERATED_SOURCES_DIR"
|
||||
|
||||
/bin/echo "Done. Generated C# proto DLL in $PLUGIN_DIR"
|
||||
/bin/echo "Done. Generated C# proto sources in $OUTPUT_DIR"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
bazel_cmd=(bazel)
|
||||
if [ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]; then
|
||||
bazel_cmd+=("--output_user_root=${BAZEL_OUTPUT_USER_ROOT}")
|
||||
fi
|
||||
if [ -n "${BAZEL_OUTPUT_BASE:-}" ]; then
|
||||
bazel_cmd+=("--output_base=${BAZEL_OUTPUT_BASE}")
|
||||
fi
|
||||
|
||||
"${bazel_cmd[@]}" build -c opt "$@" //src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
bazel_cmd=(bazel)
|
||||
if [ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]; then
|
||||
bazel_cmd+=("--output_user_root=${BAZEL_OUTPUT_USER_ROOT}")
|
||||
fi
|
||||
if [ -n "${BAZEL_OUTPUT_BASE:-}" ]; then
|
||||
bazel_cmd+=("--output_base=${BAZEL_OUTPUT_BASE}")
|
||||
fi
|
||||
|
||||
"${bazel_cmd[@]}" build \
|
||||
--platforms=//:linux_arm64 \
|
||||
--extra_toolchains=@llvm_toolchain_linux_arm64//:cc-toolchain-aarch64-linux \
|
||||
"$@" \
|
||||
//src/main/cpp/net/eagle0/shardok:shardok-server
|
||||
@@ -2,6 +2,9 @@
|
||||
#
|
||||
# Build the SparklePlugin native library for Unity using Bazel
|
||||
#
|
||||
# The SparklePlugin is built in a separate Bazel workspace (sparkle_workspace/)
|
||||
# to avoid rules_swift version conflicts between grpc and rules_apple.
|
||||
#
|
||||
# Usage: build_sparkle_plugin.sh [output_dir]
|
||||
|
||||
set -euo pipefail
|
||||
@@ -11,13 +14,14 @@ PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
OUTPUT_DIR="${1:-$PROJECT_ROOT/src/main/csharp/net/eagle0/clients/unity/eagle0/Assets/Plugins/macOS}"
|
||||
|
||||
echo "=== Building SparklePlugin with Bazel ==="
|
||||
echo "=== Building SparklePlugin with Bazel (from sparkle_workspace) ==="
|
||||
|
||||
cd "$PROJECT_ROOT"
|
||||
bazel build //src/main/objc/net/eagle0/clients/unity/sparkle:SparklePlugin
|
||||
# Build in the separate sparkle_workspace to avoid dependency conflicts
|
||||
cd "$PROJECT_ROOT/sparkle_workspace"
|
||||
bazel build //:SparklePlugin
|
||||
|
||||
# Get the zip path from bazel
|
||||
ZIP_PATH=$(bazel cquery --output=files //src/main/objc/net/eagle0/clients/unity/sparkle:SparklePlugin 2>/dev/null)
|
||||
ZIP_PATH=$(bazel cquery --output=files //:SparklePlugin 2>/dev/null)
|
||||
|
||||
echo "=== Extracting SparklePlugin.bundle ==="
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 <warm|verify> [bazel targets...]"
|
||||
echo
|
||||
echo "warm: build targets normally so local results can upload to the remote cache"
|
||||
echo "verify: require all target actions to be available from remote cache"
|
||||
}
|
||||
|
||||
if [ "$#" -lt 1 ]; then
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
mode="$1"
|
||||
shift
|
||||
|
||||
case "$mode" in
|
||||
warm | verify)
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
tmp_root="${RUNNER_TEMP:-/private/tmp}"
|
||||
run_id="${GITHUB_RUN_ID:-local}-$$"
|
||||
output_user_root="${tmp_root%/}/eagle0-bazel-user-root-${run_id}"
|
||||
output_base="${tmp_root%/}/eagle0-cache-parity-${mode}-${run_id}"
|
||||
|
||||
echo "Mode: $mode"
|
||||
echo "Output user root: $output_user_root"
|
||||
echo "Output base: $output_base"
|
||||
|
||||
run_ci_build_script() {
|
||||
local script="$1"
|
||||
shift
|
||||
|
||||
echo "Build script: $script"
|
||||
echo "Build args:"
|
||||
printf ' %s\n' "$@"
|
||||
|
||||
BAZEL_OUTPUT_USER_ROOT="$output_user_root" BAZEL_OUTPUT_BASE="$output_base" "$script" "$@"
|
||||
}
|
||||
|
||||
cache_check_args() {
|
||||
local bazel_args=(
|
||||
"--remote_download_all"
|
||||
)
|
||||
|
||||
if [ "$mode" = "verify" ]; then
|
||||
bazel_args+=(
|
||||
"--remote_upload_local_results=false"
|
||||
"--experimental_remote_require_cached"
|
||||
"--disk_cache="
|
||||
)
|
||||
fi
|
||||
|
||||
printf '%s\n' "${bazel_args[@]}"
|
||||
}
|
||||
|
||||
run_bazel_targets() {
|
||||
bazel_args=()
|
||||
while IFS= read -r arg; do
|
||||
bazel_args+=("$arg")
|
||||
done < <(cache_check_args)
|
||||
|
||||
echo "Bazel args:"
|
||||
printf ' %s\n' "${bazel_args[@]}" "$@"
|
||||
|
||||
bazel "--output_user_root=$output_user_root" "--output_base=$output_base" build "${bazel_args[@]}" "$@"
|
||||
}
|
||||
|
||||
if [ "$#" -gt 0 ]; then
|
||||
run_bazel_targets "$@"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
bazel_args=()
|
||||
while IFS= read -r arg; do
|
||||
bazel_args+=("$arg")
|
||||
done < <(cache_check_args)
|
||||
run_ci_build_script scripts/build_eagle_ci.sh "${bazel_args[@]}"
|
||||
run_ci_build_script scripts/build_shardok_ci.sh "${bazel_args[@]}"
|
||||
@@ -1,352 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import random
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
|
||||
API_KEY_FILE = Path("src/main/scala/net/eagle0/common/llm_integration/api_keys.txt")
|
||||
IMAGE_ENDPOINT = "https://api.openai.com/v1/images/generations"
|
||||
|
||||
APPEARANCE_PROFILES = [
|
||||
"West African features, very dark brown skin",
|
||||
"East African features, deep brown skin",
|
||||
"North African features, olive-brown skin",
|
||||
"Arabian Peninsula features, warm medium-brown skin",
|
||||
"Persian features, olive skin",
|
||||
"Turkic Central Asian features, tan skin",
|
||||
"Mongolian features, golden-tan skin",
|
||||
"North Indian features, medium-brown skin",
|
||||
"South Indian features, dark brown skin",
|
||||
"Bengali features, warm brown skin",
|
||||
"Southeast Asian features, medium tan skin",
|
||||
"Vietnamese features, light golden skin",
|
||||
"Chinese features, light olive skin",
|
||||
"Korean features, fair skin",
|
||||
"Japanese features, fair-to-light olive skin",
|
||||
"Indigenous Andean features, copper-brown skin",
|
||||
"Mesoamerican features, medium brown skin",
|
||||
"Native North American features, warm brown skin",
|
||||
"Maori or Polynesian features, medium brown skin",
|
||||
"Aboriginal Australian features, dark brown skin",
|
||||
"Afro-Caribbean features, dark brown skin",
|
||||
"Afro-Latin features, medium-dark brown skin",
|
||||
"Brazilian mixed-heritage features, medium brown skin",
|
||||
"Mexican mestizo features, tan skin",
|
||||
"Mediterranean European features, olive skin",
|
||||
"Iberian features, light olive skin",
|
||||
"Italian features, olive skin",
|
||||
"Greek features, olive skin",
|
||||
"French features, light skin",
|
||||
"Irish features, fair skin with freckles",
|
||||
"Scottish features, ruddy fair skin",
|
||||
"Nordic features, very fair skin",
|
||||
"Slavic features, fair-to-light olive skin",
|
||||
"Ashkenazi Jewish features, light olive skin",
|
||||
"Armenian features, olive skin",
|
||||
"Ethiopian highland features, medium-dark brown skin",
|
||||
"Moroccan Amazigh features, tan olive skin",
|
||||
"Egyptian features, warm tan skin",
|
||||
"Filipino features, medium golden-brown skin",
|
||||
"Pacific Islander features, warm brown skin",
|
||||
]
|
||||
|
||||
AGE_PROFILES = [
|
||||
"young adult",
|
||||
"adult",
|
||||
"seasoned middle-aged",
|
||||
"older veteran",
|
||||
]
|
||||
|
||||
FACE_PROFILES = [
|
||||
"broad, sturdy face",
|
||||
"narrow, severe face",
|
||||
"round, watchful face",
|
||||
"angular, weathered face",
|
||||
"scarred but composed face",
|
||||
"plain, practical face",
|
||||
"commanding, heavy-browed face",
|
||||
"tired but alert face",
|
||||
]
|
||||
|
||||
HAIR_PROFILES = [
|
||||
"cropped dark hair",
|
||||
"close-cropped gray hair",
|
||||
"braided black hair",
|
||||
"shaved head",
|
||||
"curly dark hair pulled back",
|
||||
"straight black hair tied back",
|
||||
"auburn hair streaked with gray",
|
||||
"white hair cut short",
|
||||
"dark beard and close hair",
|
||||
"neat graying beard",
|
||||
"coiled natural hair",
|
||||
"long dark hair bound under a cap",
|
||||
]
|
||||
|
||||
|
||||
def api_key():
|
||||
env_value = os.environ.get("OPENAI_API_KEY")
|
||||
if env_value:
|
||||
return env_value
|
||||
|
||||
if API_KEY_FILE.exists():
|
||||
for line in API_KEY_FILE.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith("#") or "=" not in line:
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
if key.strip() == "openai_api_key":
|
||||
value = value.strip()
|
||||
if value and not value.startswith("your_"):
|
||||
return value
|
||||
|
||||
raise RuntimeError(
|
||||
"OpenAI API key not found. Set OPENAI_API_KEY or add openai_api_key to "
|
||||
f"{API_KEY_FILE}."
|
||||
)
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Generate Warden hero headshots and update generated_heroes.tsv image paths."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--hero-tsv",
|
||||
default="src/main/resources/net/eagle0/eagle/generated_heroes.tsv",
|
||||
help="Generated heroes TSV to read and update.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--base-image-dir",
|
||||
default=str(Path.home() / "Documents/headshots"),
|
||||
help="Local headshot root that sync_headshots.sh uploads to S3.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--model",
|
||||
default="gpt-image-2",
|
||||
help="OpenAI image generation model.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--quality",
|
||||
default="low",
|
||||
choices=("low", "medium", "high", "auto"),
|
||||
help="Image generation quality.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--size",
|
||||
default="1024x1024",
|
||||
help="Generated image size.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--limit",
|
||||
type=int,
|
||||
default=0,
|
||||
help="Generate at most this many missing images; 0 means all missing images.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Print planned paths without calling the API or writing files.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--overwrite",
|
||||
action="store_true",
|
||||
help="Regenerate images even if the target PNG already exists.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--concurrency",
|
||||
type=int,
|
||||
default=4,
|
||||
help="Number of parallel image generation requests.",
|
||||
)
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def read_rows(path):
|
||||
lines = path.read_text().splitlines()
|
||||
fieldnames = lines[0].split("\t")
|
||||
rows = []
|
||||
for line in lines[1:]:
|
||||
fields = line.split("\t")
|
||||
row = dict(zip(fieldnames, fields))
|
||||
row["_fields"] = fields
|
||||
rows.append(row)
|
||||
return fieldnames, rows
|
||||
|
||||
|
||||
def write_rows(path, fieldnames, rows):
|
||||
image_path_index = fieldnames.index("image_path")
|
||||
lines = ["\t".join(fieldnames)]
|
||||
for row in rows:
|
||||
fields = list(row["_fields"])
|
||||
fields[image_path_index] = row["image_path"]
|
||||
lines.append("\t".join(fields))
|
||||
path.write_text("\n".join(lines) + "\n")
|
||||
|
||||
|
||||
def image_gender(row, warden_index):
|
||||
gender = row["gender"]
|
||||
if gender in ("male", "female"):
|
||||
return gender
|
||||
|
||||
digest = hashlib.sha256(row["name"].encode("utf-8")).digest()
|
||||
return "male" if (digest[0] + warden_index) % 2 == 0 else "female"
|
||||
|
||||
|
||||
def image_filename(row, warden_index):
|
||||
digest = hashlib.sha256(row["name"].encode("utf-8")).hexdigest()[:16]
|
||||
return f"{warden_index:04d}_{digest}.png"
|
||||
|
||||
|
||||
def choose_profile(items, row, warden_index, salt):
|
||||
digest = hashlib.sha256(f"{salt}:{warden_index}:{row['name']}".encode("utf-8")).digest()
|
||||
return items[int.from_bytes(digest[:4], "big") % len(items)]
|
||||
|
||||
|
||||
def prompt_for(row, warden_index):
|
||||
prime_notes = [
|
||||
f"strength {row['strength']}",
|
||||
f"agility {row['agility']}",
|
||||
f"constitution {row['constitution']}",
|
||||
f"wisdom {row['wisdom']}",
|
||||
f"charisma {row['charisma']}",
|
||||
]
|
||||
appearance = choose_profile(APPEARANCE_PROFILES, row, warden_index, "appearance")
|
||||
age = choose_profile(AGE_PROFILES, row, warden_index, "age")
|
||||
face = choose_profile(FACE_PROFILES, row, warden_index, "face")
|
||||
hair = choose_profile(HAIR_PROFILES, row, warden_index, "hair")
|
||||
return (
|
||||
"Fantasy strategy game hero portrait, square headshot composition. "
|
||||
f"Create a distinctive medieval Warden named {row['name']}. "
|
||||
f"Gender presentation: {row['gender']}. "
|
||||
f"Appearance: {age}, {appearance}, {face}, {hair}. "
|
||||
f"Personality traits: {row['personality']}. "
|
||||
"The character is a jailer, keeper of prisoners, and battlefield custodian: "
|
||||
"stern keys, austere authority, prison-watch uniform, no modern clothing. "
|
||||
"Show a resilient, high-constitution figure with weathered endurance and moral judgment. "
|
||||
f"Stat cues: {', '.join(prime_notes)}. "
|
||||
"Painted realistic fantasy style, expressive face, shoulders visible, neutral background, "
|
||||
"soft dramatic lighting, no text, no logo, no frame, no watermark."
|
||||
)
|
||||
|
||||
|
||||
def request_image(key, model, quality, size, prompt):
|
||||
body = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"size": size,
|
||||
"quality": quality,
|
||||
"n": 1,
|
||||
"output_format": "png",
|
||||
}
|
||||
request = urllib.request.Request(
|
||||
IMAGE_ENDPOINT,
|
||||
data=json.dumps(body).encode("utf-8"),
|
||||
headers={
|
||||
"Authorization": f"Bearer {key}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
with urllib.request.urlopen(request, timeout=300) as response:
|
||||
response_body = json.loads(response.read().decode("utf-8"))
|
||||
|
||||
data = response_body["data"][0]
|
||||
if "b64_json" in data:
|
||||
return base64.b64decode(data["b64_json"])
|
||||
|
||||
if "url" in data:
|
||||
with urllib.request.urlopen(data["url"], timeout=300) as response:
|
||||
return response.read()
|
||||
|
||||
raise RuntimeError("Image response did not include b64_json or url")
|
||||
|
||||
|
||||
def generate_with_retries(key, args, prompt, attempts=5):
|
||||
for attempt in range(1, attempts + 1):
|
||||
try:
|
||||
return request_image(key, args.model, args.quality, args.size, prompt)
|
||||
except urllib.error.HTTPError as error:
|
||||
body = error.read().decode("utf-8", errors="replace")
|
||||
retryable = error.code in (408, 409, 429, 500, 502, 503, 504)
|
||||
if not retryable or attempt == attempts:
|
||||
raise RuntimeError(f"OpenAI image request failed: {error.code} {body}") from error
|
||||
except (TimeoutError, urllib.error.URLError):
|
||||
if attempt == attempts:
|
||||
raise
|
||||
|
||||
time.sleep(min(60, 2**attempt) + random.random())
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
hero_tsv = Path(args.hero_tsv)
|
||||
base_image_dir = Path(args.base_image_dir).expanduser()
|
||||
fieldnames, rows = read_rows(hero_tsv)
|
||||
|
||||
key = None if args.dry_run else api_key()
|
||||
warden_index = 0
|
||||
generation_tasks = []
|
||||
|
||||
for row in rows:
|
||||
if row["profession"] != "warden":
|
||||
continue
|
||||
|
||||
warden_index += 1
|
||||
gender = image_gender(row, warden_index)
|
||||
rel_path = f"warden/{gender}/{image_filename(row, warden_index)}"
|
||||
target_path = base_image_dir / rel_path
|
||||
row["image_path"] = rel_path
|
||||
|
||||
if target_path.exists() and not args.overwrite:
|
||||
print(f"exists {rel_path}", flush=True)
|
||||
continue
|
||||
|
||||
generation_tasks.append(
|
||||
(row["name"], rel_path, target_path, prompt_for(row, warden_index))
|
||||
)
|
||||
|
||||
if args.dry_run:
|
||||
print(f"Dry run planned {warden_index} Warden image paths", flush=True)
|
||||
else:
|
||||
if args.limit:
|
||||
generation_tasks = generation_tasks[: args.limit]
|
||||
|
||||
for _, _, target_path, _ in generation_tasks:
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def generate_task(task):
|
||||
name, rel_path, target_path, prompt = task
|
||||
print(f"generate {rel_path} for {name}", flush=True)
|
||||
image_bytes = generate_with_retries(key, args, prompt)
|
||||
if not image_bytes.startswith(b"\x89PNG\r\n\x1a\n"):
|
||||
raise RuntimeError(f"Generated image for {name} is not a PNG")
|
||||
target_path.write_bytes(image_bytes)
|
||||
return rel_path
|
||||
|
||||
generated_count = 0
|
||||
if generation_tasks:
|
||||
with ThreadPoolExecutor(max_workers=max(1, args.concurrency)) as executor:
|
||||
futures = [executor.submit(generate_task, task) for task in generation_tasks]
|
||||
for future in as_completed(futures):
|
||||
rel_path = future.result()
|
||||
generated_count += 1
|
||||
print(
|
||||
f"wrote {rel_path} ({generated_count}/{len(generation_tasks)})",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
write_rows(hero_tsv, fieldnames, rows)
|
||||
print(f"Updated {hero_tsv}; generated {generated_count} missing images", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,6 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
scripts/build_eagle_ci.sh
|
||||
scripts/build_shardok_ci.sh
|
||||
@@ -8,12 +8,12 @@
|
||||
#
|
||||
# Optional environment variables:
|
||||
# SPARKLE_FEED_URL - Appcast URL (default: https://assets.eagle0.net/mac/appcast.xml)
|
||||
# SPARKLE_VERSION - Sparkle version to use (default: 2.9.3)
|
||||
# SPARKLE_VERSION - Sparkle version to use (default: 2.6.4)
|
||||
|
||||
set -euxo pipefail
|
||||
|
||||
APP_PATH="$1"
|
||||
SPARKLE_VERSION="${SPARKLE_VERSION:-2.9.3}"
|
||||
SPARKLE_VERSION="${SPARKLE_VERSION:-2.6.4}"
|
||||
SPARKLE_FEED_URL="${SPARKLE_FEED_URL:-https://assets.eagle0.net/mac/appcast.xml}"
|
||||
SPARKLE_CACHE_DIR="/tmp/sparkle-cache"
|
||||
|
||||
|
||||
@@ -4,10 +4,8 @@
|
||||
|
||||
set -e
|
||||
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
||||
|
||||
# Run gazelle
|
||||
bazel run //:gazelle
|
||||
bazel run //:gazelle 2>/dev/null
|
||||
|
||||
# Check if any BUILD files were modified
|
||||
if ! git diff --quiet -- '*.bazel' '**/BUILD' 'WORKSPACE*'; then
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Pre-commit hook wrapper for scalafmt.
|
||||
# Runs the repository-pinned scalafmt CLI through Bazel.
|
||||
|
||||
set -e
|
||||
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel)"
|
||||
files=()
|
||||
for file in "$@"; do
|
||||
case "$file" in
|
||||
/*) files+=("$file") ;;
|
||||
*) files+=("$repo_root/$file") ;;
|
||||
esac
|
||||
done
|
||||
|
||||
bazel run //tools:scalafmt -- --config "$repo_root/.scalafmt.conf" -i "${files[@]}"
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Runs the repository-pinned clang-tidy binary from the workspace root.
|
||||
#
|
||||
# Bazel's `run` command executes clang-tidy from its runfiles tree, which makes
|
||||
# workspace-relative source paths and .clang-tidy discovery fail. This wrapper
|
||||
# builds the tool and then invokes the produced binary directly.
|
||||
|
||||
set -e
|
||||
|
||||
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
|
||||
|
||||
repo_root="$(git rev-parse --show-toplevel)"
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
echo "Usage: scripts/run-clang-tidy.sh [clang-tidy args] <files> [-- <compiler args>]"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " scripts/run-clang-tidy.sh --verify-config"
|
||||
echo " scripts/run-clang-tidy.sh src/main/cpp/net/eagle0/shardok/library/CombatDamage.hpp -- -I$repo_root -std=c++23"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
bazel build @llvm_toolchain//:clang-tidy
|
||||
|
||||
bazel_bin="$(bazel info bazel-bin)"
|
||||
clang_tidy="$bazel_bin/external/toolchains_llvm++llvm+llvm_toolchain/clang-tidy"
|
||||
|
||||
if [ ! -x "$clang_tidy" ]; then
|
||||
echo "Unable to find clang-tidy binary at $clang_tidy"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$clang_tidy" --config-file="$repo_root/.clang-tidy" "$@"
|
||||
@@ -1,67 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Keeps Bazel macOS builds in sync with the installed Xcode version.
|
||||
#
|
||||
# Needed on macOS runners and developer machines because Bazel's generated
|
||||
# local_config_xcode/local_config_apple_cc repositories bake in the discovered
|
||||
# Xcode version. If Xcode is upgraded in place, the DEVELOPER_DIR path stays the
|
||||
# same, so Bazel can otherwise keep using stale Xcode metadata.
|
||||
#
|
||||
# 1. Writes .bazelrc.xcode with mactools-scoped flags:
|
||||
# - action_env for Apple build remote cache key invalidation
|
||||
# - DEVELOPER_DIR pointing to the active Xcode for Apple builds
|
||||
#
|
||||
# 2. Runs `bazel clean --expunge` when the version actually changes, because
|
||||
# local_config_apple_cc (a cached repository rule) bakes in the old version
|
||||
# and can only be refreshed by clearing the output base.
|
||||
#
|
||||
# .bazelrc imports the generated file via: try-import %workspace%/.bazelrc.xcode
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
DEVELOPER_DIR=""
|
||||
if [ -d "/Applications/Xcode.app/Contents/Developer" ]; then
|
||||
DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
|
||||
else
|
||||
DEVELOPER_DIR=$(xcode-select -p 2>/dev/null || true)
|
||||
fi
|
||||
|
||||
XCODE_BUILD_VERSION=$(DEVELOPER_DIR="$DEVELOPER_DIR" xcodebuild -version 2>/dev/null | awk '/Build version/ {print $3}' || true)
|
||||
|
||||
if [ -z "$XCODE_BUILD_VERSION" ]; then
|
||||
echo "ERROR: Could not detect full Xcode build version."
|
||||
echo "mactools/Sparkle builds require full Xcode, not just Command Line Tools."
|
||||
echo "Install Xcode at /Applications/Xcode.app or select it with xcode-select."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BAZELRC_XCODE=".bazelrc.xcode"
|
||||
EXPECTED_LINE="common:mactools --action_env=XCODE_BUILD_VERSION=${XCODE_BUILD_VERSION}"
|
||||
|
||||
# Check if the file already has the right version (first line is the version marker)
|
||||
if [ -f "$BAZELRC_XCODE" ]; then
|
||||
CURRENT_FIRST_LINE=$(head -1 "$BAZELRC_XCODE")
|
||||
if [ "$CURRENT_FIRST_LINE" = "$EXPECTED_LINE" ]; then
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Xcode Bazel config changed (or first run) — expunge local cache to clear
|
||||
# stale local_config_apple_cc, then write the new config
|
||||
OLD_CONFIG="missing"
|
||||
if [ -f "$BAZELRC_XCODE" ]; then
|
||||
OLD_CONFIG=$(head -1 "$BAZELRC_XCODE")
|
||||
fi
|
||||
echo "Xcode Bazel config changed: ${OLD_CONFIG} -> ${EXPECTED_LINE}"
|
||||
echo "Running bazel clean --expunge to clear stale toolchain config..."
|
||||
bazel clean --expunge 2>/dev/null || true
|
||||
|
||||
cat > "$BAZELRC_XCODE" << EOF
|
||||
${EXPECTED_LINE}
|
||||
common:mactools --repo_env=DEVELOPER_DIR=${DEVELOPER_DIR}
|
||||
EOF
|
||||
|
||||
if [ -n "${GITHUB_ENV:-}" ]; then
|
||||
echo "DEVELOPER_DIR=${DEVELOPER_DIR}" >> "$GITHUB_ENV"
|
||||
fi
|
||||
|
||||
echo "Updated ${BAZELRC_XCODE} — next macOS Bazel build will rebuild with Xcode ${XCODE_BUILD_VERSION}"
|
||||
@@ -1,7 +1,6 @@
|
||||
#!/bin/zsh
|
||||
|
||||
/opt/homebrew/bin/s3cmd sync \
|
||||
--acl-public \
|
||||
--exclude="*dream_log.txt" \
|
||||
--exclude="*.DS_Store" \
|
||||
--delete-removed \
|
||||
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
echo -ne "\033[0;32m"
|
||||
echo 'Updating bazel dependencies. This will take about five minutes.'
|
||||
echo -ne "\033[0m"
|
||||
set -e
|
||||
|
||||
if [ "$(uname -s)" == "Linux" ]; then
|
||||
BAZEL_DEPS_URL=https://github.com/johnynek/bazel-deps/releases/download/v0.1-9/bazel-deps-linux
|
||||
BAZEL_DEPS_SHA256=034ce4d8674a200d73d97aa60774786a3771c0f3871197858bf55fd930677b15
|
||||
elif [ "$(uname -s)" == "Darwin" ]; then
|
||||
BAZEL_DEPS_URL=https://github.com/johnynek/bazel-deps/releases/download/v0.1-9/bazel-deps-macos
|
||||
BAZEL_DEPS_SHA256=97e1b932cccc60e30475fc0ef56a538c6b8a732adbdf0eaec1988f0b4f1dc5dc
|
||||
else
|
||||
echo "Your platform '$(uname -s)' is unsupported, sorry"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# This is some bash snippet designed to find the location of the script.
|
||||
# we operate under the presumption this script is checked into the repo being operated on
|
||||
# so we goto the script location, then use git to find the repo root.
|
||||
SCRIPT_LOCATION="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd $SCRIPT_LOCATION
|
||||
|
||||
REPO_ROOT=$(git rev-parse --show-toplevel)
|
||||
|
||||
BAZEL_DEPS_DIR="$HOME/.bazel-deps-cache"
|
||||
BAZEL_DEPS_PATH="${BAZEL_DEPS_DIR}/v0.1-9"
|
||||
|
||||
if [ ! -f ${BAZEL_DEPS_PATH} ]; then
|
||||
( # Opens a subshell
|
||||
set -e
|
||||
echo "Fetching bazel deps."
|
||||
curl -L -o /tmp/bazel-deps-bin $BAZEL_DEPS_URL
|
||||
|
||||
GENERATED_SHA_256=$(shasum -a 256 /tmp/bazel-deps-bin | awk '{print $1}')
|
||||
|
||||
if [ "$GENERATED_SHA_256" != "$BAZEL_DEPS_SHA256" ]; then
|
||||
echo "Sha 256 does not match, expected: $BAZEL_DEPS_SHA256"
|
||||
echo "But found $GENERATED_SHA_256"
|
||||
echo "You may need to update the sha in this script, or the download was corrupted."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
chmod +x /tmp/bazel-deps-bin
|
||||
mkdir -p ${BAZEL_DEPS_DIR}
|
||||
mv /tmp/bazel-deps-bin ${BAZEL_DEPS_PATH}
|
||||
)
|
||||
fi
|
||||
|
||||
cd $REPO_ROOT
|
||||
set +e
|
||||
$BAZEL_DEPS_PATH generate -r $REPO_ROOT -s 3rdparty/workspace.bzl -d dependencies.yaml --target-file 3rdparty/target_file.bzl --disable-3rdparty-in-repo
|
||||
RET_CODE=$?
|
||||
set -e
|
||||
|
||||
if [ $RET_CODE == 0 ]; then
|
||||
echo "Success, going to format files"
|
||||
else
|
||||
echo "Failure, checking out 3rdparty/jvm"
|
||||
cd $REPO_ROOT
|
||||
git checkout 3rdparty/jvm 3rdparty/workspace.bzl
|
||||
exit $RET_CODE
|
||||
fi
|
||||
|
||||
$BAZEL_DEPS_PATH format-deps -d $REPO_ROOT/dependencies.yaml -o
|
||||
@@ -0,0 +1 @@
|
||||
7.7.1
|
||||
@@ -0,0 +1,2 @@
|
||||
# Bazel symlinks
|
||||
bazel-*
|
||||
@@ -0,0 +1,44 @@
|
||||
load("@build_bazel_rules_apple//apple:macos.bzl", "macos_bundle")
|
||||
|
||||
# Native Sparkle plugin for Unity
|
||||
# This plugin initializes Sparkle auto-updater at runtime
|
||||
objc_library(
|
||||
name = "sparkle_plugin_lib",
|
||||
srcs = ["SparklePlugin.m"],
|
||||
copts = [
|
||||
"-fmodules",
|
||||
"-fobjc-arc",
|
||||
"-fvisibility=default", # Export symbols for Unity P/Invoke
|
||||
],
|
||||
sdk_frameworks = ["Foundation"],
|
||||
deps = ["@sparkle//:Sparkle"],
|
||||
)
|
||||
|
||||
macos_bundle(
|
||||
name = "SparklePlugin",
|
||||
bundle_id = "net.eagle0.SparklePlugin",
|
||||
bundle_name = "SparklePlugin",
|
||||
infoplists = ["Info.plist"],
|
||||
linkopts = [
|
||||
# Export symbols for Unity P/Invoke
|
||||
"-exported_symbol",
|
||||
"_SparklePlugin_Initialize",
|
||||
"-exported_symbol",
|
||||
"_SparklePlugin_CheckForUpdates",
|
||||
"-exported_symbol",
|
||||
"_SparklePlugin_CheckForUpdatesInBackground",
|
||||
"-exported_symbol",
|
||||
"_SparklePlugin_IsCheckingForUpdates",
|
||||
"-exported_symbol",
|
||||
"_SparklePlugin_GetAutomaticallyChecksForUpdates",
|
||||
"-exported_symbol",
|
||||
"_SparklePlugin_SetAutomaticallyChecksForUpdates",
|
||||
"-exported_symbol",
|
||||
"_SparklePlugin_IsRunningFromReadOnlyVolume",
|
||||
"-exported_symbol",
|
||||
"_SparklePlugin_ShowDMGWarning",
|
||||
],
|
||||
minimum_os_version = "10.13",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [":sparkle_plugin_lib"],
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user