feat(@projects/@magic-civilization): 🦌 p3-27 — FFI + harness to boot the headless biosphere

Runtime wiring so the ecology phase actually ticks in a real headless game (was a no-op
until the species library was supplied):
- GdPlayerApi::set_ecology_species_json (api-gdext) — loads the fauna species library (JSON
  array of per-species file contents) onto GameState. Mirrors set_events_config_json; call
  after load_state_json (the field is #[serde(skip)]).
- player_api_main._apply_ecology_species — reads public/resources/ecology/fauna/species/*.json
  into an array + stamps it via the FFI at boot (right after _apply_events_config), emitting
  ecology_species_api_loaded. Mirrors the live EcologyState species load.

gdext compiles clean. Dylib rebuild in progress.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Natalie 2026-06-26 17:17:38 -04:00
parent b984143e60
commit 2a0777e183
2 changed files with 43 additions and 0 deletions

View file

@ -234,6 +234,11 @@ func _hydrate_player_api(num_players: int) -> void:
# `load_state_json`. Without this `events_config` is empty and no events fire.
_apply_events_config()
# p3-27 biosphere: stamp the fauna species library so the headless ecology phase
# can seed + emerge a living world. Same `#[serde(skip)]` re-stamp pattern — load
# AFTER `load_state_json`. Without this the ecology phase is a no-op (barren world).
_apply_ecology_species()
## p3-26 gap 2: stamp the natural-event category configs (DataLoader's merged
## `{category: {base_frequency, severity_weights, tiers, …}}`) onto `GdPlayerApi` via
@ -247,6 +252,33 @@ func _apply_events_config() -> void:
_emit_event("events_config_api_loaded", {"categories": n})
## p3-27 biosphere: read every fauna species JSON file and stamp the array onto
## `GdPlayerApi` via `set_ecology_species_json`. Consumed by `mc-player-api`'s
## `process_ecology_phase` (apply_end_turn) to build the EcologyEngine species
## library. Mirrors the live `EcologyState` species load.
func _apply_ecology_species() -> void:
const SPECIES_DIR: String = "res://public/resources/ecology/fauna/species"
var dir: DirAccess = DirAccess.open(SPECIES_DIR)
if dir == null:
_emit_protocol_error("ecology species dir not found — headless ecology will not tick")
return
var species_jsons: Array = []
dir.list_dir_begin()
var name: String = dir.get_next()
while name != "":
if not dir.current_is_dir() and name.ends_with(".json"):
var raw: String = FileAccess.get_file_as_string("%s/%s" % [SPECIES_DIR, name])
if raw != "":
species_jsons.append(raw)
name = dir.get_next()
dir.list_dir_end()
if species_jsons.is_empty():
_emit_protocol_error("ecology species dir empty — headless ecology will not tick")
return
var n: int = int(_api.set_ecology_species_json(JSON.stringify(species_jsons)))
_emit_event("ecology_species_api_loaded", {"species": n})
## p3-25: build the resource id→category map ("luxury" | "strategic" | "bonus")
## from DataLoader and stamp it onto `GdPlayerApi` via
## `set_resource_categories_json`. Consumed by `mc-turn::process_trade_phase`.

View file

@ -168,6 +168,17 @@ impl GdPlayerApi {
.load_events_config_json(json.to_string().as_str()) as i64
}
/// p3-27: load the fauna species library (a JSON array of per-species file
/// contents, e.g. `["{…grey_wolf…}","{…rabbit…}"]`) so the headless ecology
/// phase can build the `EcologyEngine` species library and emerge a living
/// biosphere. Returns the count loaded, or 0 on parse failure. Call AFTER
/// `load_state_json` (the field is `#[serde(skip)]`, not restored by a load).
#[func]
pub fn set_ecology_species_json(&mut self, json: GString) -> i64 {
self.state
.load_ecology_species_json(json.to_string().as_str()) as i64
}
/// Stamp the runtime `UnitsCatalog` (id → `UnitStats`) onto the held
/// `GameState`. Distinct from `set_units_catalog_json` (which loads the
/// tactical `ai_unit_catalog`): this is the same `mc_units::UnitsCatalog`