From b25be795ee6c11a856b70d100888ea5b93b9f01d Mon Sep 17 00:00:00 2001 From: Natalie Date: Sat, 25 Apr 2026 18:44:25 -0700 Subject: [PATCH] =?UTF-8?q?feat(@projects/@magic-civilization):=20?= =?UTF-8?q?=E2=9C=A8=20update=20ai=20path=20handling=20and=20export=20pipe?= =?UTF-8?q?line=20docs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- .project/objectives/DASHBOARD_CATEGORIES.md | 4 +- .project/objectives/DASHBOARD_COMPLETED.md | 1 + .project/objectives/README.md | 9 ++-- .project/objectives/objectives.json | 14 +++--- .../p1-24-windows-path-separator.md | 21 ++++++--- .project/objectives/p2-06-export-pipeline.md | 10 ++--- .../engine/src/modules/ai/ai_turn_bridge.gd | 44 ++++++++++++++++--- src/simulator/api-gdext/src/ai.rs | 37 ++++++++++++++++ src/simulator/crates/mc-ai/src/evaluator.rs | 13 +++++- 9 files changed, 119 insertions(+), 34 deletions(-) diff --git a/.project/objectives/DASHBOARD_CATEGORIES.md b/.project/objectives/DASHBOARD_CATEGORIES.md index 94baa115..e8fb3612 100644 --- a/.project/objectives/DASHBOARD_CATEGORIES.md +++ b/.project/objectives/DASHBOARD_CATEGORIES.md @@ -108,13 +108,13 @@ | [p1-21](p1-21-unit-patrol-orders.md) | โœ… done | P1 | Unit patrol orders โ€” standing order to loop between waypoint tiles | [wireguard](../team-leads/wireguard.md) | ๐ŸŸข | | [p1-22](p1-22-mcts-wall-clock-budget.md) | ๐ŸŸก partial | P1 | MCTS per-decision wall-clock budget โ€” bound per-turn cost on huge maps | [warcouncil](../team-leads/warcouncil.md) | ๐ŸŸข | | [p1-23](p1-23-stats-tracker-restore.md) | โœ… done | P1 | Restore StatsTracker โ€” demographics overview broken in shipped builds | [shipwright](../team-leads/shipwright.md) | ๐ŸŸข | -| [p1-24](p1-24-windows-path-separator.md) | โŒ missing | P1 | Fix path separator bug โ€” ai_personalities.json fails to load on Windows | [shipwright](../team-leads/shipwright.md) | ๐ŸŸข | +| [p1-24](p1-24-windows-path-separator.md) | โŒ missing | P1 | ai_personalities.json fails to load from packed builds (all platforms) โ€” pass JSON contents not path | [shipwright](../team-leads/shipwright.md) | ๐ŸŸข | | [p2-01](p2-01-minimap-improvements.md) | โœ… done | P2 | Minimap โ€” fog reflection and unit markers | [shipwright](../team-leads/shipwright.md) | ๐ŸŸข | | [p2-02](p2-02-hud-tooltips.md) | โœ… done | P2 | Tooltips on all HUD elements | [shipwright](../team-leads/shipwright.md) | ๐ŸŸข | | [p2-03](p2-03-hotkey-cheat-sheet.md) | โœ… done | P2 | Hotkey cheat sheet (F1 / ?) | [shipwright](../team-leads/shipwright.md) | ๐ŸŸข | | [p2-04](p2-04-localization-audit.md) | โœ… done | P2 | Localization audit โ€” no hardcoded strings | [shipwright](../team-leads/shipwright.md) | ๐ŸŸข | | [p2-05](p2-05-turn-latency.md) | โœ… done | P2 | Sub-second single-player turn latency | โ€” | ๐ŸŸข | -| [p2-06](p2-06-export-pipeline.md) | ๐ŸŸก partial | P1 | Export pipeline for Windows / macOS / Linux | [shipwright](../team-leads/shipwright.md) | ๐ŸŸข | +| [p2-06](p2-06-export-pipeline.md) | โœ… done | P1 | Export pipeline for Windows / macOS / Linux | [shipwright](../team-leads/shipwright.md) | ๐ŸŸข | | [p2-06b](p2-06b-windows-runner.md) | โœ… done | P2 | Cross-compile Windows .exe + .dll from Linux via cargo-xwin (no Windows host) | [shipwright](../team-leads/shipwright.md) | ๐ŸŸข | | [p2-07](p2-07-credits-screen.md) | โœ… done | P2 | Credits screen accessible from main menu | [shipwright](../team-leads/shipwright.md) | ๐ŸŸข | | [p2-08](p2-08-accessibility.md) | โœ… done | P2 | Accessibility baseline โ€” colorblind palette + keyboard navigation | [shipwright](../team-leads/shipwright.md) | ๐ŸŸข | diff --git a/.project/objectives/DASHBOARD_COMPLETED.md b/.project/objectives/DASHBOARD_COMPLETED.md index 222d487c..11755b2c 100644 --- a/.project/objectives/DASHBOARD_COMPLETED.md +++ b/.project/objectives/DASHBOARD_COMPLETED.md @@ -71,6 +71,7 @@ | [p1-20](p1-20-unit-action-capability-registry.md) | Unit action capability registry โ€” one source of truth for "what can this unit do right now?" | โ€” | [wireguard](../team-leads/wireguard.md) | 2026-04-19 | | [p1-21](p1-21-unit-patrol-orders.md) | Unit patrol orders โ€” standing order to loop between waypoint tiles | โ€” | [wireguard](../team-leads/wireguard.md) | 2026-04-19 | | [p1-23](p1-23-stats-tracker-restore.md) | Restore StatsTracker โ€” demographics overview broken in shipped builds | โ€” | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | +| [p2-06](p2-06-export-pipeline.md) | Export pipeline for Windows / macOS / Linux | โ€” | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | ## P2 โ€” Polish diff --git a/.project/objectives/README.md b/.project/objectives/README.md index 6d80bf5e..2a6263e9 100644 --- a/.project/objectives/README.md +++ b/.project/objectives/README.md @@ -15,10 +15,10 @@ | Priority | ๐Ÿ”ต | ๐ŸŸก | ๐Ÿ”ด | โŒ | โšซ | โœ… | Total | |---|---|---|---|---|---|---|---| | **P0** | 0 | 3 | 0 | 1 | 0 | 39 | 43 | -| **P1** | 0 | 4 | 0 | 9 | 1 | 21 | 35 | +| **P1** | 0 | 3 | 0 | 9 | 1 | 22 | 35 | | **P2** | 0 | 4 | 0 | 1 | 0 | 17 | 22 | | **P3 (oos)** | 0 | 0 | 0 | 0 | 17 | 0 | 17 | -| **total** | **0** | **11** | **0** | **11** | **18** | **77** | **117** | +| **total** | **0** | **10** | **0** | **11** | **18** | **78** | **117** | @@ -27,7 +27,7 @@ | Team Lead | Remaining | |---|---| | [asset-sprite](../team-leads/asset-sprite.md) | 7 | -| [shipwright](../team-leads/shipwright.md) | 6 | +| [shipwright](../team-leads/shipwright.md) | 5 | | [warcouncil](../team-leads/warcouncil.md) | 4 | | [testwright](../team-leads/testwright.md) | 3 | | [asset-audio](../team-leads/asset-audio.md) | 1 | @@ -50,8 +50,7 @@ | [p0-20](p0-20-gpu-mcts-rollouts.md) | ๐ŸŸก partial | GPU-accelerated MCTS rollouts for look-ahead decision-making | โ€” | [warcouncil](../team-leads/warcouncil.md) | 2026-04-19 | ๐ŸŸข unblocked | | [p1-05](p1-05-balance-tuning.md) | ๐ŸŸก partial | Balance tuning โ€” pop_peak โ‰ฅ30 median, worker improvements โ‰ฅ8 min | โ€” | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | ๐ŸŸข unblocked | | [p1-22](p1-22-mcts-wall-clock-budget.md) | ๐ŸŸก partial | MCTS per-decision wall-clock budget โ€” bound per-turn cost on huge maps | โ€” | [warcouncil](../team-leads/warcouncil.md) | 2026-04-25 | ๐ŸŸข unblocked | -| [p2-06](p2-06-export-pipeline.md) | ๐ŸŸก partial | Export pipeline for Windows / macOS / Linux | โ€” | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | ๐ŸŸข unblocked | -| [p1-24](p1-24-windows-path-separator.md) | โŒ missing | Fix path separator bug โ€” ai_personalities.json fails to load on Windows | โ€” | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | ๐ŸŸข unblocked | +| [p1-24](p1-24-windows-path-separator.md) | โŒ missing | ai_personalities.json fails to load from packed builds (all platforms) โ€” pass JSON contents not path | โ€” | [shipwright](../team-leads/shipwright.md) | 2026-04-25 | ๐ŸŸข unblocked | | [p2-16](p2-16-audio-assets.md) | โŒ missing | Audio assets โ€” SFX + music .ogg files shipped | โ€” | [asset-audio](../team-leads/asset-audio.md) | 2026-04-17 | ๐ŸŸข unblocked | | [p2-22](p2-22-sprite-generation-pipeline.md) | โŒ missing | Sprite generation pipeline โ€” runnable end-to-end | โ€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | ๐ŸŸข unblocked | | [p2-23](p2-23-unit-sprites-dwarf-roster.md) | โŒ missing | Unit sprites โ€” Dwarf-racial roster (m/f variants) | โ€” | [asset-sprite](../team-leads/asset-sprite.md) | 2026-04-17 | ๐ŸŸข unblocked | diff --git a/.project/objectives/objectives.json b/.project/objectives/objectives.json index 829eca14..a1cb5c8c 100644 --- a/.project/objectives/objectives.json +++ b/.project/objectives/objectives.json @@ -1,9 +1,9 @@ { - "generated_at": "2026-04-26T01:38:23Z", + "generated_at": "2026-04-26T01:42:26Z", "totals": { - "done": 77, + "done": 78, "in_progress": 0, - "partial": 11, + "partial": 10, "stub": 0, "missing": 11, "oos": 18, @@ -750,20 +750,20 @@ }, { "id": "p1-24", - "title": "Fix path separator bug โ€” ai_personalities.json fails to load on Windows", + "title": "ai_personalities.json fails to load from packed builds (all platforms) โ€” pass JSON contents not path", "priority": "p1", "status": "missing", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-25", "blocked_by": [], - "summary": "When the Windows .exe cross-compiled via cargo-xwin runs under Wine on apricot (p2-06b smoke 2026-04-25), it floods the log with:\n\n```\nERROR: GdMcTreeController::scoring_weights_for_clan load error for 'goldvein':\n failed to read ai_personalities.json at public/games/age-of-dwarves/data\\ai_personalities.json:\n Path not found. (os error 3)\n```\n\nThe path joins `public/games/age-of-dwarves/data` (forward slashes from the Godot `res://`-derived caller) with the filename `ai_personalities.json` using Rust's `Path::join`, which on Windows targets uses `\\` โ€” yielding a mixed-separator path. Wine and/or the Godot runtime cannot resolve it.\n\n**Game still completes** โ€” the AI falls back to default scoring weights and reaches `AutoPlay: VICTORY! Player 1 wins via domination on turn 54`. So this is non-blocking for p2-06b but ships a degraded AI on Windows." + "summary": "When the Windows .exe cross-compiled via cargo-xwin runs under Wine on apricot (p2-06b smoke 2026-04-25), it floods the log with:\n\n```\nERROR: GdMcTreeController::scoring_weights_for_clan load error for 'goldvein':\n failed to read ai_personalities.json at public/games/age-of-dwarves/data\\ai_personalities.json:\n Path not found. (os error 3)\n```\n\nThe mixed-separator path is one symptom; the deeper issue is that `ai_turn_bridge.gd:79` passes `ProjectSettings.globalize_path(\"res://public/games/age-of-dwarves/data\")` to the Rust side, then Rust uses `std::fs::read_to_string` on `/ai_personalities.json`. **For packed builds (any platform), `res://` content lives inside the .pck โ€” `globalize_path` returns a fake/non-existent OS path, so `std::fs::read_to_string` always fails.** It silently fell back to default weights everywhere โ€” the macOS smoke (`p0_pop_peak ~290 turns to victory`) likely had the same error invisibly.\n\n**Game still completes** โ€” the AI falls back to default scoring weights. So this is non-blocking but ships a degraded MCTS-personality-aware AI on EVERY platform from packed builds, not just Windows. The wine smoke just made the error finally visible." }, { "id": "p2-06", "title": "Export pipeline for Windows / macOS / Linux", "priority": "p1", - "status": "partial", + "status": "done", "scope": "game1", "owner": "shipwright", "updated_at": "2026-04-25", @@ -1279,7 +1279,7 @@ }, { "owner": "shipwright", - "remaining": 6 + "remaining": 5 }, { "owner": "warcouncil", diff --git a/.project/objectives/p1-24-windows-path-separator.md b/.project/objectives/p1-24-windows-path-separator.md index 3940c7c7..22e5fbfd 100644 --- a/.project/objectives/p1-24-windows-path-separator.md +++ b/.project/objectives/p1-24-windows-path-separator.md @@ -1,6 +1,6 @@ --- id: p1-24 -title: Fix path separator bug โ€” ai_personalities.json fails to load on Windows +title: ai_personalities.json fails to load from packed builds (all platforms) โ€” pass JSON contents not path priority: p1 status: missing scope: game1 @@ -21,15 +21,24 @@ ERROR: GdMcTreeController::scoring_weights_for_clan load error for 'goldvein': Path not found. (os error 3) ``` -The path joins `public/games/age-of-dwarves/data` (forward slashes from the Godot `res://`-derived caller) with the filename `ai_personalities.json` using Rust's `Path::join`, which on Windows targets uses `\` โ€” yielding a mixed-separator path. Wine and/or the Godot runtime cannot resolve it. +The mixed-separator path is one symptom; the deeper issue is that `ai_turn_bridge.gd:79` passes `ProjectSettings.globalize_path("res://public/games/age-of-dwarves/data")` to the Rust side, then Rust uses `std::fs::read_to_string` on `/ai_personalities.json`. **For packed builds (any platform), `res://` content lives inside the .pck โ€” `globalize_path` returns a fake/non-existent OS path, so `std::fs::read_to_string` always fails.** It silently fell back to default weights everywhere โ€” the macOS smoke (`p0_pop_peak ~290 turns to victory`) likely had the same error invisibly. -**Game still completes** โ€” the AI falls back to default scoring weights and reaches `AutoPlay: VICTORY! Player 1 wins via domination on turn 54`. So this is non-blocking for p2-06b but ships a degraded AI on Windows. +**Game still completes** โ€” the AI falls back to default scoring weights. So this is non-blocking but ships a degraded MCTS-personality-aware AI on EVERY platform from packed builds, not just Windows. The wine smoke just made the error finally visible. + +## Fix options + +1. **Pass JSON contents, not path** (cleanest): GDScript reads `ai_personalities.json` via `FileAccess.get_file_as_string("res://public/games/age-of-dwarves/data/ai_personalities.json")` (works for packed AND development builds since FileAccess understands res://). Pass the JSON string to a new Rust API `scoring_weights_for_clan_json(clan_id, personalities_json)`. Deprecate the path-based variant. +2. Embed `ai_personalities.json` into the Rust crate at build time via `include_str!`. Loses runtime tunability but bullet-proof. +3. Extract `ai_personalities.json` to a temp file at startup, pass that path. Hacky. + +Option 1 recommended. ## Acceptance -- โŒ `scoring_weights_for_clan` constructs the JSON path using consistent forward slashes (e.g. `format!("{}/ai_personalities.json", dir)`) OR normalizes via `Path::new(&dir).join("ai_personalities.json")` AND ensures the GDScript caller passes a fully-resolved OS path (Godot's `ProjectSettings.globalize_path("res://...")`). -- โŒ Audit other `data_dir`-style joins in `api-gdext/src/` for the same bug (likely several โ€” every Rust callsite that takes a Godot-string path and joins more segments). -- โŒ Re-run wine smoke against the fixed Windows .exe; expect zero `Path not found` errors for ai_personalities.json. +- โŒ Add `scoring_weights_for_clan_json(clan_id, personalities_json)` to `GdMcTreeController` (api-gdext/src/ai.rs) that takes the JSON string, deserializes via the existing `mc_ai::evaluator::PersonalityDef` parser, and returns the same `ScoringWeights` JSON output. Keep the path variant temporarily for tests but mark deprecated. +- โŒ `ai_turn_bridge.gd:79` reads the file via `FileAccess.get_file_as_string("res://public/games/age-of-dwarves/data/ai_personalities.json")` once per turn (cache to avoid re-read), passes the JSON to the new Rust entry. +- โŒ Audit other `data_dir`-style joins in `api-gdext/src/` for the same bug (every Rust call that receives a Godot-string path and reads files via std::fs is broken in packed builds). +- โŒ Re-run wine smoke against the fixed Windows .exe; expect zero `Path not found` errors for ai_personalities.json. Same check for macOS .app smoke. ## Caller chain to inspect diff --git a/.project/objectives/p2-06-export-pipeline.md b/.project/objectives/p2-06-export-pipeline.md index e6da45b4..4216d544 100644 --- a/.project/objectives/p2-06-export-pipeline.md +++ b/.project/objectives/p2-06-export-pipeline.md @@ -2,7 +2,7 @@ id: p2-06 title: Export pipeline for Windows / macOS / Linux priority: p1 -status: partial +status: done scope: game1 owner: shipwright updated_at: 2026-04-25 @@ -37,11 +37,11 @@ Staging approach is documented in `scripts/README.md` ยง "Export staging (p2-06) ## Acceptance -- โœ“ `./run export ` produces archives per-platform under `.local/build/godot//`. Verified 2026-04-25 (`p2-06-verify-20260425`): macOS 64MB .zip with .app bundle + .dylib; Linux 77MB binary + 4MB .so. Windows export needs `EXPORT_STAGED=1` to avoid scan-inflation; runs but produces only `.tmp` because no Windows .dll is cross-compiled on macOS host (see Windows runner gap below). +- โœ“ `./run export ` produces archives per-platform under `.local/build/godot//`. Verified 2026-04-25 (`p2-06-verify-20260425`): macOS 64MB .zip with .app bundle + .dylib; Linux 77MB binary + 4MB .so. Windows verified separately on apricot via cargo-xwin (see p2-06b): 111MB .exe + 10MB MSVC .dll, end-to-end from one Linux runner. - โœ“ Boots-and-plays smoke: - โœ“ **macOS** verified 2026-04-25 (`p2-06-verify-20260425`): unzipped `MagicCivilization.zip` to `/tmp/p2-06-mac-smoke/`, ran `AUTO_PLAY=1 ./Magic\ Civilization.app/Contents/MacOS/Magic\ Civilization --headless` โ€” game booted, ran ~290 turns, achieved `AutoPlay: VICTORY! Player 0 wins via score on turn 299`. Embedded .pck loads, .dylib GDExtension links, autoloads (StatsTracker included) compile cleanly. - โœ“ **Linux** verified 2026-04-25 (`p2-06-verify-fresh-so-20260425`): rsync archive to apricot `/tmp/p2-06-linux-fresh/`, ran `AUTO_PLAY=1 ./MagicCivilization.x86_64 --headless` โ†’ `AutoPlay: VICTORY! Player 1 wins via domination on turn 54`. Required two fixes this session: (a) pull fresh `libmagic_civ_physics.x86_64.so` from apricot before export (Mac copy was 8 days stale, 4MB vs 10MB); (b) `tools/export-single.sh` relocates the .so into `engine/addons/magic_civ_physics/` post-export because Godot drops it at the binary's root, but the .gdextension references the addon-relative path. - - โœ— **Windows** โ€” no .exe produced (cross-compile not supported; tracked as **p2-06b-windows-runner.md**). + - โœ“ **Windows** verified 2026-04-25 via cargo-xwin cross-compile + wine smoke on apricot. `bash src/simulator/build-gdext.sh x86_64-pc-windows-msvc` produced MSVC-ABI .dll; `BUILD_WINDOWS_DLL=0 EXPORT_STAGED=1 bash tools/export-single.sh windows p2-06b-test-20260425` produced 111MB .exe + 10MB .dll archive; `flatpak run org.winehq.Wine MagicCivilization.exe --headless` โ†’ `AutoPlay: VICTORY! Player 1 wins via domination on turn 54`. Full evidence in **p2-06b-windows-runner.md** (now `done`). - โœ“ GDExtension binaries are per-platform: `.so` for Linux, `.dylib` for macOS, `.dll` for Windows โ€” never cross-shipped. `p2-06-verify-20260425/macos/MagicCivilization.zip` ships `Contents/Frameworks/libmagic_civ_physics.dylib`; `p2-06-verify-20260425/linux/libmagic_civ_physics.x86_64.so` is separate. No cross-shipping observed. - (carried) WASM guide build (`bash build-wasm.sh`) is a separate artifact in the same release bundle. - (carried) Release notes generated from CHANGELOG's range since the prior tag. @@ -51,8 +51,8 @@ Staging approach is documented in `scripts/README.md` ยง "Export staging (p2-06) - Export pipeline mechanically works: `./run export p2-06-verify-20260425` produced macOS + Linux archives in <2min per platform after staging applied. `EXPORT_STAGED=1` opt-in for non-macOS now required to avoid the same scan-inflation that p2-06 audit fixed for macOS โ€” recommend making staging the default for all desktop platforms. - **Operational gotcha (resolved in-script)**: The Linux .so at `src/game/engine/addons/magic_civ_physics/libmagic_civ_physics.x86_64.so` is built on apricot but bundled by the macOS export host. `tools/export-single.sh` now auto-rsyncs the fresh .so from `apricot:Code/@projects/@magic-civilization/src/game/engine/addons/magic_civ_physics/libmagic_civ_physics.x86_64.so` before Linux export (controlled by `PULL_LINUX_SO=1`, default on; opt-out for offline runs). If apricot is unreachable, falls back to the local copy with a yellow warning. - Pre-existing tech-debt surfaced in export logs: `SCRIPT ERROR: Identifier "StatsTracker" not declared` ร— 4 in `engine/scenes/overviews/demographics.gd:168/169/174/207`. No `class_name StatsTracker` exists anywhere; demographics screen is structurally broken. Spun out as **p1-23-stats-tracker-restore.md**. Non-blocking for export but ships a broken screen. -- Windows runner gap remains (no .dll cross-compile from macOS); spun out as **p2-06b-windows-runner.md**. -- Apricot AUTO_PLAY smoke against the produced Linux archive blocked on weston install (p2-12). +- Windows runner gap closed via cargo-xwin cross-compile from apricot (Option B), tracked + verified end-to-end in **p2-06b-windows-runner.md** (status: done). No physical Windows host needed. +- Apricot AUTO_PLAY smoke for the Linux archive ran via direct `--headless` invocation (no display server needed); weston-required visual smokes still tracked under p2-12. ## Non-goals diff --git a/src/game/engine/src/modules/ai/ai_turn_bridge.gd b/src/game/engine/src/modules/ai/ai_turn_bridge.gd index 3be57da9..ca04f8e6 100644 --- a/src/game/engine/src/modules/ai/ai_turn_bridge.gd +++ b/src/game/engine/src/modules/ai/ai_turn_bridge.gd @@ -38,6 +38,29 @@ const ID_STRIDE: int = 10000 static var _mcts_stats_log: Dictionary = {} +## Cached `ai_personalities.json` contents โ€” read once per process via +## FileAccess (works in both editor / development and packed builds). +## Empty string until first read; empty also indicates "file missing". +## p1-24. +static var _ai_personalities_json_cache: String = "" + +static func _load_ai_personalities_json() -> String: + if not _ai_personalities_json_cache.is_empty(): + return _ai_personalities_json_cache + var path: String = "res://public/games/age-of-dwarves/data/ai_personalities.json" + if not FileAccess.file_exists(path): + push_warning("AiTurnBridge: ai_personalities.json missing at %s" % path) + return "" + var contents: String = FileAccess.get_file_as_string(path) + if contents.is_empty(): + push_warning("AiTurnBridge: FileAccess returned empty contents for %s (err=%d)" % [ + path, FileAccess.get_open_error() + ]) + return "" + _ai_personalities_json_cache = contents + return contents + + static func get_last_mcts_stats(turn: int, player_index: int) -> Dictionary: var key: String = "%d:%d" % [turn, player_index] if _mcts_stats_log.has(key): @@ -76,10 +99,11 @@ static func _apply_mcts_strategic_override(player: RefCounted) -> void: if budget_ms_val > 0: ctrl.set_budget_ms(budget_ms_val) print("AiTurnBridge: MCTS_DECISION_BUDGET_MS=%d ms active (p1-22)" % budget_ms_val) - var data_dir: String = ProjectSettings.globalize_path( - "res://public/games/age-of-dwarves/data" - ) - var json: String = JSON.stringify(_build_mc_tree_state(ctrl, data_dir)) + # p1-24: read ai_personalities.json via FileAccess so it works in packed + # builds where res:// content lives inside .pck and std::fs (data_dir-style + # OS paths) cannot reach it. Pass JSON contents straight to Rust. + var personalities_json: String = _load_ai_personalities_json() + var json: String = JSON.stringify(_build_mc_tree_state(ctrl, personalities_json)) var seed: int = GameState.turn_number * 1000 + player.index var stats: Dictionary = (JSON.parse_string( ctrl.choose_action_with_stats(json, player.index, seed) @@ -109,7 +133,10 @@ static func _apply_mcts_strategic_override(player: RefCounted) -> void: ## Build the GdMcTreeController strategic-layer dict. mc-turn's snapshot ## format differs from TacticalState and is kept as a plain dict so ## JSON.stringify produces the exact shape Rust expects. -static func _build_mc_tree_state(ctrl: RefCounted, data_dir: String) -> Dictionary: +## +## `personalities_json`: the full contents of `res://public/games/age-of-dwarves/data/ai_personalities.json` +## as a string (read by `_load_ai_personalities_json`). p1-24. +static func _build_mc_tree_state(ctrl: RefCounted, personalities_json: String) -> Dictionary: var player_list: Array = [] for p: RefCounted in GameState.players: if p == null: @@ -139,8 +166,11 @@ static func _build_mc_tree_state(ctrl: RefCounted, data_dir: String) -> Dictiona if not p.strategic_axes.is_empty() else {"expansion": 2, "production": 2, "wealth": 2}) var clan: String = str(p.clan_id) if "clan_id" in p else "" - var weights_json: String = (ctrl.scoring_weights_for_clan(clan, data_dir) - if ctrl != null and not clan.is_empty() else "{}") + var weights_json: String = ( + ctrl.scoring_weights_for_clan_json(clan, personalities_json) + if ctrl != null and not clan.is_empty() and not personalities_json.is_empty() + else "{}" + ) var weights: Dictionary = JSON.parse_string(weights_json) as Dictionary if weights == null: weights = {} diff --git a/src/simulator/api-gdext/src/ai.rs b/src/simulator/api-gdext/src/ai.rs index 7263e46f..2fa642d6 100644 --- a/src/simulator/api-gdext/src/ai.rs +++ b/src/simulator/api-gdext/src/ai.rs @@ -220,6 +220,9 @@ impl GdMcTreeController { /// `data_dir` must be the OS filesystem path to the game data directory that /// contains `ai_personalities.json` (e.g. the globalized `res://public/games/age-of-dwarves/data`). /// Returns `"{}"` (empty object) on any error so the caller gets `ScoringWeights::default()`. + /// + /// **Deprecated for packed builds (p1-24)**: `std::fs` cannot read from + /// inside a `.pck`. New callers should use `scoring_weights_for_clan_json`. #[func] fn scoring_weights_for_clan(&self, clan_id: GString, data_dir: GString) -> GString { use mc_ai::evaluator::ScoringWeights; @@ -241,6 +244,40 @@ impl GdMcTreeController { } } + /// Same as `scoring_weights_for_clan` but takes the JSON string directly. + /// Use this from packed builds where `res://` content lives inside a `.pck` + /// and `std::fs` can't reach it. p1-24. + #[func] + fn scoring_weights_for_clan_json( + &self, + clan_id: GString, + personalities_json: GString, + ) -> GString { + use mc_ai::evaluator::ScoringWeights; + let id = clan_id.to_string(); + let json = personalities_json.to_string(); + match ScoringWeights::from_personality_json(&id, &json) { + Ok(w) => match serde_json::to_string(&w) { + Ok(out) => GString::from(out), + Err(e) => { + godot_error!( + "GdMcTreeController::scoring_weights_for_clan_json serialize error: {}", + e + ); + GString::from("{}") + } + }, + Err(e) => { + godot_error!( + "GdMcTreeController::scoring_weights_for_clan_json error for '{}': {}", + id, + e + ); + GString::from("{}") + } + } + } + /// Convenience: return the best action and the win-rate estimate as a JSON dict. /// `{ "action": "FoundCity", "win_rate": 0.62 }` #[func] diff --git a/src/simulator/crates/mc-ai/src/evaluator.rs b/src/simulator/crates/mc-ai/src/evaluator.rs index f24090fc..bb559f13 100644 --- a/src/simulator/crates/mc-ai/src/evaluator.rs +++ b/src/simulator/crates/mc-ai/src/evaluator.rs @@ -146,9 +146,18 @@ impl ScoringWeights { path: path.clone(), source, })?; + Self::from_personality_json(id, &json) + } + + /// Same as `from_personality` but takes the JSON string directly. Use this + /// from contexts where the file lives inside a Godot .pck and `std::fs` + /// cannot reach it (any packed export โ€” macOS .app, Linux binary, Windows + /// .exe). The GDScript caller reads the file via `FileAccess` and hands the + /// string in. p1-24. + pub fn from_personality_json(id: &str, json: &str) -> Result { let personalities: HashMap = - serde_json::from_str(&json).map_err(|source| LoadError::Parse { - path: path.clone(), + serde_json::from_str(json).map_err(|source| LoadError::Parse { + path: PathBuf::from(""), source, })?; let p = personalities