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
|