Compare commits

..
Author SHA1 Message Date
adminandClaude Opus 4.6 690d8c8903 Remove 1100+ lines of dead code from TutorialContentDefinitions
RegisterAll() had an early return followed by all the original tutorial
content definitions — unreachable since the narrative dialogue system
replaced them. Remove the dead code, keeping only the empty method stub.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-18 16:22:23 -07:00
3975 changed files with 721012 additions and 41855 deletions
+4 -14
View File
@@ -1,4 +1,4 @@
# bazel-1.0.0.bazelrc
bazel-1.0.0.bazelrc
# for now: filter out annoying TASTY warnings
common --ui_event_filters=-INFO
@@ -29,10 +29,6 @@ common --host_cxxopt="--std=c++23"
common --javacopt="-Xlint:-options"
# 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
@@ -41,17 +37,11 @@ common:macos --host_linkopt=-Wl,-no_warn_duplicate_libraries
# See: https://github.com/grpc/grpc/issues/37619
common:macos --features=-module_maps
# Pin DEVELOPER_DIR so Apple repo rules use full Xcode instead of Command Line
# Tools. The sync script writes .bazelrc.xcode with an Xcode build-version
# marker so in-place Xcode updates refresh Bazel's generated Apple toolchains.
# Pin DEVELOPER_DIR so the apple_cc_autoconf repo rule doesn't re-evaluate
# when Xcode updates in-place. The sync script overrides this for mactools.
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 macOS builds. Generated by scripts/sync_bazel_xcode.sh.
# Xcode config for mactools builds only. Generated by scripts/sync_bazel_xcode.sh.
# Bakes the Xcode build version into action cache keys and sets DEVELOPER_DIR.
try-import %workspace%/.bazelrc.xcode
@@ -6,44 +6,8 @@ on:
- cron: '0 */6 * * *'
workflow_dispatch:
permissions:
contents: read
actions: write
jobs:
cleanup-expired:
runs-on: ubuntu-latest
steps:
- name: Delete 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\(.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 artifacts created before $cutoff"
deleted=0
while IFS=$'\t' read -r id created_at name; do
if [[ "$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 expired artifacts out of $total total."
rm -f /tmp/all_artifacts.txt
check-storage:
needs: cleanup-expired
runs-on: ubuntu-latest
steps:
+5 -16
View File
@@ -11,7 +11,6 @@ on:
- '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'
- '.bazelrc'
- '.github/workflows/auth_build.yml'
workflow_dispatch:
@@ -25,25 +24,17 @@ on:
permissions:
contents: read
concurrency:
group: auth-build-deploy
cancel-in-progress: false
jobs:
build-auth:
runs-on: [self-hosted, bazel, halfdan]
runs-on: [self-hosted, bazel]
outputs:
image_tag: ${{ steps.push-auth.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
persist-credentials: false
lfs: false
- name: Ensure Bazel installed
run: ./ci/github_actions/ensure_bazel_installed.sh
- name: Build Auth Server Docker image
id: build-auth
run: |
@@ -136,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
@@ -149,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)
+5 -8
View File
@@ -1,22 +1,19 @@
name: Bazel Cache Cleanup
on:
# Disabled: bazel clean fails when another runner shares the output_base
# on /Volumes/remote_cache (unlinkat "Directory not empty" race).
# See https://github.com/nolen777/eagle0/issues/TBD for details.
# schedule:
# - cron: '0 0 * * 0'
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, halfdan]
runs-on: [self-hosted, bazel]
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
persist-credentials: false
lfs: false
- name: Show disk usage before cleanup
+5 -45
View File
@@ -9,7 +9,6 @@ on:
- 'MODULE.bazel'
- 'BUILD.bazel'
- '.bazelrc'
- 'ci/github_actions/ensure_bazel_installed.sh'
- '.github/workflows/bazel_test.yml'
- '!src/main/csharp/**'
- '!src/test/csharp/**'
@@ -20,7 +19,6 @@ on:
- 'MODULE.bazel'
- 'BUILD.bazel'
- '.bazelrc'
- 'ci/github_actions/ensure_bazel_installed.sh'
- '.github/workflows/bazel_test.yml'
- '!src/main/csharp/**'
- '!src/test/csharp/**'
@@ -42,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
@@ -82,30 +62,10 @@ 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: Ensure Bazel installed
run: ./ci/github_actions/ensure_bazel_installed.sh
- name: Run tests
run: bazel test --build_event_json_file=test.json //src/test/... //src/main/go/...
- name: Collect failed test logs
@@ -142,14 +102,14 @@ 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
- 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/
+2 -6
View File
@@ -11,18 +11,14 @@ permissions:
jobs:
cleanup:
runs-on: [self-hosted, bazel, halfdan]
runs-on: [self-hosted, bazel]
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
persist-credentials: false
lfs: false
clean: false
- name: Ensure Bazel installed
run: ./ci/github_actions/ensure_bazel_installed.sh
- name: Clean up unreferenced blobs
env:
ACCESS_KEY_ID: ${{ secrets.ACCESS_KEY_ID }}
+6 -10
View File
@@ -27,15 +27,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
uses: actions/checkout@v4
- name: Build sysroot
run: ./tools/sysroot/build_sysroot.sh
- name: Upload sysroot artifact
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: ubuntu-noble-sysroot-amd64
path: tools/sysroot/output/
@@ -83,23 +81,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
persist-credentials: false
uses: actions/checkout@v4
- name: Set up QEMU for ARM64 emulation
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
with:
platforms: arm64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
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@v7
uses: actions/upload-artifact@v4
with:
name: ubuntu-noble-sysroot-arm64
path: tools/sysroot/output/
+7 -49
View File
@@ -18,7 +18,6 @@ on:
- '!src/main/protobuf/net/eagle0/eagle/internal/auth_internal.proto'
- 'src/main/resources/**'
- 'ci/BUILD.bazel'
- 'ci/github_actions/ensure_bazel_installed.sh'
- 'MODULE.bazel'
- '.bazelrc'
- 'docker-compose.prod.yml'
@@ -48,7 +47,7 @@ jobs:
# This uses 1 runner slot instead of 4, and Bazel parallelizes internally
# NOTE: Must run on a runner with the bazel label to ensure consistent Bazel cache
build-all:
runs-on: [self-hosted, bazel, halfdan]
runs-on: [self-hosted, bazel]
outputs:
eagle_image_tag: ${{ steps.push-images.outputs.eagle_image_tag }}
admin_image_tag: ${{ steps.push-images.outputs.admin_image_tag }}
@@ -69,38 +68,14 @@ 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'
run: ./ci/github_actions/ensure_bazel_installed.sh
- 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/*"
- name: Build all Docker images
@@ -142,7 +117,7 @@ jobs:
- name: Upload warmup binary
if: steps.check-latest.outputs.skip != 'true'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: warmup-binary
path: scripts/bin/warmup
@@ -227,7 +202,7 @@ jobs:
echo "=== All images pushed successfully ==="
deploy:
runs-on: [self-hosted, bazel, halfdan]
runs-on: [self-hosted, bazel]
needs: [build-all]
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.push_images == 'true')
environment: production
@@ -269,26 +244,9 @@ jobs:
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
@@ -299,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/
@@ -322,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
# =================================================================
+1 -22
View File
@@ -11,7 +11,6 @@ on:
- 'MODULE.bazel'
- 'BUILD.bazel'
- '.bazelrc'
- 'ci/github_actions/ensure_bazel_installed.sh'
- '.github/workflows/eagle_build.yml'
concurrency:
@@ -26,29 +25,9 @@ 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
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
persist-credentials: false
lfs: false
- name: Ensure Bazel installed
run: ./ci/github_actions/ensure_bazel_installed.sh
- name: Build Eagle server
run: bazel build //src/main/scala/net/eagle0/eagle:eagle_server
+2 -25
View File
@@ -5,12 +5,10 @@ on:
branches: [ "main" ]
paths:
- ".github/workflows/installer_build.yml"
- "ci/github_actions/ensure_bazel_installed.sh"
- "src/main/go/net/eagle0/clients/win/installer/**"
pull_request:
paths:
- ".github/workflows/installer_build.yml"
- "ci/github_actions/ensure_bazel_installed.sh"
- "src/main/go/net/eagle0/clients/win/installer/**"
workflow_dispatch:
@@ -23,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: Ensure Bazel installed
run: ./ci/github_actions/ensure_bazel_installed.sh
- name: Build Go installer for Windows
env:
MANIFEST_PUBLIC_KEY: ${{ secrets.MANIFEST_PUBLIC_KEY }}
@@ -73,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/
+5 -77
View File
@@ -108,7 +108,7 @@ jobs:
build-unity:
needs: check-changes
if: needs.check-changes.outputs.should_build == 'true'
runs-on: [self-hosted, macOS, testflight, halfdan]
runs-on: [self-hosted, macOS, testflight]
steps:
- name: Prune stale PR refs
@@ -122,26 +122,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
@@ -152,8 +134,6 @@ 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
- name: Ensure Unity version installed
@@ -184,69 +164,17 @@ jobs:
- 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 }}"
archive-and-upload:
needs: build-unity
runs-on: [self-hosted, macOS, testflight, halfdan]
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: Install Signing Certificate
env:
IOS_CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }}
@@ -336,7 +264,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
+12 -76
View File
@@ -20,7 +20,6 @@ on:
- "scripts/notarize_wait.sh"
- "ci/github_actions/build_mac.sh"
- "ci/github_actions/build_unity_mac.sh"
- "ci/github_actions/ensure_bazel_installed.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"
@@ -40,7 +39,6 @@ on:
- "scripts/notarize_wait.sh"
- "ci/github_actions/build_mac.sh"
- "ci/github_actions/build_unity_mac.sh"
- "ci/github_actions/ensure_bazel_installed.sh"
- "ci/mac/**"
workflow_dispatch:
inputs:
@@ -66,7 +64,7 @@ env:
jobs:
build-and-sign:
runs-on: [self-hosted, macOS, unity-mac, halfdan]
runs-on: [self-hosted, macOS, unity-mac]
outputs:
submission_id: ${{ steps.notarize-submit.outputs.submission_id }}
should_deploy: ${{ steps.check-deploy.outputs.should_deploy }}
@@ -83,26 +81,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
@@ -129,16 +109,11 @@ jobs:
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 mac
- name: Ensure Bazel installed
run: ./ci/github_actions/ensure_bazel_installed.sh
- name: Sync Bazel Xcode config
run: ./scripts/sync_bazel_xcode.sh
@@ -147,9 +122,7 @@ jobs:
- 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: Upload Addressables to CDN
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
@@ -254,7 +227,7 @@ jobs:
- name: Upload signed app
if: success() && steps.check-deploy.outputs.should_deploy == 'true'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: signed-mac-app-${{ github.run_id }}
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC/eagle0.app.zip
@@ -262,7 +235,7 @@ jobs:
- name: Archive Build Log
if: success() || failure()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: editor_mac.log
path: ${{ env.EAGLE0_BUILD_DIR }}/editor_mac.log
@@ -274,35 +247,18 @@ jobs:
wait-notarization:
needs: build-and-sign
if: needs.build-and-sign.outputs.should_deploy == 'true'
runs-on: [self-hosted, macOS, notarize, halfdan]
runs-on: [self-hosted, macOS, notarize]
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
- uses: actions/checkout@v4
with:
persist-credentials: false
sparse-checkout: scripts
- name: Clean download directory
run: rm -rf ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
- name: Download signed app
uses: actions/download-artifact@v8
uses: actions/download-artifact@v4
with:
name: signed-mac-app-${{ github.run_id }}
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
@@ -330,7 +286,7 @@ jobs:
ditto -c -k --keepParent eagle0.app eagle0.app.zip
- name: Upload notarized app
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
@@ -342,37 +298,20 @@ jobs:
deploy:
needs: [build-and-sign, wait-notarization]
if: needs.build-and-sign.outputs.should_deploy == 'true'
runs-on: [self-hosted, macOS, notarize, halfdan]
runs-on: [self-hosted, macOS, unity-mac]
outputs:
deployed_version: ${{ steps.deploy-mac.outputs.deployed_version }}
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
- uses: actions/checkout@v4
with:
persist-credentials: false
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@v8
uses: actions/download-artifact@v4
with:
name: notarized-mac-app-${{ github.run_id }}
path: ${{ env.EAGLE0_BUILD_DIR }}/eagle0MAC
@@ -383,9 +322,6 @@ jobs:
ditto -x -k eagle0.app.zip .
rm eagle0.app.zip
- name: Ensure Bazel installed
run: ./ci/github_actions/ensure_bazel_installed.sh
- name: Sync Bazel Xcode config
run: ./scripts/sync_bazel_xcode.sh
-86
View File
@@ -1,86 +0,0 @@
name: S3 Archive Cleanup
on:
schedule:
# Run weekly on Sunday at 05:00 UTC
- cron: '0 5 * * 0'
workflow_dispatch: # Allow manual trigger
permissions:
contents: read
jobs:
cleanup:
runs-on: [self-hosted, bazel, halfdan]
steps:
- name: Ensure AWS CLI is available
run: |
if ! command -v aws &> /dev/null; then
brew install awscli
fi
- name: Delete old archived game folders
env:
AWS_ACCESS_KEY_ID: ${{ secrets.DO_SPACES_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.DO_SPACES_SECRET_KEY }}
run: |
set -euo pipefail
S3_ENDPOINT="https://sfo3.digitaloceanspaces.com"
BUCKET="s3://eagle0/eagle/archived/"
# macOS date syntax
CUTOFF=$(date -v-1m +%s)
DELETED=0
SKIPPED=0
NO_DIR_FILE=0
echo "Cutoff date: $(date -r ${CUTOFF} '+%Y-%m-%dT%H:%M:%S')"
echo ""
FOLDERS=$(aws s3 ls "$BUCKET" --endpoint-url "$S3_ENDPOINT" \
| awk '/PRE/{gsub(/\/$/,"",$2); print $2}')
if [ -z "$FOLDERS" ]; then
echo "No archived folders found."
exit 0
fi
TOTAL=$(echo "$FOLDERS" | wc -l | tr -d ' ')
CURRENT=0
for game_id in $FOLDERS; do
CURRENT=$((CURRENT + 1))
DIR_INFO=$(aws s3 ls "${BUCKET}${game_id}/directory.e0i" \
--endpoint-url "$S3_ENDPOINT" 2>/dev/null || true)
if [ -z "$DIR_INFO" ]; then
echo "[$CURRENT/$TOTAL] DELETE $game_id (no directory.e0i)"
aws s3 rm --recursive "${BUCKET}${game_id}/" \
--endpoint-url "$S3_ENDPOINT" > /dev/null 2>&1
DELETED=$((DELETED + 1))
NO_DIR_FILE=$((NO_DIR_FILE + 1))
continue
fi
FILE_DATE=$(echo "$DIR_INFO" | awk '{print $1 " " $2}')
FILE_EPOCH=$(date -j -f '%Y-%m-%d %H:%M:%S' "$FILE_DATE" +%s 2>/dev/null || echo "0")
if [ "$FILE_EPOCH" -lt "$CUTOFF" ]; then
echo "[$CURRENT/$TOTAL] DELETE $game_id (directory.e0i from $FILE_DATE)"
aws s3 rm --recursive "${BUCKET}${game_id}/" \
--endpoint-url "$S3_ENDPOINT" > /dev/null 2>&1
DELETED=$((DELETED + 1))
else
echo "[$CURRENT/$TOTAL] KEEP $game_id (directory.e0i from $FILE_DATE)"
SKIPPED=$((SKIPPED + 1))
fi
done
echo ""
echo "=== Summary ==="
echo "Total folders: $TOTAL"
echo "Deleted: $DELETED"
echo "Kept (recent): $SKIPPED"
+25 -53
View File
@@ -9,7 +9,6 @@ on:
- 'src/main/protobuf/net/eagle0/common/**'
- 'src/main/resources/net/eagle0/shardok/**'
- 'ci/BUILD.bazel'
- 'ci/github_actions/ensure_bazel_installed.sh'
- 'MODULE.bazel'
- '.bazelrc'
- '.github/workflows/shardok_arm64_build.yml'
@@ -30,19 +29,15 @@ permissions:
jobs:
build-shardok-arm64:
runs-on: [self-hosted, bazel, halfdan]
runs-on: [self-hosted, bazel]
outputs:
image_tag: ${{ steps.push-shardok.outputs.image_tag }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
persist-credentials: false
lfs: false
- name: Ensure Bazel installed
run: ./ci/github_actions/ensure_bazel_installed.sh
- name: Build Shardok ARM64 binary (cross-compile for Linux ARM64)
run: |
set -ex
@@ -170,7 +165,7 @@ jobs:
echo "Also tagged as: registry.digitalocean.com/eagle0/shardok-server:arm64-latest"
deploy-hetzner:
runs-on: [self-hosted, bazel, halfdan]
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
@@ -185,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
@@ -251,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
+3 -24
View File
@@ -5,14 +5,13 @@ on:
pull_request:
paths:
- 'src/main/cpp/**'
- 'src/main/protobuf/net/eagle0/shardok/**'
- 'src/main/protobuf/net/eagle0/common/**'
- 'src/main/proto/net/eagle0/shardok/**'
- 'src/main/proto/net/eagle0/common/**'
- 'src/main/go/net/eagle0/build/**'
- 'WORKSPACE'
- 'MODULE.bazel'
- 'BUILD.bazel'
- '.bazelrc'
- 'ci/github_actions/ensure_bazel_installed.sh'
- '.github/workflows/shardok_build.yml'
concurrency:
@@ -27,29 +26,9 @@ 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
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v4
with:
persist-credentials: false
lfs: false
- name: Ensure Bazel installed
run: ./ci/github_actions/ensure_bazel_installed.sh
- name: Build Shardok server
run: bazel build -c opt //src/main/cpp/net/eagle0/shardok:shardok-server
+10 -56
View File
@@ -15,7 +15,6 @@ on:
- "scripts/build_plugins.sh"
- "scripts/build_windows_plugin.sh"
- "ci/github_actions/build_unity.sh"
- "ci/github_actions/ensure_bazel_installed.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"
@@ -35,7 +34,6 @@ on:
- "scripts/build_plugins.sh"
- "scripts/build_windows_plugin.sh"
- "ci/github_actions/build_unity.sh"
- "ci/github_actions/ensure_bazel_installed.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"
@@ -55,7 +53,7 @@ env:
jobs:
windows-unity:
runs-on: [self-hosted, macOS, unity-windows, halfdan]
runs-on: [self-hosted, macOS, unity-windows]
outputs:
deployed_version: ${{ steps.get-version.outputs.deployed_version }}
@@ -71,86 +69,42 @@ 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
- 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
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
- name: Ensure Unity version installed
run: ./ci/github_actions/ensure_unity_installed.sh windows
- name: Ensure Bazel installed
run: ./ci/github_actions/ensure_bazel_installed.sh
- name: Build Windows unity
run: ./ci/github_actions/build_unity.sh "${{ env.EAGLE0_BUILD_DIR }}/eagle0WIN"
- name: Save build SHA for Bee/ cache invalidation
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
- 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
run: git rev-parse HEAD > src/main/csharp/net/eagle0/clients/unity/eagle0/Library/.last_built_sha
- name: Upload Addressables to CDN
if: success() && github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
@@ -198,7 +152,7 @@ jobs:
- 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
@@ -218,4 +172,4 @@ jobs:
run: |
curl -X POST "https://admin.eagle0.net/notify-update?platform=windows&version=${{ needs.windows-unity.outputs.deployed_version }}&required=false" \
-H "X-Notify-Secret: $NOTIFY_SECRET" \
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
--fail --silent --show-error || echo "Warning: Failed to notify clients (non-fatal)"
-3
View File
@@ -23,7 +23,6 @@ bazel-bin
bazel-eagle0*
bazel-out
bazel-testlogs
.bazelrc.local
.bazelrc.xcode
.ijwb
.clwb
@@ -44,5 +43,3 @@ 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
+2 -2
View File
@@ -34,8 +34,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:
+3 -5
View File
@@ -4,11 +4,9 @@
**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 use `git -C`.** Instead, cd to the repo root and run git commands from there.
**NEVER push directly to main/master.** No exceptions. Not for "small changes." Not for docs. Not ever.
@@ -228,7 +226,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.9f1) 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 +350,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
-12
View File
@@ -295,9 +295,6 @@ maven.install(
# Testing
"org.scalamock:scalamock_3:7.4.1",
# Developer tools
"org.scalameta:scalafmt-cli_2.13:3.9.9",
# AWS SDK
"software.amazon.awssdk:s3-transfer-manager:%s" % AWS_SDK_VERSION,
"software.amazon.awssdk:s3:%s" % AWS_SDK_VERSION,
@@ -340,15 +337,6 @@ maven.install(
],
)
# 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 = "3.3.6",
)
# Force specific versions for dependencies with conflicts between grpc-java and protobuf
maven.artifact(
artifact = "gson",
+7 -38
View File
@@ -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",
-22
View File
@@ -38,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
-16
View File
@@ -53,22 +53,6 @@ if [ $UNITY_EXIT_CODE -ne 0 ]; then
exit $UNITY_EXIT_CODE
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"
-22
View File
@@ -38,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
@@ -1,82 +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}"
write_developer_dir_override() {
if [ "$(uname -s)" != "Darwin" ]; then
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
{
echo "# Generated by ci/github_actions/ensure_bazel_installed.sh; do not edit."
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_developer_dir_override
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
+1 -31
View File
@@ -11,46 +11,16 @@
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
echo "LFS objects before pull:"
git lfs ls-files | wc -l
GIT_LFS_PULL=(git 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)
fi
for i in $(seq 1 $MAX_ATTEMPTS); do
if "${GIT_LFS_PULL[@]}" "$@"; then
if git lfs pull "$@"; then
echo "LFS objects after pull:"
git lfs ls-files | wc -l
exit 0
-57
View File
@@ -1,57 +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.
"""
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.",
),
},
)
+6 -7
View File
@@ -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)
+9 -18
View File
@@ -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
-335
View File
@@ -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 13 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 23 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 13, 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 23. 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 14) 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 03 provinces, 05 heroes, 010 changed entities total, plus 02 notifications. So the typical insert is ~520 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 100500 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 34.
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 23 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 13, 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 23.
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.
-291
View File
@@ -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.
+1 -1
View File
@@ -32,7 +32,7 @@ When a new player starts their first game in tutorial mode, they experience:
- 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**: 31
- **Origin Province**: 32
### Flee Trigger
Attacker flees when ANY of these conditions are met:
-322
View File
@@ -1,322 +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 first-battle scenario (Tarn vs. John Ranil)
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 at line 17 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`).
**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`).
### 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
All triggers are raised via `TutorialTriggerRegistry`. Today these flow to `DialogueManager.TriggerDialogue()` and are matched against the dialogue JSON; if step-UI sequences are re-registered, they will also route there. File:line citations are for the registry unless noted; line numbers may drift.
### Bootstrap / first-session
| Trigger | Fires from | When |
|---------|-----------|------|
| `game_started` | `EagleGameController.SetUpGame()` | First time entering a tutorial game |
| `first_battle_available` | `OnModelUpdated()` ~L204 | A `RunningShardokGameModel` first appears |
| `battle_entered` | `OnBattleEntered()` ~L793 | User clicks **Battle!** |
| `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 (no dialogue script 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 Battle! to enter tactical combat.",
"highlightTarget": "GoToBattleButton",
"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 so it can be replayed.
- Settings → "Reset Tutorials" calls `TutorialManager.ResetAllProgress()`, which also calls `DialogueManager.ResetCompletedScripts()`.
`DialogueManager` tracks completed scripts in memory only — they reset whenever `TutorialState` does.
---
## 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 **Battle!** button (`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 12 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) |
### 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.
-173
View File
@@ -1,173 +0,0 @@
# URP Migration Plan
## Why
Unity is removing Built-In Render Pipeline (BiRP) support after Unity 6.7. We're currently on Unity 6.4. Each Unity minor version ships roughly quarterly, giving us a few release cycles before BiRP is dropped. This document lays out the migration plan.
## Strategy: Long-Lived Feature Branch
All URP conversion work happens on a **long-lived `urp-migration` branch**. Main stays fully functional on BiRP throughout the migration.
- **Main branch**: Continues using BiRP. All gameplay, AI, and server development proceeds normally.
- **URP branch**: Accumulates rendering pipeline changes across all phases.
- **Regular rebasing**: Periodically rebase/merge `main` into the URP branch to stay current. Shader and material changes rarely conflict with gameplay code, so merge conflicts should be manageable.
- **Merge to main**: Only when everything renders correctly and visual QA passes.
This avoids the "everything is pink" problem of switching the pipeline on main before shaders are converted.
## Current State
| Category | Count | Notes |
|---|---|---|
| Shaders | 47 | 7 custom Eagle, 1 hex mesh, 20 Polytope, 13 TextMesh Pro, 6 other third-party |
| Materials | 325 | Mix of Standard shader, custom, and third-party |
| ShaderGraph files | 4 | All TextMesh Pro (URP + HDRP variants already exist) |
| Scenes with baked lighting | 6 | Gameplay, Connection, Eagle, Shardok, Shared, Map Editor |
| Post-processing | 1 profile | PPP_Orc.asset (Post Processing Stack v2, FXAA/TAA, AO) |
| C# rendering scripts | 16 | Material/shader property manipulation only |
| Rendering path | Forward | Matches URP default |
### Positive Findings
- **No OnRenderImage, Graphics.Blit, CommandBuffer, or GL.\* usage** in C# code
- Forward rendering already in use (matches URP default)
- TextMesh Pro already has URP ShaderGraph variants in the project
- No custom render passes or ScriptableRenderFeatures
- Shader property manipulation (SetTexture, SetFloat, SetColor) is URP-compatible
### Known Risk Areas
- **GrabPass shaders** (HeatShimmerShader, PT_Water_Shader) have no direct URP equivalent
- **ProvinceMapShader** is complex (province ID lookup, border rendering, ocean animation, faction highlighting, UI clipping)
- **Surface shaders** (`#pragma surface surf Standard`) must be rewritten as HLSL or ShaderGraph
- **Polytope Studio shaders** (20 shaders) have no vendor-provided URP variants
- **Post Processing Stack v2** must be replaced with URP's integrated Volume system
- **Tessellation** (PT_Water_Shader) is not natively supported in URP
## Shader Inventory
### Custom Eagle Shaders (7) -- HIGH PRIORITY
| Shader | Complexity | Key Issues |
|---|---|---|
| ProvinceMapShader | High | Province ID texture lookup, border rendering, ocean animation, faction highlighting, `UnityUI.cginc` dependency, stencil/clipping |
| ProvinceWeatherMapShader | Medium | Weather overlay rendering |
| ProvinceWeatherShader | Medium | Province weather effects |
| ProvinceParticleShader | Low | Custom particle rendering |
| HeatShimmerShader | High | **GrabPass** for screen distortion, province masking |
| ClipRectParticleUnlit | Low | UI-clipped particle shader |
| maskShader | Low | UI masking |
### Hex Mesh Shader (1) -- MODERATE PRIORITY
Uses `#pragma surface surf Standard` with GPU instancing. Straightforward conversion to URP Lit or ShaderGraph.
### Third-Party Shaders (39)
| Source | Count | Complexity | Notes |
|---|---|---|---|
| Polytope Studio | 20 | Moderate-High | PBR, Toon, vegetation (custom lighting), water (**GrabPass + tessellation**). No vendor URP pack available. |
| TextMesh Pro | 13 | Low | URP ShaderGraph variants already exist in project |
| RRFreelance | 3 | Low | Standard surface shaders, direct conversion |
| Clown.fat | 2 | Moderate | Custom ToonRamp lighting model |
| GUI Pro Kit | 1 | Low | Hidden particle shader |
### GrabPass Shaders (Require Special Handling)
GrabPass does not exist in URP. These must be reimplemented using `ScriptableRenderPass` + `Renderer Features` or Blit-based alternatives:
1. **HeatShimmerShader** -- Screen distortion effect (currently has a shimmer-disabled TODO, may be deprioritized)
2. **PT_Water_Shader** -- Water refraction/transparency (PT_Water_Shader_WebGl exists without GrabPass as a reference)
## Phased Approach
### Phase 1: Pipeline Setup + Auto-Conversion (Days 1-3)
- Install URP package
- Create URP Pipeline Asset and Renderer Asset
- Configure basic pipeline settings (forward rendering, shadow settings)
- Run Unity's **Render Pipeline Converter** (Edit > Rendering > Render Pipeline Converter)
- Auto-converts Standard shader materials and some built-in shaders
- Handles a significant portion of the 325 materials
- Will NOT touch custom shaders
- Create a test scene to validate basic rendering
- Migrate viewport clipping system (`RendererViewportClipper`, `ViewportClipper`, `PopupClipper`) to use URP-compatible global shader properties
### Phase 2: Custom Eagle Shaders (Weeks 1-3)
This is the critical path. Without these, the game is unplayable.
**Week 1-2: ProvinceMapShader**
- Convert from BiRP Cg/HLSL to URP HLSL
- Replace `UnityCG.cginc` includes with `Core.hlsl` / `Common.hlsl`
- Replace `UnityUI.cginc` with custom URP-compatible UI clipping
- Preserve province ID texture lookup, border rendering, ocean animation, faction highlighting
- Validate stencil operations and render queue ordering
**Week 2-3: Remaining Eagle shaders**
- ProvinceWeatherMapShader + ProvinceWeatherShader
- ProvinceParticleShader + ClipRectParticleUnlit
- maskShader
- Hex Mesh Shader (convert surface shader to URP Lit)
- HeatShimmerShader (reimplement without GrabPass, or defer if shimmer remains disabled)
### Phase 3: Third-Party Shaders (Weeks 4-6)
**Polytope Studio (20 shaders)**
- Convert PBR shaders (Armors, NPC, Weapons, Props, Rock) from surface shaders to URP Lit / ShaderGraph
- Convert Toon shaders to custom URP shader or ShaderGraph with custom lighting
- Convert vegetation shaders (custom `StandardCustom` lighting) to ShaderGraph
- PT_Water_Shader: Reimplement without GrabPass and tessellation (use PT_Water_Shader_WebGl as reference for non-GrabPass approach)
**Other third-party**
- Clown.fat ToonRamp shaders: Convert custom lighting model to ShaderGraph
- RRFreelance shaders: Direct Standard-to-URP-Lit conversion
- GUI Pro Kit particle shader: Convert to URP particle shader
### Phase 4: Materials, Lighting & Post-Processing (Weeks 7-8)
**Materials**
- Batch-update any remaining materials not handled by the auto-converter
- Verify all 325 materials render correctly
- Fix any visual differences from lighting model changes
**Lighting**
- Rebake lightmaps for all 6 scenes (Gameplay, Connection, Eagle, Shardok, Shared, Map Editor)
- Configure URP shadow cascade settings to match current quality levels
- Verify HDR rendering
**Post-Processing**
- Remove Post Processing Stack v2 dependency (`com.unity.postprocessing: 3.5.1`)
- Replace with URP integrated Volume system
- Recreate PPP_Orc effects (FXAA/TAA, Ambient Occlusion) using URP Volume overrides
### Phase 5: Testing & Validation (Weeks 9-11)
- Visual QA across all 6 scenes
- Verify beast/character rendering (vertex colors, animations)
- Verify weather effects (blizzard, drought, flood)
- Verify particle systems (fire & explosion effects, UI particles)
- Verify UI rendering (GUI Pro Kit, Modern UI Pack, TextMesh Pro)
- Performance profiling (URP has different performance characteristics)
- Test on target platforms
- Bug fixes and visual polish
## Effort Estimates
| Phase | Best Case | Realistic | Worst Case |
|---|---|---|---|
| 1. Pipeline Setup | 1-2 days | 2-3 days | 3-5 days |
| 2. Custom Eagle Shaders | 1.5 weeks | 2-3 weeks | 3-4 weeks |
| 3. Third-Party Shaders | 1.5 weeks | 2-3 weeks | 3-4 weeks |
| 4. Materials & Lighting | 3-5 days | 1-1.5 weeks | 2 weeks |
| 5. Testing & Validation | 1 week | 1.5-2 weeks | 2-3 weeks |
| **Total** | **6-8 weeks** | **8-12 weeks** | **12-16 weeks** |
The biggest variable is Polytope Studio shader conversion. If vendor URP packs become available, Phase 3 shrinks significantly.
## Key Reminders
- **Don't leave GrabPass rewrites and ProvinceMapShader for the end** -- they're the riskiest pieces and should be tackled early.
- The migration window (~1 year) is comfortable. Spreading the work across this window is fine, but front-load the hard shader work.
- C# code changes should be minimal -- the codebase avoids direct rendering API usage.
- `UnityUI.cginc` has no direct URP equivalent. Custom implementation will be needed for UI clipping in shaders.
+53 -111
View File
@@ -8,25 +8,13 @@ The AI attempts to complete quests via `FulfillQuestsCommandSelector`, which is
## Quests the AI Actively Completes
The AI processes quest handlers in priority order. The first handler that produces a valid command wins.
### 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
@@ -36,96 +24,72 @@ The AI processes quest handlers in priority order. The first handler that produc
| `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 |
| `SpendOnFeastsQuest` | `SpendOnFeastsQuestCommandChooser` | Spend gold on feasts |
| `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. |
### Riot Quests
| Quest | Handler | Notes |
|-------|---------|-------|
| `SuppressRiotByForceQuest` | `CommandChoiceHelpers.handleRiotSelectedCommand` | When this quest exists and the faction has battalions, the AI prefers CrackDown over Give when handling riots. Not a quest command chooser — modifies existing riot handling priority. |
### 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 Does Not Yet Attempt to Complete
## Quests the AI Does Not Attempt to Complete
The following quests have no handler and must be completed naturally through gameplay. They are listed in rough priority order for future implementation.
The following quests have no handler in `FulfillQuestsCommandSelector` and must be completed naturally through gameplay:
### Planned for Implementation
### 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
None currently planned.
### Expansion Quests
- `ExpandToProvincesQuest` - Expand to control a certain number of provinces
- `SpecificExpansionQuest` - Conquer a specific province
- `BorderSecurityQuest` - Have troops in a border province
### Not Planned
### 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
These quests are too situational, passive, or risky to actively pursue. They may be completed naturally through gameplay.
### 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
- `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.
### Reconnaissance Quests
- `ReconProvincesQuest` - Reconnoiter a number of provinces
- `ReconSpecificProvincesQuest` - Reconnoiter specific provinces
### 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
### 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
### Miscellaneous
- `BattalionDiversityQuest` - Have diverse battalion types
- `SwearBrotherhoodWithHeroQuest` - Swear brotherhood with a specific hero
- `BetrayAllyQuest` - Betray an allied faction
## Implementation Details
@@ -138,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. SpendOnFeasts
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
-829
View File
File diff suppressed because it is too large Load Diff
+1 -3
View File
@@ -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
-22
View File
@@ -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[@]}"
+13 -26
View File
@@ -1,12 +1,11 @@
#!/usr/bin/env bash
# Keeps Bazel macOS builds in sync with the installed Xcode version.
# Keeps Bazel mactools 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.
# Only needed on runners that do mactools builds (Mac/Sparkle). Non-Mac builds
# don't run this script; their apple_cc_autoconf stays cached because none of
# its environ vars change, so they're unaffected by Xcode version changes.
#
# 1. Writes .bazelrc.xcode with macOS-scoped flags:
# 1. Writes .bazelrc.xcode with mactools-scoped flags:
# - action_env for remote cache key invalidation
# - DEVELOPER_DIR pointing to the active Xcode
#
@@ -18,24 +17,17 @@
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)
XCODE_BUILD_VERSION=$(xcodebuild -version 2>/dev/null | awk '/Build version/ {print $3}')
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
echo "Warning: Could not detect Xcode build version, skipping"
exit 0
fi
DEVELOPER_DIR=$(xcode-select -p 2>/dev/null)
BAZELRC_XCODE=".bazelrc.xcode"
EXPECTED_LINE="common:macos --action_env=XCODE_BUILD_VERSION=${XCODE_BUILD_VERSION}"
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
@@ -57,11 +49,6 @@ bazel clean --expunge 2>/dev/null || true
cat > "$BAZELRC_XCODE" << EOF
${EXPECTED_LINE}
common:macos --repo_env=DEVELOPER_DIR=${DEVELOPER_DIR}
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}"
echo "Updated ${BAZELRC_XCODE} — next mactools build will rebuild with Xcode ${XCODE_BUILD_VERSION}"
@@ -423,35 +423,6 @@ bool AICommandFilter::IsWastefulAction(
break;
}
case CommandType::FEAR_COMMAND: {
// Solo attackers gain nothing from Fear: the target is stunned for a round but
// there is no teammate to capitalize, and the caster ends its turn no closer to
// killing units or capturing castles. Defenders, by contrast, benefit from stalling,
// so this restriction only applies to the attacking side.
//
// Exception: if the victim is standing on a fire tile, stunning locks them in place
// while the fire modifier keeps chewing through their unit value — the burn does the
// work that a follow-up attacker would normally provide.
if (isDefender) { break; }
if (CountPlayerUnits(gameState, pid) > 1) { break; }
const int targetRow = cmd.GetTargetRow();
const int targetCol = cmd.GetTargetColumn();
if (targetRow < 0 || targetCol < 0) {
throw ShardokInternalErrorException(
"FEAR_COMMAND missing required target information");
}
const Coords victimLocation(
static_cast<int8_t>(targetRow),
static_cast<int8_t>(targetCol));
const auto* victimTerrain = GetTerrain(gameState->hex_map(), victimLocation);
if (!victimTerrain->modifier().fire().present()) {
return true; // Solo attacker fear with no fire-follow-up is a wasted action
}
break;
}
case CommandType::EXTINGUISH_FIRE_COMMAND: {
// Extinguish fire filtering - don't extinguish fires on enemy-occupied tiles
const int targetRow = cmd.GetTargetRow();
@@ -177,8 +177,6 @@ void ShardokGameController::DoAIThread() {
engine->PostCommand(playerId, results.chosenIndex);
LockedNotifyClients();
engine->PostWhileCurrentPlayerHasOnlyOneOption(nullptr);
LockedNotifyClients();
aiThreadKeepGoing = !engine->GameIsOver();
}
} catch (const std::exception &e) {
@@ -342,21 +340,6 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
updates.filteredResults.emplace_back(fid, actionResultViews, acs);
}
// Per-watcher filtered views. A watcher is a non-participant Eagle
// faction allied to one or more participants — they get a view that
// treats their allied participants' units as fully visible (hero
// stats, hidden-from-opponents actions like meteor casts, etc.).
// Participants whose allies list named this watcher are passed in as
// alliedPids so UnitFilter / ActionResultFilter reveal those units to
// the watcher.
for (const auto &[watcherFid, allyPids] : watcherAllies) {
// Watchers can't post commands, so availableCommands is null.
updates.filteredResults.emplace_back(
watcherFid,
engine->FilterNewResultsForWatcher(-1, allyPids, startingActionId),
nullptr);
}
updates.filteredResults.emplace_back(
-1,
engine->FilterNewResults(-1, startingActionId),
@@ -364,25 +347,6 @@ auto ShardokGameController::GetUpdates(const int64_t startingActionId) -> AllUpd
auto gameStateBytes = engine->GetCurrentGameStateBytes();
updates.currentGameState.swap(gameStateBytes);
// Extract battle progress data from the FlatBuffer game state
const auto *gs = engine->GetCurrentGameState().Get();
updates.currentRound = gs->current_round();
for (const auto *player : *gs->player_infos()) {
int32_t troopCount = 0;
for (const auto *unit : *gs->units()) {
if (unit->player_id() != player->player_id()) continue;
switch (unit->status()) {
case net::eagle0::shardok::storage::fb::UnitStatus_NORMAL_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
case net::eagle0::shardok::storage::fb::UnitStatus_NEVER_ENTERED_UNIT:
troopCount += unit->battalion().size();
break;
default: break;
}
}
updates.playerTroopCounts.emplace_back(player->player_id(), troopCount);
}
}
return updates;
@@ -481,9 +445,7 @@ auto ShardokGameController::WaitForUpdatesAndPush(
updates.mainResults,
updates.filteredResults,
updates.newUnfilteredCount,
updates.currentGameState,
updates.currentRound,
updates.playerTroopCounts);
updates.currentGameState);
}
// Tutorial battle reset: defender lost, signal reset instead of game over
@@ -73,9 +73,7 @@ public:
const vector<ActionResult>& mainResults,
const vector<OnePlayerUpdates>& filteredResults,
int32_t newUnfilteredCount,
const byte_vector& currentGameState,
int32_t currentRound,
const vector<std::pair<int32_t, int32_t>>& playerTroopCounts) = 0;
const byte_vector& currentGameState) = 0;
/// Called when the game ends
virtual void OnGameOver(const GameOverInfo& info) = 0;
@@ -120,11 +118,6 @@ private:
vector<shared_ptr<ShardokAIClient>> aiClients;
std::thread aiThread;
// Each entry maps an Eagle faction id of a non-participant watcher to the
// Shardok pids of participants whose allies list named this watcher. Used
// when generating per-watcher filtered views in GetUpdates.
const vector<std::pair<int32_t, vector<PlayerId>>> watcherAllies;
static auto MakeLogFilePath() -> string {
const time_t timer = time(nullptr);
char buf[255];
@@ -144,16 +137,14 @@ public:
ShardokGameController(
unique_ptr<ShardokEngine> e,
string mapName,
string serializedRequest = "",
vector<std::pair<int32_t, vector<PlayerId>>> watcherAllies = {})
string serializedRequest = "")
: serializedRequest(std::move(serializedRequest)),
engine(std::move(e)),
cachedGameId(engine->GetGameId()),
mapName(std::move(mapName)),
logFilePath(MakeLogFilePath()),
aiClients(MakeAIClients(engine)),
aiThread(&ShardokGameController::DoAIThread, this),
watcherAllies(std::move(watcherAllies)) {
aiThread(&ShardokGameController::DoAIThread, this) {
std::unique_lock lk(masterLock);
aiCondition.notify_one();
}
@@ -180,8 +171,6 @@ public:
vector<OnePlayerUpdates> filteredResults;
int32_t newUnfilteredCount;
byte_vector currentGameState;
int32_t currentRound = 0;
vector<std::pair<int32_t, int32_t>> playerTroopCounts; // (playerId, troopCount)
};
auto GetUpdates(int64_t startingActionId) -> AllUpdates;
auto GetCurrentGameStateBytes() -> byte_vector;
@@ -217,58 +217,6 @@ auto ShardokEngine::GetGameStateView(const PlayerId askingPlayer) const
return filteredHistory;
}
[[nodiscard]] auto ShardokEngine::FilterNewResultsForWatcher(
const PlayerId askingPlayer,
const vector<PlayerId> &alliedPids,
const int64_t previousActionCount) const
-> vector<net::eagle0::shardok::api::ActionResultView> {
if (previousActionCount < startingHistoryCount) {
throw ShardokInternalErrorException("Unable to fetch history before startingHistoryCount");
}
vector<ShardokActionWithResultingState> unfilteredHistory = GetGameHistory(previousActionCount);
vector<net::eagle0::shardok::api::ActionResultView> filteredHistory{};
GameStateW startingState = (previousActionCount == 0)
? GameStateW{}
: GetGameStateAtStartOfAction(previousActionCount);
GameStateView startingView = (previousActionCount == 0) ? GameStateView{}
: GameStateFilteredForPlayerWithAllies(
settingsGetter,
startingState,
askingPlayer,
alliedPids);
GameStateW &previousState = startingState;
GameState *previousStatePtr = nullptr;
GameStateView &previousView = startingView;
for (const ShardokActionWithResultingState &awrs : unfilteredHistory) {
GameStateView viewAfter = GameStateFilteredForPlayerWithAllies(
settingsGetter,
GameStateW::FromByteString(awrs.state_after_fb()),
askingPlayer,
alliedPids);
if (auto filteredResult = ActionResultFilteredForPlayerWithAllies(
previousStatePtr,
previousView,
viewAfter,
settingsGetter,
awrs.action_result(),
askingPlayer,
alliedPids);
filteredResult.has_value()) {
filteredHistory.push_back(*filteredResult);
}
previousState = GameStateW::FromByteString(awrs.state_after_fb());
previousStatePtr = previousState.Get();
previousView = viewAfter;
}
return filteredHistory;
}
auto ShardokEngine::GetFilteredGameHistory(const PlayerId askingPlayer) const
-> vector<net::eagle0::shardok::api::ActionResultView> {
return FilterNewResults(askingPlayer, 0);
@@ -152,16 +152,6 @@ public:
[[nodiscard]] auto FilterNewResults(PlayerId askingPlayer, int64_t previousActionCount) const
-> vector<net::eagle0::shardok::api::ActionResultView>;
// Variant for non-participant watchers: askingPlayer is a sentinel (not in
// player_infos) and alliedPids names the Shardok pids whose units the
// watcher should see as "self/ally" — fully visible, including hero stats
// and hidden actions.
[[nodiscard]] auto FilterNewResultsForWatcher(
PlayerId askingPlayer,
const vector<PlayerId> &alliedPids,
int64_t previousActionCount) const
-> vector<net::eagle0::shardok::api::ActionResultView>;
[[nodiscard]] auto GetFilteredGameHistory(PlayerId askingPlayer) const
-> vector<net::eagle0::shardok::api::ActionResultView>;
@@ -94,30 +94,10 @@ auto BurnStructuresResult(const GameStateW &gameState, const SettingsGetter &set
}
}
auto WindSpeedMultiplierForConditions(
Weather::Conditions conditions,
const SettingsGetter &settings) -> double {
switch (conditions) {
case Weather::Conditions::Weather_Conditions_SUN:
return settings.Backing().wind_speed_multiplier_sun();
case Weather::Conditions::Weather_Conditions_CLOUDS:
return settings.Backing().wind_speed_multiplier_clouds();
case Weather::Conditions::Weather_Conditions_RAIN:
return settings.Backing().wind_speed_multiplier_rain();
case Weather::Conditions::Weather_Conditions_SNOW:
return settings.Backing().wind_speed_multiplier_snow();
case Weather::Conditions::Weather_Conditions_THUNDERSTORM:
return settings.Backing().wind_speed_multiplier_thunderstorm();
case Weather::Conditions::Weather_Conditions_BLIZZARD:
return settings.Backing().wind_speed_multiplier_blizzard();
default: return 1.0;
}
}
auto NewWeather(
const net::eagle0::shardok::storage::fb::MonthlyWeather &monthlyWeather,
const WeatherFb &oldWeather,
const SettingsGetter &settings,
const SettingsGetter & /*settings*/,
const std::shared_ptr<RandomGenerator> &randomGenerator) -> Weather {
const Weather::Conditions newConditions =
ConditionsByMonth(monthlyWeather, randomGenerator->Percentile());
@@ -144,9 +124,7 @@ auto NewWeather(
newWindDirection -= 6;
const double windSpeedRoll = randomGenerator->OpenEndedHighPercentile();
const double conditionsMultiplier = WindSpeedMultiplierForConditions(newConditions, settings);
const uint8_t newWindSpeed =
windSpeedRoll / 100.0 * monthlyWeather.max_wind_mph() * conditionsMultiplier;
const uint8_t newWindSpeed = windSpeedRoll / 100.0 * monthlyWeather.max_wind_mph();
return MakeWeather(
newConditions,
@@ -19,17 +19,10 @@
#include "src/main/cpp/net/eagle0/shardok/library/action_factories/UndeadChangeActionFactory.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/hex_map.hpp"
#include "src/main/protobuf/net/eagle0/shardok/common/weather.pb.h"
namespace shardok {
using HexMap = net::eagle0::shardok::storage::fb::HexMap;
// Returns the wind-speed multiplier configured in settings for the given
// weather conditions. Exposed for unit tests.
[[nodiscard]] auto WindSpeedMultiplierForConditions(
net::eagle0::shardok::common::Weather::Conditions conditions,
const SettingsGetter& settings) -> double;
class NewRoundAction : public ShardokAction {
private:
[[nodiscard]] auto InternalExecute(
@@ -91,10 +91,12 @@ void ArcheryCommandFactory::AddAvailableArcheryCommands(
const int windDir = (int)weather->wind().direction();
for (const Coords &farCoords : TilesWithExactDistance(hexMap, position, 3)) {
auto dirTo = DirectionsTo(hexMap, position, farCoords);
// 60° cone: tile must be in the wind direction's sector, or on its
// boundary (where the secondary direction matches the wind).
bool inCone = ((int)dirTo.main == windDir);
if (!inCone && dirTo.usesSecondary) { inCone = ((int)dirTo.secondary == windDir); }
int relMain = ((int)dirTo.main - windDir + 6) % 6;
bool inCone = (relMain == 0 || relMain == 1 || relMain == 5);
if (!inCone && dirTo.usesSecondary) {
int relSec = ((int)dirTo.secondary - windDir + 6) % 6;
inCone = (relSec == 0 || relSec == 1 || relSec == 5);
}
if (inCone && !HexMapUtils::LineIsBlockedByMountains(hexMap, position, farCoords)) {
targetCoords.Add(farCoords);
}
@@ -30,7 +30,6 @@ auto ControlCommand::InternalExecute(
result.set_type(ActionType::CONTROLLED);
result.mutable_player()->set_value(GetPlayerId());
result.mutable_actor()->set_value(actorId);
result.mutable_target_unit()->set_value(targetId);
AddChangedUnit(result, actorAfter);
return {result};
@@ -4,9 +4,6 @@
#include "ActionResultFilter.hpp"
#include <algorithm>
#include <ranges>
#include "src/main/cpp/net/eagle0/shardok/library/unit/Unit.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateFilter.hpp"
#include "src/main/cpp/net/eagle0/shardok/library/view_filters/GameStateViewDiffer.hpp"
@@ -63,24 +60,6 @@ auto TargetIsHiddenFromOpponents(const ActionResultType type) -> bool {
const SettingsGetter &settings,
const ActionResult &result,
const PlayerId askingPlayer) -> std::optional<ActionResultView> {
return ActionResultFilteredForPlayerWithAllies(
beforeState,
beforeView,
afterView,
settings,
result,
askingPlayer,
std::vector<PlayerId>{});
}
[[nodiscard]] auto ActionResultFilteredForPlayerWithAllies(
const GameState *beforeState,
const net::eagle0::shardok::api::GameStateView &beforeView,
const net::eagle0::shardok::api::GameStateView &afterView,
const SettingsGetter &settings,
const ActionResult &result,
const PlayerId askingPlayer,
const std::vector<PlayerId> &alliedPids) -> std::optional<ActionResultView> {
double opponentKnowledgeOfActor = 0.0;
bool hiddenActor = false;
bool wasHidden = false;
@@ -97,16 +76,13 @@ auto TargetIsHiddenFromOpponents(const ActionResultType type) -> bool {
beforeState->units()->size() > static_cast<unsigned int>(actorId) &&
beforeState->units()->Get(result.actor().value())->hidden();
}
const PlayerId actorPlayer = result.has_player() ? result.player().value() : -1;
const bool actorIsSelfOrAlly =
askingPlayer == actorPlayer || std::ranges::contains(alliedPids, actorPlayer);
if (IsHiddenFromOpponents(
settings.Backing().minimum_knowledge_for_profession(),
result.type(),
opponentKnowledgeOfActor,
hiddenActor,
wasHidden) &&
!actorIsSelfOrAlly) {
askingPlayer != result.player().value()) {
return std::optional<ActionResultView>();
}
@@ -131,7 +107,8 @@ auto TargetIsHiddenFromOpponents(const ActionResultType type) -> bool {
}
if (result.has_target_coords() &&
(actorIsSelfOrAlly || !TargetIsHiddenFromOpponents(result.type()))) {
((result.has_player() && result.player().value() == askingPlayer) ||
!TargetIsHiddenFromOpponents(result.type()))) {
*resultView.mutable_target_coords() = result.target_coords();
} else {
resultView.clear_target_coords();
@@ -6,7 +6,6 @@
#define EAGLE0_ACTIONRESULTFILTER_HPP
#include <optional>
#include <vector>
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
@@ -26,19 +25,6 @@ using GameState = net::eagle0::shardok::storage::fb::GameState;
const net::eagle0::shardok::storage::ActionResult &result,
PlayerId askingPlayer) -> std::optional<net::eagle0::shardok::api::ActionResultView>;
// Variant that takes the asker's allied Shardok pids explicitly. Allied actor
// actions (like hidden meteor casts) are revealed to the asker as if the
// asker were the actor's owner.
[[nodiscard]] auto ActionResultFilteredForPlayerWithAllies(
const GameState *beforeState,
const net::eagle0::shardok::api::GameStateView &beforeView,
const net::eagle0::shardok::api::GameStateView &afterView,
const SettingsGetter &settings,
const net::eagle0::shardok::storage::ActionResult &result,
PlayerId askingPlayer,
const std::vector<PlayerId> &alliedPids)
-> std::optional<net::eagle0::shardok::api::ActionResultView>;
} // namespace shardok
#endif // EAGLE0_ACTIONRESULTFILTER_HPP
@@ -19,18 +19,6 @@ auto GameStateFilteredForPlayer(
const SettingsGetter& settings,
const GameState* gameState,
const PlayerId askingPlayer) -> GameStateView {
return GameStateFilteredForPlayerWithAllies(
settings,
gameState,
askingPlayer,
AlliedPids(gameState, askingPlayer));
}
auto GameStateFilteredForPlayerWithAllies(
const SettingsGetter& settings,
const GameState* gameState,
const PlayerId askingPlayer,
const std::vector<PlayerId>& alliedPids) -> GameStateView {
GameStateView gsv{};
gsv.set_asking_player(askingPlayer);
@@ -42,6 +30,8 @@ auto GameStateFilteredForPlayerWithAllies(
*gsv.add_player_infos() = fb::ToPlayerInfoProto(info);
}
const auto& alliedPids = AlliedPids(gameState, askingPlayer);
for (const auto& unit : *gameState->units()) {
switch (unit->status()) {
case net::eagle0::shardok::storage::fb::UnitStatus_RESERVE_UNIT:
@@ -5,8 +5,6 @@
#ifndef EAGLE0_GAMESTATEFILTER_HPP
#define EAGLE0_GAMESTATEFILTER_HPP
#include <vector>
#include "src/main/cpp/net/eagle0/shardok/library/ShardokCTypes.h"
#include "src/main/cpp/net/eagle0/shardok/library/settings/GameSettings.hpp"
#include "src/main/flatbuffer/net/eagle0/shardok/storage/game_state.hpp"
@@ -21,16 +19,6 @@ using GameState = net::eagle0::shardok::storage::fb::GameState;
const GameState* gameState,
PlayerId askingPlayer) -> net::eagle0::shardok::api::GameStateView;
// Variant that takes the asker's allied Shardok pids explicitly. Used for
// non-participant watchers who have no entry in the GameState's player_infos
// but should still see units owned by their participating allies as if those
// units were their own.
[[nodiscard]] auto GameStateFilteredForPlayerWithAllies(
const SettingsGetter& settings,
const GameState* gameState,
PlayerId askingPlayer,
const std::vector<PlayerId>& alliedPids) -> net::eagle0::shardok::api::GameStateView;
} // namespace shardok
#endif // EAGLE0_GAMESTATEFILTER_HPP
@@ -196,23 +196,6 @@ void EagleInterfaceImpl::StartGame(const NewGameRequest &request) {
string unitsString;
for (const auto &hid : heroIds) { unitsString += std::to_string(hid) + " "; }
// Build the watcher → allied-Shardok-pids mapping. A non-participant
// faction "watches" via any participant that lists it as an ally.
std::vector<std::pair<int32_t, std::vector<PlayerId>>> watcherAllies;
watcherAllies.reserve(request.watcher_eagle_faction_ids_size());
for (const int32_t watcherFid : request.watcher_eagle_faction_ids()) {
std::vector<PlayerId> allyPids;
for (int i = 0; i < request.user_infos_size(); i++) {
for (const auto &ally : request.user_infos(i).allies()) {
if (ally.eagle_faction_id() == watcherFid) {
allyPids.push_back(static_cast<PlayerId>(i));
break;
}
}
}
if (!allyPids.empty()) { watcherAllies.emplace_back(watcherFid, std::move(allyPids)); }
}
printf("Starting game %s with units %s\n", gameId.c_str(), unitsString.c_str());
gamesManager->UnlockedCreateSpecifiedGame(
gameId,
@@ -221,8 +204,7 @@ void EagleInterfaceImpl::StartGame(const NewGameRequest &request) {
customMapBytes,
month,
request.tutorial_battle_config(),
request.SerializeAsString(),
watcherAllies);
request.SerializeAsString());
}
auto EagleInterfaceImpl::PostCommand(
@@ -351,13 +333,6 @@ void EagleInterfaceImpl::PopulateGameStatusResponse(
*response->mutable_game_update_response()->mutable_current_game_state() =
string(controller->GetCurrentGameStateBytes());
response->mutable_game_update_response()->set_current_round(results.currentRound);
for (const auto &[playerId, troopCount] : results.playerTroopCounts) {
auto *ptc = response->mutable_game_update_response()->add_player_troop_counts();
ptc->set_player_id(playerId);
ptc->set_troop_count(troopCount);
}
}
}
@@ -449,9 +424,7 @@ auto EagleInterfaceImpl::SubscribeToGame(
const vector<ActionResult> &mainResults,
const vector<OnePlayerUpdates> &filteredResults,
int32_t newUnfilteredCount,
const byte_vector &currentGameState,
int32_t currentRound,
const vector<std::pair<int32_t, int32_t>> &playerTroopCounts) override {
const byte_vector &currentGameState) override {
if (!active_) return;
GameStatusResponse response;
@@ -479,13 +452,6 @@ auto EagleInterfaceImpl::SubscribeToGame(
*response.mutable_game_update_response()->mutable_current_game_state() =
std::string(currentGameState.begin(), currentGameState.end());
response.mutable_game_update_response()->set_current_round(currentRound);
for (const auto &[playerId, troopCount] : playerTroopCounts) {
auto *ptc = response.mutable_game_update_response()->add_player_troop_counts();
ptc->set_player_id(playerId);
ptc->set_troop_count(troopCount);
}
if (!writer_->Write(response)) { active_ = false; }
}
@@ -32,9 +32,7 @@ auto SetUpController(
const vector<Unit> &units,
const vector<PlayerInfoProto> &playerSetupInfos,
const TutorialBattleConfigProto &tutorialConfig,
const string &serializedRequest,
const std::vector<std::pair<int32_t, std::vector<PlayerId>>> &watcherAllies = {})
-> shared_ptr<ShardokGameController>;
const string &serializedRequest) -> shared_ptr<ShardokGameController>;
ShardokGamesManager::ShardokGamesManager(const std::vector<std::string> &extraSettings) {
gameSettings = SettingsLoader::LoadSettings();
@@ -87,8 +85,7 @@ auto ShardokGamesManager::UnlockedCreateSpecifiedGame(
const string &customMapBytes,
const int month,
const TutorialBattleConfigProto &tutorialConfig,
const string &serializedRequest,
const std::vector<std::pair<int32_t, std::vector<PlayerId>>> &watcherAllies) -> GameId {
const string &serializedRequest) -> GameId {
// Check for existing game with this ID and early-exit
std::shared_lock<std::shared_mutex> lk(runningControllersLock);
const auto existingGame = runningControllers.find(gameId);
@@ -123,8 +120,7 @@ auto ShardokGamesManager::UnlockedCreateSpecifiedGame(
units,
playerInfos,
tutorialConfig,
serializedRequest,
watcherAllies);
serializedRequest);
return LockedCreateGame(controller, playerSetupInfos, serializedRequest);
}
@@ -151,9 +147,7 @@ auto SetUpController(
const vector<Unit> &units,
const vector<PlayerInfoProto> &playerInfos,
const TutorialBattleConfigProto &tutorialConfig,
const string &serializedRequest,
const std::vector<std::pair<int32_t, std::vector<PlayerId>>> &watcherAllies)
-> shared_ptr<ShardokGameController> {
const string &serializedRequest) -> shared_ptr<ShardokGameController> {
auto unplacedUnits = std::vector<Unit>();
UnitId nextUnitId = 0;
@@ -226,11 +220,7 @@ auto SetUpController(
engine->SetTutorialBattleConfig(tutorialConfig);
return std::make_shared<ShardokGameController>(
std::move(engine),
mapName,
serializedRequest,
watcherAllies);
return std::make_shared<ShardokGameController>(std::move(engine), mapName, serializedRequest);
}
} // namespace shardok
@@ -110,9 +110,7 @@ public:
const string &customMapBytes,
int month,
const TutorialBattleConfigProto &tutorialConfig,
const string &serializedRequest,
const std::vector<std::pair<int32_t, std::vector<PlayerId>>> &watcherAllies = {})
-> GameId;
const string &serializedRequest) -> GameId;
auto UnlockedCreateFromGameStateBytes(
const byte_vector &gameStateBytes,
File diff suppressed because it is too large Load Diff
@@ -36,12 +36,7 @@ ModelImporter:
- first:
74: -3419257869308726280
second: walk
externalObjects:
- first:
type: UnityEngine:Material
assembly: UnityEngine.CoreModule
name: Smilodon.001
second: {fileID: 2100000, guid: 3eac01f037701614d931178d43b43f43, type: 2}
externalObjects: {}
materials:
materialImportMode: 1
materialName: 0
@@ -36,12 +36,7 @@ ModelImporter:
- first:
74: -3419257869308726280
second: walk
externalObjects:
- first:
type: UnityEngine:Material
assembly: UnityEngine.CoreModule
name: Lion
second: {fileID: 2100000, guid: ad3b6d598b37af14ab1837e95886e807, type: 2}
externalObjects: {}
materials:
materialImportMode: 1
materialName: 0
@@ -36,12 +36,7 @@ ModelImporter:
- first:
74: -3419257869308726280
second: walk
externalObjects:
- first:
type: UnityEngine:Material
assembly: UnityEngine.CoreModule
name: Lioness
second: {fileID: 2100000, guid: ce62ac8db8ed065468ad2b25bf8ceb76, type: 2}
externalObjects: {}
materials:
materialImportMode: 1
materialName: 0
@@ -24,12 +24,7 @@ ModelImporter:
- first:
74: -3419257869308726280
second: walk
externalObjects:
- first:
type: UnityEngine:Material
assembly: UnityEngine.CoreModule
name: rhinocerosMateriale
second: {fileID: 2100000, guid: bae71c57f6d69494b9ca14349944d9ad, type: 2}
externalObjects: {}
materials:
materialImportMode: 1
materialName: 0
@@ -0,0 +1,329 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!91 &9100000
AnimatorController:
m_ObjectHideFlags: 0
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: Bat_anim_controller
serializedVersion: 5
m_AnimatorParameters: []
m_AnimatorLayers:
- serializedVersion: 5
m_Name: Base Layer
m_StateMachine: {fileID: 1107560824570907306}
m_Mask: {fileID: 0}
m_Motions: []
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: -1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}
--- !u!1101 &1101113585808636062
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102172938596919840}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.625
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101311254650508446
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102172938596919840}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.625
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101425328541616982
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102602612077029620}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.625
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101659207022591444
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102602612077029620}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.625
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101693637957700276
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102135739997646400}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.625
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101702585948742942
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102172938596919840}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.25
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101733954648381344
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102135739997646400}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.625
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101811524882186866
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102467681057209352}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.625
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1102 &1102135739997646400
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: fly
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101113585808636062}
- {fileID: 1101425328541616982}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 847019d84b5186f4f8ee35d3490c897f, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102172938596919840
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: fly idle
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101733954648381344}
- {fileID: 1101811524882186866}
- {fileID: 1101659207022591444}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: a6022161507a0e44b800af5385917d5c, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102467681057209352
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: fly fast
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101702585948742942}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: d919092276a961249a57501a6daa62bc, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102602612077029620
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: bite 0
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101311254650508446}
- {fileID: 1101693637957700276}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 88f00cc992df0af438665498fe245018, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1107 &1107560824570907306
AnimatorStateMachine:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: Base Layer
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: 1102172938596919840}
m_Position: {x: -264, y: 180, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102602612077029620}
m_Position: {x: 36, y: 192, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102467681057209352}
m_Position: {x: -300, y: 312, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102135739997646400}
m_Position: {x: -48, y: 288, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 50, y: 20, z: 0}
m_EntryPosition: {x: 50, y: 120, z: 0}
m_ExitPosition: {x: 396, y: 108, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: 1102172938596919840}
@@ -0,0 +1,17 @@
fileFormatVersion: 2
guid: 064c487683909644fa0e07a7279a0d98
timeCreated: 1554279898
licenseType: Store
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 144071
packageName: Animal pack deluxe v2
packageVersion: 1.0
assetPath: Assets/Animal pack deluxe v2/Animation controllers/Bat_anim_controller.controller
uploadId: 305234
@@ -0,0 +1,861 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!91 &9100000
AnimatorController:
m_ObjectHideFlags: 0
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: Beagle_anim_controller
serializedVersion: 5
m_AnimatorParameters: []
m_AnimatorLayers:
- serializedVersion: 5
m_Name: Base Layer
m_StateMachine: {fileID: 1107195232226088826}
m_Mask: {fileID: 0}
m_Motions: []
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: -1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}
--- !u!1101 &1101075968305685516
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102813841958713228}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.96875
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101204436099882918
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102813841958713228}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.40000004
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101230077983219436
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102963082160574692}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.8333333
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101277574923325858
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102744896821533782}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.7692308
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101323543173306372
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102963082160574692}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.877551
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101352603736299106
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102963082160574692}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.40000004
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101431142677881358
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102813841958713228}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.8333333
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101474704461077778
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102985470162812094}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.96875
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101486726423062942
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102494830435296190}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.96875
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101497467710743154
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102256439295339874}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.875
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101518559534640766
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102548429920880972}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.96875
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101609192415281274
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102137655918487642}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.96875
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101695288186159088
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102729176207254308}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.96875
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101739408214469196
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102963082160574692}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.79310346
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101744629420707322
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102256439295339874}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.7692308
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101777182728255084
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102933537361102428}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.96875
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101798331976731090
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102963082160574692}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.7692308
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101872290631142330
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102963082160574692}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.9285714
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101890465863091648
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102303040055108398}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.96875
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101925980618501134
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102208669642020522}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.96875
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101945317666245412
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102963082160574692}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.9444445
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101950871925132236
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102963082160574692}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.93877554
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1102 &1102137655918487642
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: walk
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101230077983219436}
- {fileID: 1101431142677881358}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 734b2e909da2b0343b38be90fd124040, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102208669642020522
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: bark
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101323543173306372}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: d2c385fdf836fac4cb4738bb50b627fc, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102256439295339874
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: stand
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101798331976731090}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 8f3b291a4d688eb458f8529120075fbd, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102303040055108398
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: idle C
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101950871925132236}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: a7083efa1b7c2d44ba775d8955524235, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102494830435296190
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: idle B
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101945317666245412}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 25e7d2a79f0793b47bec4d2d0cbe0b83, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102548429920880972
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: sit
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101744629420707322}
- {fileID: 1101277574923325858}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 480f3615f8c87234c9db4330d6aacc1c, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102729176207254308
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: run
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101352603736299106}
- {fileID: 1101204436099882918}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 99a7e6b74fb7efa4381a2d710beb0e33, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102744896821533782
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: idle sit
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101497467710743154}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: a493f4d8b1705dd45bcda6ef86654336, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102813841958713228
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: die
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: b9091c9307d0869478c59efeadda0f20, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102933537361102428
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: attack
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101739408214469196}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 5a71015594fc1e14993c9f82a6c7a8f4, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102963082160574692
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: idle A
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101486726423062942}
- {fileID: 1101890465863091648}
- {fileID: 1101695288186159088}
- {fileID: 1101075968305685516}
- {fileID: 1101777182728255084}
- {fileID: 1101474704461077778}
- {fileID: 1101925980618501134}
- {fileID: 1101518559534640766}
- {fileID: 1101609192415281274}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 402ca02b60905ed43840edc212dd9c01, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102985470162812094
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: eat
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101872290631142330}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: e899fe91eda9ecd43b72878bdb427eab, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1107 &1107195232226088826
AnimatorStateMachine:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: Base Layer
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: 1102963082160574692}
m_Position: {x: -228, y: 156, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102729176207254308}
m_Position: {x: 96, y: 156, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102813841958713228}
m_Position: {x: 264, y: 324, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102208669642020522}
m_Position: {x: -72, y: 432, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102933537361102428}
m_Position: {x: -444, y: 72, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102494830435296190}
m_Position: {x: -204, y: 264, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102303040055108398}
m_Position: {x: -420, y: 240, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102985470162812094}
m_Position: {x: 72, y: 240, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102744896821533782}
m_Position: {x: 264, y: 60, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102548429920880972}
m_Position: {x: 12, y: 84, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102256439295339874}
m_Position: {x: 12, y: 24, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102137655918487642}
m_Position: {x: -372, y: 372, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 120, y: -108, z: 0}
m_EntryPosition: {x: -216, y: -48, z: 0}
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: 1102963082160574692}
@@ -0,0 +1,17 @@
fileFormatVersion: 2
guid: 62ced55f252905340a68b536385c608b
timeCreated: 1544549887
licenseType: Store
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 144071
packageName: Animal pack deluxe v2
packageVersion: 1.0
assetPath: Assets/Animal pack deluxe v2/Animation controllers/Beagle_anim_controller.controller
uploadId: 305234
@@ -0,0 +1,451 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!91 &9100000
AnimatorController:
m_ObjectHideFlags: 0
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: Box_turtle_anim_controller
serializedVersion: 5
m_AnimatorParameters: []
m_AnimatorLayers:
- serializedVersion: 5
m_Name: Base Layer
m_StateMachine: {fileID: 1107078516229809058}
m_Mask: {fileID: 0}
m_Motions: []
m_Behaviours: []
m_BlendingMode: 0
m_SyncedLayerIndex: -1
m_DefaultWeight: 0
m_IKPass: 0
m_SyncedLayerAffectsTiming: 0
m_Controller: {fileID: 9100000}
--- !u!1101 &1101263505607472796
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102305608714457030}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.625
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101340488907419766
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102701267282544424}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.625
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101354778844709030
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102301295623669820}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.8125
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101380512244845824
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102444166050730126}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.953125
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101441784704177910
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102826899076362810}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.8125
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101568407740868618
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102305608714457030}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.8125
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101588566431840030
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102305608714457030}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.625
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101744493414995364
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102301295623669820}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.953125
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101760169605538656
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102547900735549970}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.953125
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101787598118767764
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102826899076362810}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.953125
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1101 &1101814547616991654
AnimatorStateTransition:
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name:
m_Conditions: []
m_DstStateMachine: {fileID: 0}
m_DstState: {fileID: 1102444166050730126}
m_Solo: 0
m_Mute: 0
m_IsExit: 0
serializedVersion: 3
m_TransitionDuration: 0.25
m_TransitionOffset: 0
m_ExitTime: 0.625
m_HasExitTime: 1
m_HasFixedDuration: 1
m_InterruptionSource: 0
m_OrderedInterruption: 1
m_CanTransitionToSelf: 1
--- !u!1102 &1102301295623669820
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: die
m_Speed: 1
m_CycleOffset: 0
m_Transitions: []
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 9efcd6b20916df84db758bf06f91345a, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102305608714457030
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: idle
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101760169605538656}
- {fileID: 1101744493414995364}
- {fileID: 1101380512244845824}
- {fileID: 1101787598118767764}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: f2f3fd75fe951474280d1ddd2e2631a9, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102444166050730126
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: walk
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101568407740868618}
- {fileID: 1101441784704177910}
- {fileID: 1101354778844709030}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: b40d0049d3217cc4ab51038f94d3516b, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102547900735549970
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: hide
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101340488907419766}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: f3dcb5a16f3bd274ab1220e288b99c26, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102701267282544424
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: hide reverse
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101263505607472796}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: 907e1f02762447740ac6676f2d3c0735, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1102 &1102826899076362810
AnimatorState:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: bite
m_Speed: 1
m_CycleOffset: 0
m_Transitions:
- {fileID: 1101588566431840030}
- {fileID: 1101814547616991654}
m_StateMachineBehaviours: []
m_Position: {x: 50, y: 50, z: 0}
m_IKOnFeet: 0
m_WriteDefaultValues: 1
m_Mirror: 0
m_SpeedParameterActive: 0
m_MirrorParameterActive: 0
m_CycleOffsetParameterActive: 0
m_TimeParameterActive: 0
m_Motion: {fileID: 7400000, guid: bbea99ec01f44d14fa80b4963b267156, type: 3}
m_Tag:
m_SpeedParameter:
m_MirrorParameter:
m_CycleOffsetParameter:
m_TimeParameter:
--- !u!1107 &1107078516229809058
AnimatorStateMachine:
serializedVersion: 5
m_ObjectHideFlags: 1
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_Name: Base Layer
m_ChildStates:
- serializedVersion: 1
m_State: {fileID: 1102305608714457030}
m_Position: {x: -216, y: 192, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102547900735549970}
m_Position: {x: 12, y: 216, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102444166050730126}
m_Position: {x: -168, y: 384, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102826899076362810}
m_Position: {x: -396, y: 312, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102301295623669820}
m_Position: {x: 84, y: 360, z: 0}
- serializedVersion: 1
m_State: {fileID: 1102701267282544424}
m_Position: {x: 12, y: 288, z: 0}
m_ChildStateMachines: []
m_AnyStateTransitions: []
m_EntryTransitions: []
m_StateMachineTransitions: {}
m_StateMachineBehaviours: []
m_AnyStatePosition: {x: 50, y: 20, z: 0}
m_EntryPosition: {x: 50, y: 120, z: 0}
m_ExitPosition: {x: 800, y: 120, z: 0}
m_ParentStateMachinePosition: {x: 800, y: 20, z: 0}
m_DefaultState: {fileID: 1102305608714457030}
@@ -0,0 +1,17 @@
fileFormatVersion: 2
guid: 0b032bbef401c55419a905be51f2a344
timeCreated: 1554279911
licenseType: Store
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 9100000
userData:
assetBundleName:
assetBundleVariant:
AssetOrigin:
serializedVersion: 1
productId: 144071
packageName: Animal pack deluxe v2
packageVersion: 1.0
assetPath: Assets/Animal pack deluxe v2/Animation controllers/Box_turtle_anim_controller.controller
uploadId: 305234

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