Compare commits

...
Author SHA1 Message Date
admin 415acf27cd Use Input System as active input backend 2026-06-11 07:34:26 -07:00
adminandGitHub c4ad4529d8 Update firstpass csproj for Input System (#7060) 2026-06-11 05:48:00 -07:00
adminandGitHub 44e87fbe49 Route remaining pointer input through GameInput (#7058) 2026-06-11 04:55:20 -07:00
adminandGitHub 4b5897210c Route touch input through GameInput (#7057) 2026-06-10 22:30:30 -07:00
adminandGitHub d1191cf494 Install dmgbuild in a workflow venv (#7056) 2026-06-10 22:21:18 -07:00
adminandGitHub f4250d2b82 Document writable temp file locations (#7055) 2026-06-10 22:18:14 -07:00
adminandGitHub 1a4656992c Route Shardok shortcut input through GameInput (#7054) 2026-06-10 22:17:48 -07:00
adminandGitHub 8b67adbe00 Align scala3 staging version (#7053) 2026-06-10 22:08:51 -07:00
adminandGitHub 6e8c683d9f Gate Shardok grid input until model is ready (#7051) 2026-06-10 22:04:02 -07:00
adminandGitHub fb5ed2365f Update protobuf module (#7052) 2026-06-10 17:50:56 -07:00
adminandGitHub 44ae2f36a7 Update ScalaPB core (#7050) 2026-06-10 17:28:16 -07:00
adminandGitHub 41407d4ad0 Render Shardok bridges below unit labels (#7040) 2026-06-10 16:25:03 -07:00
adminandGitHub 78d92fee37 Update Nimbus JOSE JWT (#7049) 2026-06-10 16:17:35 -07:00
adminandGitHub 4e32bd7c51 Update apple_support (#7047) 2026-06-10 15:50:48 -07:00
adminandGitHub 2227cce8a6 Update Guava (#7046) 2026-06-10 15:40:41 -07:00
adminandGitHub 3e618b1e6f Update OkHttp (#7048) 2026-06-10 15:34:05 -07:00
adminandGitHub c392895e12 Update Error Prone annotations (#7044) 2026-06-10 15:32:55 -07:00
adminandGitHub f86fb86870 Update Gson (#7045) 2026-06-10 15:19:30 -07:00
adminandGitHub 3cacfc5225 Update Scalamock (#7043) 2026-06-10 15:19:12 -07:00
adminandGitHub 6e94c82098 Update AWS Lambda Java libraries (#7042) 2026-06-10 15:14:03 -07:00
adminandGitHub 1a3d0a82a6 Update AWS SDK (#7041) 2026-06-10 15:13:38 -07:00
adminandGitHub ea083c8c6c Update Sentry SDK (#7039) 2026-06-10 15:06:28 -07:00
adminandGitHub dd4ba80a2b Route pointer reads through GameInput (#7030)
* Route pointer reads through GameInput

* Set clicked panel alpha

* Revert accidental scroll view alpha change
2026-06-10 14:56:42 -07:00
adminandGitHub aea5bc614b Update Scalafmt CLI (#7038) 2026-06-10 14:55:11 -07:00
adminandGitHub e006d9e5a0 Update SQLite JDBC driver (#7036) 2026-06-10 14:47:22 -07:00
adminandGitHub 2daf624538 Remove stale LFS hooks before non-LFS checkouts (#7037) 2026-06-10 14:46:50 -07:00
adminandGitHub 7d6d8f3606 Update PostgreSQL JDBC driver (#7034) 2026-06-10 12:52:29 -07:00
adminandGitHub 0bac3ab38f Scope non-LFS checkout config (#7035) 2026-06-10 12:01:13 -07:00
adminandGitHub 7486af58b8 Disable LFS filters before deploy checkouts (#7032) 2026-06-10 10:23:30 -07:00
adminandGitHub 475321a86e Ensure AWS CLI for Addressables upload (#7033) 2026-06-10 10:22:15 -07:00
adminandGitHub 690ae25c88 Document gh PR body-file usage (#7031) 2026-06-10 09:49:37 -07:00
adminandGitHub e1e714a818 Update SLF4J dependencies (#7029) 2026-06-10 09:46:41 -07:00
adminandGitHub 27cd673038 Brighten clicked province panel background (#7027) 2026-06-10 09:44:39 -07:00
adminandGitHub 1da9fe3405 Include Python user bin for Mac DMG deploy (#7028) 2026-06-10 09:44:26 -07:00
adminandGitHub 401fdd26cd Update Netty dependencies (#7026) 2026-06-10 09:39:23 -07:00
adminandGitHub ad142c6f65 Render clicked province panel above map (#7025) 2026-06-10 09:30:43 -07:00
adminandGitHub 96d6d64f77 Give sqlite history test more time (#7024) 2026-06-10 09:22:09 -07:00
adminandGitHub 5e80ef41d6 Add Input System facade for shortcuts (#7023) 2026-06-10 09:09:23 -07:00
adminandGitHub 58553a4de1 Update Clang toolchain patch version (#7021) 2026-06-10 09:05:26 -07:00
adminandGitHub dc237d6e8f Enable Input System compatibility mode (#7022) 2026-06-10 08:36:53 -07:00
adminandGitHub 42fa4831f0 Remove host-specific workflow runner labels (#7018) 2026-06-10 08:00:40 -07:00
adminandGitHub cdab19e920 Update LLVM Bazel toolchain (#7020)
* Update Go Bazel rules

* Update Rust Bazel rules

* Update LLVM Bazel toolchain
2026-06-10 08:00:08 -07:00
adminandGitHub 05de5fd007 Stop caching Shardok game state locally (#7015) 2026-06-10 07:47:32 -07:00
adminandGitHub f1429b9560 Update Rust Bazel rules (#7019)
* Update Go Bazel rules

* Update Rust Bazel rules
2026-06-10 07:27:59 -07:00
adminandGitHub d9452bd763 Update Go Bazel rules (#7017) 2026-06-10 07:19:01 -07:00
adminandGitHub 8f24ef1fa7 Fix Unity Mac module verification path (#7016) 2026-06-10 07:17:49 -07:00
adminandGitHub e9a9475deb Update low-risk Bazel module dependencies (#7014) 2026-06-10 07:01:29 -07:00
adminandGitHub 21b56b8098 Upgrade Unity editor to 6000.4.10f1 (#7013) 2026-06-10 06:49:54 -07:00
adminandGitHub de04afc47f Report Eagle cache load rejection reasons (#7011) 2026-06-09 16:53:44 -07:00
adminandGitHub 1573fc4a82 Log and persist Eagle cache resume counts (#7010) 2026-06-09 16:45:35 -07:00
adminandGitHub e4a6edf7d7 Only warn for Eagle settings references when Eagle is loaded (#7009) 2026-06-09 16:35:49 -07:00
adminandGitHub df781f5643 Cache active Shardok histories with Eagle state (#7008) 2026-06-09 16:05:08 -07:00
adminandGitHub 6401db7043 Cache Eagle game state locally (#7007) 2026-06-09 14:08:32 -07:00
adminandGitHub 9cb3a60501 Remove retired Gameplay scene assets (#7006) 2026-06-09 13:31:13 -07:00
adminandGitHub 0f8880353b Add scene split validation tests (#7004) 2026-06-09 11:32:00 -07:00
adminandGitHub f1b89de5b3 Batch Postgres history replay reads (#7005) 2026-06-09 11:31:26 -07:00
adminandGitHub d99d2f5a53 Bulk read text state during recovery (#7003) 2026-06-09 09:22:37 -07:00
adminandGitHub 72efdff6bc Filter cached client text in storage queries (#7002) 2026-06-09 08:42:20 -07:00
adminandGitHub 9e8a506601 Hide superseded backstory text from clients (#7001) 2026-06-09 08:07:37 -07:00
adminandGitHub 77e1dd41cc Cache completed client text locally (#7000)
* Cache completed client text locally

* Fix completed client text cache scoping

* Avoid Unity API in client text cache writes
2026-06-09 08:06:01 -07:00
adminandGitHub aeb89e265e Staticize TMP fallback font asset (#6998)
* Staticize TMP fallback font asset

* Bake TMP fallback glyph coverage
2026-06-09 07:32:52 -07:00
adminandGitHub 9046e3d7ce Show JFR sidecar errors in admin (#6999) 2026-06-09 07:05:25 -07:00
adminandGitHub fc6b48793e Remove stale Gameplay scene references (#6997) 2026-06-09 06:32:24 -07:00
adminandGitHub 7d2050db5e Update scene split wording (#6996) 2026-06-09 05:11:22 -07:00
adminandGitHub 7e0d8360c4 Rename Eagle scene lighting setup (#6995) 2026-06-08 22:39:45 -07:00
adminandGitHub a0912bbc9e Close Postgres resources on unload (#6994) 2026-06-08 22:16:58 -07:00
adminandGitHub 987390b244 Remove connection readiness marker (#6993) 2026-06-08 22:12:08 -07:00
adminandGitHub e1c7036780 Ignore empty Postgres histories on load (#6992) 2026-06-08 22:07:27 -07:00
adminandGitHub da1a1a64c7 Add bulk SQLite to Postgres migration action (#6991) 2026-06-08 22:05:32 -07:00
adminandGitHub 138c0fcfa9 Remove stale Unity project script entries (#6990) 2026-06-08 22:03:37 -07:00
adminandGitHub 8e7acdfd97 Add admin SQLite to Postgres game migration (#6987) 2026-06-08 18:29:25 -07:00
adminandGitHub e1d8fd7031 Remove legacy Gameplay scene (#6988) 2026-06-08 18:29:00 -07:00
adminandGitHub f56289899b Update Eagle scene setup docs (#6986) 2026-06-08 17:16:55 -07:00
adminandGitHub 9e365b637c Store client text in Postgres for Postgres games (#6985) 2026-06-08 17:14:26 -07:00
adminandGitHub 15d5519634 Remove obsolete scene split meta files (#6984) 2026-06-08 17:12:11 -07:00
adminandGitHub bbf9d23c1f Remove obsolete scene split migration artifacts (#6983) 2026-06-08 17:02:34 -07:00
adminandGitHub 7685f4470c Default Eagle history backend to Postgres (#6982) 2026-06-08 16:58:01 -07:00
adminandGitHub ed47b5769d Remove legacy scene migration tooling (#6980) 2026-06-08 16:34:48 -07:00
adminandGitHub 11d734ce7f Make Postgres history load failures explicit (#6981) 2026-06-08 16:34:10 -07:00
adminandGitHub 275a72256f Bulk read Postgres action results (#6979) 2026-06-08 16:17:46 -07:00
adminandGitHub 13fa2ce8da Cache Postgres history replay reads (#6978) 2026-06-08 15:49:25 -07:00
adminandGitHub 06cd96f921 Retire legacy Gameplay startup fallback (#6973) 2026-06-08 15:47:12 -07:00
adminandGitHub 83ea7ce485 Render Shardok fire below unit labels (#6976) 2026-06-08 15:40:10 -07:00
adminandGitHub 9bdfad34e1 Unregister hero dropdown text listeners on destroy (#6977) 2026-06-08 15:26:17 -07:00
adminandGitHub 14ba4b3933 Restore notification beast animations (#6975)
* Restore notification beast visual layout timing

* Render beast effects on Eagle UI layer

* Wire Eagle canvases to main camera

* Render beast effects as UI overlays

* Add beast renderer diagnostics

* Render Eagle canvases through main camera

* Restore legacy Eagle ambient lighting
2026-06-08 15:21:22 -07:00
adminandGitHub 462bba117e Control history backend from admin settings (#6974) 2026-06-08 14:41:01 -07:00
adminandGitHub 0080f17a19 Wire Unity split scene runtime references (#6968)
* Wire split scene runtime references

* Activate connection scene root on startup

* Hide drop game confirmation on startup

* Preserve stored account UI on split startup

* Initialize OAuth before connection UI startup

* Ensure main queue for split scene callbacks

* Allow lobby rows without Eagle textures loaded

* Resolve Eagle controller from loaded scene

* Wait for Eagle controller after scene load

* Resolve additive scenes from sceneLoaded callback

* Ignore stale Eagle notifications after scene teardown

* Guard deferred Eagle update subscription

* Keep duplicate MainQueue from destroying scene UI

* Provide shared SoundManager for split scenes

* Wire Eagle map audio from shared SoundManager

* Match Main scene camera background to Gameplay

* Move Eagle texture catalog into Shared scene

* Load Shardok assets from Shared scene

* Wire Shardok audio and gate grid input

* Wire Shardok canvases to runtime camera

* Flatten Shardok grid overlay rendering

* Parent Shardok foreground to visible grid

* Let Shardok grid labels pass clicks through

* Allow Shardok clicks through map visuals

* Prefer Shardok grid hits over UI raycasts

* Render Shardok hex meshes through canvas

* Size Shardok canvas hex mesh bounds

* Log Shardok hex mesh canvas state

* Avoid transparent Shardok hex mesh fills

* Revert "Avoid transparent Shardok hex mesh fills"

This reverts commit 34b6ad9301ee1519ab774fa293aa72463c69165a.

* Revert "Log Shardok hex mesh canvas state"

This reverts commit 15516200859584cde46ab54eb1dd5f5a20415e13.

* Revert "Size Shardok canvas hex mesh bounds"

This reverts commit 25c3994f39abacd7ff6bea7376a045e557ca2e36.

* Revert "Render Shardok hex meshes through canvas"

This reverts commit 8cf8f3b78fbfdd904a3e01a2165ac37cb66f0c34.

* Render Shardok canvases through runtime camera

* Layer Shardok labels above hex overlays

* Move Settings UI toward shared scene wiring

* Move persistent UI into Shared scene

* Tighten split scene transition wiring
2026-06-08 13:46:12 -07:00
adminandGitHub aff09aedc3 Add Postgres history smoke check (#6972) 2026-06-08 08:35:53 -07:00
adminandGitHub 47b813fba8 Bind structured action blobs as binary nulls (#6971) 2026-06-08 08:25:52 -07:00
adminandGitHub 5da3a75e7c Add Postgres game history backend (#6970) 2026-06-08 08:20:47 -07:00
197 changed files with 46043 additions and 112255 deletions
+15 -1
View File
@@ -31,12 +31,26 @@ concurrency:
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: Prepare non-LFS checkout
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
GIT_CONFIG_NOSYSTEM: "1"
run: |
git config --global --unset-all filter.lfs.process || true
git config --global filter.lfs.smudge "cat"
git config --global filter.lfs.clean "cat"
git config --global filter.lfs.required false
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
- name: Checkout repository
uses: actions/checkout@v6
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
GIT_CONFIG_NOSYSTEM: "1"
with:
persist-credentials: false
lfs: false
+1 -1
View File
@@ -10,7 +10,7 @@ on:
jobs:
cleanup:
runs-on: [self-hosted, bazel, halfdan]
runs-on: [self-hosted, bazel]
steps:
- name: Checkout
+1 -1
View File
@@ -11,7 +11,7 @@ permissions:
jobs:
cleanup:
runs-on: [self-hosted, bazel, halfdan]
runs-on: [self-hosted, bazel]
steps:
- uses: actions/checkout@v6
+8 -2
View File
@@ -48,7 +48,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 }}
@@ -227,7 +227,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
@@ -264,6 +264,7 @@ jobs:
FASTMAIL_API_TOKEN: ${{ secrets.FASTMAIL_API_TOKEN }}
FASTMAIL_FROM_EMAIL: ${{ secrets.FASTMAIL_FROM_EMAIL }}
FASTMAIL_FROM_NAME: ${{ secrets.FASTMAIL_FROM_NAME }}
EAGLE_HISTORY_BACKEND: ${{ secrets.EAGLE_HISTORY_BACKEND }}
EAGLE_POSTGRES_HOST: ${{ secrets.EAGLE_POSTGRES_HOST }}
EAGLE_POSTGRES_PORT: ${{ secrets.EAGLE_POSTGRES_PORT }}
EAGLE_POSTGRES_DATABASE: ${{ secrets.EAGLE_POSTGRES_DATABASE }}
@@ -379,6 +380,10 @@ jobs:
validate_env "EAGLE_IMAGE" "${EAGLE_IMAGE}" "" || VALIDATION_FAILED=1
validate_env "JWT_PRIVATE_KEY" "${JWT_PRIVATE_KEY}" "" || VALIDATION_FAILED=1
validate_env "DO_REGISTRY_TOKEN" "${DO_REGISTRY_TOKEN}" "" || VALIDATION_FAILED=1
validate_env "EAGLE_POSTGRES_HOST" "${EAGLE_POSTGRES_HOST}" "" || VALIDATION_FAILED=1
validate_env "EAGLE_POSTGRES_DATABASE" "${EAGLE_POSTGRES_DATABASE}" "" || VALIDATION_FAILED=1
validate_env "EAGLE_POSTGRES_USER" "${EAGLE_POSTGRES_USER}" "" || VALIDATION_FAILED=1
validate_env "EAGLE_POSTGRES_PASSWORD" "${EAGLE_POSTGRES_PASSWORD}" "" || VALIDATION_FAILED=1
if [ "\${VALIDATION_FAILED}" -eq 1 ]; then
echo ""
@@ -426,6 +431,7 @@ jobs:
export FASTMAIL_API_TOKEN="${FASTMAIL_API_TOKEN}"
export FASTMAIL_FROM_EMAIL="${FASTMAIL_FROM_EMAIL}"
export FASTMAIL_FROM_NAME="${FASTMAIL_FROM_NAME}"
export EAGLE_HISTORY_BACKEND="${EAGLE_HISTORY_BACKEND:-postgres}"
export EAGLE_POSTGRES_HOST="${EAGLE_POSTGRES_HOST}"
export EAGLE_POSTGRES_PORT="${EAGLE_POSTGRES_PORT}"
export EAGLE_POSTGRES_DATABASE="${EAGLE_POSTGRES_DATABASE}"
+2 -2
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
@@ -209,7 +209,7 @@ jobs:
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
+10 -5
View File
@@ -66,7 +66,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 }}
@@ -274,7 +274,7 @@ 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
@@ -342,7 +342,7 @@ 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, notarize]
outputs:
deployed_version: ${{ steps.deploy-mac.outputs.deployed_version }}
@@ -401,8 +401,13 @@ jobs:
echo "$SPARKLE_EDDSA_PRIVATE_KEY" > "$SPARKLE_PRIVATE_KEY_PATH"
chmod 600 "$SPARKLE_PRIVATE_KEY_PATH"
# Install dmgbuild (creates .DS_Store programmatically, no AppleScript needed)
pip3 install dmgbuild
# Install dmgbuild in an isolated venv. Homebrew Python rejects system
# installs under PEP 668, and mac_build_handler shells out to dmgbuild.
DMGBUILD_VENV="${{ env.EAGLE0_BUILD_DIR }}/dmgbuild-venv"
python3 -m venv "$DMGBUILD_VENV"
"$DMGBUILD_VENV/bin/python" -m pip install --upgrade pip
"$DMGBUILD_VENV/bin/python" -m pip install dmgbuild
export PATH="$DMGBUILD_VENV/bin:$PATH"
# Background image for styled DMG
BACKGROUND_PATH="$(pwd)/ci/mac/dmg/background.png"
+1 -1
View File
@@ -11,7 +11,7 @@ permissions:
jobs:
cleanup:
runs-on: [self-hosted, bazel, halfdan]
runs-on: [self-hosted, bazel]
steps:
- name: Ensure AWS CLI is available
+16 -2
View File
@@ -30,12 +30,26 @@ 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: Prepare non-LFS checkout
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
GIT_CONFIG_NOSYSTEM: "1"
run: |
git config --global --unset-all filter.lfs.process || true
git config --global filter.lfs.smudge "cat"
git config --global filter.lfs.clean "cat"
git config --global filter.lfs.required false
rm -f .git/hooks/post-checkout .git/hooks/post-merge .git/hooks/pre-push
- name: Checkout repository
uses: actions/checkout@v6
env:
GIT_CONFIG_GLOBAL: ${{ runner.temp }}/gitconfig-no-lfs
GIT_CONFIG_NOSYSTEM: "1"
with:
persist-credentials: false
lfs: false
@@ -170,7 +184,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
+1 -1
View File
@@ -57,7 +57,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 }}
+12 -1
View File
@@ -49,6 +49,17 @@ When code changes appear correct locally, create the PR promptly even if long-ru
Opening the PR starts CI on the build machines, uploads results to the Bazel remote cache, and can speed up subsequent
local validation. Continue monitoring any already-started local and CI validation after opening the PR.
## GitHub CLI PR Bodies
When creating or editing PR descriptions with multiline bodies, write the body to a temporary markdown file and pass it
with `gh pr create --body-file <path>` or `gh pr edit <number> --body-file <path>`. Do not use multiline `$'...'`
shell quoting for PR bodies; Codex's permission matching may treat that as a complex shell command instead of a clean
`gh pr` invocation, causing unnecessary approval prompts.
Write these temporary body files, and any other scratch files needed for commands, under a known writable location such
as `/private/tmp`, `$TMPDIR`, or the current repository workspace. Do not use protected paths that require additional
user permission just to create or edit temporary files.
---
This file provides guidance to Codex (Codex.ai/code) when working with code in this repository.
@@ -253,7 +264,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.4.10f1) 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/`
+1 -1
View File
@@ -228,7 +228,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.4.10f1) 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/`
+47 -44
View File
@@ -3,25 +3,29 @@ module(name = "net_eagle0")
# Version constants
SCALA_VERSION = "3.7.4"
NETTY_VERSION = "4.1.127.Final"
NETTY_VERSION = "4.1.135.Final"
SCALAPB_VERSION = "1.0.0-alpha.1"
SCALAPB_VERSION = "1.0.0-alpha.5"
AWS_SDK_VERSION = "2.41.18"
SCALAPB_JSON4S_VERSION = "1.0.0-alpha.1"
AWS_SDK_VERSION = "2.46.7"
SLF4J_VERSION = "2.0.18"
#
# Core Build Tools
#
bazel_dep(name = "bazel_skylib", version = "1.9.0")
bazel_dep(name = "platforms", version = "1.0.0")
bazel_dep(name = "platforms", version = "1.1.0")
bazel_dep(name = "rules_pkg", version = "1.2.0")
#
# Language Support - Scala
#
bazel_dep(name = "rules_scala", version = "7.2.1")
bazel_dep(name = "rules_scala", version = "7.2.5")
scala_config = use_extension(
"@rules_scala//scala/extensions:config.bzl",
@@ -41,21 +45,21 @@ scala_deps.scala_proto()
# Language Support - C++
#
bazel_dep(name = "toolchains_llvm", version = "1.6.0")
bazel_dep(name = "toolchains_llvm", version = "1.7.0")
llvm = use_extension("@toolchains_llvm//toolchain/extensions:llvm.bzl", "llvm")
# Native toolchain (macOS -> macOS, Linux -> Linux)
llvm.toolchain(
name = "llvm_toolchain",
llvm_version = "20.1.4",
llvm_version = "20.1.8",
)
# Cross-compilation toolchain (macOS -> Linux x86_64)
# Uses the same LLVM distribution but with a Linux sysroot
llvm.toolchain(
name = "llvm_toolchain_linux",
llvm_version = "20.1.4",
llvm_version = "20.1.8",
)
# Linux x86_64 sysroot for cross-compilation
@@ -68,7 +72,7 @@ llvm.sysroot(
# Cross-compilation toolchain (macOS -> Linux ARM64)
llvm.toolchain(
name = "llvm_toolchain_linux_arm64",
llvm_version = "20.1.4",
llvm_version = "20.1.8",
)
# Linux ARM64 sysroot for cross-compilation
@@ -77,7 +81,6 @@ llvm.sysroot(
label = "@linux_sysroot_arm64//sysroot",
targets = ["linux-aarch64"],
)
use_repo(llvm, "llvm_toolchain", "llvm_toolchain_linux", "llvm_toolchain_linux_arm64")
# Download the Linux sysroots (Ubuntu 24.04 Noble for C++23 support)
@@ -103,11 +106,11 @@ sysroot(
# Language Support - Go
#
bazel_dep(name = "rules_go", version = "0.59.0", repo_name = "io_bazel_rules_go")
bazel_dep(name = "gazelle", version = "0.47.0", repo_name = "bazel_gazelle")
bazel_dep(name = "rules_go", version = "0.61.1", repo_name = "io_bazel_rules_go")
bazel_dep(name = "gazelle", version = "0.51.3", repo_name = "bazel_gazelle")
go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
go_sdk.download(version = "1.23.3")
go_sdk.download(version = "1.25.11")
use_repo(go_sdk, "go_default_sdk")
go_deps = use_extension("@bazel_gazelle//:extensions.bzl", "go_deps")
@@ -130,7 +133,7 @@ use_repo(
# Language Support - Rust
#
bazel_dep(name = "rules_rust", version = "0.68.1")
bazel_dep(name = "rules_rust", version = "0.70.0")
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
rust.toolchain(edition = "2021")
@@ -150,7 +153,7 @@ use_repo(crate, "map_generator_crates")
# Platform Support - Apple/iOS
#
bazel_dep(name = "apple_support", version = "1.24.1", repo_name = "build_bazel_apple_support")
bazel_dep(name = "apple_support", version = "1.24.5", repo_name = "build_bazel_apple_support")
bazel_dep(name = "rules_apple", version = "4.3.3", repo_name = "build_bazel_rules_apple")
# Register Apple CC toolchain for Objective-C compilation
@@ -164,7 +167,7 @@ use_repo(apple_cc_configure, "local_config_apple_cc", "local_config_apple_cc_too
# Protocol Buffers & RPC
#
bazel_dep(name = "protobuf", version = "33.5", repo_name = "com_google_protobuf")
bazel_dep(name = "protobuf", version = "33.6", repo_name = "com_google_protobuf")
# Use pre-built protoc binaries instead of compiling from source.
# See: https://protobuf.dev/reference/cpp/cpp-generated/#invocation
@@ -183,7 +186,7 @@ use_repo(
)
bazel_dep(name = "rules_proto", version = "7.1.0")
bazel_dep(name = "rules_cc", version = "0.2.14")
bazel_dep(name = "rules_cc", version = "0.2.19")
bazel_dep(name = "grpc", version = "1.78.0")
# Patch grpc 1.78.0 to fix rules_python incompatibility: grpc requests Python
@@ -197,7 +200,7 @@ single_version_override(
version = "1.78.0",
)
bazel_dep(name = "grpc-java", version = "1.78.0")
bazel_dep(name = "grpc-java", version = "1.78.0.bcr.1")
bazel_dep(name = "rules_proto_grpc_csharp", version = "5.8.0")
bazel_dep(name = "flatbuffers", version = "25.12.19")
@@ -205,13 +208,13 @@ bazel_dep(name = "flatbuffers", version = "25.12.19")
# Testing
#
bazel_dep(name = "googletest", version = "1.17.0")
bazel_dep(name = "googletest", version = "1.17.0.bcr.2")
#
# Container Images (OCI)
#
bazel_dep(name = "rules_oci", version = "2.2.7")
bazel_dep(name = "rules_oci", version = "2.3.0")
bazel_dep(name = "aspect_bazel_lib", version = "2.22.5")
oci = use_extension("@rules_oci//oci:extensions.bzl", "oci")
@@ -261,7 +264,7 @@ use_repo(oci, "alpine_linux", "alpine_linux_linux_amd64", "eclipse_temurin_25_jd
# Java/Scala Dependencies
#
bazel_dep(name = "rules_java", version = "9.3.0")
bazel_dep(name = "rules_java", version = "9.6.1")
bazel_dep(name = "rules_jvm_external", version = "6.10")
maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
@@ -281,7 +284,7 @@ maven.install(
# ScalaPB
"com.thesamet.scalapb:lenses_3:%s" % SCALAPB_VERSION,
"com.thesamet.scalapb:scalapb-json4s_3:%s" % SCALAPB_VERSION,
"com.thesamet.scalapb:scalapb-json4s_3:%s" % SCALAPB_JSON4S_VERSION,
"com.thesamet.scalapb:scalapb-runtime_3:%s" % SCALAPB_VERSION,
"com.thesamet.scalapb:scalapb-runtime-grpc_3:%s" % SCALAPB_VERSION,
"com.thesamet.scalapb:compilerplugin_3:%s" % SCALAPB_VERSION,
@@ -293,10 +296,10 @@ maven.install(
"org.json4s:json4s-native_3:4.1.0-M8",
# Testing
"org.scalamock:scalamock_3:7.4.1",
"org.scalamock:scalamock_3:7.5.5",
# Developer tools
"org.scalameta:scalafmt-cli_2.13:3.9.9",
"org.scalameta:scalafmt-cli_2.13:3.11.1",
# AWS SDK
"software.amazon.awssdk:s3-transfer-manager:%s" % AWS_SDK_VERSION,
@@ -308,38 +311,38 @@ maven.install(
"software.amazon.awssdk:http-client-spi:%s" % AWS_SDK_VERSION,
# AWS Lambda
"com.amazonaws:aws-lambda-java-core:1.2.3",
"com.amazonaws:aws-lambda-java-events:3.13.0",
"com.amazonaws:aws-lambda-java-core:1.4.0",
"com.amazonaws:aws-lambda-java-events:3.16.1",
# Logging
"org.slf4j:slf4j-api:2.0.16",
"org.slf4j:slf4j-simple:2.0.16",
"org.slf4j:slf4j-api:%s" % SLF4J_VERSION,
"org.slf4j:slf4j-simple:%s" % SLF4J_VERSION,
# Other
"org.reactivestreams:reactive-streams:1.0.4",
"javax.xml.bind:jaxb-api:2.3.1",
# OkHttp (for SSE with read timeout support, OAuth HTTP calls)
"com.squareup.okhttp3:okhttp:4.12.0",
"com.squareup.okhttp3:okhttp-sse:4.12.0",
"com.squareup.okhttp3:okhttp-jvm:5.4.0",
"com.squareup.okhttp3:okhttp-sse:5.4.0",
# JWT (for OAuth token handling)
"com.nimbusds:nimbus-jose-jwt:9.37.3",
"com.nimbusds:nimbus-jose-jwt:10.9.1",
# Error tracking
"io.sentry:sentry:8.31.0",
"io.sentry:sentry:8.43.2",
# SQLite for client text storage
"org.xerial:sqlite-jdbc:3.46.1.0",
"org.xerial:sqlite-jdbc:3.53.2.0",
# Postgres for production history benchmarking and migration work
"org.postgresql:postgresql:42.7.4",
"org.postgresql:postgresql:42.7.11",
],
duplicate_version_warning = "error",
fail_if_repin_required = True,
lock_file = "//:maven_install.json",
repositories = [
"https://repo1.maven.org/maven2",
"https://repo.maven.apache.org/maven2",
],
)
@@ -349,7 +352,7 @@ maven.artifact(
artifact = "scala3-staging_3",
exclusions = ["org.scala-lang:scala3-compiler_3"],
group = "org.scala-lang",
version = "3.3.6",
version = SCALA_VERSION,
)
# Force specific versions for dependencies with conflicts between grpc-java and protobuf
@@ -357,21 +360,20 @@ maven.artifact(
artifact = "gson",
force_version = True,
group = "com.google.code.gson",
version = "2.11.0",
version = "2.14.0",
)
maven.artifact(
artifact = "error_prone_annotations",
force_version = True,
group = "com.google.errorprone",
version = "2.30.0",
version = "2.50.0",
)
maven.artifact(
artifact = "guava",
force_version = True,
group = "com.google.guava",
version = "33.3.1-android",
version = "33.6.0-android",
)
use_repo(maven, "maven", "unpinned_maven")
#
@@ -379,6 +381,7 @@ use_repo(maven, "maven", "unpinned_maven")
#
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_file = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")
# GTL (for parallel_hashmap)
@@ -431,24 +434,24 @@ http_archive(
# Fallback: busybox.net (can be unreliable/slow)
http_file(
name = "busybox_x86_64",
downloaded_file_path = "busybox",
executable = True,
sha256 = "6e123e7f3202a8c1e9b1f94d8941580a25135382b99e8d3e34fb858bba311348",
urls = [
"https://eagle0-sysroot.sfo3.digitaloceanspaces.com/busybox/busybox-1.35.0-x86_64-linux-musl",
"https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox",
],
downloaded_file_path = "busybox",
executable = True,
)
http_file(
name = "busybox_aarch64",
downloaded_file_path = "busybox",
executable = True,
sha256 = "141adb1b625a6f44c4b114f76b4387b4ea4f7ab802b88eb40e0d2f6adcccb1c3",
urls = [
# TODO: Upload aarch64 binary to GitHub release when needed
"https://busybox.net/downloads/binaries/1.35.0-aarch64-linux-musl/busybox",
],
downloaded_file_path = "busybox",
executable = True,
)
# LLVM MinGW toolchain for Windows cross-compilation from macOS
+226 -201
View File
File diff suppressed because one or more lines are too long
+12 -7
View File
@@ -34,6 +34,7 @@ echo "Platform: ${PLATFORM}"
# Check if Unity is installed and has required modules
check_modules_installed() {
local unity_path="${UNITY_INSTALL_PATH}/${UNITY_VERSION}"
local unity_app_contents_path="${unity_path}/Unity.app/Contents"
if [ ! -d "$unity_path" ]; then
return 1
@@ -49,7 +50,8 @@ check_modules_installed() {
;;
mac)
# Mac IL2CPP module
if [ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport/Variations/macos_development_il2cpp" ] && \
if [ ! -d "${unity_app_contents_path}/PlaybackEngines/MacStandaloneSupport/Source/Player/MacPlayer/GameAssembly.xcodeproj" ] && \
[ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport/Variations/macos_development_il2cpp" ] && \
[ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport" ]; then
echo "✗ Mac IL2CPP module not installed for Unity ${UNITY_VERSION}"
return 1
@@ -69,7 +71,8 @@ check_modules_installed() {
echo "✗ iOS module missing"
missing=1
fi
if [ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport" ]; then
if [ ! -d "${unity_app_contents_path}/PlaybackEngines/MacStandaloneSupport/Source/Player/MacPlayer/GameAssembly.xcodeproj" ] && \
[ ! -d "${unity_path}/PlaybackEngines/MacStandaloneSupport" ]; then
echo "✗ Mac module missing"
missing=1
fi
@@ -191,8 +194,10 @@ echo "Running: ${INSTALL_CMD[*]}"
echo ""
# Capture output to check for "already installed" messages
OUTPUT=$("${INSTALL_CMD[@]}" 2>&1) || true
set +e
OUTPUT=$("${INSTALL_CMD[@]}" 2>&1)
EXIT_CODE=$?
set -e
echo "$OUTPUT"
# Check if modules are already installed (Unity Hub returns error but modules are present)
@@ -219,13 +224,13 @@ else
exit 1
fi
# Verify installation
# Verify installation and requested modules
echo ""
if [ -d "${UNITY_INSTALL_PATH}/${UNITY_VERSION}" ]; then
echo "✓ Verified: Unity ${UNITY_VERSION} is now installed"
if check_modules_installed; then
echo "✓ Verified: Unity ${UNITY_VERSION} is now installed with ${PLATFORM} support"
exit 0
else
echo "✗ Unity ${UNITY_VERSION} installation could not be verified"
echo "✗ Unity ${UNITY_VERSION} installation with ${PLATFORM} support could not be verified"
echo " Expected path: ${UNITY_INSTALL_PATH}/${UNITY_VERSION}"
echo ""
echo "The installation may still be in progress, or may require manual intervention."
+1 -1
View File
@@ -38,7 +38,7 @@ fi
echo "Using git-lfs at $(command -v git-lfs)"
git lfs version
git lfs install
git lfs install --force
echo "LFS objects before pull:"
git lfs ls-files | wc -l
+12
View File
@@ -19,6 +19,18 @@ SERVER_DATA="$UNITY_PROJECT/ServerData/$BUILD_TARGET"
DO_ENDPOINT="https://sfo3.digitaloceanspaces.com"
DO_BUCKET="eagle0-assets"
COMMON_PATHS="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
export PATH="${COMMON_PATHS}:${PATH}"
if ! command -v aws >/dev/null 2>&1; then
if command -v brew >/dev/null 2>&1; then
brew install awscli
else
echo "ERROR: aws CLI is unavailable, and Homebrew is not on PATH"
exit 1
fi
fi
if [ ! -d "$SERVER_DATA" ]; then
echo "No Addressables bundles found at $SERVER_DATA"
echo "Skipping upload (this is expected if Addressables are bundled locally)"
+2
View File
@@ -32,6 +32,7 @@ services:
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
EAGLE_HISTORY_BACKEND: "${EAGLE_HISTORY_BACKEND:-postgres}"
EAGLE_POSTGRES_HOST: "${EAGLE_POSTGRES_HOST:-}"
EAGLE_POSTGRES_PORT: "${EAGLE_POSTGRES_PORT:-}"
EAGLE_POSTGRES_DATABASE: "${EAGLE_POSTGRES_DATABASE:-}"
@@ -87,6 +88,7 @@ services:
DO_SPACES_ENDPOINT: "${DO_SPACES_ENDPOINT:-https://sfo3.digitaloceanspaces.com}"
DO_SPACES_ACCESS_KEY: "${DO_SPACES_ACCESS_KEY:-}"
DO_SPACES_SECRET_KEY: "${DO_SPACES_SECRET_KEY:-}"
EAGLE_HISTORY_BACKEND: "${EAGLE_HISTORY_BACKEND:-postgres}"
EAGLE_POSTGRES_HOST: "${EAGLE_POSTGRES_HOST:-}"
EAGLE_POSTGRES_PORT: "${EAGLE_POSTGRES_PORT:-}"
EAGLE_POSTGRES_DATABASE: "${EAGLE_POSTGRES_DATABASE:-}"
+1 -1
View File
@@ -68,7 +68,7 @@ private GameObject GetEffectPrefab(string beastName) {
### 4. Wire up in the scene
In `Gameplay.unity`, find the `ProvinceBeastsController` component and assign your new prefab to the new field.
In `Assets/Scenes/Eagle.unity`, find the `ProvinceBeastsController` component on the Eagle map object and assign your new prefab to the new field.
### 5. Test
+1 -1
View File
@@ -136,7 +136,7 @@ Create a tool to programmatically regenerate the Eagle strategic map images with
| `Assets/Eagle/Materials/*.png` | Replaced province masks |
| `Assets/Eagle/map_bw_labels.png` | Either: regenerated, or removed if using dynamic labels |
| `province_map.tsv` | Add centroid columns |
| `Assets/Gameplay.unity` | Add ProvinceLabelsController, label prefab instances |
| `Assets/Scenes/Eagle.unity` | Add ProvinceLabelsController, label prefab instances |
---
-197
View File
@@ -1,197 +0,0 @@
# Unity Scene Separation Plan
## Current State
The Unity client currently has both a large legacy scene and an unfinished split-scene setup.
- `Assets/Gameplay.unity` is still the production-sized monolithic scene. It contains connection/lobby UI, Eagle strategic UI, Shardok tactical UI, shared UI, camera/event-system objects, and miscellaneous persistent controllers.
- `Assets/Scenes/Main.unity`, `Connection.unity`, `Eagle.unity`, `Shardok.unity`, and `Shared.unity` already exist, but they are small shell scenes.
- `ProjectSettings/EditorBuildSettings.asset` includes both `Assets/Gameplay.unity` and the split scenes.
- `Assets/UI/Scripts/BootstrapManager.cs` is explicitly transitional and currently defaults to loading `Gameplay` additively through `useOriginalGameplayScene = true`.
- Additional migration notes exist under `Assets/SCENE_SEPARATION_PLAN.md`, `Assets/Scenes/README.md`, and `Assets/NEXT_STEPS.md`, but there is no repo-level plan in `docs/`.
This means the project is partway through a scene-separation migration, but runtime still depends on the monolithic scene.
## Recommendation
Finish the split-scene architecture. Keep one small bootstrap scene and load mode-specific UI scenes additively.
The target architecture should be:
- `Main`: startup/bootstrap only. Owns scene loading and truly persistent app services.
- `Shared`: shared UI and app-wide interaction infrastructure, such as the event system, error panels, confirmation modals, connection status overlays, settings, and update notifications.
- `Connection`: login, account selection, lobby, custom battle creation, and game-selection UI.
- `Eagle`: strategic map, strategic command panels, province/hero/battalion UI, notifications, and Eagle-specific effects.
- `Shardok`: tactical battle UI, hex grid, battle command UI, unit rows, tactical effects, and battle-only camera/input helpers.
Do not treat large prefabs as inherently bad. A large prefab is acceptable when it represents a coherent, reusable feature root. The problematic pattern is a large scene that owns unrelated application states and long-lived object references.
## Goals
- Reduce merge conflicts and accidental Unity serialization churn.
- Make login/lobby, Eagle, and Shardok changes independently editable.
- Make scene lifecycle explicit: load only the mode currently needed.
- Avoid cross-scene inspector references that are hard to reason about.
- Preserve a safe fallback until each migrated section is validated.
## Non-Goals
- Do not refactor gameplay logic and scene structure in the same change.
- Do not delete `Gameplay.unity` until all migrated scenes are verified in Editor, local builds, and CI.
- Do not move large vendor asset packs as part of this work.
- Do not split every small widget into its own scene. Prefer prefabs for reusable UI components and scenes for application modes.
## Target Runtime Flow
```text
Main
Load Shared
Load Connection
Connection -> Eagle
Unload Connection
Ensure Shared remains loaded
Load Eagle
Eagle -> Shardok
Keep Eagle loaded unless the battle UI must exclusively own the viewport
Load Shardok additively
Shardok -> Eagle
Unload Shardok
Keep Eagle and Shared loaded
Eagle -> Connection
Unload Shardok if loaded
Unload Eagle
Load Connection
```
## Ownership Rules
Use these rules during migration to avoid ambiguous object lifetimes.
- `Main` owns `GameSceneManager`, app bootstrap, and any service object that must survive across login, lobby, Eagle, and Shardok.
- `Shared` owns exactly one active `EventSystem` and shared modal/overlay UI.
- `Connection` owns `ConnectionHandler` and connection/lobby panel instances.
- `Eagle` owns `EagleGameController` and strategic-mode scene roots.
- `Shardok` owns `ShardokGameController` and tactical-mode scene roots.
- Cross-mode communication should happen through model/controller APIs, not direct serialized references to objects in another loadable scene.
- If a scene needs an object from another scene, prefer resolving it during scene initialization and failing loudly when required dependencies are missing.
## Migration Plan
### Phase 1: Make Bootstrap Explicit
- Make `Main.unity` the intended startup scene.
- Update `BootstrapManager` so the split-scene path is the default in development builds.
- Keep a temporary fallback flag for loading `Gameplay.unity`.
- Ensure build settings order is intentional: `Main` first, then loadable scenes. Leave `Gameplay` present only while fallback is still required.
- Add logging that states which startup path was chosen.
Validation:
- Enter Play Mode from `Main.unity`.
- Confirm `Shared` and `Connection` load without duplicate `EventSystem` objects.
- Confirm fallback still loads `Gameplay.unity` if explicitly enabled.
### Phase 2: Migrate Shared Infrastructure
- Move shared error UI, update UI, settings UI, connection status overlays, and global modal roots into `Shared.unity`.
- Keep only one event system active when additive scenes are loaded.
- Remove duplicated shared UI from `Gameplay.unity` only after migrated UI is validated.
Validation:
- Trigger an error modal from Connection, Eagle, and Shardok flows.
- Open/close settings from each mode that supports it.
- Verify no duplicate event-system warnings.
### Phase 3: Migrate Connection and Lobby
- Move `ConnectionCanvas`, login controls, account selector, lobby, custom battle UI, and game-selection rows into `Connection.unity`.
- Make `ConnectionHandler` initialize after the scene is loaded rather than assuming objects exist in `Gameplay.unity`.
- Replace show/hide transitions that activate large inactive subtrees with scene transitions where appropriate.
Validation:
- Start unauthenticated, authenticate, list available games, create/drop a custom battle, and enter a game.
- Return from an active game to the lobby.
- Test stored account flows.
### Phase 4: Migrate Eagle Strategic UI
- Move strategic map, command panel roots, hero/battalion/province UI, Eagle notifications, and strategic effects into `Eagle.unity`.
- Keep reusable row/panel widgets as prefabs under `Assets/Eagle/`.
- Remove direct dependencies on connection-scene objects.
Validation:
- Load a game from the lobby.
- Select provinces, heroes, battalions, command buttons, and notifications.
- Issue at least one representative command from each major command-panel family.
- Confirm generated/dynamic text listeners clean up when leaving the scene.
### Phase 5: Migrate Shardok Tactical UI
- Move Shardok canvas, hex grid, battle panels, tactical rows, battle effects, and battle-only input/camera roots into `Shardok.unity`.
- Decide whether Eagle remains visible/loaded under Shardok. Prefer keeping Eagle loaded if returning from battle must preserve strategic UI state.
- Ensure Shardok scene unload disposes generated text listeners, subscriptions, object pools, and pending animation state.
Validation:
- Enter battle from Eagle.
- Select units, issue movement/attack/fire/extinguish/end-turn commands.
- Resolve a battle and return to Eagle.
- Start a second battle in the same session to catch stale singleton/listener state.
### Phase 6: Remove Legacy Scene Dependency
- Flip the default so `Gameplay.unity` is not loaded during normal startup.
- Remove `Gameplay.unity` from build settings once no supported flow needs it.
- Keep `Gameplay.unity` in the repo temporarily as a reference snapshot if useful, then delete it in a separate cleanup PR.
- Remove transitional helpers whose only purpose was loading or testing the legacy scene.
Validation:
- Local Editor smoke test through login, Eagle, Shardok, and return flows.
- Unity edit-mode test suite if available.
- CI Unity build.
- TestFlight build before deleting the legacy scene.
## Review Checklist For Each Migration PR
- The PR moves one coherent subsystem, not multiple unrelated UI areas.
- Scene files remain small enough to review.
- Prefab changes are limited to the migrated subsystem.
- No required Inspector field is hidden behind a defensive null check.
- No duplicate `EventSystem`, camera, global canvas, or singleton is active.
- Leaving and re-entering the migrated scene works in one app session.
- The old `Gameplay.unity` object is removed only after the new scene object is verified.
## Risks
- Serialized cross-scene references will break when objects move. Prefer runtime lookup/initialization through explicit scene controllers.
- Singletons may survive scene unload and hold stale references. Audit `OnDestroy`, `Dispose`, and listener cleanup for every moved controller.
- Additive scenes can accidentally create duplicate input/event/camera objects. Keep these centralized in `Shared` or `Main`.
- Loading Shardok additively over Eagle can expose hidden assumptions about active canvases, cameras, and selected input targets.
- Unity scene YAML changes are easy to corrupt manually. Prefer moving objects in the Unity Editor and reviewing diffs carefully.
## Open Decisions
- Whether Shardok should overlay Eagle additively or unload most Eagle UI while battle is active.
- Whether the primary persistent game connection object belongs in `Main` or should be recreated per selected game.
- Whether `Shared` should always load at startup or only after successful connection.
- How long to keep `Gameplay.unity` after the split scene flow is fully validated.
## Suggested First PR
The first implementation PR should not move Eagle or Shardok. It should:
1. Make `Main.unity` the startup scene in build settings.
2. Make the split-scene bootstrap path explicit and logged.
3. Load `Shared` plus `Connection` by default.
4. Keep a fallback flag for loading `Gameplay.unity`.
5. Validate login/lobby startup in Editor and CI.
That gives a controlled base for subsequent subsystem migrations.
+2 -2
View File
@@ -22,7 +22,7 @@ This avoids the "everything is pink" problem of switching the pipeline on main b
| 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 |
| Scenes with baked lighting | 5 | 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 |
@@ -132,7 +132,7 @@ This is the critical path. Without these, the game is unplayable.
- Fix any visual differences from lighting model changes
**Lighting**
- Rebake lightmaps for all 6 scenes (Gameplay, Connection, Eagle, Shardok, Shared, Map Editor)
- Rebake lightmaps for all 5 scenes (Connection, Eagle, Shardok, Shared, Map Editor)
- Configure URP shadow cascade settings to match current quality levels
- Verify HDR rendering
+1 -1
View File
@@ -87,7 +87,7 @@ Create these prefabs in `Assets/Eagle/Effects/`:
### Scene Setup
Wire up in `Gameplay.unity`:
Wire up in `Assets/Scenes/Eagle.unity`:
1. Add controller components to the Map GameObject
2. Assign `mapContainer`, `centroidsJson` (shared with other controllers)
3. Assign the effect prefab for each controller
+6 -4
View File
@@ -2,7 +2,7 @@ module github.com/nolen777/eagle0
go 1.23.0
toolchain go1.23.3
toolchain go1.25.11
require (
github.com/aws/aws-sdk-go-v2 v1.32.8
@@ -12,9 +12,9 @@ require (
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
golang.org/x/sys v0.30.0
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.36.3
golang.org/x/sys v0.33.0
google.golang.org/grpc v1.71.0
google.golang.org/protobuf v1.36.10
)
require (
@@ -32,5 +32,7 @@ require (
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.8 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.33.6 // indirect
github.com/aws/smithy-go v1.22.1 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/text v0.25.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
)
+10
View File
@@ -43,14 +43,24 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 h1:VQpB2SpK88C6B5lPHTuSZKb2Qee1QWwiFlC5CKY4AW0=
github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:+2Yz8+CLJbIfL9z73EW45avw8Lmge3xVElCP9zEKi50=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/grpc v1.71.0 h1:kF77BGdPTQ4/JZWMlb9VpJ5pa25aqvVqogsxNHHdeBg=
google.golang.org/grpc v1.71.0/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+1256 -684
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -24,34 +24,35 @@ public class MoveCamera : MonoBehaviour {
void Update() {
// Get the left mouse button
if (Input.GetMouseButtonDown(0)) {
if (GameInput.PrimaryPointerPressedThisFrame()) {
// Get mouse origin
mouseOrigin = Input.mousePosition;
mouseOrigin = GameInput.PointerPosition();
isRotating = true;
}
// Get the right mouse button
if (Input.GetMouseButtonDown(1)) {
if (GameInput.SecondaryPointerPressedThisFrame()) {
// Get mouse origin
mouseOrigin = Input.mousePosition;
mouseOrigin = GameInput.PointerPosition();
isPanning = true;
}
// Get the middle mouse button
if (Input.GetMouseButtonDown(2)) {
if (GameInput.MiddlePointerPressedThisFrame()) {
// Get mouse origin
mouseOrigin = Input.mousePosition;
mouseOrigin = GameInput.PointerPosition();
isZooming = true;
}
// Disable movements on button release
if (!Input.GetMouseButton(0)) isRotating = false;
if (!Input.GetMouseButton(1)) isPanning = false;
if (!Input.GetMouseButton(2)) isZooming = false;
if (!GameInput.PrimaryPointerPressed()) isRotating = false;
if (!GameInput.SecondaryPointerPressed()) isPanning = false;
if (!GameInput.MiddlePointerPressed()) isZooming = false;
// Rotate camera along X and Y axis
if (isRotating) {
Vector3 pos = Camera.main.ScreenToViewportPoint(Input.mousePosition - mouseOrigin);
Vector3 pos = Camera.main.ScreenToViewportPoint(
(Vector3)GameInput.PointerPosition() - mouseOrigin);
transform.RotateAround(transform.position, transform.right, -pos.y * turnSpeed);
transform.RotateAround(transform.position, Vector3.up, pos.x * turnSpeed);
@@ -59,7 +60,8 @@ public class MoveCamera : MonoBehaviour {
// Move the camera on it's XY plane
if (isPanning) {
Vector3 pos = Camera.main.ScreenToViewportPoint(Input.mousePosition - mouseOrigin);
Vector3 pos = Camera.main.ScreenToViewportPoint(
(Vector3)GameInput.PointerPosition() - mouseOrigin);
Vector3 move = new Vector3(pos.x * panSpeed, pos.y * panSpeed, 0);
transform.Translate(move, Space.Self);
@@ -67,10 +69,11 @@ public class MoveCamera : MonoBehaviour {
// Move the camera linearly along Z axis
if (isZooming) {
Vector3 pos = Camera.main.ScreenToViewportPoint(Input.mousePosition - mouseOrigin);
Vector3 pos = Camera.main.ScreenToViewportPoint(
(Vector3)GameInput.PointerPosition() - mouseOrigin);
Vector3 move = pos.y * zoomSpeed * transform.forward;
transform.Translate(move, Space.World);
}
}
}
}
@@ -204,6 +204,9 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
}
void Start() {
MainQueue.EnsureExists();
EnsureOAuthManager();
var currentResolution = Screen.currentResolution;
var resolutions = Screen.resolutions;
@@ -213,6 +216,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
ShowConnectionPanel();
if (errorHandler == null) { errorHandler = GameSceneManager.Instance?.GetErrorHandler(); }
errorHandler.gameObject.SetActive(true);
// Initialize OAuth UI
@@ -235,6 +239,15 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
TryRestoreSession();
}
private static void EnsureOAuthManager() {
if (OAuthManager.Instance != null) return;
if (FindAnyObjectByType<OAuthManager>(FindObjectsInactive.Include) != null) return;
var oauthManager = new GameObject("OAuth Manager");
oauthManager.AddComponent<OAuthManager>();
}
private void SetupConnectToQAButton() {
if (connectToQAButton == null) return;
@@ -321,13 +334,11 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
}
private void RefreshStoredAccountButtons() {
// Clear ALL children of the container
// Disable immediately (Destroy is deferred to end of frame)
if (storedAccountsContainer != null) {
foreach (Transform child in storedAccountsContainer.transform) {
child.gameObject.SetActive(false);
Destroy(child.gameObject);
}
foreach (var storedAccountButton in _storedAccountButtons) {
if (storedAccountButton == null) continue;
storedAccountButton.gameObject.SetActive(false);
Destroy(storedAccountButton.gameObject);
}
_storedAccountButtons.Clear();
@@ -1095,8 +1106,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
private void ShowConnectionPanel() {
connectionCanvas.gameObject.SetActive(true);
connectionCanvas.enabled = true;
eagleCanvas.gameObject.SetActive(false);
shardokContainer.SetActive(false);
HideGamePanels();
connectionPanel.gameObject.SetActive(true);
if (connectionBackgroundLayer != null) { connectionBackgroundLayer.SetActive(true); }
@@ -1107,8 +1117,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
private void ShowLobbyPanel() {
connectionCanvas.gameObject.SetActive(true);
connectionCanvas.enabled = true;
eagleCanvas.gameObject.SetActive(false);
shardokContainer.SetActive(false);
HideGamePanels();
connectionPanel.gameObject.SetActive(false);
if (connectionBackgroundLayer != null) { connectionBackgroundLayer.SetActive(false); }
@@ -1119,8 +1128,7 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
private void ShowCustomBattlePanel() {
connectionCanvas.gameObject.SetActive(true);
connectionCanvas.enabled = true;
eagleCanvas.gameObject.SetActive(false);
shardokContainer.SetActive(false);
HideGamePanels();
connectionPanel.gameObject.SetActive(false);
if (connectionBackgroundLayer != null) { connectionBackgroundLayer.SetActive(false); }
@@ -1128,24 +1136,62 @@ public class ConnectionHandler : MonoBehaviour, ILobbySubscriber, IDisposable {
customBattlePanel.gameObject.SetActive(true);
}
private void ShowEaglePanel() {
connectionCanvas.enabled = false;
connectionCanvas.gameObject.SetActive(false);
shardokContainer.SetActive(false);
eagleCanvas.gameObject.SetActive(true);
private void HideGamePanels() {
var loadedEagleCanvas =
eagleCanvas != null ? eagleCanvas : GameSceneManager.Instance?.GetEagleCanvas();
if (loadedEagleCanvas != null) { loadedEagleCanvas.gameObject.SetActive(false); }
var loadedShardokContainer = shardokContainer != null
? shardokContainer
: GameSceneManager.Instance?.GetShardokContainer();
if (loadedShardokContainer != null) { loadedShardokContainer.SetActive(false); }
}
private void SelectEagleGame(long gameId) {
private void ShowEaglePanel(Canvas loadedEagleCanvas) {
connectionCanvas.enabled = false;
connectionCanvas.gameObject.SetActive(false);
var loadedShardokContainer = shardokContainer != null
? shardokContainer
: GameSceneManager.Instance?.GetShardokContainer();
if (loadedShardokContainer != null) { loadedShardokContainer.SetActive(false); }
loadedEagleCanvas.gameObject.SetActive(true);
}
private async void SelectEagleGame(long gameId) {
var gameInfo = fetchedRunningGames.Find(gi => gi.GameId == gameId);
if (gameInfo != null) {
SetLobbyActive(false);
eagleCanvas.GetComponent<EagleGameController>().SetUpGame(
var selectedPersistentConnection = _persistentClientConnection;
var selectedEnvironmentName = ConnectedEnvironmentName;
var eagleController = GameSceneManager.Instance == null
? eagleCanvas?.GetComponent<EagleGameController>()
: await GameSceneManager.Instance.TransitionToEagle();
if (eagleController == null) {
Debug.LogError(
"Cannot select Eagle game because the Eagle scene did not provide an " +
"EagleGameController.");
SetLobbyActive(true);
ShowLobbyPanel();
return;
}
eagleCanvas = eagleController.GetComponent<Canvas>();
if (eagleCanvas == null) {
Debug.LogError(
"Cannot select Eagle game because EagleGameController is not attached to " +
"a Canvas.");
SetLobbyActive(true);
ShowLobbyPanel();
return;
}
eagleController.SetUpGame(
gameInfo.GameId,
gameInfo.FactionId,
_persistentClientConnection,
ConnectedEnvironmentName);
ShowEaglePanel();
selectedPersistentConnection,
selectedEnvironmentName);
ShowEaglePanel(eagleCanvas);
}
}
}
@@ -68,7 +68,7 @@ public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
private bool _youAreDefender;
readonly Dictionary<int, string> _heroNames = new();
readonly Dictionary<int, string> _heroImages = new();
public void HandleCustomBattleResponse(CustomBattleResponse newGameResponse) {
public async void HandleCustomBattleResponse(CustomBattleResponse newGameResponse) {
Int16 yourPid = (Int16)(_youAreDefender ? 1 : 0);
HexMap map = _gameRequest.CustomMapBytes.IsEmpty
@@ -103,10 +103,26 @@ public class CustomBattleHandler : MonoBehaviour, IClientConnectionSubscriber {
heroImages: _heroImages,
battalionNames: new Dictionary<int, string> { { 0, "no name" } });
shardokContainer = GameSceneManager.Instance == null
? shardokContainer
: await GameSceneManager.Instance.TransitionToShardok();
if (shardokContainer == null) {
throw new InvalidOperationException(
"Cannot start custom battle because the Shardok scene did not provide a " +
"Shardok container.");
}
shardokContainer.SetActive(true);
var shardokController = shardokContainer.GetComponent<ShardokGameController>();
if (shardokController == null) {
throw new InvalidOperationException(
"Cannot start custom battle because the Shardok container is missing a " +
"ShardokGameController component.");
}
PersistentUIManager.Instance?.connectionStatusUI?.SetShardokGameStateProvider(
_shardokModel);
shardokContainer.GetComponent<ShardokGameController>().SetUpGame(_shardokModel);
shardokController.SetUpGame(_shardokModel);
GameId = newGameResponse.EagleGameId;
Register();
@@ -9,6 +9,8 @@ public class DropGameConfirmationPanel : MonoBehaviour {
private long _pendingGameId;
void Awake() { gameObject.SetActive(false); }
public void Show(long gameId, string message) {
_pendingGameId = gameId;
if (messageText != null) { messageText.text = message; }
@@ -48,9 +48,14 @@ public class RunningGameItem : MonoBehaviour {
leaderName,
DisplayNames.ProfessionNames[leader.Profession]);
// Set profession image
professionImage.texture = EagleCommonTextures.Instance.Profession(leader.Profession);
professionImage.color = professionImage.texture != null ? Color.white : Color.clear;
// Set profession image. The connection lobby can render before the Eagle scene has loaded
// its shared texture catalog, so the icon is optional here.
var professionTexture =
EagleCommonTextures.Instance == null
? null
: EagleCommonTextures.Instance.Profession(leader.Profession);
professionImage.texture = professionTexture;
professionImage.color = professionTexture != null ? Color.white : Color.clear;
// Show/hide tutorial indicator
if (tutorialIndicator != null) {
@@ -56,6 +56,8 @@ namespace eagle {
private static readonly int EatHash = Animator.StringToHash("eat");
private static readonly int AttackHash = Animator.StringToHash("attack");
private static readonly int DieHash = Animator.StringToHash("die");
private const string BeastSortingLayerName = "UI Base Overlay";
private const int BeastCreatureSortingOrder = 103;
void Start() { SpawnAnimals(); }
@@ -129,7 +131,8 @@ namespace eagle {
instance.transform.localPosition = startPos;
foreach (var renderer in instance.GetComponentsInChildren<Renderer>()) {
renderer.sortingOrder = 103;
renderer.sortingLayerName = BeastSortingLayerName;
renderer.sortingOrder = BeastCreatureSortingOrder;
}
foreach (var smr in instance.GetComponentsInChildren<SkinnedMeshRenderer>()) {
smr.updateWhenOffscreen = true;
@@ -22,6 +22,8 @@ namespace eagle {
private ParticleSystem _birdSystem;
private Material _materialInstance;
private const string BeastSortingLayerName = "UI Base Overlay";
private const int BeastBirdSortingOrder = 102;
void Start() { SetupParticleSystems(); }
@@ -140,7 +142,8 @@ namespace eagle {
renderer.material = _materialInstance;
}
}
renderer.sortingOrder = 102;
renderer.sortingLayerName = BeastSortingLayerName;
renderer.sortingOrder = BeastBirdSortingOrder;
}
}
@@ -0,0 +1,144 @@
using System;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
namespace eagle {
public static class ClientTextDiskCache {
private const int MaxDirtyEntriesBeforeFlush = 50;
private static readonly TimeSpan MaxTimeBetweenFlushes = TimeSpan.FromSeconds(1);
[Serializable]
private class CacheEntry {
public string llmId;
public string text;
}
[Serializable]
private class CacheFile {
public List<CacheEntry> entries = new();
}
private class GameCache {
public Dictionary<string, string> entries = new();
public int dirtyEntries;
public DateTime lastFlushUtc = DateTime.MinValue;
public bool loaded;
}
private static readonly object Lock = new();
private static readonly Dictionary<long, GameCache> Caches = new();
private static string _cacheDirectory;
public static Dictionary<string, string> LoadCompletedTexts(long gameId) {
EnsureCacheDirectoryInitialized();
lock (Lock) {
var cache = CacheFor(gameId);
EnsureLoaded(gameId, cache);
return new Dictionary<string, string>(cache.entries);
}
}
public static void SaveCompletedText(long gameId, string llmId, string text) {
if (String.IsNullOrEmpty(llmId)) { return; }
lock (Lock) {
if (_cacheDirectory == null) {
Debug.LogWarning(
"Cannot save client text cache before the cache directory has " +
"been initialized on the main thread.");
return;
}
var cache = CacheFor(gameId);
EnsureLoaded(gameId, cache);
var normalizedText = text ?? "";
if (cache.entries.TryGetValue(llmId, out var existing) &&
existing == normalizedText) {
return;
}
cache.entries[llmId] = normalizedText;
cache.dirtyEntries++;
if (ShouldFlush(cache)) { FlushLocked(gameId, cache); }
}
}
private static void FlushAll() {
lock (Lock) {
if (_cacheDirectory == null) { return; }
foreach (var (gameId, cache) in Caches) {
if (cache.dirtyEntries > 0) { FlushLocked(gameId, cache); }
}
}
}
private static GameCache CacheFor(long gameId) {
if (!Caches.TryGetValue(gameId, out var cache)) {
cache = new GameCache();
Caches[gameId] = cache;
}
return cache;
}
private static void EnsureLoaded(long gameId, GameCache gameCache) {
if (gameCache.loaded) { return; }
gameCache.loaded = true;
var path = CachePath(gameId);
if (!File.Exists(path)) { return; }
try {
var cache = JsonUtility.FromJson<CacheFile>(File.ReadAllText(path));
if (cache?.entries == null) { return; }
foreach (var entry in cache.entries) {
if (!String.IsNullOrEmpty(entry.llmId)) {
gameCache.entries[entry.llmId] = entry.text ?? "";
}
}
} catch (Exception e) {
Debug.LogWarning($"Failed to load client text cache for game {gameId}: {e}");
gameCache.entries.Clear();
}
}
private static void EnsureCacheDirectoryInitialized() {
if (_cacheDirectory != null) { return; }
_cacheDirectory = Path.Combine(Application.persistentDataPath, "eagle0", "client_text");
Application.quitting -= FlushAll;
Application.quitting += FlushAll;
}
private static bool ShouldFlush(GameCache cache) => cache.dirtyEntries
>= MaxDirtyEntriesBeforeFlush
|| DateTime.UtcNow - cache.lastFlushUtc
>= MaxTimeBetweenFlushes;
private static void FlushLocked(long gameId, GameCache gameCache) {
try {
Directory.CreateDirectory(CacheDirectory);
var cache = new CacheFile();
foreach (var (id, cachedText) in gameCache.entries) {
cache.entries.Add(new CacheEntry { llmId = id, text = cachedText });
}
File.WriteAllText(CachePath(gameId), JsonUtility.ToJson(cache));
gameCache.dirtyEntries = 0;
gameCache.lastFlushUtc = DateTime.UtcNow;
} catch (Exception e) {
Debug.LogWarning($"Failed to save client text cache for game {gameId}: {e}");
}
}
private static string CacheDirectory => _cacheDirectory;
private static string CachePath(long gameId) =>
Path.Combine(CacheDirectory, $"{gameId:X}.json");
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 5f2c53e67d8043d492e8d3277c6e5794
guid: 3a7621b5f8514e8d8b2d4d8cbb8ee5d2
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -52,6 +52,21 @@ namespace eagle {
lock (_lock) { return new Dictionary<string, TextEntry>(_streamingTexts); }
}
public void ReplaceWithCachedCompletedTexts(Dictionary<String, String> completedTexts) {
if (completedTexts == null) { throw new ArgumentNullException(nameof(completedTexts)); }
lock (_lock) {
_streamingTexts.Clear();
_pendingUpdates.Clear();
foreach (var (llmId, text) in completedTexts) {
if (String.IsNullOrEmpty(llmId)) { continue; }
_streamingTexts[llmId] = new TextEntry(text ?? "", true);
_pendingUpdates.Add(llmId);
}
}
}
/// <summary>
/// Updates the text dictionary. Thread-safe - can be called from any thread.
/// Listeners are NOT notified here; call ProcessPendingUpdates from main thread.
@@ -145,10 +145,7 @@ namespace eagle {
public void Update() {
// '?' key (Shift + /) toggles help panel when a command is selected
if (Selector != null && Input.GetKeyDown(KeyCode.Slash) &&
(Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift))) {
InfoButtonClicked();
}
if (Selector != null && GameInput.QuestionMarkPressed()) { InfoButtonClicked(); }
}
public void PointerEnteredCommitButton() {
@@ -84,6 +84,8 @@ namespace eagle {
private List<SingleEntryListener> listeners = new();
private void OnDestroy() { ClearListeners(); }
private void ClearListeners() {
foreach (var listener in listeners) {
ClientTextProvider.Provider.RemoveListener(listener);
@@ -1,19 +1,10 @@
using UnityEngine;
namespace eagle {
public class KeyModifiedAmount {
private static bool Shift() {
return Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift);
}
private static bool Control() {
return Input.GetKey(KeyCode.LeftControl) || Input.GetKey(KeyCode.RightControl);
}
public static int Amount() {
if (Shift() && Control()) { return 1000; }
if (Shift()) { return 100; }
if (Control()) { return 10; }
if (GameInput.ShiftPressed() && GameInput.ControlPressed()) { return 1000; }
if (GameInput.ShiftPressed()) { return 100; }
if (GameInput.ControlPressed()) { return 10; }
return 1;
}
}
}
}
@@ -115,7 +115,7 @@ namespace eagle {
// Update is called once per frame
void Update() {
if (Input.GetButtonUp("Toggle Dominion Table") && disclosureToggle != null) {
if (GameInput.ToggleDominionTableReleased() && disclosureToggle != null) {
disclosureToggle.isOn = !disclosureToggle.isOn;
}
}
@@ -140,4 +140,4 @@ namespace eagle {
mapController.OverrideTargetedProvinces = new List<ProvinceId>();
}
}
}
}
@@ -68,6 +68,8 @@ namespace eagle {
private static readonly int FlyBreathFireHash = Animator.StringToHash("flyBreathFire");
private static readonly int FlyFastHash = Animator.StringToHash("flyFast");
private static readonly int FlyHitHash = Animator.StringToHash("flyHit");
private const string BeastSortingLayerName = "UI Base Overlay";
private const int BeastCreatureSortingOrder = 103;
void Awake() {
SpawnDragon();
@@ -85,7 +87,8 @@ namespace eagle {
FaceCameraAtSpawn(_dragonInstance.transform);
foreach (var renderer in _dragonInstance.GetComponentsInChildren<Renderer>()) {
renderer.sortingOrder = 103;
renderer.sortingLayerName = BeastSortingLayerName;
renderer.sortingOrder = BeastCreatureSortingOrder;
}
if (clipToViewport) { RendererViewportClipper.Attach(_dragonInstance); }
@@ -139,6 +139,9 @@ namespace eagle {
dominionPanelControllerNarrow.gameObject.SetActive(false);
showChronicleButton.gameObject.SetActive(false);
if (errorHandler == null) {
errorHandler = GameSceneManager.Instance?.GetErrorHandler();
}
errorHandler.gameObject.SetActive(true);
ArrangeLayout();
@@ -286,7 +289,7 @@ namespace eagle {
}
}
if (Input.GetButtonUp("Windowed Mode")) {
if (GameInput.WindowedModeReleased()) {
#if UNITY_EDITOR
// do nothing
#else
@@ -356,8 +359,11 @@ namespace eagle {
}
// Fire-and-forget - subscription is awaited internally and failures are logged
MainQueue.Q.EnqueueForNextUpdate(
() => { _ = ModelUpdater.StartListeningForUpdates(); });
var modelUpdater = ModelUpdater;
MainQueue.Q.EnqueueForNextUpdate(() => {
if (this == null || ModelUpdater != modelUpdater) { return; }
_ = modelUpdater.StartListeningForUpdates();
});
// Initialize tutorial system (onboarding starts after first model update in SwapModel)
TutorialManager.Instance?.Initialize(this, null);
@@ -820,16 +826,12 @@ namespace eagle {
DoCommitCommand();
}
public void GoToBattle() {
public async void GoToBattle() {
// End any active dialogue before transitioning to battle
TutorialManager.Instance?.DialogueManager?.EndDialogueWithoutCompletion();
if (Model == null) { throw new InvalidOperationException("GoToBattle: Model is null"); }
if (shardokContainer == null) {
throw new InvalidOperationException("GoToBattle: shardokContainer is null");
}
ShardokGameModel selectedModel;
var runningBattles = Model.RunningShardokGameModels.Values;
@@ -844,32 +846,28 @@ namespace eagle {
}
var shardokModel = Model.ShardokGameModels[selectedModel.ShardokGameId];
shardokContainer.SetActive(true);
var shardokController = shardokContainer.GetComponent<ShardokGameController>();
if (shardokController == null) {
throw new InvalidOperationException(
"GoToBattle: shardokContainer missing ShardokGameController component");
}
PersistentUIManager.Instance?.connectionStatusUI?.SetShardokGameStateProvider(
shardokModel);
shardokController.SetUpGame(shardokModel);
gameObject.SetActive(false);
await ShowShardokBattle(shardokModel);
}
public void GoToBattle(string shardokGameId) {
public async void GoToBattle(string shardokGameId) {
TutorialManager.Instance?.DialogueManager?.EndDialogueWithoutCompletion();
battleProgressPanel.gameObject.SetActive(false);
if (Model == null) { throw new InvalidOperationException("GoToBattle: Model is null"); }
var shardokModel = Model.ShardokGameModels[shardokGameId];
await ShowShardokBattle(shardokModel);
}
private async System.Threading.Tasks.Task ShowShardokBattle(ShardokGameModel shardokModel) {
shardokContainer = GameSceneManager.Instance == null
? shardokContainer
: await GameSceneManager.Instance.TransitionToShardok();
if (shardokContainer == null) {
throw new InvalidOperationException("GoToBattle: shardokContainer is null");
}
var shardokModel = Model.ShardokGameModels[shardokGameId];
shardokContainer.SetActive(true);
var shardokController = shardokContainer.GetComponent<ShardokGameController>();
if (shardokController == null) {
@@ -888,6 +886,17 @@ namespace eagle {
textField.text = textField.text == "" ? ModelUpdater.GameId.ToString("X") : "";
}
private void OnDestroy() { factionTextUpdater?.Dispose(); }
private void OnDestroy() {
if (ModelUpdater != null) {
ModelUpdater.StopListeningForUpdates();
ModelUpdater.UpdateAction = null;
ModelUpdater.ActionResultAction = null;
ModelUpdater.NoteRecipient = null;
ModelUpdater.ErrorHandler = null;
ModelUpdater = null;
}
factionTextUpdater?.Dispose();
}
}
}
@@ -93,7 +93,7 @@ namespace eagle {
/// </summary>
public Func<long, bool> HasPendingCommandWithToken { get; set; }
private void Notify(Notification notification) { NoteRecipient(notification); }
private void Notify(Notification notification) { NoteRecipient?.Invoke(notification); }
private readonly ResourceFetcher resourceFetcher;
@@ -257,6 +257,8 @@ namespace eagle {
PersistentClientConnection pers) {
GameId = eagleGameId;
PersistentConnection = pers;
ClientTextProvider.Provider.ReplaceWithCachedCompletedTexts(
ClientTextDiskCache.LoadCompletedTexts(GameId));
_currentModel.LastPostedToken = -1;
_currentModel.PlayerId = factionId;
@@ -268,6 +270,34 @@ namespace eagle {
new ConcurrentDictionary<ShardokGameId, ShardokGameModel>();
_currentModel.BattalionTypes = new Dictionary<BattalionTypeId, BattalionType>();
RestoreCachedStateIfAvailable(factionId);
}
private void RestoreCachedStateIfAvailable(FactionId? factionId) {
var cachedState = EagleGameStateDiskCache.Load(GameId, factionId);
if (cachedState == null) {
_connectionLogger.LogLine(
$"[STATE_CACHE] Cache miss game={GameId} " +
$"player={factionId?.ToString() ?? "spectator"} " +
$"reason={EagleGameStateDiskCache.LastLoadDiagnostic}");
return;
}
_currentModel.GsView = cachedState.GameStateView;
_currentModel.BattalionTypes = cachedState.GameStateView.BattalionTypes.ToDictionary(
bt => bt.TypeId,
bt => bt);
_currentModel.ChronicleEntries = cachedState.GameStateView.ChronicleEntries.ToList();
lock (_resultCountLock) {
_lastUnfilteredResultCount = cachedState.UnfilteredResultCount;
}
_lastAppliedUnfilteredCount = cachedState.UnfilteredResultCount;
_connectionLogger.LogLine(
$"[STATE_CACHE] Restored game {GameId} at count " +
$"{cachedState.UnfilteredResultCount}; Shardok state will refresh from server");
}
private ShardokGameModel MakeGameModel(ShardokGameId shardokGameId) {
@@ -379,8 +409,13 @@ namespace eagle {
_connectionLogger.LogLine(
"[UPDATE] Processed update and invoked UpdateAction");
} else {
RecordAppliedUnfilteredCount(
updateItem.ActionResultResponse.UnfilteredResultCountAfter);
SaveCurrentStateToCache(
updateItem.ActionResultResponse.UnfilteredResultCountAfter);
_connectionLogger.LogLine(
"[UPDATE] SKIPPED - no results, has commands, token matches");
"[UPDATE] SKIPPED - no results, has commands, token matches; " +
"persisted accepted count");
}
// Check for turn state mismatch after processing update
@@ -443,6 +478,8 @@ namespace eagle {
shardokGameModel;
}
SaveCurrentStateToCache();
// If we created new models, re-subscribe so the server knows to keep
// sending updates for these games. The original subscription didn't
// include them because ShardokGameModels was empty at subscribe time.
@@ -477,6 +514,7 @@ namespace eagle {
case GameUpdate.GameUpdateDetailsOneofCase.BattleProgressResponse:
_currentModel.BattleDay = updateItem.BattleProgressResponse.CurrentDay;
ApplyBattleProgressUpdate(updateItem.BattleProgressResponse.UpdatedBattles);
SaveCurrentStateToCache();
if (UpdateAction != null) UpdateAction.Invoke(_currentModel);
break;
@@ -689,8 +727,35 @@ namespace eagle {
}
if (countAfter > _lastAppliedUnfilteredCount) {
_lastAppliedUnfilteredCount = countAfter;
RecordAppliedUnfilteredCount(countAfter);
}
SaveCurrentStateToCache(countAfter);
}
private void RecordAppliedUnfilteredCount(int countAfter) {
if (countAfter <= _lastAppliedUnfilteredCount) { return; }
_connectionLogger.LogLine(
$"[STATE_CACHE] Accepted Eagle count {_lastAppliedUnfilteredCount} -> {countAfter}");
_lastAppliedUnfilteredCount = countAfter;
}
private void SaveCurrentStateToCache(int unfilteredResultCount) {
if (unfilteredResultCount <= 0 || _currentModel.GsView == null) { return; }
EagleGameStateDiskCache.Save(
GameId,
_currentModel.PlayerId,
unfilteredResultCount,
_currentModel.GsView);
_connectionLogger.LogLine(
$"[STATE_CACHE] Saved game={GameId} count={unfilteredResultCount}");
}
private void SaveCurrentStateToCache() {
int count;
lock (_resultCountLock) { count = _lastUnfilteredResultCount; }
SaveCurrentStateToCache(count);
}
private static HashSet<FactionId> InvitationAcceptedFactionIds(
@@ -1080,11 +1145,14 @@ namespace eagle {
private void HandleNewStreamingText(StreamingTextResponse str) {
if (str == null) return;
ClientTextProvider.Provider.HandleNewStreamingText(
var updatedText = ClientTextProvider.Provider.HandleNewStreamingText(
str.LlmIdentifier,
str.NewText,
str.StartingByteCount,
str.Completed);
if (str.Completed) {
ClientTextDiskCache.SaveCompletedText(GameId, str.LlmIdentifier, updatedText);
}
}
private void MaybeSendNotification(
@@ -0,0 +1,146 @@
using System;
using System.IO;
using Google.Protobuf;
using Net.Eagle0.Eagle.Views;
using UnityEngine;
namespace eagle {
public static class EagleGameStateDiskCache {
private const int CurrentSchemaVersion = 3;
[Serializable]
private class CacheFile {
public int schemaVersion;
public long gameId;
public bool hasPlayerId;
public int playerId;
public int unfilteredResultCount;
public string gameStateViewBase64;
public string savedAtUtc;
}
public class CachedGameState {
public GameStateView GameStateView { get; set; }
public int UnfilteredResultCount { get; set; }
}
private static string _cacheDirectory;
public static string LastLoadDiagnostic { get; private set; }
public static CachedGameState Load(long gameId, int? playerId) {
EnsureCacheDirectoryInitialized();
var path = CachePath(gameId, playerId);
LastLoadDiagnostic = $"path={path}";
if (!File.Exists(path)) {
LastLoadDiagnostic = $"file not found at {path}";
return null;
}
try {
var cache = JsonUtility.FromJson<CacheFile>(File.ReadAllText(path));
if (cache == null) {
LastLoadDiagnostic = $"cache JSON parsed to null at {path}";
return null;
}
if (cache.schemaVersion != CurrentSchemaVersion) {
LastLoadDiagnostic =
$"schema mismatch at {path}: cache={cache.schemaVersion} current={CurrentSchemaVersion}";
return null;
}
if (cache.gameId != gameId) {
LastLoadDiagnostic =
$"game id mismatch at {path}: cache={cache.gameId} requested={gameId}";
return null;
}
if (cache.hasPlayerId != playerId.HasValue) {
LastLoadDiagnostic =
$"player presence mismatch at {path}: cacheHas={cache.hasPlayerId} requestedHas={playerId.HasValue}";
return null;
}
if (cache.hasPlayerId && cache.playerId != playerId.Value) {
LastLoadDiagnostic =
$"player id mismatch at {path}: cache={cache.playerId} requested={playerId.Value}";
return null;
}
if (cache.unfilteredResultCount <= 0) {
LastLoadDiagnostic =
$"invalid unfiltered count at {path}: {cache.unfilteredResultCount}";
return null;
}
if (String.IsNullOrEmpty(cache.gameStateViewBase64)) {
LastLoadDiagnostic = $"missing GameStateView payload at {path}";
return null;
}
var bytes = Convert.FromBase64String(cache.gameStateViewBase64);
var gameStateView = GameStateView.Parser.ParseFrom(bytes);
if (gameStateView.GameId == 0) {
gameStateView.GameId = gameId;
} else if (gameStateView.GameId != gameId) {
LastLoadDiagnostic =
$"GameStateView game id mismatch at {path}: view={gameStateView.GameId} requested={gameId}";
return null;
}
LastLoadDiagnostic = $"loaded {path} count={cache.unfilteredResultCount}";
return new CachedGameState {
GameStateView = gameStateView,
UnfilteredResultCount = cache.unfilteredResultCount
};
} catch (Exception e) {
LastLoadDiagnostic = $"exception loading {path}: {e}";
Debug.LogWarning($"Failed to load Eagle game state cache for game {gameId}: {e}");
TryDelete(path);
return null;
}
}
public static void
Save(long gameId, int? playerId, int unfilteredResultCount, GameStateView gameStateView) {
if (unfilteredResultCount <= 0 || gameStateView == null) { return; }
EnsureCacheDirectoryInitialized();
try {
Directory.CreateDirectory(CacheDirectory);
var cache = new CacheFile {
schemaVersion = CurrentSchemaVersion,
gameId = gameId,
hasPlayerId = playerId.HasValue,
playerId = playerId ?? 0,
unfilteredResultCount = unfilteredResultCount,
gameStateViewBase64 = Convert.ToBase64String(gameStateView.ToByteArray()),
savedAtUtc = DateTime.UtcNow.ToString("O")
};
File.WriteAllText(CachePath(gameId, playerId), JsonUtility.ToJson(cache));
} catch (Exception e) {
Debug.LogWarning($"Failed to save Eagle game state cache for game {gameId}: {e}");
}
}
private static void EnsureCacheDirectoryInitialized() {
if (_cacheDirectory != null) { return; }
_cacheDirectory =
Path.Combine(Application.persistentDataPath, "eagle0", "eagle_game_state");
}
private static string CacheDirectory => _cacheDirectory;
private static string CachePath(long gameId, int? playerId) {
var playerKey = playerId.HasValue ? playerId.Value.ToString() : "spectator";
return Path.Combine(CacheDirectory, $"{gameId:X}-{playerKey}.json");
}
private static void TryDelete(string path) {
try {
File.Delete(path);
} catch (Exception e) {
Debug.LogWarning($"Failed to delete corrupt Eagle game state cache {path}: {e}");
}
}
}
}
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: 7d07c0b7fb8a4e45ac80c1479562dd9a
guid: 187990bd7c5c4c02a67ee224d3f6e519
MonoImporter:
externalObjects: {}
serializedVersion: 2
@@ -0,0 +1,67 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TMPro;
using UnityEngine;
namespace eagle {
public static class GeneratedTextGlyphValidator {
#if UNITY_EDITOR || DEVELOPMENT_BUILD
private static readonly HashSet<string> ReportedMissingGlyphs = new();
#endif
public static void
LogMissingGlyphs(TMP_Text textComponent, string renderedText, string context) {
#if UNITY_EDITOR || DEVELOPMENT_BUILD
if (String.IsNullOrEmpty(renderedText)) { return; }
var fontAsset = textComponent.font ?? TMP_Settings.defaultFontAsset;
if (fontAsset == null) { return; }
var visibleCharacters = VisibleCharacters(renderedText);
if (visibleCharacters.Length == 0) { return; }
if (fontAsset.HasCharacters(
visibleCharacters,
out uint[] missingUnicodes,
true,
false)) {
return;
}
var newlyReported = missingUnicodes
.Where(unicode => ReportedMissingGlyphs.Add(
ReportKey(fontAsset, unicode)))
.Select(DescribeUnicode)
.ToList();
if (newlyReported.Count == 0) { return; }
Debug.LogWarning(
$"Generated text contains glyphs missing from TMP font asset '{fontAsset.name}' and its fallbacks: " +
$"{String.Join(", ", newlyReported)}. Context: {context}. " +
"Bake these glyphs into the static TMP fallback font before shipping text that can contain them.",
textComponent);
#endif
}
#if UNITY_EDITOR || DEVELOPMENT_BUILD
private static string VisibleCharacters(string text) {
var builder = new StringBuilder(text.Length);
foreach (var character in text.Where(character => !Char.IsControl(character))) {
builder.Append(character);
}
return builder.ToString();
}
private static string ReportKey(TMP_FontAsset fontAsset, uint unicode) {
return $"{fontAsset.GetInstanceID()}:{unicode:X}";
}
private static string DescribeUnicode(uint unicode) {
return $"U+{unicode:X4} '{Char.ConvertFromUtf32((int)unicode)}'";
}
#endif
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 330d986b5aa54ebc81e3518425ce03ed
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
@@ -45,7 +46,7 @@ namespace eagle {
private void UpdateView() {
if (String.IsNullOrEmpty(TextId)) {
if (TextId == "") { GetComponent<TMP_Text>().text = PrefixedText; }
if (TextId == "") { SetRenderedText(PrefixedText); }
return;
}
@@ -56,7 +57,32 @@ namespace eagle {
for (var i = 0; i < dots; i++) { text += "."; }
}
GetComponent<TMP_Text>().text = PrefixedText + text;
SetRenderedText(PrefixedText + text);
}
private void SetRenderedText(string renderedText) {
var textComponent = GetComponent<TMP_Text>();
GeneratedTextGlyphValidator.LogMissingGlyphs(
textComponent,
renderedText,
TextContext());
textComponent.text = renderedText;
}
private string TextContext() {
return String.IsNullOrEmpty(TextId) ? GameObjectPath(transform)
: $"{TextId} at {GameObjectPath(transform)}";
}
private static string GameObjectPath(Transform current) {
var names = new List<string>();
while (current != null) {
names.Add(current.name);
current = current.parent;
}
names.Reverse();
return String.Join("/", names);
}
private void OnTextUpdate(TextEntry entry) {
@@ -88,4 +114,4 @@ namespace eagle {
private void OnDisable() { ClientTextProvider.Provider.RemoveListener(this); }
}
}
}
@@ -220,6 +220,16 @@ namespace eagle {
void Start() {
Enabled = true;
_previouslyHoveredProvinceId = null;
PlaceClickedProvincePanelAboveMap();
}
private void PlaceClickedProvincePanelAboveMap() {
var clickedProvincePanel = provinceNameText.transform.parent;
clickedProvincePanel.SetAsLastSibling();
foreach (var graphic in clickedProvincePanel.GetComponentsInChildren<Graphic>()) {
graphic.raycastTarget = false;
}
}
int? ProvinceFromPoint(Vector2 point) {
@@ -260,7 +270,7 @@ namespace eagle {
}
_activeHighlights.Clear();
var maybeHoveredProvinceId = ProvinceFromPoint(Input.mousePosition);
var maybeHoveredProvinceId = ProvinceFromPoint(GameInput.PointerPosition());
if (Model != null && maybeHoveredProvinceId != _previouslyHoveredProvinceId) {
_previouslyHoveredProvinceId = maybeHoveredProvinceId;
hoveredProvinceChangedCallback.Invoke(maybeHoveredProvinceId);
@@ -68,12 +68,12 @@ namespace eagle {
}
private void HandleTouchInput() {
if (Input.touchCount == 2) {
Touch touch0 = Input.GetTouch(0);
Touch touch1 = Input.GetTouch(1);
if (GameInput.TouchCount() == 2) {
var touch0 = GameInput.GetTouch(0);
var touch1 = GameInput.GetTouch(1);
float currentDistance = Vector2.Distance(touch0.position, touch1.position);
Vector2 currentCenter = (touch0.position + touch1.position) / 2f;
float currentDistance = Vector2.Distance(touch0.Position, touch1.Position);
Vector2 currentCenter = (touch0.Position + touch1.Position) / 2f;
if (!_isPinching) {
_isPinching = true;
@@ -43,6 +43,8 @@ namespace eagle {
private static readonly int SpecialATrigger = Animator.StringToHash("SpecialATrigger");
private static readonly int MoveStateHash = Animator.StringToHash("Move");
private static readonly int IdleStateHash = Animator.StringToHash("Idle");
private const string BeastSortingLayerName = "UI Base Overlay";
private const int BeastCreatureSortingOrder = 103;
void Start() { SpawnMonsters(); }
@@ -61,7 +63,8 @@ namespace eagle {
instance.transform.localPosition = startPos;
foreach (var renderer in instance.GetComponentsInChildren<Renderer>()) {
renderer.sortingOrder = 103;
renderer.sortingLayerName = BeastSortingLayerName;
renderer.sortingOrder = BeastCreatureSortingOrder;
}
if (clipToViewport) { RendererViewportClipper.Attach(instance); }
@@ -24,6 +24,8 @@ namespace eagle {
private bool _beastEffectsLoading;
private string _pendingBeastVisualName;
private GameObject _activeBeastVisual;
private Coroutine _spawnBeastVisualCoroutine;
private bool _isDestroying;
private const float BeastVisualForwardZ = -25f;
private const float BeastVisualNotificationScale = 2f;
@@ -31,7 +33,10 @@ namespace eagle {
// to skip adding if a dismiss happened since they were enqueued
private int _dismissGeneration = 0;
private void OnNoteTextChanged() { SetPopupInfos(); }
private void OnNoteTextChanged() {
if (_isDestroying) { return; }
SetPopupInfos();
}
private void TrackNote(Notification note) { note.TextChanged += OnNoteTextChanged; }
@@ -41,6 +46,8 @@ namespace eagle {
}
private void SetPopupInfos() {
if (_isDestroying) { return; }
PopupInfos = _notes.Select(note => new PopupInfo {
titleText = note.Title,
mainText = note.Text,
@@ -51,8 +58,6 @@ namespace eagle {
})
.ToList();
SetUpPanel();
// If there are no images, add a spacer to the panel
spacer.gameObject.SetActive(!imagesAndNamesContainer.gameObject.activeSelf);
if (PopupInfos.Count == 1) {
@@ -66,6 +71,15 @@ namespace eagle {
void Awake() { HideBeastVisual(); }
void OnDestroy() {
_isDestroying = true;
_dismissGeneration++;
foreach (var note in _notes) { ReleaseNote(note); }
_notes.Clear();
ClearBeastVisual();
OnAllDismissed = null;
}
void OnEnable() {
if (HasInfo) { UpdateBeastVisual(); }
}
@@ -88,6 +102,11 @@ namespace eagle {
// we'll skip adding the note
var capturedGeneration = _dismissGeneration;
MainQueue.Q.Enqueue(() => {
if (_isDestroying || this == null) {
ReleaseNote(notification);
return;
}
// Skip if DismissAll was clicked since this was enqueued
if (capturedGeneration != _dismissGeneration) {
ReleaseNote(notification);
@@ -135,7 +154,7 @@ namespace eagle {
return;
}
SpawnBeastVisual(beastName);
QueueSpawnBeastVisual(beastName);
}
private System.Collections.IEnumerator LoadBeastEffects() {
@@ -151,7 +170,7 @@ namespace eagle {
_beastEffectsLoading = false;
if (!string.IsNullOrEmpty(_pendingBeastVisualName)) {
SpawnBeastVisual(_pendingBeastVisualName);
QueueSpawnBeastVisual(_pendingBeastVisualName);
}
},
() => {
@@ -160,6 +179,22 @@ namespace eagle {
});
}
private void QueueSpawnBeastVisual(string beastName) {
CancelQueuedBeastVisualSpawn();
_spawnBeastVisualCoroutine = StartCoroutine(SpawnBeastVisualWhenReady(beastName));
}
private System.Collections.IEnumerator SpawnBeastVisualWhenReady(string beastName) {
yield return null;
Canvas.ForceUpdateCanvases();
_spawnBeastVisualCoroutine = null;
if (!isActiveAndEnabled || _pendingBeastVisualName != beastName) { yield break; }
SpawnBeastVisual(beastName);
}
private void SpawnBeastVisual(string beastName) {
ClearBeastVisual();
@@ -180,11 +215,23 @@ namespace eagle {
rectTransform.anchorMax = new Vector2(0.5f, 0.5f);
rectTransform.pivot = new Vector2(0.5f, 0.5f);
rectTransform.anchoredPosition3D = new Vector3(0f, 0f, BeastVisualForwardZ);
rectTransform.sizeDelta = beastVisualContainer.rect.size;
rectTransform.sizeDelta = ResolveBeastVisualSize(beastVisualContainer);
rectTransform.localScale = Vector3.one;
}
}
private static Vector2 ResolveBeastVisualSize(RectTransform container) {
var size = container.rect.size;
if (size.x >= 1f && size.y >= 1f) { return size; }
var layoutElement = container.GetComponent<LayoutElement>();
if (layoutElement == null) { return size; }
return new Vector2(
layoutElement.preferredWidth > 0f ? layoutElement.preferredWidth : size.x,
layoutElement.preferredHeight > 0f ? layoutElement.preferredHeight : size.y);
}
private static void ConfigureContainedSingleBeast(GameObject effect) {
var beastsEffect = effect.GetComponent<BeastsEffect>();
if (beastsEffect != null) {
@@ -241,12 +288,20 @@ namespace eagle {
}
private void ClearBeastVisual() {
CancelQueuedBeastVisualSpawn();
if (_activeBeastVisual != null) {
Destroy(_activeBeastVisual);
_activeBeastVisual = null;
}
}
private void CancelQueuedBeastVisualSpawn() {
if (_spawnBeastVisualCoroutine == null) { return; }
StopCoroutine(_spawnBeastVisualCoroutine);
_spawnBeastVisualCoroutine = null;
}
public void DismissButtonClicked() {
ReleaseNote(_notes.Dequeue());
SetPopupInfos();
@@ -285,10 +285,15 @@ namespace eagle {
lock (_pendingSubscriptionAcks) { _pendingSubscriptionAcks[gameId] = ackTcs; }
var shardokCount = shardokStatuses.Count();
var streamingTextStatuses = streamGameRequest.StreamingTextStatuses;
var completeTextCount = streamingTextStatuses.Count(status => status.KnownComplete);
var incompleteTextCount = streamingTextStatuses.Count - completeTextCount;
LogFlow($"SUBSCRIBE game={gameId} unfilteredCount={subscriber.LastUnfilteredResultCount} shardokGames={shardokCount}");
_remoteEagleClientLogger.LogLine(
$"[SUBSCRIBE] Sending StreamGameRequest for game {gameId}, " +
$"unfilteredCount={subscriber.LastUnfilteredResultCount}");
$"unfilteredCount={subscriber.LastUnfilteredResultCount}, " +
$"shardokGames={shardokCount}, completeTexts={completeTextCount}, " +
$"incompleteTexts={incompleteTextCount}");
var sendSuccess = await DoWithStreamingCall(async (sc) => {
await sc.RequestStream.WriteAsync(request);
@@ -1040,11 +1045,17 @@ namespace eagle {
// Listeners are notified later via ProcessPendingUpdates() on main thread.
var str = gameUpdate.StreamingTextResponse;
if (str != null) {
ClientTextProvider.Provider.HandleNewStreamingText(
var updatedText = ClientTextProvider.Provider.HandleNewStreamingText(
str.LlmIdentifier,
str.NewText,
str.StartingByteCount,
str.Completed);
if (str.Completed) {
ClientTextDiskCache.SaveCompletedText(
gameUpdate.GameId,
str.LlmIdentifier,
updatedText);
}
}
// No MainQueue enqueue needed - ProcessPendingUpdates handles listener
// notification
@@ -187,8 +187,9 @@ namespace eagle {
private static readonly HashSet<string> ButcherBeastNames =
new(StringComparer.Ordinal) { "psychopath", "maniac" };
private static GameObject
LookupPrefab(IReadOnlyDictionary<string, GameObject> effectPrefabs, string prefabName) {
private static GameObject LookupPrefab(
IReadOnlyDictionary<string, GameObject> effectPrefabs,
string prefabName) {
effectPrefabs.TryGetValue(prefabName, out var prefab);
if (prefab == null) {
Debug.LogWarning(
@@ -380,11 +380,11 @@ namespace eagle {
panel.SetActive(true);
}
if (!WideScreen && Input.GetButtonUp("Toggle Dominion Table")) {
if (!WideScreen && GameInput.ToggleDominionTableReleased()) {
dominionViewToggle.isOn = !dominionViewToggle.isOn;
}
}
private void OnDestroy() { factionTextUpdater?.Dispose(); }
}
}
}
@@ -1,388 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using common.GUIUtils;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
public static class ConnectionSceneMigrationTool {
private const string GameplayScenePath = "Assets/Gameplay.unity";
private const string ConnectionScenePath = "Assets/Scenes/Connection.unity";
private const string EagleScenePath = "Assets/Scenes/Eagle.unity";
private const string ShardokScenePath = "Assets/Scenes/Shardok.unity";
private const string SharedScenePath = "Assets/Scenes/Shared.unity";
private const string ConnectionCanvasName = "ConnectionCanvas";
private const string EagleCanvasName = "Eagle Canvas";
private const string EaglePlaceholderCanvasName = "EagleCanvas";
private const string ErrorCanvasName = "ErrorCanvas";
private const string ShardokContainerName = "Shardok Container";
private const string ShardokPlaceholderCanvasName = "ShardokCanvas";
private const string CopiedSceneIncompleteReason =
"Connection scene was copied from Gameplay but still needs cross-scene reference " +
"migration before it can replace Gameplay.";
[MenuItem("Eagle0/Scenes/Rebuild Connection Scene From Gameplay")]
public static void RebuildConnectionSceneFromGameplay() {
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) {
Debug.Log("Connection scene rebuild canceled because modified scenes were not saved.");
return;
}
if (!EditorUtility.DisplayDialog(
"Rebuild Connection Scene",
"This will replace the ConnectionCanvas in Assets/Scenes/Connection.unity " +
"with a copy from Assets/Gameplay.unity. The copied canvas will stay " +
"inactive and marked not production-ready.",
"Rebuild",
"Cancel")) {
Debug.Log("Connection scene rebuild canceled.");
return;
}
var gameplayScene = EditorSceneManager.OpenScene(GameplayScenePath, OpenSceneMode.Single);
var sourceConnectionCanvas = FindRootByName(gameplayScene, ConnectionCanvasName);
if (sourceConnectionCanvas == null) {
Debug.LogError(
$"Could not find root GameObject '{ConnectionCanvasName}' in {GameplayScenePath}.");
return;
}
var connectionScene =
EditorSceneManager.OpenScene(ConnectionScenePath, OpenSceneMode.Additive);
foreach (var existingCanvas in FindRootsByName(connectionScene, ConnectionCanvasName)) {
Object.DestroyImmediate(existingCanvas);
}
var copiedCanvas = Object.Instantiate(sourceConnectionCanvas);
copiedCanvas.name = ConnectionCanvasName;
copiedCanvas.SetActive(false);
SceneManager.MoveGameObjectToScene(copiedCanvas, connectionScene);
MarkSceneNotReady(copiedCanvas);
LogConnectionHandlerReferenceReport(copiedCanvas);
EditorSceneManager.MarkSceneDirty(connectionScene);
EditorSceneManager.SaveScene(connectionScene);
EditorSceneManager.OpenScene(ConnectionScenePath, OpenSceneMode.Single);
Debug.Log(
$"Rebuilt {ConnectionScenePath} from {GameplayScenePath}. " +
$"{ConnectionCanvasName} is inactive and productionReady is false.");
}
[MenuItem("Eagle0/Scenes/Rebuild Eagle And Shardok Scenes From Gameplay")]
public static void RebuildEagleAndShardokScenesFromGameplay() {
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) {
Debug.Log(
"Eagle/Shardok scene rebuild canceled because modified scenes were not saved.");
return;
}
if (!EditorUtility.DisplayDialog(
"Rebuild Eagle And Shardok Scenes",
"This will replace the placeholder content in Assets/Scenes/Eagle.unity and " +
"Assets/Scenes/Shardok.unity with copies from Assets/Gameplay.unity. " +
"References that belong to other split scenes will be cleared so they " +
"can be wired at runtime.",
"Rebuild",
"Cancel")) {
Debug.Log("Eagle/Shardok scene rebuild canceled.");
return;
}
var gameplayScene = EditorSceneManager.OpenScene(GameplayScenePath, OpenSceneMode.Single);
var copiedEagleCanvas = RebuildSceneObjectFromGameplay(
gameplayScene,
EagleScenePath,
EagleCanvasName,
EagleCanvasName,
EaglePlaceholderCanvasName,
EagleCanvasName);
if (copiedEagleCanvas == null) return;
ClearEagleSceneExternalReferences(copiedEagleCanvas);
int clearedEagleReferences =
ClearExternalSceneReferences(copiedEagleCanvas, EagleScenePath);
var eagleScene = SceneManager.GetSceneByPath(EagleScenePath);
EditorSceneManager.MarkSceneDirty(eagleScene);
EditorSceneManager.SaveScene(eagleScene);
var copiedShardokContainer = RebuildSceneObjectFromGameplay(
gameplayScene,
ShardokScenePath,
ShardokContainerName,
ShardokContainerName,
ShardokPlaceholderCanvasName,
ShardokContainerName);
if (copiedShardokContainer == null) return;
ClearShardokSceneExternalReferences(copiedShardokContainer);
int clearedShardokReferences =
ClearExternalSceneReferences(copiedShardokContainer, ShardokScenePath);
var shardokScene = SceneManager.GetSceneByPath(ShardokScenePath);
EditorSceneManager.MarkSceneDirty(shardokScene);
EditorSceneManager.SaveScene(shardokScene);
EditorSceneManager.OpenScene(EagleScenePath, OpenSceneMode.Single);
Debug.Log(
$"Rebuilt {EagleScenePath} and {ShardokScenePath} from {GameplayScenePath}. " +
"EagleGameController.errorHandler, EagleGameController.shardokContainer, and " +
"ShardokGameController.eagleCanvas were cleared for runtime scene wiring. " +
$"Cleared {clearedEagleReferences} additional Eagle scene references and " +
$"{clearedShardokReferences} additional Shardok scene references.");
}
[MenuItem("Eagle0/Scenes/Rebuild Shared Error UI From Gameplay")]
public static void RebuildSharedErrorUIFromGameplay() {
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) {
Debug.Log("Shared error UI rebuild canceled because modified scenes were not saved.");
return;
}
if (!EditorUtility.DisplayDialog(
"Rebuild Shared Error UI",
"This will replace the ErrorCanvas in Assets/Scenes/Shared.unity with a copy " +
"from Assets/Gameplay.unity.",
"Rebuild",
"Cancel")) {
Debug.Log("Shared error UI rebuild canceled.");
return;
}
var gameplayScene = EditorSceneManager.OpenScene(GameplayScenePath, OpenSceneMode.Single);
var sourceErrorCanvas = FindRootByName(gameplayScene, ErrorCanvasName);
if (sourceErrorCanvas == null) {
Debug.LogError(
$"Could not find root GameObject '{ErrorCanvasName}' in {GameplayScenePath}.");
return;
}
var sharedScene = EditorSceneManager.OpenScene(SharedScenePath, OpenSceneMode.Additive);
foreach (var existingErrorCanvas in FindRootsByName(sharedScene, ErrorCanvasName)) {
Object.DestroyImmediate(existingErrorCanvas);
}
var copiedErrorCanvas = Object.Instantiate(sourceErrorCanvas);
copiedErrorCanvas.name = ErrorCanvasName;
ClearSharedErrorUIExternalReferences(copiedErrorCanvas);
SceneManager.MoveGameObjectToScene(copiedErrorCanvas, sharedScene);
EditorSceneManager.MarkSceneDirty(sharedScene);
EditorSceneManager.SaveScene(sharedScene);
EditorSceneManager.OpenScene(SharedScenePath, OpenSceneMode.Single);
Debug.Log($"Rebuilt {SharedScenePath} {ErrorCanvasName} from {GameplayScenePath}.");
}
private static GameObject RebuildSceneObjectFromGameplay(
Scene gameplayScene,
string targetScenePath,
string sourceObjectName,
string copiedObjectName,
params string[] objectNamesToReplace) {
var sourceObject = FindObjectByName(gameplayScene, sourceObjectName);
if (sourceObject == null) {
Debug.LogError(
$"Could not find GameObject '{sourceObjectName}' in {GameplayScenePath}.");
return null;
}
var targetScene = EditorSceneManager.OpenScene(targetScenePath, OpenSceneMode.Additive);
foreach (var objectNameToReplace in objectNamesToReplace) {
var existingObjects = FindObjectsByName(targetScene, objectNameToReplace).ToList();
foreach (var existingObject in existingObjects) {
Object.DestroyImmediate(existingObject);
}
}
var copiedObject = Object.Instantiate(sourceObject);
copiedObject.name = copiedObjectName;
SceneManager.MoveGameObjectToScene(copiedObject, targetScene);
return copiedObject;
}
private static void ClearEagleSceneExternalReferences(GameObject eagleCanvas) {
var eagleController = eagleCanvas.GetComponent<eagle.EagleGameController>();
if (eagleController == null) return;
eagleController.errorHandler = null;
eagleController.shardokContainer = null;
EditorUtility.SetDirty(eagleController);
}
private static void ClearShardokSceneExternalReferences(GameObject shardokContainer) {
var shardokController = shardokContainer.GetComponent<Shardok.ShardokGameController>();
if (shardokController == null) return;
shardokController.eagleCanvas = null;
EditorUtility.SetDirty(shardokController);
}
private static int ClearExternalSceneReferences(GameObject sceneRoot, string targetScenePath) {
int clearedReferences = 0;
foreach (var component in sceneRoot.GetComponentsInChildren<Component>(
includeInactive: true)) {
if (component == null) continue;
var serializedObject = new SerializedObject(component);
var property = serializedObject.GetIterator();
bool modified = false;
while (property.NextVisible(enterChildren: true)) {
if (property.propertyType != SerializedPropertyType.ObjectReference) continue;
if (property.objectReferenceValue == null) continue;
if (!ReferencesDifferentScene(property.objectReferenceValue, targetScenePath))
continue;
property.objectReferenceValue = null;
clearedReferences++;
modified = true;
}
if (modified) {
serializedObject.ApplyModifiedPropertiesWithoutUndo();
EditorUtility.SetDirty(component);
}
}
return clearedReferences;
}
private static bool ReferencesDifferentScene(Object referencedObject, string targetScenePath) {
var referencedGameObject = GetReferencedGameObject(referencedObject);
if (referencedGameObject == null) return false;
var referencedScene = referencedGameObject.scene;
return referencedScene.IsValid() && !string.IsNullOrEmpty(referencedScene.path) &&
referencedScene.path != targetScenePath;
}
private static void ClearSharedErrorUIExternalReferences(GameObject errorCanvas) {
var errorHandler = errorCanvas.GetComponent<ErrorHandler>();
if (errorHandler == null) { return; }
errorHandler.bugReportPanel = null;
EditorUtility.SetDirty(errorHandler);
}
[MenuItem("Eagle0/Scenes/Validate Connection Scene Readiness")]
public static void ValidateConnectionSceneReadiness() {
if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) {
Debug.Log(
"Connection scene validation canceled because modified scenes were not saved.");
return;
}
var connectionScene =
EditorSceneManager.OpenScene(ConnectionScenePath, OpenSceneMode.Single);
var connectionCanvases = FindRootsByName(connectionScene, ConnectionCanvasName).ToList();
if (connectionCanvases.Count != 1) {
Debug.LogError(
$"{ConnectionScenePath} should contain exactly one root named " +
$"{ConnectionCanvasName}, but found {connectionCanvases.Count}.");
return;
}
var connectionCanvas = connectionCanvases[0];
var readiness = connectionCanvas.GetComponent<ConnectionSceneReadiness>();
if (readiness == null) {
Debug.LogError($"{ConnectionCanvasName} is missing ConnectionSceneReadiness.");
return;
}
if (readiness.IsReady(out var reason)) {
var handler = connectionCanvas.GetComponentInChildren<ConnectionHandler>(
includeInactive: true);
if (handler == null) {
Debug.LogError(
$"{ConnectionScenePath} is marked ready but {ConnectionCanvasName} " +
"does not contain a ConnectionHandler.");
return;
}
Debug.Log($"{ConnectionScenePath} is marked production-ready.");
LogConnectionHandlerReferenceReport(connectionCanvas);
return;
}
if (connectionCanvas.activeSelf) {
Debug.LogWarning(
$"{ConnectionCanvasName} is active while the scene is not production-ready. " +
"Bootstrap will still fall back, but the scene should stay inert until migration " +
"is complete.");
}
Debug.Log($"{ConnectionScenePath} is not production-ready: {reason}");
LogConnectionHandlerReferenceReport(connectionCanvas);
}
private static GameObject FindRootByName(Scene scene, string rootName) {
return FindRootsByName(scene, rootName).FirstOrDefault();
}
private static IEnumerable<GameObject> FindRootsByName(Scene scene, string rootName) {
return scene.GetRootGameObjects().Where(root => root.name == rootName);
}
private static GameObject FindObjectByName(Scene scene, string objectName) {
return FindObjectsByName(scene, objectName).FirstOrDefault();
}
private static IEnumerable<GameObject> FindObjectsByName(Scene scene, string objectName) {
return scene.GetRootGameObjects()
.SelectMany(root => root.GetComponentsInChildren<Transform>(includeInactive: true))
.Select(transform => transform.gameObject)
.Where(gameObject => gameObject.name == objectName);
}
private static void MarkSceneNotReady(GameObject connectionCanvas) {
var readiness = connectionCanvas.GetComponent<ConnectionSceneReadiness>();
if (readiness == null)
readiness = connectionCanvas.AddComponent<ConnectionSceneReadiness>();
var serializedReadiness = new SerializedObject(readiness);
serializedReadiness.FindProperty("productionReady").boolValue = false;
serializedReadiness.FindProperty("incompleteReason").stringValue =
CopiedSceneIncompleteReason;
serializedReadiness.ApplyModifiedPropertiesWithoutUndo();
}
private static void LogConnectionHandlerReferenceReport(GameObject connectionCanvas) {
var handler =
connectionCanvas.GetComponentInChildren<ConnectionHandler>(includeInactive: true);
if (handler == null) {
Debug.LogWarning($"{ConnectionCanvasName} does not contain a ConnectionHandler yet.");
return;
}
LogReferenceSceneWarning(handler.eagleCanvas, nameof(handler.eagleCanvas));
LogReferenceSceneWarning(handler.shardokContainer, nameof(handler.shardokContainer));
LogReferenceSceneWarning(handler.errorHandler, nameof(handler.errorHandler));
}
private static void LogReferenceSceneWarning(Object referencedObject, string fieldName) {
if (referencedObject == null) {
Debug.LogWarning($"ConnectionHandler.{fieldName} is not assigned.");
return;
}
var referencedGameObject = GetReferencedGameObject(referencedObject);
if (referencedGameObject == null) return;
var scene = referencedGameObject.scene;
if (scene.IsValid() && scene.path != ConnectionScenePath) {
Debug.LogWarning(
$"ConnectionHandler.{fieldName} still references '{referencedGameObject.name}' " +
$"from {scene.path}. Move or replace this dependency before marking " +
"Connection production-ready.");
}
}
private static GameObject GetReferencedGameObject(Object referencedObject) {
if (referencedObject is GameObject gameObject) return gameObject;
if (referencedObject is Component component) return component.gameObject;
return null;
}
}
@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Linq;
using common.GUIUtils;
using NUnit.Framework;
using Shardok;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace eagle0.Tests {
[TestFixture]
public class SceneSplitValidationTests {
private const string MainScenePath = "Assets/Scenes/Main.unity";
private const string ConnectionScenePath = "Assets/Scenes/Connection.unity";
private const string EagleScenePath = "Assets/Scenes/Eagle.unity";
private const string ShardokScenePath = "Assets/Scenes/Shardok.unity";
private const string SharedScenePath = "Assets/Scenes/Shared.unity";
[Test]
public void BuildSettings_UseSplitScenesOnly() {
var enabledScenes = EditorBuildSettings.scenes.Where(scene => scene.enabled)
.Select(scene => scene.path)
.ToList();
CollectionAssert.AreEqual(
new[] {
MainScenePath,
ConnectionScenePath,
EagleScenePath,
ShardokScenePath,
SharedScenePath,
},
enabledScenes);
CollectionAssert.DoesNotContain(enabledScenes, "Assets/Gameplay.unity");
}
[Test]
public void SplitSceneAssetsExist() {
foreach (var scenePath in SplitScenePaths()) {
Assert.NotNull(
AssetDatabase.LoadAssetAtPath<SceneAsset>(scenePath),
$"{scenePath} must exist.");
}
Assert.IsFalse(
AssetDatabase.IsValidFolder("Assets/Gameplay"),
"Retired Gameplay scene assets should not remain in Assets/Gameplay.");
}
[Test]
public void MainSceneContainsBootstrapInfrastructure() {
var scene = OpenScene(MainScenePath);
AssertHasComponent<BootstrapManager>(scene);
AssertHasComponent<GameSceneManager>(scene);
}
[Test]
public void ConnectionSceneContainsConnectionController() {
var scene = OpenScene(ConnectionScenePath);
var connectionRoot = AssertRoot(scene, "ConnectionCanvas");
Assert.NotNull(
connectionRoot.GetComponent<ConnectionHandler>(),
"ConnectionCanvas must contain ConnectionHandler.");
}
[Test]
public void EagleSceneContainsEagleController() {
var scene = OpenScene(EagleScenePath);
var eagleRoot = AssertRoot(scene, "Eagle Canvas");
Assert.NotNull(
eagleRoot.GetComponent<eagle.EagleGameController>(),
"Eagle Canvas must contain EagleGameController.");
}
[Test]
public void ShardokSceneContainsShardokController() {
var scene = OpenScene(ShardokScenePath);
var shardokRoot = AssertRoot(scene, "Shardok Container");
Assert.NotNull(
shardokRoot.GetComponent<ShardokGameController>(),
"Shardok Container must contain ShardokGameController.");
}
[Test]
public void SharedSceneContainsSharedUiInfrastructure() {
var scene = OpenScene(SharedScenePath);
AssertHasComponent<SettingsPanelController>(scene);
AssertHasComponent<ErrorHandler>(scene);
}
private static IEnumerable<string> SplitScenePaths() {
yield return MainScenePath;
yield return ConnectionScenePath;
yield return EagleScenePath;
yield return ShardokScenePath;
yield return SharedScenePath;
}
private static Scene OpenScene(string scenePath) {
return EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
}
private static GameObject AssertRoot(Scene scene, string rootName) {
var root = scene.GetRootGameObjects().FirstOrDefault(obj => obj.name == rootName);
Assert.NotNull(root, $"{scene.path} must contain root object '{rootName}'.");
return root;
}
private static void AssertHasComponent<T>(Scene scene)
where T : Component {
var component = Resources.FindObjectsOfTypeAll<T>().FirstOrDefault(
item => item.gameObject.scene == scene);
Assert.NotNull(component, $"{scene.path} must contain {typeof(T).Name}.");
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9f3e0bc72ed64160b35d557710f32f35
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using TMPro;
using UnityEditor;
using UnityEngine;
public static class TmpFallbackFontTool {
private const string FallbackFontAssetPath =
"Assets/TextMesh Pro/Resources/Fonts & Materials/LiberationSans SDF - Fallback.asset";
[MenuItem("Eagle0/Fonts/Bake TMP Fallback Font")]
public static void BakeFallbackFont() {
var fontAsset = AssetDatabase.LoadAssetAtPath<TMP_FontAsset>(FallbackFontAssetPath);
if (fontAsset == null) {
throw new InvalidOperationException(
$"Could not load TMP fallback font at {FallbackFontAssetPath}");
}
var unicodes = CuratedFallbackUnicodes();
var characters = UnicodeString(unicodes);
fontAsset.atlasPopulationMode = AtlasPopulationMode.Dynamic;
fontAsset.TryAddCharacters(unicodes, out _);
fontAsset.atlasPopulationMode = AtlasPopulationMode.Static;
EditorUtility.SetDirty(fontAsset);
AssetDatabase.SaveAssets();
if (fontAsset.HasCharacters(characters, out uint[] missingUnicodes)) {
Debug.Log(
$"Baked {unicodes.Length} curated glyphs into {FallbackFontAssetPath} and set it to Static.");
return;
}
Debug.LogWarning(
$"Baked TMP fallback font, but the source font does not contain {missingUnicodes.Length} curated glyphs: " +
String.Join(", ", missingUnicodes.Select(DescribeUnicode)));
}
private static uint[] CuratedFallbackUnicodes() {
var unicodes = new SortedSet<uint>();
AddRange(unicodes, 0x0020, 0x007E); // Basic Latin, printable ASCII
AddRange(unicodes, 0x00A0, 0x00FF); // Latin-1 Supplement
AddRange(unicodes, 0x0100, 0x017F); // Latin Extended-A
AddRange(unicodes, 0x0180, 0x024F); // Latin Extended-B
uint[] commonSymbols = {
0x2013, // EN DASH
0x2014, // EM DASH
0x2018, // LEFT SINGLE QUOTATION MARK
0x2019, // RIGHT SINGLE QUOTATION MARK
0x201C, // LEFT DOUBLE QUOTATION MARK
0x201D, // RIGHT DOUBLE QUOTATION MARK
0x2020, // DAGGER
0x2021, // DOUBLE DAGGER
0x2022, // BULLET
0x2026, // HORIZONTAL ELLIPSIS
0x2030, // PER MILLE SIGN
0x2032, // PRIME
0x2033, // DOUBLE PRIME
0x2039, // SINGLE LEFT-POINTING ANGLE QUOTATION MARK
0x203A, // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
0x2044, // FRACTION SLASH
0x20AC, // EURO SIGN
0x2122, // TRADE MARK SIGN
0x2190, // LEFTWARDS ARROW
0x2191, // UPWARDS ARROW
0x2192, // RIGHTWARDS ARROW
0x2193, // DOWNWARDS ARROW
0x2194, // LEFT RIGHT ARROW
0x2195, // UP DOWN ARROW
0x2211, // N-ARY SUMMATION
0x2212, // MINUS SIGN
0x221A, // SQUARE ROOT
0x221E, // INFINITY
0x221F, // RIGHT ANGLE
0x2229, // INTERSECTION
0x222B, // INTEGRAL
0x2248, // ALMOST EQUAL TO
0x2260, // NOT EQUAL TO
0x2261, // IDENTICAL TO
0x2264, // LESS-THAN OR EQUAL TO
0x2265, // GREATER-THAN OR EQUAL TO
0x25A0, // BLACK SQUARE
0x25A1, // WHITE SQUARE
0x25B2, // BLACK UP-POINTING TRIANGLE
0x25BA, // BLACK RIGHT-POINTING POINTER
0x25BC, // BLACK DOWN-POINTING TRIANGLE
0x25C4, // BLACK LEFT-POINTING POINTER
0x25CA, // LOZENGE
0x25CB, // WHITE CIRCLE
0x25CF, // BLACK CIRCLE
0x25E6, // WHITE BULLET
};
foreach (var unicode in commonSymbols) { unicodes.Add(unicode); }
return unicodes.ToArray();
}
private static void AddRange(ISet<uint> unicodes, uint first, uint last) {
for (var unicode = first; unicode <= last; unicode++) { unicodes.Add(unicode); }
}
private static string UnicodeString(IEnumerable<uint> unicodes) {
var builder = new StringBuilder();
foreach (var unicode in unicodes) { builder.Append(Char.ConvertFromUtf32((int)unicode)); }
return builder.ToString();
}
private static string DescribeUnicode(uint unicode) {
return $"U+{unicode:X4} '{Char.ConvertFromUtf32((int)unicode)}'";
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 97718fa2c55143a6b9c09a0eba312194
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -22,9 +22,9 @@ public class CtrPanel : MonoBehaviour {
void Update() {
if (panels.Count <= 0 || !isReady) return;
if (Input.GetKeyDown(KeyCode.LeftArrow)) {
if (GameInput.LeftArrowPressed()) {
Click_Prev();
} else if (Input.GetKeyDown(KeyCode.RightArrow)) {
} else if (GameInput.RightArrowPressed()) {
Click_Next();
}
}
File diff suppressed because it is too large Load Diff
@@ -1,9 +0,0 @@
fileFormatVersion: 2
guid: 265b489d182ad614e940fb7e18012b0d
timeCreated: 1513468984
licenseType: Free
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,117 +0,0 @@
fileFormatVersion: 2
guid: 5f8740f39f9a94ca2b5870243dead5a8
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 1
seamlessCubemap: 1
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 2
aniso: 0
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 1
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 0
spriteTessellationDetail: -1
textureType: 0
textureShape: 2
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 4
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 100
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 4
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
customData:
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spriteCustomMetadata:
entries: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:
@@ -46,7 +46,7 @@ public class HoveringTooltip : MonoBehaviour {
// ItemRect is in screen pixels. Position the tooltip to the right of the item.
Vector2 screenPos;
if (HugsCursor) {
screenPos = (Vector2)Input.mousePosition + new Vector2(15, -10);
screenPos = GameInput.PointerPosition() + new Vector2(15, -10);
} else {
screenPos = new Vector2(ItemRect.xMax + 10, ItemRect.center.y);
}
@@ -55,7 +55,7 @@ public class HoveringTooltip : MonoBehaviour {
float screenWidth = rectTransform.rect.width * canvas.scaleFactor;
if (screenPos.x + screenWidth > Screen.width) {
if (HugsCursor) {
screenPos.x = Input.mousePosition.x - screenWidth - 15;
screenPos.x = GameInput.PointerPosition().x - screenWidth - 15;
} else {
screenPos.x = ItemRect.xMin - screenWidth - 10;
}
@@ -16,7 +16,23 @@ public class MainQueue : MonoBehaviour {
private MainQueue() {}
void Awake() {
if (__singletonInstance == null) { __singletonInstance = this; }
if (__singletonInstance == null) {
__singletonInstance = this;
DontDestroyOnLoad(gameObject);
return;
}
if (__singletonInstance != this) {
if (IsDedicatedMainQueueObject()) {
Destroy(gameObject);
} else {
Destroy(this);
}
}
}
private bool IsDedicatedMainQueueObject() {
return gameObject.name == "Main Queue" && GetComponents<Component>().Length == 2;
}
// Update is called once per frame
@@ -76,5 +92,20 @@ public class MainQueue : MonoBehaviour {
}
}
void OnDestroy() { __singletonInstance = null; }
public static MainQueue EnsureExists() {
if (__singletonInstance != null) { return __singletonInstance; }
var existingMainQueue = FindAnyObjectByType<MainQueue>(FindObjectsInactive.Include);
if (existingMainQueue != null) {
__singletonInstance = existingMainQueue;
return __singletonInstance;
}
var mainQueueObject = new GameObject("Main Queue");
return mainQueueObject.AddComponent<MainQueue>();
}
void OnDestroy() {
if (__singletonInstance == this) { __singletonInstance = null; }
}
}
@@ -5,6 +5,7 @@ using UnityEngine.EventSystems;
public interface IClickReceiver {
HexGrid HexGrid { get; }
bool CanReceiveGridInput { get; }
void HandleLongHover(int? gridIndex, Vector3 mousePosition);
void HandleLongUIHover(IEnumerable<GameObject> hoveredObjects);
void HandlePrimaryClick(int clickedIndex);
@@ -49,22 +50,26 @@ public class MouseHandler : MonoBehaviour {
}
void HandleTouchLongPress() {
// Handle touch input for long-press detection
if (Input.touchCount > 0) {
Touch touch = Input.GetTouch(0);
if (clickReceiver?.CanReceiveGridInput != true) { return; }
if (touch.phase == TouchPhase.Began) {
// Handle touch input for long-press detection
if (GameInput.TouchCount() > 0) {
var touch = GameInput.GetTouch(0);
if (touch.Phase == TouchPhase.Began) {
// Touch started - begin tracking
if (!EventSystem.current.IsPointerOverGameObject(touch.fingerId)) {
var touchDownGridIndex = currentGridIndex(touch.Position);
if (touchDownGridIndex.HasValue ||
!IsPointerOverBlockingUI(touch.FingerId, touch.Position)) {
_touchDownTime = Time.time;
_touchDownPosition = touch.position;
_touchDownGridIndex = currentGridIndex();
_touchDownPosition = touch.Position;
_touchDownGridIndex = touchDownGridIndex;
_longPressTriggered = false;
}
} else if (touch.phase == TouchPhase.Moved || touch.phase == TouchPhase.Stationary) {
} else if (touch.Phase == TouchPhase.Moved || touch.Phase == TouchPhase.Stationary) {
// Touch ongoing - check for long-press
if (_touchDownTime.HasValue && !_longPressTriggered) {
var moved = Vector2.Distance(touch.position, _touchDownPosition);
var moved = Vector2.Distance(touch.Position, _touchDownPosition);
if (moved > MovementTolerancePixels) {
// Finger moved too much, cancel long-press
_touchDownTime = null;
@@ -76,20 +81,20 @@ public class MouseHandler : MonoBehaviour {
}
}
}
} else if (touch.phase == TouchPhase.Ended || touch.phase == TouchPhase.Canceled) {
} else if (touch.Phase == TouchPhase.Ended || touch.Phase == TouchPhase.Canceled) {
// Touch ended
_touchDownTime = null;
_touchDownGridIndex = null;
}
} else {
_touchDownTime = null;
_touchDownGridIndex = null;
}
}
private List<RaycastResult> RaycastMouse() {
PointerEventData pointerData = new PointerEventData(EventSystem.current) {
pointerId = -1,
};
pointerData.position = Input.mousePosition;
private List<RaycastResult> RaycastPointer(Vector2 position, int pointerId) {
PointerEventData pointerData = new PointerEventData(
EventSystem.current) { pointerId = pointerId, position = position };
List<RaycastResult> results = new List<RaycastResult>();
EventSystem.current.RaycastAll(pointerData, results);
@@ -97,19 +102,38 @@ public class MouseHandler : MonoBehaviour {
return results;
}
private List<RaycastResult> RaycastMouse() {
return RaycastPointer(GameInput.PointerPosition(), -1);
}
private bool IsPointerOverBlockingUI(int pointerId, Vector2 position) {
if (EventSystem.current == null || clickReceiver?.HexGrid == null) { return false; }
var raycastResults = RaycastPointer(position, pointerId);
return raycastResults.Any(
raycastResult =>
!clickReceiver.HexGrid.ContainsMapVisual(raycastResult.gameObject));
}
void HandleMouse() {
if (clickReceiver.HexGrid == null) { return; }
if (clickReceiver?.CanReceiveGridInput != true) { return; }
int? possibleGridIndex = currentGridIndex();
bool pointerOverBlockingUI = IsPointerOverBlockingUI(-1, GameInput.PointerPosition());
Vector3 currentPosition = Input.mousePosition;
Vector3 currentPosition = GameInput.PointerPosition();
if (currentPosition == hoverPosition) {
if (Time.time - hoverStartSeconds > HOVER_THRESHOLD_SECONDS) {
isInLongHover = true;
if (possibleGridIndex.HasValue) {
clickReceiver.HandleLongHover(possibleGridIndex.Value, currentPosition);
} else if (EventSystem.current.IsPointerOverGameObject()) {
var raycastResults = RaycastMouse();
} else if (pointerOverBlockingUI) {
var raycastResults =
RaycastMouse()
.Where(raycastResult =>
!clickReceiver.HexGrid.ContainsMapVisual(
raycastResult.gameObject))
.ToList();
var gameObjects =
raycastResults.Select(raycastResult => raycastResult.gameObject);
if (raycastResults.Count > 0) { clickReceiver.HandleLongUIHover(gameObjects); }
@@ -126,7 +150,7 @@ public class MouseHandler : MonoBehaviour {
}
}
if (!possibleGridIndex.HasValue || EventSystem.current.IsPointerOverGameObject()) {
if (!possibleGridIndex.HasValue) {
if (lastHoveredIndex is int index) {
clickReceiver.HandleStopHover(index);
lastHoveredIndex = null;
@@ -139,9 +163,9 @@ public class MouseHandler : MonoBehaviour {
clickReceiver.HandleStopHover(lhIndex);
lastHoveredIndex = null;
}
if (Input.GetMouseButton(0)) {
if (GameInput.PrimaryPointerPressed()) {
handleLeftMouseDown(currentMouseGridIndex);
} else if (Input.GetMouseButton(1)) {
} else if (GameInput.SecondaryPointerPressed()) {
handleRightMouseDown(currentMouseGridIndex);
} else {
if (leftButtonDownIndex.HasValue) {
@@ -170,13 +194,6 @@ public class MouseHandler : MonoBehaviour {
}
void handleLeftMouseDown(int gridIndex) {
if (EventSystem.current.IsPointerOverGameObject()) {
leftButtonDownIndex = null;
lastConveyedIndex = null;
return;
}
if (!leftButtonDownIndex.HasValue) { leftButtonDownIndex = gridIndex; }
if (gridIndex != lastConveyedIndex) {
lastConveyedIndex = gridIndex;
@@ -185,13 +202,6 @@ public class MouseHandler : MonoBehaviour {
}
void handleRightMouseDown(int gridIndex) {
if (EventSystem.current.IsPointerOverGameObject()) {
rightButtonDownIndex = null;
lastRightConveyedIndex = null;
return;
}
if (!rightButtonDownIndex.HasValue) { rightButtonDownIndex = gridIndex; }
if (gridIndex != lastRightConveyedIndex) {
lastRightConveyedIndex = gridIndex;
@@ -199,7 +209,9 @@ public class MouseHandler : MonoBehaviour {
}
}
int? currentGridIndex() {
return clickReceiver.HexGrid.GridIndexForScreenPoint(Input.mousePosition);
int? currentGridIndex() { return currentGridIndex(GameInput.PointerPosition()); }
int? currentGridIndex(Vector2 screenPosition) {
return clickReceiver.HexGrid.GridIndexForScreenPoint(screenPosition);
}
}
@@ -1,113 +1,23 @@
# Next Steps for Scene Separation
# Scene Split Status
## Current Status
- Created Main.unity with GameSceneManager
- Created empty scenes: Connection.unity, Eagle.unity, Shardok.unity, Shared.unity
- Created BootstrapManager for transitional loading
- Created SceneMigrationHelper for analysis
- Basic scene loading system works
## Current Status
## Immediate Next Steps
- `Main.unity` is the startup scene.
- `BootstrapManager` starts the client by loading `Shared.unity` and `Connection.unity`.
- `Connection.unity`, `Eagle.unity`, `Shardok.unity`, and `Shared.unity` are included in build settings.
- Runtime scene references are wired by `GameSceneManager` after additive scene loads.
### 1. Add New Scenes to Build Settings
In Unity:
1. File → Build Settings
2. Add the new scenes to the build:
- Assets/Scenes/Eagle.unity
- Assets/Scenes/Shardok.unity
- Assets/Scenes/Shared.unity
3. Ensure they're all checked for inclusion
## Follow-Up Validation
### 2. Test Scene Loading System
1. In Main.unity, add a BootstrapManager component to the SceneManager GameObject
2. In the BootstrapManager inspector:
- Set `testSceneLoading` to true
- Set `useOriginalGameplayScene` to false
3. Play the scene and check Console
4. Should see: "✓ Connection scene loaded", "✓ Eagle scene loaded", etc.
Keep playtesting the scene flow through connection, Eagle, custom battles, Shardok battles, and return-to-lobby.
### 3. Test with Original Gameplay Scene
1. In BootstrapManager inspector:
- Set `testSceneLoading` to false
- Set `useOriginalGameplayScene` to true
2. Add Gameplay.unity to Build Settings if not already there
3. Play the scene
4. Should load both Main.unity and Gameplay.unity together
5. The game should work exactly as before
## Validation Checklist
## Migration Strategy (Once Testing Works)
### Phase 1: Move Error/Popup UI (Safest)
These are the least connected components:
1. Find ErrorCanvas in Gameplay.unity hierarchy
2. Right-click → Copy
3. Open Shared.unity
4. Right-click in hierarchy → Paste
5. Save Shared.unity
6. Test that error dialogs still work
7. Delete ErrorCanvas from Gameplay.unity
### Phase 2: Move Connection UI
1. Find ConnectionCanvas in Gameplay.unity
2. Move Connection Panel, Game Selection Panel, Custom Battle Panel
3. Update ConnectionHandler to work with scene loading
### Phase 3: Move Eagle UI
1. Move EagleCanvas and related UI
2. Update EagleGameController
### Phase 4: Move Shardok UI
1. Move ShardokCanvas and battle UI
2. Update battle controllers
## Tools Created to Help
### SceneMigrationHelper
- Attach to any GameObject in Gameplay.unity
- Right-click component → "Analyze Scene Structure"
- Shows which GameObjects belong to which scene
- Right-click → "Find Cross-Scene References" to see dependencies
### BootstrapManager
- Handles loading original scene OR new scene system
- Has testing mode to verify all scenes load correctly
- Provides smooth transition during migration
### GameSceneManager
- Handles async scene loading/unloading
- Prevents duplicate scene loads
- Provides transition methods for different game states
## Safety Features
1. **Gradual Migration**: Move one component at a time
2. **Fallback Option**: Can always switch back to original Gameplay.unity
3. **Testing**: Each step can be verified independently
4. **No Data Loss**: Original scene preserved until migration complete
## Expected Benefits Once Complete
- **Faster Unity startup** (scenes load quickly)
- **Better collaboration** (less merge conflicts)
- **Easier debugging** (isolate UI issues to specific scenes)
- **Memory efficiency** (only load needed UI)
- **Cleaner organization** (logical separation of concerns)
## Troubleshooting
### If Scene Loading Fails:
1. Check all scenes are in Build Settings
2. Check Console for specific errors
3. Ensure no duplicate EventSystems (should be only in Shared.unity)
### If Game Functionality Breaks:
1. Switch BootstrapManager back to `useOriginalGameplayScene = true`
2. This loads the full original scene as before
3. No functionality should be lost
### If References Break:
1. Use SceneMigrationHelper to find cross-scene dependencies
2. Consider using ScriptableObjects or singletons for cross-scene communication
3. Update scripts to find objects by type instead of direct reference
This approach ensures we can safely transition without breaking the working game!
- Start from `Main.unity`.
- Connect with a stored account and with a fresh sign-in.
- List running games and enter an Eagle game.
- Open and close Settings with the button and Escape.
- Enter Shardok from Eagle and return to Eagle.
- Start a custom battle and exit back to the lobby.
- Confirm no duplicate `EventSystem` or `AudioListener` warnings.
- Confirm CI Unity build passes.
@@ -1,159 +0,0 @@
# Scene Separation Plan
## Current Situation
- Single massive Gameplay.unity scene (7.5MB, 255k lines)
- Contains ALL game UI, connection screens, battle UI, etc.
- High risk of corruption when editing
## Proposed Multi-Scene Architecture
### Core Scenes (Always Loaded)
1. **Main.unity** - Minimal bootstrap scene
- Scene manager
- Persistent game systems
- Audio manager
- Network connection manager
### Loadable Scenes
2. **Connection.unity** - Login and lobby UI
- ConnectionCanvas
- Connection Panel
- Game Selection Panel
- Custom Battle Panel
3. **Eagle.unity** - Strategic game UI
- Eagle Canvas
- Map Canvas
- Command Canvas
- Heroes and Battalions UI
4. **Shardok.unity** - Tactical battle UI
- Shardok Canvas
- Battle grid
- Unit placement UI
- Combat UI
5. **Shared.unity** - Common UI elements
- Error Canvas
- Popup Canvas
- Confirmation dialogs
- Overlay canvases
## Scene Loading Strategy
```csharp
// Example scene loading flow
Main.unity (always loaded)
Connection.unity (additive load)
↓ (on game select)
Unload Connection.unity
Load Eagle.unity + Shared.unity (additive)
↓ (on battle start)
Load Shardok.unity (additive)
↓ (on battle end)
Unload Shardok.unity
```
## Implementation Steps
### Phase 1: Create Scene Structure
1. Create new empty scenes
2. Set up basic scene loading system
3. Test scene transitions work
### Phase 2: Move Non-Critical Elements
1. Start with Shared UI elements (popups, errors)
2. These are least likely to break core gameplay
3. Test thoroughly
### Phase 3: Move Connection UI
1. Connection/lobby UI is self-contained
2. Only needs to communicate via ConnectionHandler
3. Natural separation point
### Phase 4: Separate Eagle and Shardok
1. These are already somewhat separated
2. Different canvases for each mode
3. Can be loaded/unloaded as needed
## Benefits
1. **Reduced Complexity**: Each scene is manageable size
2. **Parallel Development**: Multiple developers can work on different scenes
3. **Memory Efficiency**: Only load what's needed
4. **Faster Load Times**: Smaller scenes load faster
5. **Version Control**: Fewer merge conflicts
6. **Easier Debugging**: Isolate issues to specific scenes
## Safe Migration Process
### Step 1: Create SceneManager
```csharp
public class GameSceneManager : MonoBehaviour
{
public static GameSceneManager Instance;
void Awake()
{
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
}
public async Task LoadSceneAdditive(string sceneName)
{
await SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
}
public async Task UnloadScene(string sceneName)
{
await SceneManager.UnloadSceneAsync(sceneName);
}
}
```
### Step 2: Test with Empty Scenes First
1. Create Main.unity with just SceneManager
2. Create empty Connection.unity
3. Test loading Connection additively
4. Test unloading Connection
5. Verify no crashes or issues
### Step 3: Move One Component at a Time
1. Move ErrorCanvas to Shared.unity
2. Test it still works
3. Move next component
4. Repeat until all moved
## Scene Settings
Each scene should have:
- Appropriate lighting settings
- Required cameras (if any)
- Event systems (only one active)
- Canvas scalers configured
## Risks and Mitigations
| Risk | Mitigation |
|------|------------|
| References break between scenes | Use ScriptableObjects or singleton patterns |
| UI events don't work | Ensure EventSystem is present |
| Performance overhead | Profile scene loading, use async |
| Missing dependencies | Document all cross-scene dependencies |
## Testing Checklist
- [ ] Main.unity loads without errors
- [ ] Can load Connection.unity additively
- [ ] Can transition from Connection to Eagle
- [ ] Can start battle (load Shardok)
- [ ] Can end battle (unload Shardok)
- [ ] All UI elements visible and functional
- [ ] No memory leaks from scene loading/unloading
- [ ] Build succeeds with multi-scene setup
This approach is MUCH safer than trying to extract prefabs from a massive scene file!
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: 5e0e3b2b2183b4e50bdb154c5a3c9b29
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -11151,29 +11151,13 @@ GameObject:
- component: {fileID: 1124833191}
- component: {fileID: 1124833190}
- component: {fileID: 1124833189}
- component: {fileID: 1124833188}
m_Layer: 5
m_Name: ConnectionCanvas
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
--- !u!114 &1124833188
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 1124833187}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 5f2c53e67d8043d492e8d3277c6e5794, type: 3}
m_Name:
m_EditorClassIdentifier: Assembly-CSharp::ConnectionSceneReadiness
productionReady: 0
incompleteReason: Connection scene was copied from Gameplay but still needs cross-scene
reference migration before it can replace Gameplay.
m_IsActive: 1
--- !u!114 &1124833189
MonoBehaviour:
m_ObjectHideFlags: 0
@@ -56483,7 +56483,7 @@ MonoBehaviour:
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 0.392}
m_Color: {r: 1, g: 1, b: 1, a: 0.5686275}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
@@ -57318,7 +57318,7 @@ GameObject:
- component: {fileID: 424602579}
- component: {fileID: 424602578}
m_Layer: 0
m_Name: Main Gameplay
m_Name: Eagle Play Area
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
@@ -1,5 +1,5 @@
fileFormatVersion: 2
guid: b736cb7c83db6417cabec00d0bba9b88
guid: 460d684f2d3b4e80bc80eb2d682c5fa6
folderAsset: yes
DefaultImporter:
externalObjects: {}
@@ -212,7 +212,7 @@ Camera:
m_Enabled: 1
serializedVersion: 2
m_ClearFlags: 2
m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0}
m_BackGroundColor: {r: 0.6471669, g: 0.7315526, b: 0.83823526, a: 0}
m_projectionMatrixMode: 1
m_GateFitMode: 2
m_FOVAxisMode: 0
@@ -265,10 +265,6 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: cb9eb60c28181495da4ef437c5e787e4, type: 3}
m_Name:
m_EditorClassIdentifier:
useNewSceneSystem: 1
useOriginalGameplayScene: 0
fallbackToOriginalGameplaySceneWhenSplitSceneIncomplete: 1
testSceneLoading: 0
--- !u!1660057539 &9223372036854775807
SceneRoots:
m_ObjectHideFlags: 0
@@ -1,86 +0,0 @@
# Scene Separation Implementation Guide
## Current Status
I've created the basic infrastructure for separating the massive Gameplay.unity into multiple scenes:
### Created Files:
1. **Main.unity** - Minimal bootstrap scene with SceneManager
2. **Connection.unity** - Empty scene for connection/lobby UI
3. **GameSceneManager.cs** - Scene loading/unloading manager
4. **SceneLoadTester.cs** - Test script to verify scene loading works
## Next Steps
### 1. Test Basic Scene Loading (Do This First!)
1. Open Unity
2. Open `Assets/Scenes/Main.unity`
3. Add the SceneLoadTester component to the SceneManager GameObject (optional, for testing)
4. Add both scenes to Build Settings:
- File → Build Settings
- Add Open Scenes (Main.unity)
- Add `Assets/Scenes/Connection.unity`
5. Play the scene and check console for any errors
### 2. If Scene Loading Works:
Create the remaining empty scenes:
- Eagle.unity
- Shardok.unity
- Shared.unity
### 3. Start Moving UI Components
**Safest Order:**
1. **ErrorCanvas** → Shared.unity (least dependencies)
2. **PopupCanvas** → Shared.unity
3. **ConnectionCanvas + panels** → Connection.unity
4. **Eagle UI** → Eagle.unity
5. **Shardok UI** → Shardok.unity
### 4. How to Move Components Safely
For each component:
1. In Gameplay.unity, find the GameObject
2. Right-click → Copy
3. Open target scene (e.g., Connection.unity)
4. Right-click in Hierarchy → Paste
5. Save the target scene
6. Test that everything still works
7. Only then delete from Gameplay.unity
### 5. Update ConnectionHandler
Once Connection UI is moved, update ConnectionHandler to work with scene loading:
```csharp
// Instead of:
connectionCanvas.gameObject.SetActive(true);
// Use:
await GameSceneManager.Instance.TransitionToConnection();
```
## Important Notes
1. **Always test after each move** - Don't move multiple components at once
2. **Keep Gameplay.unity as backup** - Don't delete it until everything works
3. **Check references** - Some scripts may reference GameObjects across scenes
4. **One EventSystem** - Ensure only one EventSystem is active across all loaded scenes
## Benefits Once Complete
- Faster Unity startup (smaller scenes)
- Easier collaboration (less merge conflicts)
- Better performance (load only what's needed)
- Cleaner organization
- Easier to debug specific UI sections
## If Something Goes Wrong
1. Check Build Settings - all scenes must be added
2. Check for missing references in Inspector
3. Ensure EventSystem exists in one of the loaded scenes
4. Check console for specific error messages
5. Can always revert to using original Gameplay.unity
This approach is MUCH safer than trying to edit the 7.5MB scene file directly!
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: a28b46a2ab5484cb5a6ea85229d2885d
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
File diff suppressed because it is too large Load Diff
@@ -11,8 +11,8 @@ public class SettingsKeyListener : MonoBehaviour {
void Start() { settingsPanelController.ApplyPersistedSettings(); }
void Update() {
if (Input.GetButtonUp("Cancel")) { settingsPanelController.HandleEscape(); }
if (GameInput.CancelReleased()) { settingsPanelController.HandleEscape(); }
if (Input.GetKeyDown(KeyCode.F11)) { settingsPanelController.HandleF11(); }
if (GameInput.F11Pressed()) { settingsPanelController.HandleF11(); }
}
}
@@ -70,64 +70,77 @@ public class SettingsPanelController : MonoBehaviour {
/// controls. Called early from SettingsKeyListener.Start() so settings take effect even while
/// the Settings Panel prefab is inactive.
/// </summary>
private bool _overridesMissing;
public void ApplyPersistedSettings() {
// Diagnostic: detect prefab overrides failing to apply on Windows builds.
// Log which fields are null and bail out so the game can still launch.
string missing = "";
if (soundEffectsSource == null) missing += " soundEffectsSource";
if (musicSource == null) missing += " musicSource";
if (hoveringTooltip == null) missing += " hoveringTooltip";
if (provinceLabelsController == null) missing += " provinceLabelsController";
if (missing.Length > 0) {
UnityEngine.Debug.LogError(
"ApplyPersistedSettings: null Inspector fields (prefab overrides missing?):" +
missing);
_overridesMissing = true;
return;
}
LogMissingRuntimeReferences();
soundEffectsSource.volume = PlayerPrefs.GetFloat(SoundEffectsVolumeKey, 0.5f);
musicSource.volume = PlayerPrefs.GetFloat(MusicVolumeKey, 0.5f);
if (soundEffectsSource != null) {
soundEffectsSource.volume = PlayerPrefs.GetFloat(SoundEffectsVolumeKey, 0.5f);
}
if (musicSource != null) {
musicSource.volume = PlayerPrefs.GetFloat(MusicVolumeKey, 0.5f);
}
AmbientVolume = PlayerPrefs.GetFloat(AmbientVolumeKey, 0.5f);
hoveringTooltip.HugsCursor = PlayerPrefs.GetInt(TooltipHugsCursorKey, 0) == 1;
if (hoveringTooltip != null) {
hoveringTooltip.HugsCursor = PlayerPrefs.GetInt(TooltipHugsCursorKey, 0) == 1;
}
provinceLabelsController.baseFontSize = PlayerPrefs.GetFloat(ProvinceLabelFontSizeKey, 11f);
if (provinceLabelsController != null) {
provinceLabelsController.baseFontSize =
PlayerPrefs.GetFloat(ProvinceLabelFontSizeKey, 11f);
}
}
void Start() {
if (_overridesMissing) return;
// Sync _active with the panel's current state rather than forcing it hidden.
// Start() runs the first time the panel becomes active (e.g. via ToggleActive),
// so forcing _active=false here would immediately undo the activation.
_active = panel.activeSelf;
modalBlocker.SetActive(_active);
if (modalBlocker != null) { modalBlocker.SetActive(_active); }
// Add click-to-dismiss on modal blocker as safety net
var blockerButton = modalBlocker.GetComponent<Button>();
if (blockerButton == null) { blockerButton = modalBlocker.AddComponent<Button>(); }
blockerButton.onClick.AddListener(DismissButtonClicked);
if (modalBlocker != null) {
var blockerButton = modalBlocker.GetComponent<Button>();
if (blockerButton == null) { blockerButton = modalBlocker.AddComponent<Button>(); }
blockerButton.onClick.AddListener(DismissButtonClicked);
}
// Sync UI controls to current values (ApplyPersistedSettings already applied
// the actual settings at load time via SettingsKeyListener.Start)
soundEffectsSlider.value = soundEffectsSource.volume;
musicSlider.value = musicSource.volume;
if (soundEffectsSlider != null) {
soundEffectsSlider.value = soundEffectsSource != null
? soundEffectsSource.volume
: PlayerPrefs.GetFloat(SoundEffectsVolumeKey, 0.5f);
}
if (musicSlider != null) {
musicSlider.value = musicSource != null ? musicSource.volume
: PlayerPrefs.GetFloat(MusicVolumeKey, 0.5f);
}
ambientSlider.value = AmbientVolume;
tooltipHugsCursorToggle.isOn = hoveringTooltip.HugsCursor;
if (tooltipHugsCursorToggle != null) {
tooltipHugsCursorToggle.isOn = hoveringTooltip != null && hoveringTooltip.HugsCursor;
}
showAnimationTestsToggle.gameObject.SetActive(Application.isEditor);
showAnimationTestsToggle.isOn = PlayerPrefs.GetInt(AnimationToggleKey, 0) == 1;
animationTestController.gameObject.SetActive(showAnimationTestsToggle.isOn);
if (showAnimationTestsToggle != null) {
bool showAnimationTests = Application.isEditor && animationTestController != null;
showAnimationTestsToggle.gameObject.SetActive(showAnimationTests);
showAnimationTestsToggle.isOn =
showAnimationTests && PlayerPrefs.GetInt(AnimationToggleKey, 0) == 1;
if (animationTestController != null) {
animationTestController.gameObject.SetActive(showAnimationTestsToggle.isOn);
}
}
autoScrollSpeedSlider.value = AutoScrollingText.GlobalScrollSpeedMultiplier;
UpdateAutoScrollSpeedLabel(autoScrollSpeedSlider.value);
provinceLabelFontSizeSlider.value = provinceLabelsController.baseFontSize;
UpdateProvinceLabelFontSizeLabel(provinceLabelsController.baseFontSize);
float provinceLabelFontSize = provinceLabelsController != null
? provinceLabelsController.baseFontSize
: PlayerPrefs.GetFloat(ProvinceLabelFontSizeKey, 11f);
provinceLabelFontSizeSlider.value = provinceLabelFontSize;
provinceLabelFontSizeSlider.interactable = provinceLabelsController != null;
UpdateProvinceLabelFontSizeLabel(provinceLabelFontSize);
fullscreenToggle.isOn = Screen.fullScreen;
@@ -148,14 +161,12 @@ public class SettingsPanelController : MonoBehaviour {
}
public void HandleEscape() {
if (_overridesMissing) return;
// Close attributions panel if open
if (attributionsController.panel.activeSelf) {
if (attributionsController != null && attributionsController.panel.activeSelf) {
attributionsController.Hide();
}
// Don't toggle settings if bug report panel is handling escape
else if (!bugReportPanel.panel.activeSelf) {
else if (bugReportPanel == null || !bugReportPanel.panel.activeSelf) {
ToggleActive();
}
}
@@ -165,7 +176,7 @@ public class SettingsPanelController : MonoBehaviour {
private void ToggleActive() {
_active = !_active;
panel.SetActive(_active);
modalBlocker.SetActive(_active);
if (modalBlocker != null) { modalBlocker.SetActive(_active); }
}
public void SettingsButtonClicked() { ToggleActive(); }
@@ -177,18 +188,18 @@ public class SettingsPanelController : MonoBehaviour {
public void DismissButtonClicked() {
_active = false;
panel.SetActive(_active);
modalBlocker.SetActive(_active);
if (modalBlocker != null) { modalBlocker.SetActive(_active); }
}
private static float VolumeFunc(float val) { return val * val; }
public void OnSoundEffectSliderChange(float val) {
soundEffectsSource.volume = VolumeFunc(val);
if (soundEffectsSource != null) { soundEffectsSource.volume = VolumeFunc(val); }
PlayerPrefs.SetFloat(SoundEffectsVolumeKey, val);
}
public void OnMusicSliderChange(float val) {
musicSource.volume = VolumeFunc(val);
if (musicSource != null) { musicSource.volume = VolumeFunc(val); }
PlayerPrefs.SetFloat(MusicVolumeKey, val);
}
@@ -229,12 +240,12 @@ public class SettingsPanelController : MonoBehaviour {
public void OnCursorHugToggleChange(bool val) {
PlayerPrefs.SetInt(TooltipHugsCursorKey, val ? 1 : 0);
hoveringTooltip.HugsCursor = val;
if (hoveringTooltip != null) { hoveringTooltip.HugsCursor = val; }
}
public void OnAnimationTestToggleChange(bool val) {
PlayerPrefs.SetInt(AnimationToggleKey, val ? 1 : 0);
animationTestController.gameObject.SetActive(val);
if (animationTestController != null) { animationTestController.gameObject.SetActive(val); }
}
public void OnAutoScrollSpeedSliderChange(float val) {
@@ -251,9 +262,11 @@ public class SettingsPanelController : MonoBehaviour {
}
public void OnProvinceLabelFontSizeSliderChange(float val) {
provinceLabelsController.baseFontSize = val;
provinceLabelsController.RefreshLabels();
PlayerPrefs.SetFloat(ProvinceLabelFontSizeKey, val);
if (provinceLabelsController != null) {
provinceLabelsController.baseFontSize = val;
provinceLabelsController.RefreshLabels();
}
UpdateProvinceLabelFontSizeLabel(val);
}
@@ -271,6 +284,8 @@ public class SettingsPanelController : MonoBehaviour {
}
public void OnReportBugClick() {
if (bugReportPanel == null) { return; }
// Configure the bug report panel with game state providers
bugReportPanel.Configure(
gameModelProvider: () => eagleGameController?.GameModel,
@@ -280,12 +295,24 @@ public class SettingsPanelController : MonoBehaviour {
bugReportPanel.Show();
}
private void HandleLobby() {
eagleGameController.StopAll();
eagleGameController.gameObject.SetActive(false);
ShardokContainer.SetActive(false);
connectionHandler.gameObject.SetActive(true);
// connectionHandler.StartListeningForLobbyUpdates();
private async void HandleLobby() {
if (eagleGameController != null) { eagleGameController.StopAll(); }
var sceneManager = GameSceneManager.Instance;
if (sceneManager == null) {
throw new InvalidOperationException(
"Cannot return to lobby because GameSceneManager is missing.");
}
await sceneManager.TransitionToConnection();
connectionHandler = sceneManager.GetConnectionHandler();
if (connectionHandler == null) {
throw new InvalidOperationException(
"Cannot return to lobby because Connection scene did not provide a " +
"ConnectionHandler.");
}
connectionHandler.ConnectEagle();
ClientTextProvider.Provider.Clear();
@@ -294,6 +321,23 @@ public class SettingsPanelController : MonoBehaviour {
modalBlocker.SetActive(_active);
}
private void LogMissingRuntimeReferences() {
string missing = "";
if (soundEffectsSource == null) missing += " soundEffectsSource";
if (musicSource == null) missing += " musicSource";
var eagleSceneReferencesExpected = eagleGameController != null;
if (eagleSceneReferencesExpected) {
if (hoveringTooltip == null) missing += " hoveringTooltip";
if (provinceLabelsController == null) missing += " provinceLabelsController";
}
if (missing.Length == 0) { return; }
UnityEngine.Debug.LogWarning(
"Settings runtime references are not fully wired yet:" + missing);
}
private void HandleQuit() {
ConnectionKiller.ShutdownAll();
#if UNITY_EDITOR
@@ -82,7 +82,9 @@ public class HexGrid : MonoBehaviour {
public Color backgroundColor = new(r: 1.0f, g: 1.0f, b: 1.0f, a: 0.35f);
private Texture2D _softCircleTexture;
private RectTransform _foregroundCanvasRoot;
private RectTransform _backingsContainer; // Renders behind foreground elements
private RectTransform _foregroundContainer;
readonly List<OverlayCellInfo> overlayCells = new();
readonly HashSet<int> _bgHighlightedCells = new();
@@ -93,13 +95,23 @@ public class HexGrid : MonoBehaviour {
static readonly float BaseBorderWidth = 0.01f;
private const float OverlayBorderMaxWidth = 0.2f;
private const string HexMeshSortingLayer = "UI Base Overlay";
private const int HexBaseSortingOrder = 100;
private const int HexOverlaySortingOrder = 101;
private const int TerrainModifierEffectSortingOrder = 101;
private const int ForegroundCanvasSortingOrder = 102;
private const string ForegroundGraphicShaderName = "Shardok/Foreground UI Overlay";
private const string ForegroundTextShaderName = "TextMeshPro/Distance Field Overlay";
HexMetrics metrics;
Cell[] cells;
private Material _foregroundGraphicMaterial;
private Material _foregroundTextMaterial;
// Track mapArea size for resolution change detection
private Vector2 _lastMapAreaSize;
private bool _isSetUp = false;
public bool IsSetUp => _isSetUp && metrics != null;
/// <summary>
/// Event fired after the grid is rebuilt due to resolution change.
@@ -115,17 +127,7 @@ public class HexGrid : MonoBehaviour {
return;
}
// Create backings container as first child of overlayContent so it renders behind
if (_backingsContainer == null) {
var backingsObj = new GameObject("BackingsContainer");
_backingsContainer = backingsObj.AddComponent<RectTransform>();
_backingsContainer.SetParent(overlayContent, false);
_backingsContainer.anchorMin = Vector2.zero;
_backingsContainer.anchorMax = Vector2.one;
_backingsContainer.offsetMin = Vector2.zero;
_backingsContainer.offsetMax = Vector2.zero;
_backingsContainer.SetAsFirstSibling();
}
EnsureForegroundContainers();
cells = new Cell[cellCount];
@@ -137,10 +139,106 @@ public class HexGrid : MonoBehaviour {
private void SetUpMeshesIfNecessary() {
if (metrics != null) { return; }
NormalizeOverlayContent();
metrics = new HexMetrics(mapArea: mapArea, columns: width, rows: height);
hexMesh.SetUp(m: metrics);
hexMesh.GetComponent<MeshRenderer>().sortingLayerName = "UI Base Overlay";
overlayMesh.SetUp(m: metrics);
SetMeshRendererSorting(hexMesh, HexBaseSortingOrder);
SetMeshRendererSorting(overlayMesh, HexOverlaySortingOrder);
}
private void NormalizeOverlayContent() {
overlayContent.SetAsLastSibling();
var overlayCanvas = overlayContent.GetComponent<Canvas>();
if (overlayCanvas != null) { overlayCanvas.enabled = false; }
var overlayRaycaster = overlayContent.GetComponent<GraphicRaycaster>();
if (overlayRaycaster != null) { overlayRaycaster.enabled = false; }
}
private Transform ForegroundContentParent() { return _foregroundContainer; }
private static void IgnoreRaycasts(Graphic graphic) { graphic.raycastTarget = false; }
private Material ForegroundGraphicMaterial() {
if (_foregroundGraphicMaterial != null) { return _foregroundGraphicMaterial; }
Shader shader = Shader.Find(ForegroundGraphicShaderName);
if (shader == null) {
throw new InvalidOperationException(
message: $"Could not find required shader {ForegroundGraphicShaderName}");
}
_foregroundGraphicMaterial = new Material(shader) {
name = "Shardok Foreground UI Overlay",
hideFlags = HideFlags.HideAndDontSave,
};
return _foregroundGraphicMaterial;
}
private Material ForegroundTextMaterial() {
if (_foregroundTextMaterial != null) { return _foregroundTextMaterial; }
Shader shader = Shader.Find(ForegroundTextShaderName);
if (shader == null) {
throw new InvalidOperationException(
message: $"Could not find required shader {ForegroundTextShaderName}");
}
_foregroundTextMaterial = new Material(mainFont.material) {
shader = shader,
name = "Shardok Foreground TMP Overlay",
hideFlags = HideFlags.HideAndDontSave,
};
return _foregroundTextMaterial;
}
private void UseForegroundMaterial(Graphic graphic) {
graphic.material = ForegroundGraphicMaterial();
}
private void UseForegroundTextMaterial(TMP_Text label) {
label.fontSharedMaterial = ForegroundTextMaterial();
}
private static void SetMeshRendererSorting(HexMesh mesh, int sortingOrder) {
var meshRenderer = mesh.GetComponent<MeshRenderer>();
meshRenderer.sortingLayerName = HexMeshSortingLayer;
meshRenderer.sortingOrder = sortingOrder;
}
private void EnsureForegroundContainers() {
if (_foregroundCanvasRoot != null) { return; }
var foregroundRootObj = new GameObject("ForegroundCanvas");
_foregroundCanvasRoot = foregroundRootObj.AddComponent<RectTransform>();
_foregroundCanvasRoot.SetParent(gridTransform, worldPositionStays: false);
StretchToParent(_foregroundCanvasRoot);
var foregroundCanvas = foregroundRootObj.AddComponent<Canvas>();
foregroundCanvas.overrideSorting = true;
foregroundCanvas.sortingLayerName = HexMeshSortingLayer;
foregroundCanvas.sortingOrder = ForegroundCanvasSortingOrder;
var backingsObj = new GameObject("BackingsContainer");
_backingsContainer = backingsObj.AddComponent<RectTransform>();
_backingsContainer.SetParent(_foregroundCanvasRoot, worldPositionStays: false);
StretchToParent(_backingsContainer);
_backingsContainer.SetAsFirstSibling();
var foregroundObj = new GameObject("ForegroundContent");
_foregroundContainer = foregroundObj.AddComponent<RectTransform>();
_foregroundContainer.SetParent(_foregroundCanvasRoot, worldPositionStays: false);
StretchToParent(_foregroundContainer);
_foregroundContainer.SetAsLastSibling();
}
private static void StretchToParent(RectTransform rectTransform) {
rectTransform.anchorMin = Vector2.zero;
rectTransform.anchorMax = Vector2.one;
rectTransform.offsetMin = Vector2.zero;
rectTransform.offsetMax = Vector2.zero;
}
private Texture2D GetOrCreateSoftCircleTexture() {
@@ -240,10 +338,12 @@ public class HexGrid : MonoBehaviour {
}
}
// Destroy backings container (will be recreated)
if (_backingsContainer != null) {
Destroy(_backingsContainer.gameObject);
// Destroy foreground containers (will be recreated)
if (_foregroundCanvasRoot != null) {
Destroy(_foregroundCanvasRoot.gameObject);
_foregroundCanvasRoot = null;
_backingsContainer = null;
_foregroundContainer = null;
}
// Clear cells and metrics to force recreation
@@ -437,11 +537,13 @@ public class HexGrid : MonoBehaviour {
parent: gridTransform,
worldPositionStays: false);
newImage.gameObject.name = $"Terrain{coordSuffix}";
IgnoreRaycasts(newImage);
newImage.color = Color.white;
newImage.rectTransform.anchoredPosition =
new Vector2(x: position.x, y: position.z + metrics.OuterRadius / 2);
newImage.rectTransform.sizeDelta =
new Vector2(x: 2 * metrics.InnerRadius, y: 3 * metrics.OuterRadius);
newImage.rectTransform.SetAsFirstSibling();
newImage.gameObject.SetActive(value: false);
return newImage;
@@ -490,9 +592,11 @@ public class HexGrid : MonoBehaviour {
foreach (int column in new[] { -1, 1, -3, 3 }) {
RawImage newImage = Instantiate(
original: followUpImagePrefab,
parent: overlayContent,
parent: ForegroundContentParent(),
worldPositionStays: false);
newImage.gameObject.name = $"SecondaryImage{coordSuffix}_{imageIndex}";
IgnoreRaycasts(newImage);
UseForegroundMaterial(newImage);
newImage.rectTransform.anchoredPosition = new Vector2(
x: position.x + column * singleWidth * 0.5f,
y: position.z + 1 - row * singleHeight);
@@ -511,10 +615,13 @@ public class HexGrid : MonoBehaviour {
RawImage newImage = Instantiate(
original: backgroundImagePrefab,
parent: overlayContent,
parent: ForegroundContentParent(),
worldPositionStays: false);
newImage.gameObject.name = $"TerrainModifier{coordSuffix}";
newImage.rectTransform.SetParent(parent: overlayContent, worldPositionStays: false);
IgnoreRaycasts(newImage);
newImage.rectTransform.SetParent(
parent: ForegroundContentParent(),
worldPositionStays: false);
newImage.rectTransform.anchoredPosition = new Vector2(x: position.x, y: position.z);
newImage.rectTransform.sizeDelta = new Vector2(x: squareSize, y: squareSize);
newImage.gameObject.SetActive(value: false);
@@ -535,6 +642,8 @@ public class HexGrid : MonoBehaviour {
parent: _backingsContainer,
worldPositionStays: false);
backing.gameObject.name = $"UnitTypeBacking{coordSuffix}";
IgnoreRaycasts(backing);
UseForegroundMaterial(backing);
backing.texture = GetOrCreateSoftCircleTexture();
backing.color = imageBackingColor;
backing.rectTransform.anchoredPosition = anchoredPos;
@@ -544,9 +653,11 @@ public class HexGrid : MonoBehaviour {
// Create main image
RawImage newImage = Instantiate(
original: followUpImagePrefab,
parent: overlayContent,
parent: ForegroundContentParent(),
worldPositionStays: false);
newImage.gameObject.name = $"UnitType{coordSuffix}";
IgnoreRaycasts(newImage);
UseForegroundMaterial(newImage);
newImage.rectTransform.anchoredPosition = anchoredPos;
newImage.rectTransform.sizeDelta = new Vector2(x: size, y: size);
newImage.gameObject.SetActive(value: false);
@@ -567,6 +678,8 @@ public class HexGrid : MonoBehaviour {
parent: _backingsContainer,
worldPositionStays: false);
backing.gameObject.name = $"ProfessionBacking{coordSuffix}";
IgnoreRaycasts(backing);
UseForegroundMaterial(backing);
backing.texture = GetOrCreateSoftCircleTexture();
backing.color = imageBackingColor;
backing.rectTransform.anchoredPosition = anchoredPos;
@@ -576,9 +689,11 @@ public class HexGrid : MonoBehaviour {
// Create main image
RawImage newImage = Instantiate(
original: followUpImagePrefab,
parent: overlayContent,
parent: ForegroundContentParent(),
worldPositionStays: false);
newImage.gameObject.name = $"Profession{coordSuffix}";
IgnoreRaycasts(newImage);
UseForegroundMaterial(newImage);
newImage.rectTransform.anchoredPosition = anchoredPos;
newImage.rectTransform.sizeDelta = new Vector2(x: size, y: size);
newImage.gameObject.SetActive(value: false);
@@ -598,6 +713,8 @@ public class HexGrid : MonoBehaviour {
parent: _backingsContainer,
worldPositionStays: false);
backing.gameObject.name = backingName;
IgnoreRaycasts(backing);
UseForegroundMaterial(backing);
backing.texture = GetOrCreateSoftCircleTexture();
backing.color = labelBackingColor;
backing.rectTransform.anchoredPosition = anchoredPos;
@@ -608,9 +725,10 @@ public class HexGrid : MonoBehaviour {
// Create text label
TMP_Text label = Instantiate(
original: cellLabelPrefab,
parent: overlayContent,
parent: ForegroundContentParent(),
worldPositionStays: false);
label.gameObject.name = name;
IgnoreRaycasts(label);
// None of the hex-grid labels (hero names, battalion sizes, odds %, action points,
// coord debug strings) need rich-text tags. Disabling richText skips TMP's per-update
// tag-scanning pass and makes the labels robust against accidental '<' characters in
@@ -621,6 +739,7 @@ public class HexGrid : MonoBehaviour {
label.rectTransform.anchoredPosition = anchoredPos;
label.rectTransform.sizeDelta = labelSize;
label.font = mainFont;
UseForegroundTextMaterial(label);
label.fontSizeMin = 10;
label.fontSize = 300;
label.fontSizeMax = 300;
@@ -673,6 +792,18 @@ public class HexGrid : MonoBehaviour {
return GridIndexForPoint(worldPosition);
}
public bool ContainsMapVisual(GameObject gameObject) {
if (gameObject == null) { return false; }
var visualTransform = gameObject.transform;
return IsSameOrChild(visualTransform, gridTransform) ||
IsSameOrChild(visualTransform, overlayContent) ||
IsSameOrChild(visualTransform, mapArea);
}
private static bool IsSameOrChild(Transform transform, Transform parent) {
return transform == parent || transform.IsChildOf(parent);
}
/// <summary>
/// Gets the local position of a cell for use with effects (arrows, particles).
/// Returns the anchoredPosition which maps to localPosition in the rotated canvas.
@@ -822,6 +953,9 @@ public class HexGrid : MonoBehaviour {
newEffect.transform.localPosition =
new Vector3(x: imagePosition.x, y: imagePosition.y - h / 4.0f, z: 0);
newEffect.transform.localScale = new Vector3(x: 0.35f, y: 0.45f, z: 0.35f);
var effectRenderer = newEffect.GetComponent<ParticleSystemRenderer>();
effectRenderer.sortingLayerName = HexMeshSortingLayer;
effectRenderer.sortingOrder = TerrainModifierEffectSortingOrder;
newEffect.gameObject.SetActive(value: true);
cells[cellIndex].TerrainModifierEffect = newEffect;
}
@@ -1,5 +1,6 @@
fileFormatVersion: 2
guid: e4b2b2bb94e5a471e86597b8838642c9
guid: 3527cb6d31f54c3faf9b729605a92664
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
@@ -0,0 +1,99 @@
Shader "Shardok/Foreground UI Overlay" {
Properties {
[PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
_Color ("Tint", Color) = (1,1,1,1)
_StencilComp ("Stencil Comparison", Float) = 8
_Stencil ("Stencil ID", Float) = 0
_StencilOp ("Stencil Operation", Float) = 0
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255
_ColorMask ("Color Mask", Float) = 15
[Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
}
SubShader {
Tags {
"Queue" = "Overlay"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
"PreviewType" = "Plane"
"CanUseSpriteAtlas" = "True"
}
Stencil {
Ref [_Stencil]
Comp [_StencilComp]
Pass [_StencilOp]
ReadMask [_StencilReadMask]
WriteMask [_StencilWriteMask]
}
Cull Off
Lighting Off
ZWrite Off
ZTest Always
Blend SrcAlpha OneMinusSrcAlpha
ColorMask [_ColorMask]
Pass {
Name "Default"
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 2.0
#include "UnityCG.cginc"
#include "UnityUI.cginc"
#pragma multi_compile_local _ UNITY_UI_CLIP_RECT
#pragma multi_compile_local _ UNITY_UI_ALPHACLIP
struct appdata_t {
float4 vertex : POSITION;
float4 color : COLOR;
float2 texcoord : TEXCOORD0;
UNITY_VERTEX_INPUT_INSTANCE_ID
};
struct v2f {
float4 vertex : SV_POSITION;
fixed4 color : COLOR;
float2 texcoord : TEXCOORD0;
float4 worldPosition : TEXCOORD1;
UNITY_VERTEX_OUTPUT_STEREO
};
sampler2D _MainTex;
fixed4 _Color;
fixed4 _TextureSampleAdd;
float4 _ClipRect;
float4 _MainTex_ST;
v2f vert(appdata_t v) {
v2f OUT;
UNITY_SETUP_INSTANCE_ID(v);
UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
OUT.worldPosition = v.vertex;
OUT.vertex = UnityObjectToClipPos(OUT.worldPosition);
OUT.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
OUT.color = v.color * _Color;
return OUT;
}
fixed4 frag(v2f IN) : SV_Target {
half4 color = (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd) * IN.color;
#ifdef UNITY_UI_CLIP_RECT
color.a *= UnityGet2DClipping(IN.worldPosition.xy, _ClipRect);
#endif
#ifdef UNITY_UI_ALPHACLIP
clip(color.a - 0.001);
#endif
return color;
}
ENDCG
}
}
}
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: ccd58ed8b9624c54a8bbd9554a4a8777
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:
@@ -90,6 +90,7 @@ namespace Shardok {
public class ShardokGameController : MonoBehaviour, IClickReceiver {
public HexGrid hexGrid;
public HexGrid HexGrid => hexGrid;
public bool CanReceiveGridInput => hexGrid.IsSetUp && Model?.Map != null;
public Camera mainCamera;
public UnitInfoPanelController unitInfoPanel;
public EventBasedTable armiesTable;
@@ -359,13 +360,12 @@ namespace Shardok {
_setupGameGeneration == setupGameGeneration;
}
public void EndTurn() {
public async void EndTurn() {
endTurnButton.interactable = false;
var gameOver = Model.CanExitBattle;
if (gameOver) {
PersistentUIManager.Instance?.connectionStatusUI?.SetShardokGameStateProvider(null);
ClearActiveModel();
eagleCanvas.gameObject.SetActive(true);
hexGrid.ClearCellLabels();
hexGrid.ClearCellModifierImages();
hexGrid.ClearCellModifierEffects();
@@ -374,6 +374,33 @@ namespace Shardok {
_lastRetrievedHistoryCount = 0;
turnHistoryPanel.Clear();
this.gameObject.SetActive(false);
var sceneManager = GameSceneManager.Instance;
if (sceneManager == null) {
throw new InvalidOperationException(
"Cannot exit Shardok battle because GameSceneManager is missing.");
}
if (sceneManager.IsSceneLoaded(sceneManager.eagleSceneName)) {
eagleCanvas = sceneManager.GetEagleCanvas();
if (eagleCanvas == null) {
throw new InvalidOperationException(
"Cannot exit Shardok battle because Eagle scene is loaded but " +
"does not provide an Eagle canvas.");
}
eagleCanvas.gameObject.SetActive(true);
} else {
await sceneManager.TransitionToConnection();
var connectionHandler = sceneManager.GetConnectionHandler();
if (connectionHandler == null) {
throw new InvalidOperationException(
"Cannot exit custom Shardok battle because Connection scene did " +
"not provide a ConnectionHandler.");
}
connectionHandler.ConnectEagle();
}
} else {
hexGrid.ClearOverlays();
// Notify tutorial system of turn end
@@ -1035,12 +1062,15 @@ namespace Shardok {
}
private bool IsPrimaryOrSecondaryPressActive() {
if (Input.GetMouseButton(0) || Input.GetMouseButton(1)) { return true; }
if (GameInput.PrimaryPointerPressed() || GameInput.SecondaryPointerPressed()) {
return true;
}
if (Input.touchCount == 0) { return false; }
int touchCount = GameInput.TouchCount();
if (touchCount == 0) { return false; }
for (int i = 0; i < Input.touchCount; i++) {
var phase = Input.GetTouch(i).phase;
for (int i = 0; i < touchCount; i++) {
var phase = GameInput.GetTouch(i).Phase;
if (phase != TouchPhase.Ended && phase != TouchPhase.Canceled) { return true; }
}
@@ -1446,54 +1476,22 @@ namespace Shardok {
}
void HandleButton() {
/* else if (Input.GetButtonUp("Lightning")) {
LightningButtonClicked();
}
else if (Input.GetButtonUp("Meteor")) {
StartMeteorButtonClicked();
}
else if (Input.GetButtonUp("Move")) {
SetDisplayActionsUIState(DisplayActionsUIState.standardState);
handleOverlaysForSelection();
}
else if (Input.GetButtonUp("Extinguish")) {
ExtinguishButtonClicked();
}
else if (Input.GetButtonUp("StartFire")) {
StartFireButtonClicked();
}*/
// if (Input.GetButton("Horizontal"))
//{
// float horizontalValue = Input.GetAxis("Horizontal");
// Vector3 v = mainCamera.transform.position;
// v.x -= horizontalValue;
// mainCamera.transform.position = v;
// }
// if (Input.GetButton("Vertical"))
//{
// float verticalValue = Input.GetAxis("Vertical");
// Vector3 v = mainCamera.transform.position;
// v.z -= verticalValue;
// mainCamera.transform.position = v;
// }
if (Input.GetButton("Zoom")) {
float zoomValue = Input.GetAxis("Zoom");
float zoomValue = GameInput.ShardokZoomAxis();
if (!Mathf.Approximately(a: zoomValue, b: 0f)) {
var transform1 = mainCamera.transform;
Vector3 v = transform1.position;
v.y += zoomValue;
transform1.position = v;
}
if (Input.GetButtonDown("Hex Coords")) { SetCoordsLabels(); }
if (Input.GetButtonUp("Hex Coords")) { SetHeroLabels(); }
if (GameInput.HexCoordsPressed()) { SetCoordsLabels(); }
if (GameInput.HexCoordsReleased()) { SetHeroLabels(); }
if (Input.GetButtonDown("Profession Attack")) {
if (GameInput.ProfessionAttackPressed()) {
var prev = _displayedCommandGroup;
SetDisplayedCommandGroup(CommandTypeUIManager.ProfessionAttackGroup);
_previouslyDisplayedCommandGroup = prev;
}
if (Input.GetButtonUp("Profession Attack")) {
if (GameInput.ProfessionAttackReleased()) {
SetDisplayedCommandGroup(_previouslyDisplayedCommandGroup ?? 0);
}
}
@@ -1809,7 +1807,7 @@ namespace Shardok {
// Position tooltip to the right of the cursor (pivot is centered,
// tooltip is 130 units wide, so offset by ~half-width + padding)
var cursorScreenPos = (Vector2)Input.mousePosition + new Vector2(160, -15);
var cursorScreenPos = GameInput.PointerPosition() + new Vector2(160, -15);
RectTransformUtility.ScreenPointToWorldPointInRectangle(
_canvasRect,
cursorScreenPos,
@@ -5,6 +5,7 @@ using Net.Eagle0.Shardok.Common;
using UnityEngine;
public class SoundManager : MonoBehaviour {
public AudioSource soundEffectsSource;
public AudioSource musicSource;
private class MusicTypeInfo {
@@ -239,7 +240,7 @@ public class SoundManager : MonoBehaviour {
};
}
public void Start() {
public void Awake() {
_musicInfoForType = new Dictionary<MusicType, MusicTypeInfo> {
{ MusicType.MusicNone, null },
{ MusicType.MusicSpring, _springMusic },
@@ -250,7 +251,9 @@ public class SoundManager : MonoBehaviour {
{ MusicType.MusicBattle, _battleMusic },
{ MusicType.MusicVictory, _victoryMusic }
};
}
public void Start() {
StartCoroutine(LoadAllMusic());
StartCoroutine(LoadSoundEffects());
}
@@ -11,20 +11,20 @@ public class TouchHandler : MonoBehaviour {
const float LONG_TOUCH_SECONDS = 2.0f;
// Use this for initialization
void Start() { Input.simulateMouseWithTouches = false; }
void Start() { GameInput.DisableMouseSimulationWithTouches(); }
// Update is called once per frame
void Update() {
if (gameController.hexGrid == null) { return; }
int numFingersDown = Input.touchCount;
int numFingersDown = GameInput.TouchCount();
if (numFingersDown > 0) {
Touch firstFingerTouch = Input.GetTouch(0);
var firstFingerTouch = GameInput.GetTouch(0);
if (EventSystem.current.IsPointerOverGameObject(firstFingerTouch.fingerId)) { return; }
if (EventSystem.current.IsPointerOverGameObject(firstFingerTouch.FingerId)) { return; }
Ray firstFingerInputRay = Camera.main.ScreenPointToRay(firstFingerTouch.position);
Ray firstFingerInputRay = Camera.main.ScreenPointToRay(firstFingerTouch.Position);
RaycastHit firstFingerHit;
if (Physics.Raycast(firstFingerInputRay, out firstFingerHit)) {
int? firstFingerGridIndex =
@@ -1,89 +0,0 @@
# Safe Scene Refactoring Plan
## Problem
The Gameplay.unity file is massive (255k lines, 7.5MB) making it:
- Prone to crashes when edited
- Difficult to maintain and review
- Risk of corruption when multiple developers work on it
## Safe Solution Strategy
Instead of directly editing the massive scene file, we'll use a gradual approach:
### Phase 1: Create Empty Prefab Structures (SAFE)
1. Create new, minimal prefab versions of UI components
2. These will be empty initially and won't affect the game
3. Test that Unity can load and build successfully
### Phase 2: Runtime Loading System (SAFE)
1. Create a script that can instantiate prefabs at runtime
2. Test this system in isolation first
3. Gradually migrate components one by one
### Phase 3: Component Migration (SAFE)
1. Start with the simplest components first
2. Use A/B testing - keep old version as fallback
3. Migrate one component at a time with thorough testing
## Phase 1 Implementation
### Step 1.1: Create UI Directory Structure
```
Assets/UI/
├── Prefabs/
│ ├── Connection/
│ │ ├── ConnectionPanel.prefab
│ │ ├── GameSelectionPanel.prefab
│ │ └── CustomBattlePanel.prefab
│ ├── Game/
│ │ ├── CommandCanvas.prefab
│ │ ├── PopupCanvas.prefab
│ │ └── ReservesCanvas.prefab
│ └── Shared/
│ ├── ErrorCanvas.prefab
│ └── ConfirmationCanvas.prefab
└── Scripts/
├── PrefabLoader.cs
└── UIManager.cs
```
### Step 1.2: Create Empty Prefabs
- Start with ConnectionPanel.prefab (simplest connection UI)
- Create minimal structure with required components
- Test Unity can load without errors
### Step 1.3: Runtime Loading Test
- Create PrefabLoader script that can instantiate prefabs
- Test loading prefabs at runtime
- Ensure no conflicts with existing UI
## Benefits of This Approach
1. **Zero Risk**: Won't affect existing functionality
2. **Incremental**: Can be done component by component
3. **Testable**: Each step can be verified independently
4. **Reversible**: Can rollback any step if needed
5. **Collaborative**: Multiple developers can work safely
## Implementation Order
1. ConnectionPanel (smallest, least complex)
2. CustomBattlePanel (isolated functionality)
3. GameSelectionPanel (moderate complexity)
4. Command-related canvases (high complexity)
5. Game UI canvases (highest complexity)
## Testing Strategy
- After each component: Unity loads without errors
- After each component: Game functionality unchanged
- After each component: Build succeeds
- Before final migration: Comprehensive gameplay testing
## Rollback Plan
- Keep original Gameplay.unity as Gameplay_backup.unity
- Each prefab has a feature flag for enable/disable
- Can instantly revert to original scene if issues arise
This approach ensures we never break the working game while gradually improving the scene structure.
@@ -1,7 +0,0 @@
fileFormatVersion: 2
guid: b097cd2b606a540029692ab16a1fc29a
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:
@@ -1,104 +1,33 @@
using System.Collections;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.SceneManagement;
/// <summary>
/// Bootstrap manager that starts the game by loading appropriate scenes.
/// Keeps a legacy Gameplay.unity fallback while the split-scene migration is in progress.
/// Bootstrap manager that starts the game by loading the shared and connection scenes.
/// </summary>
public class BootstrapManager : MonoBehaviour {
[Header("Startup Configuration")]
public bool useNewSceneSystem = true;
public bool useOriginalGameplayScene = false;
public bool fallbackToOriginalGameplaySceneWhenSplitSceneIncomplete = true;
[Header("Development Testing")]
public bool testSceneLoading = false;
void Start() { StartCoroutine(InitializeGame()); }
IEnumerator InitializeGame() {
Debug.Log("BootstrapManager: Initializing game...");
if (testSceneLoading) {
yield return StartCoroutine(TestAllScenes());
if (GameSceneManager.Instance == null) {
Debug.LogError("BootstrapManager: GameSceneManager.Instance is null!");
yield break;
}
if (useNewSceneSystem) {
Debug.Log("BootstrapManager: Loading split-scene startup path (Shared + Connection).");
if (GameSceneManager.Instance != null) {
yield return StartCoroutine(LoadNewSceneSystemCoroutine());
} else {
Debug.LogError("BootstrapManager: GameSceneManager.Instance is null!");
}
Debug.Log("BootstrapManager: Loading startup scenes (Shared + Connection).");
yield return StartCoroutine(LoadSplitSceneStartupCoroutine());
if (!IsConnectionSceneReady(out var incompleteReason)) {
Debug.LogWarning(
$"BootstrapManager: Split-scene startup incomplete: {incompleteReason}");
if (fallbackToOriginalGameplaySceneWhenSplitSceneIncomplete) {
yield return StartCoroutine(UnloadSplitStartupScenesCoroutine());
yield return StartCoroutine(LoadOriginalGameplaySceneCoroutine());
}
}
} else if (useOriginalGameplayScene) {
yield return StartCoroutine(LoadOriginalGameplaySceneCoroutine());
} else {
Debug.LogWarning(
"BootstrapManager: No startup scene path is enabled. Enable split scenes or the legacy fallback.");
if (FindAnyObjectByType<ConnectionHandler>(FindObjectsInactive.Include) == null) {
Debug.LogError(
"BootstrapManager: Connection scene loaded but does not contain a ConnectionHandler.");
yield break;
}
Debug.Log("BootstrapManager: Game initialization complete");
}
IEnumerator LoadOriginalGameplaySceneCoroutine() {
Debug.Log("BootstrapManager: Loading legacy Gameplay.unity scene through fallback path.");
CleanupSplitSceneGlobalsBeforeLegacyFallback();
var asyncLoad = SceneManager.LoadSceneAsync("Gameplay", LoadSceneMode.Additive);
while (!asyncLoad.isDone) { yield return null; }
Debug.Log("BootstrapManager: Legacy Gameplay.unity scene loaded successfully");
}
IEnumerator UnloadSplitStartupScenesCoroutine() {
if (GameSceneManager.Instance == null) { yield break; }
var unloadConnectionTask = GameSceneManager.Instance.UnloadScene("Connection");
yield return new WaitUntil(() => unloadConnectionTask.IsCompleted);
var unloadSharedTask = GameSceneManager.Instance.UnloadScene("Shared");
yield return new WaitUntil(() => unloadSharedTask.IsCompleted);
}
bool IsConnectionSceneReady(out string incompleteReason) {
var readiness = FindAnyObjectByType<ConnectionSceneReadiness>(FindObjectsInactive.Include);
if (readiness != null) { return readiness.IsReady(out incompleteReason); }
if (FindAnyObjectByType<ConnectionHandler>(FindObjectsInactive.Include) != null) {
incompleteReason = "";
return true;
}
incompleteReason =
"Connection scene has no readiness marker and does not contain a ConnectionHandler.";
return false;
}
void CleanupSplitSceneGlobalsBeforeLegacyFallback() {
foreach (var eventSystem in FindObjectsByType<EventSystem>(FindObjectsInactive.Include)) {
Destroy(eventSystem.gameObject);
}
foreach (var audioListener in FindObjectsByType<AudioListener>(
FindObjectsInactive.Include)) {
Destroy(audioListener);
}
}
IEnumerator LoadNewSceneSystemCoroutine() {
IEnumerator LoadSplitSceneStartupCoroutine() {
// Load connection UI first (this is where users start)
var connectionTask = GameSceneManager.Instance.TransitionToConnection();
yield return new WaitUntil(() => connectionTask.IsCompleted);
@@ -110,89 +39,4 @@ public class BootstrapManager : MonoBehaviour {
Debug.Log("BootstrapManager: Connection scene loaded successfully");
}
}
/// <summary>
/// Test all scenes load and unload correctly
/// </summary>
IEnumerator TestAllScenes() {
Debug.Log("BootstrapManager: Testing all scene loading...");
if (GameSceneManager.Instance == null) {
Debug.LogError("BootstrapManager: GameSceneManager.Instance is null!");
yield break;
}
// Test Connection scene
Debug.Log("Testing Connection scene...");
var connectionTask = GameSceneManager.Instance.LoadSceneAdditive("Connection");
yield return new WaitUntil(() => connectionTask.IsCompleted);
if (connectionTask.IsFaulted) {
Debug.LogError($"Failed to load Connection: {connectionTask.Exception}");
} else {
Debug.Log("✓ Connection scene loaded");
}
yield return new WaitForSeconds(1f);
// Test Eagle scene
Debug.Log("Testing Eagle scene...");
var eagleTask = GameSceneManager.Instance.LoadSceneAdditive("Eagle");
yield return new WaitUntil(() => eagleTask.IsCompleted);
if (eagleTask.IsFaulted) {
Debug.LogError($"Failed to load Eagle: {eagleTask.Exception}");
} else {
Debug.Log("✓ Eagle scene loaded");
}
yield return new WaitForSeconds(1f);
// Test Shardok scene
Debug.Log("Testing Shardok scene...");
var shardokTask = GameSceneManager.Instance.LoadSceneAdditive("Shardok");
yield return new WaitUntil(() => shardokTask.IsCompleted);
if (shardokTask.IsFaulted) {
Debug.LogError($"Failed to load Shardok: {shardokTask.Exception}");
} else {
Debug.Log("✓ Shardok scene loaded");
}
yield return new WaitForSeconds(1f);
// Test Shared scene
Debug.Log("Testing Shared scene...");
var sharedTask = GameSceneManager.Instance.LoadSceneAdditive("Shared");
yield return new WaitUntil(() => sharedTask.IsCompleted);
if (sharedTask.IsFaulted) {
Debug.LogError($"Failed to load Shared: {sharedTask.Exception}");
} else {
Debug.Log("✓ Shared scene loaded");
}
yield return new WaitForSeconds(2f);
// Test unloading
Debug.Log("Testing scene unloading...");
var unloadShardokTask = GameSceneManager.Instance.UnloadScene("Shardok");
yield return new WaitUntil(() => unloadShardokTask.IsCompleted);
Debug.Log("✓ Shardok scene unloaded");
var unloadEagleTask = GameSceneManager.Instance.UnloadScene("Eagle");
yield return new WaitUntil(() => unloadEagleTask.IsCompleted);
Debug.Log("✓ Eagle scene unloaded");
var unloadConnectionTask = GameSceneManager.Instance.UnloadScene("Connection");
yield return new WaitUntil(() => unloadConnectionTask.IsCompleted);
Debug.Log("✓ Connection scene unloaded");
var unloadSharedTask = GameSceneManager.Instance.UnloadScene("Shared");
yield return new WaitUntil(() => unloadSharedTask.IsCompleted);
Debug.Log("✓ Shared scene unloaded");
Debug.Log("🎉 All scene loading tests passed!");
}
}
@@ -1,28 +0,0 @@
using UnityEngine;
/// <summary>
/// Declares whether the split Connection scene has enough production UI to replace Gameplay.
/// </summary>
public class ConnectionSceneReadiness : MonoBehaviour {
[SerializeField]
private bool productionReady = false;
[SerializeField]
private string incompleteReason =
"Connection scene is still a shell and does not contain the production ConnectionHandler.";
public bool IsReady(out string reason) {
if (!productionReady) {
reason = incompleteReason;
return false;
}
if (FindAnyObjectByType<ConnectionHandler>(FindObjectsInactive.Include) == null) {
reason = "Connection scene is marked ready but does not contain a ConnectionHandler.";
return false;
}
reason = "";
return true;
}
}
@@ -1,8 +1,12 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Auth;
using common;
using common.GUIUtils;
using eagle;
using Shardok;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.SceneManagement;
public class GameSceneManager : MonoBehaviour {
@@ -32,19 +36,33 @@ public class GameSceneManager : MonoBehaviour {
}
}
public async Task LoadSceneAdditive(string sceneName) {
public async Task<Scene> LoadSceneAdditive(string sceneName) {
if (loadedScenes.Contains(sceneName)) {
Debug.LogWarning($"Scene {sceneName} is already loaded");
return;
return SceneManager.GetSceneByName(sceneName);
}
Debug.Log($"Loading scene: {sceneName}");
var asyncLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
while (!asyncLoad.isDone) { await Task.Yield(); }
var sceneLoadedTask = new TaskCompletionSource<Scene>();
void OnSceneLoaded(Scene scene, LoadSceneMode mode) {
if (scene.name != sceneName || mode != LoadSceneMode.Additive) { return; }
loadedScenes.Add(sceneName);
Debug.Log($"Scene {sceneName} loaded successfully");
SceneManager.sceneLoaded -= OnSceneLoaded;
loadedScenes.Add(sceneName);
Debug.Log($"Scene {sceneName} loaded successfully");
sceneLoadedTask.SetResult(scene);
}
SceneManager.sceneLoaded += OnSceneLoaded;
var asyncLoad = SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive);
if (asyncLoad == null) {
SceneManager.sceneLoaded -= OnSceneLoaded;
throw new System.InvalidOperationException(
$"Failed to start loading scene {sceneName}");
}
return await sceneLoadedTask.Task;
}
public async Task UnloadScene(string sceneName) {
@@ -69,44 +87,171 @@ public class GameSceneManager : MonoBehaviour {
// Load shared infrastructure before the connection UI.
await LoadSceneAdditive(sharedSceneName);
EnsureSharedInfrastructure();
await LoadSceneAdditive(connectionSceneName);
ActivateConnectionSceneRoot();
WireConnectionSceneReferences();
WireSharedUIReferences();
}
public async Task TransitionToEagle() {
// Unload connection scene
if (loadedScenes.Contains(connectionSceneName)) { await UnloadScene(connectionSceneName); }
public async Task<EagleGameController> TransitionToEagle() {
// Ensure shared scene is loaded
if (!loadedScenes.Contains(sharedSceneName)) { await LoadSceneAdditive(sharedSceneName); }
EnsureSharedInfrastructure();
// Load Eagle scene
await LoadSceneAdditive(eagleSceneName);
var eagleScene = await LoadSceneAdditive(eagleSceneName);
var eagleController = GetEagleController(eagleScene);
if (eagleController == null) { LogSceneRoots(eagleScene); }
WireEagleSceneReferences();
WireSharedUIReferences();
return eagleController;
}
public async Task TransitionToShardok() {
public async Task<GameObject> TransitionToShardok() {
// Ensure shared scene is loaded
if (!loadedScenes.Contains(sharedSceneName)) { await LoadSceneAdditive(sharedSceneName); }
EnsureSharedInfrastructure();
// Load Shardok scene additively (on top of Eagle)
await LoadSceneAdditive(shardokSceneName);
WireShardokSceneReferences();
WireEagleSceneReferences();
WireSharedUIReferences();
return GetShardokContainer();
}
public async Task ExitShardok() {
// Just unload Shardok scene, keep Eagle loaded
if (loadedScenes.Contains(shardokSceneName)) { await UnloadScene(shardokSceneName); }
WireSharedUIReferences();
}
public bool IsSceneLoaded(string sceneName) { return loadedScenes.Contains(sceneName); }
public EagleGameController GetEagleController() {
var eagleScene = SceneManager.GetSceneByName(eagleSceneName);
if (eagleScene.IsValid() && eagleScene.isLoaded) {
var eagleController = GetEagleController(eagleScene);
if (eagleController != null) { return eagleController; }
}
return FindAnyObjectByType<EagleGameController>(FindObjectsInactive.Include);
}
public EagleGameController GetEagleController(Scene eagleScene) {
var eagleRoot = FindSceneRootByName(eagleScene, "Eagle Canvas");
return eagleRoot == null ? null : eagleRoot.GetComponent<EagleGameController>();
}
public Canvas GetEagleCanvas() {
var eagleController = GetEagleController();
return eagleController == null ? null : eagleController.GetComponent<Canvas>();
}
public GameObject GetShardokContainer() {
var shardokController =
FindAnyObjectByType<ShardokGameController>(FindObjectsInactive.Include);
return shardokController == null ? null : shardokController.gameObject;
}
public ConnectionHandler GetConnectionHandler() {
return FindAnyObjectByType<ConnectionHandler>(FindObjectsInactive.Include);
}
public ErrorHandler GetErrorHandler() {
return FindAnyObjectByType<ErrorHandler>(FindObjectsInactive.Include);
}
private void ActivateConnectionSceneRoot() {
var connectionHandler = GetConnectionHandler();
if (connectionHandler != null) { connectionHandler.gameObject.SetActive(true); }
}
private void WireConnectionSceneReferences() {
EnsureOAuthManager();
var connectionHandler = FindAnyObjectByType<ConnectionHandler>(FindObjectsInactive.Include);
var connectionHandler = GetConnectionHandler();
if (connectionHandler == null) { return; }
var errorHandler = FindAnyObjectByType<ErrorHandler>(FindObjectsInactive.Include);
var errorHandler = GetErrorHandler();
if (errorHandler != null) { connectionHandler.errorHandler = errorHandler; }
var eagleCanvas = GetEagleCanvas();
if (eagleCanvas != null) { connectionHandler.eagleCanvas = eagleCanvas; }
var shardokContainer = GetShardokContainer();
if (shardokContainer != null) {
connectionHandler.shardokContainer = shardokContainer;
if (connectionHandler.customBattleHandler != null) {
connectionHandler.customBattleHandler.shardokContainer = shardokContainer;
}
}
}
private void WireEagleSceneReferences() {
var eagleController = FindAnyObjectByType<EagleGameController>(FindObjectsInactive.Include);
if (eagleController == null) { return; }
var errorHandler = GetErrorHandler();
if (errorHandler != null) { eagleController.errorHandler = errorHandler; }
var soundManager = GetSoundManager();
if (soundManager != null) {
eagleController.soundManager = soundManager;
WireEagleAudioSources(eagleController, soundManager.soundEffectsSource);
}
var mainCamera = GetMainCamera();
if (mainCamera != null) { WireEagleCanvasCameras(eagleController, mainCamera); }
ApplyEagleSceneLighting();
var shardokContainer = GetShardokContainer();
if (shardokContainer != null) { eagleController.shardokContainer = shardokContainer; }
}
private void WireShardokSceneReferences() {
var shardokController =
FindAnyObjectByType<ShardokGameController>(FindObjectsInactive.Include);
if (shardokController == null) { return; }
var soundManager = GetSoundManager();
if (soundManager != null) {
shardokController.soundManager = soundManager;
shardokController.audioClipSource = soundManager.soundEffectsSource;
}
var mainCamera = GetMainCamera();
if (mainCamera != null) {
shardokController.mainCamera = mainCamera;
WireShardokCanvasCameras(shardokController, mainCamera);
}
var eagleCanvas = GetEagleCanvas();
if (eagleCanvas != null) { shardokController.eagleCanvas = eagleCanvas; }
}
private static void WireEagleAudioSources(
EagleGameController eagleController,
AudioSource audioSource) {
if (audioSource == null) { return; }
if (eagleController.provinceActionAnimator != null) {
eagleController.provinceActionAnimator.audioSource = audioSource;
}
if (eagleController.mapController?.battleInProgressController != null) {
eagleController.mapController.battleInProgressController.audioSource = audioSource;
}
}
private void EnsureSharedInfrastructure() {
MainQueue.EnsureExists();
EnsureOAuthManager();
EnsureSoundManager();
WireSharedCanvasCameras();
WireSharedUIReferences();
}
private static void EnsureOAuthManager() {
@@ -117,4 +262,175 @@ public class GameSceneManager : MonoBehaviour {
var oauthManager = new GameObject("OAuth Manager");
oauthManager.AddComponent<OAuthManager>();
}
private SoundManager GetSoundManager() {
return FindAnyObjectByType<SoundManager>(FindObjectsInactive.Include);
}
private Camera GetMainCamera() {
return Camera.main ?? FindAnyObjectByType<Camera>(FindObjectsInactive.Include);
}
private void WireSharedCanvasCameras() {
var sharedScene = SceneManager.GetSceneByName(sharedSceneName);
if (!sharedScene.IsValid() || !sharedScene.isLoaded) { return; }
var mainCamera = GetMainCamera();
if (mainCamera == null) { return; }
foreach (var root in sharedScene.GetRootGameObjects()) {
foreach (var canvas in root.GetComponentsInChildren<Canvas>(includeInactive: true)) {
if (canvas.renderMode == RenderMode.ScreenSpaceOverlay ||
canvas.renderMode == RenderMode.WorldSpace) {
continue;
}
canvas.worldCamera = mainCamera;
}
}
}
private void WireSharedUIReferences() {
var settingsPanel =
FindAnyObjectByType<SettingsPanelController>(FindObjectsInactive.Include);
if (settingsPanel == null) { return; }
var soundManager = GetSoundManager();
if (soundManager != null) {
settingsPanel.soundEffectsSource = soundManager.soundEffectsSource;
settingsPanel.musicSource = soundManager.musicSource;
}
settingsPanel.hoveringTooltip =
FindAnyObjectByType<HoveringTooltip>(FindObjectsInactive.Include);
settingsPanel.animationTestController =
FindAnyObjectByType<AnimationTestController>(FindObjectsInactive.Include);
settingsPanel.provinceLabelsController =
FindAnyObjectByType<ProvinceLabelsController>(FindObjectsInactive.Include);
settingsPanel.hexGrid = FindAnyObjectByType<HexGrid>(FindObjectsInactive.Include);
settingsPanel.eagleGameController = GetEagleController();
settingsPanel.ShardokContainer = GetShardokContainer();
settingsPanel.connectionHandler =
FindAnyObjectByType<ConnectionHandler>(FindObjectsInactive.Include);
settingsPanel.bugReportPanel =
FindAnyObjectByType<BugReportPanelController>(FindObjectsInactive.Include);
settingsPanel.attributionsController =
FindAnyObjectByType<AttributionsController>(FindObjectsInactive.Include);
if (settingsPanel.bugReportPanel != null) {
settingsPanel.bugReportPanel.settingsPanel = settingsPanel.panel;
}
foreach (var keyListener in FindObjectsByType<SettingsKeyListener>(
FindObjectsInactive.Include)) {
keyListener.settingsPanelController = settingsPanel;
}
settingsPanel.ApplyPersistedSettings();
}
private static void WireShardokCanvasCameras(
ShardokGameController shardokController,
Camera mainCamera) {
foreach (var canvas in shardokController.GetComponentsInChildren<Canvas>(
includeInactive: true)) {
if (canvas.renderMode == RenderMode.WorldSpace) { continue; }
canvas.renderMode = RenderMode.ScreenSpaceCamera;
canvas.worldCamera = mainCamera;
}
}
private static void WireEagleCanvasCameras(
EagleGameController eagleController,
Camera mainCamera) {
foreach (var canvas in eagleController.GetComponentsInChildren<Canvas>(
includeInactive: true)) {
if (canvas.renderMode == RenderMode.WorldSpace) { continue; }
canvas.renderMode = RenderMode.ScreenSpaceCamera;
canvas.worldCamera = mainCamera;
}
}
private static void ApplyEagleSceneLighting() {
RenderSettings.ambientMode = AmbientMode.Flat;
RenderSettings.ambientLight = Color.white;
RenderSettings.ambientIntensity = 1f;
}
private void EnsureSoundManager() {
var existingSoundManager = GetSoundManager();
if (existingSoundManager != null) {
EnsureSoundManagerAudioSources(existingSoundManager);
return;
}
var soundManagerObject = new GameObject("Sound Manager");
var soundManager = soundManagerObject.AddComponent<SoundManager>();
EnsureSoundManagerAudioSources(soundManager);
var sharedScene = SceneManager.GetSceneByName(sharedSceneName);
if (sharedScene.IsValid() && sharedScene.isLoaded) {
SceneManager.MoveGameObjectToScene(soundManagerObject, sharedScene);
}
}
private static void EnsureSoundManagerAudioSources(SoundManager soundManager) {
if (soundManager.soundEffectsSource == null) {
soundManager.soundEffectsSource =
CreateAudioSourceChild(soundManager.transform, "Sound Effects Source");
}
if (soundManager.musicSource == null) {
soundManager.musicSource =
CreateAudioSourceChild(soundManager.transform, "Music Source");
}
}
private static AudioSource CreateAudioSourceChild(Transform parent, string name) {
var sourceObject = new GameObject(name);
sourceObject.transform.SetParent(parent, false);
return sourceObject.AddComponent<AudioSource>();
}
private static GameObject FindSceneRootByName(string sceneName, string rootName) {
var scene = SceneManager.GetSceneByName(sceneName);
if (!scene.IsValid() || !scene.isLoaded) { return null; }
return FindSceneRootByName(scene, rootName);
}
private static GameObject FindSceneRootByName(Scene scene, string rootName) {
foreach (var root in scene.GetRootGameObjects()) {
if (root.name == rootName) { return root; }
}
return null;
}
private static void LogSceneRoots(string sceneName) {
var scene = SceneManager.GetSceneByName(sceneName);
LogSceneRoots(scene);
}
private static void LogSceneRoots(Scene scene) {
if (!scene.IsValid()) {
Debug.LogError("Scene is not valid after load.");
return;
}
if (!scene.isLoaded) {
Debug.LogError($"Scene {scene.name} is valid but not loaded.");
return;
}
var rootNames = new List<string>();
foreach (var root in scene.GetRootGameObjects()) { rootNames.Add(root.name); }
Debug.LogError(
$"Scene {scene.name} is loaded but no EagleGameController was found. " +
$"Root objects: {string.Join(", ", rootNames)}");
}
}
@@ -1,53 +0,0 @@
using System.Collections;
using UnityEngine;
/// <summary>
/// Test script to verify scene loading works correctly.
/// This should be attached to a GameObject in Main.unity for testing.
/// </summary>
public class SceneLoadTester : MonoBehaviour {
void Start() {
Debug.Log("SceneLoadTester: Starting scene load test...");
StartCoroutine(TestSceneLoading());
}
IEnumerator TestSceneLoading() {
// Wait for SceneManager to initialize
yield return new WaitForSeconds(1f);
if (GameSceneManager.Instance == null) {
Debug.LogError("SceneLoadTester: GameSceneManager.Instance is null!");
yield break;
}
Debug.Log("SceneLoadTester: Testing Connection scene load...");
// Test loading Connection scene
var loadTask = GameSceneManager.Instance.LoadSceneAdditive("Connection");
yield return new WaitUntil(() => loadTask.IsCompleted);
if (loadTask.IsFaulted) {
Debug.LogError(
$"SceneLoadTester: Failed to load Connection scene: {loadTask.Exception}");
} else {
Debug.Log("SceneLoadTester: Connection scene loaded successfully!");
}
// Wait a bit
yield return new WaitForSeconds(2f);
// Test unloading Connection scene
Debug.Log("SceneLoadTester: Testing Connection scene unload...");
var unloadTask = GameSceneManager.Instance.UnloadScene("Connection");
yield return new WaitUntil(() => unloadTask.IsCompleted);
if (unloadTask.IsFaulted) {
Debug.LogError(
$"SceneLoadTester: Failed to unload Connection scene: {unloadTask.Exception}");
} else {
Debug.Log("SceneLoadTester: Connection scene unloaded successfully!");
}
Debug.Log("SceneLoadTester: Test complete!");
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: b3d8f9175cb2b4c4d929c4ee2e8f7058
@@ -1,100 +0,0 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
/// <summary>
/// Helper class to assist with migrating GameObjects from Gameplay.unity to separate scenes.
/// This should be run in the Unity Editor with Gameplay.unity loaded.
/// </summary>
public class SceneMigrationHelper : MonoBehaviour {
[Header("Analysis Results")]
public List<string> connectionUIObjects = new List<string>();
public List<string> eagleUIObjects = new List<string>();
public List<string> shardokUIObjects = new List<string>();
public List<string> sharedUIObjects = new List<string>();
[Header("Key GameObjects")]
public GameObject connectionCanvas;
public GameObject eagleCanvas;
public GameObject shardokContainer;
public GameObject errorCanvas;
/// <summary>
/// Run this in the Unity Editor to analyze which GameObjects belong to which scene.
/// </summary>
[ContextMenu("Analyze Scene Structure")]
public void AnalyzeSceneStructure() {
connectionUIObjects.Clear();
eagleUIObjects.Clear();
shardokUIObjects.Clear();
sharedUIObjects.Clear();
// Find key canvases
Canvas[] allCanvases = FindObjectsByType<Canvas>(FindObjectsInactive.Include);
foreach (Canvas canvas in allCanvases) {
string canvasName = canvas.gameObject.name.ToLower();
if (canvasName.Contains("connection") || canvasName.Contains("lobby")) {
connectionUIObjects.Add($"Canvas: {canvas.gameObject.name}");
AddChildrenToList(canvas.transform, connectionUIObjects, " ");
} else if (canvasName.Contains("eagle") && !canvasName.Contains("shardok")) {
eagleUIObjects.Add($"Canvas: {canvas.gameObject.name}");
AddChildrenToList(canvas.transform, eagleUIObjects, " ");
} else if (canvasName.Contains("shardok")) {
shardokUIObjects.Add($"Canvas: {canvas.gameObject.name}");
AddChildrenToList(canvas.transform, shardokUIObjects, " ");
} else if (
canvasName.Contains("error") || canvasName.Contains("popup") ||
canvasName.Contains("confirmation") || canvasName.Contains("overlay")) {
sharedUIObjects.Add($"Canvas: {canvas.gameObject.name}");
AddChildrenToList(canvas.transform, sharedUIObjects, " ");
}
}
// Find panels that might not be under canvases
GameObject[] allGameObjects = FindObjectsByType<GameObject>(FindObjectsInactive.Include);
foreach (GameObject go in allGameObjects) {
if (go.GetComponent<Canvas>() != null) continue; // Skip canvases we already processed
string goName = go.name.ToLower();
if (goName.Contains("connection panel") || goName.Contains("game selection panel") ||
goName.Contains("custom battle panel")) {
connectionUIObjects.Add($"Panel: {go.name}");
}
}
Debug.Log($"Analysis complete!");
Debug.Log($"Connection UI Objects: {connectionUIObjects.Count}");
Debug.Log($"Eagle UI Objects: {eagleUIObjects.Count}");
Debug.Log($"Shardok UI Objects: {shardokUIObjects.Count}");
Debug.Log($"Shared UI Objects: {sharedUIObjects.Count}");
}
private void AddChildrenToList(Transform parent, List<string> list, string indent) {
foreach (Transform child in parent) {
list.Add($"{indent}{child.name}");
// Don't recurse too deep to keep the list manageable
if (indent.Length < 6) { AddChildrenToList(child, list, indent + " "); }
}
}
/// <summary>
/// Logs which components reference GameObjects that might be in different scenes.
/// </summary>
[ContextMenu("Find Cross-Scene References")]
public void FindCrossSceneReferences() {
ConnectionHandler connectionHandler = FindAnyObjectByType<ConnectionHandler>();
if (connectionHandler != null) {
Debug.Log("ConnectionHandler references:");
Debug.Log($" - connectionPanel: {connectionHandler.connectionPanel?.name}");
Debug.Log($" - gameSelectionPanel: {connectionHandler.gameSelectionPanel?.name}");
Debug.Log($" - customBattlePanel: {connectionHandler.customBattlePanel?.name}");
Debug.Log($" - connectionCanvas: {connectionHandler.connectionCanvas?.name}");
Debug.Log($" - eagleCanvas: {connectionHandler.eagleCanvas?.name}");
Debug.Log($" - shardokContainer: {connectionHandler.shardokContainer?.name}");
}
}
}
@@ -1,2 +0,0 @@
fileFormatVersion: 2
guid: 3ee8c3dd45a034f97b05e47387df8c31
@@ -4,9 +4,8 @@ using UnityEngine.UI;
using System.Collections.Generic;
/// <summary>
/// Simple error handler for the new scene system.
/// This provides basic error display functionality without the complexity of the original
/// ErrorHandler.
/// Simple shared-scene error handler.
/// This mirrors Unity errors into the lightweight shared error display.
/// </summary>
public class SimpleErrorHandler : MonoBehaviour {
[Header("UI References")]

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