feat(@projects/@magic-civilization): update ai path handling and export pipeline docs

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Natalie 2026-04-25 18:44:25 -07:00
parent 5aa848f545
commit b25be795ee
9 changed files with 119 additions and 34 deletions

View file

@ -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) | 🟢 |

View file

@ -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

View file

@ -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** |
</td><td valign='top' style='padding-left:2em'>
@ -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 |

View file

@ -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 `<dir>/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",

View file

@ -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 `<dir>/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

View file

@ -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 <version>` produces archives per-platform under `.local/build/godot/<version>/`. 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 <version>` produces archives per-platform under `.local/build/godot/<version>/`. 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

View file

@ -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 = {}

View file

@ -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]

View file

@ -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<Self, LoadError> {
let personalities: HashMap<String, PersonalityDef> =
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("<inline-json>"),
source,
})?;
let p = personalities