feat(api-gdext): ✨ Introduce new extension API endpoints for simulator core interactions
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
324ccf93c9
commit
171d69bdb6
2 changed files with 409 additions and 0 deletions
15
src/simulator/api-gdext/Cargo.toml
Normal file
15
src/simulator/api-gdext/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "magic-civ-physics-gdext"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
mc-core = { path = "../crates/mc-core" }
|
||||
mc-climate = { path = "../crates/mc-climate" }
|
||||
mc-mapgen = { path = "../crates/mc-mapgen" }
|
||||
godot = "0.2"
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
394
src/simulator/api-gdext/src/lib.rs
Normal file
394
src/simulator/api-gdext/src/lib.rs
Normal file
|
|
@ -0,0 +1,394 @@
|
|||
/// GDExtension API surface — exposes Rust simulation to Godot via godot-rust (gdext v0.2).
|
||||
/// Registers GdClimatePhysics, GdEcologyPhysics, GdMapGenerator, GdGridState as Godot classes.
|
||||
|
||||
use godot::prelude::*;
|
||||
|
||||
use mc_climate::atmosphere::step_atmospheric_chemistry;
|
||||
use mc_climate::physics::ClimatePhysics;
|
||||
use mc_climate::ecology::EcologyPhysics;
|
||||
use mc_climate::spec;
|
||||
use mc_core::grid::GridState;
|
||||
use mc_mapgen::MapGenerator;
|
||||
|
||||
struct MagicCivPhysicsExtension;
|
||||
|
||||
#[gdextension]
|
||||
unsafe impl ExtensionLibrary for MagicCivPhysicsExtension {}
|
||||
|
||||
// ── GdGridState ─────────────────────────────────────────────────────────
|
||||
|
||||
/// Godot-visible grid wrapper. Holds the flat-array GridState used by all physics.
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=RefCounted)]
|
||||
pub struct GdGridState {
|
||||
pub(crate) inner: GridState,
|
||||
base: Base<RefCounted>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl IRefCounted for GdGridState {
|
||||
fn init(base: Base<RefCounted>) -> Self {
|
||||
Self {
|
||||
inner: GridState::new(0, 0),
|
||||
base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl GdGridState {
|
||||
/// Create a grid of given dimensions with default tile values.
|
||||
#[func]
|
||||
fn create(width: i64, height: i64) -> Gd<GdGridState> {
|
||||
Gd::from_init_fn(|base| GdGridState {
|
||||
inner: GridState::new(width as i32, height as i32),
|
||||
base,
|
||||
})
|
||||
}
|
||||
|
||||
/// Deserialize a grid from JSON (full GridState).
|
||||
#[func]
|
||||
fn from_json(json: GString) -> Gd<GdGridState> {
|
||||
let inner: GridState = serde_json::from_str(&json.to_string())
|
||||
.unwrap_or_else(|e| {
|
||||
godot_error!("GdGridState::from_json parse error: {}", e);
|
||||
GridState::new(0, 0)
|
||||
});
|
||||
Gd::from_init_fn(|base| GdGridState { inner, base })
|
||||
}
|
||||
|
||||
/// Serialize the grid to JSON.
|
||||
#[func]
|
||||
fn to_json(&self) -> GString {
|
||||
match serde_json::to_string(&self.inner) {
|
||||
Ok(s) => GString::from(s),
|
||||
Err(e) => {
|
||||
godot_error!("GdGridState::to_json error: {}", e);
|
||||
GString::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[func]
|
||||
fn get_width(&self) -> i64 {
|
||||
self.inner.width as i64
|
||||
}
|
||||
|
||||
#[func]
|
||||
fn get_height(&self) -> i64 {
|
||||
self.inner.height as i64
|
||||
}
|
||||
|
||||
#[func]
|
||||
fn get_global_avg_temp(&self) -> f64 {
|
||||
self.inner.global_avg_temp as f64
|
||||
}
|
||||
|
||||
#[func]
|
||||
fn get_ocean_dead_fraction(&self) -> f64 {
|
||||
self.inner.ocean_dead_fraction as f64
|
||||
}
|
||||
|
||||
#[func]
|
||||
fn set_ocean_dead_fraction(&mut self, value: f64) {
|
||||
self.inner.ocean_dead_fraction = value as f32;
|
||||
}
|
||||
|
||||
#[func]
|
||||
fn get_tile_count(&self) -> i64 {
|
||||
self.inner.tiles.len() as i64
|
||||
}
|
||||
|
||||
/// Get a single tile's data as a Dictionary (for GDScript interop).
|
||||
#[func]
|
||||
fn get_tile_dict(&self, col: i64, row: i64) -> Dictionary {
|
||||
match self.inner.tile(col as i32, row as i32) {
|
||||
Some(tile) => tile_to_dict(tile),
|
||||
None => Dictionary::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set tile fields from a Dictionary. Only writes fields present in the dict.
|
||||
#[func]
|
||||
fn set_tile_dict(&mut self, col: i64, row: i64, dict: Dictionary) {
|
||||
if let Some(tile) = self.inner.tile_mut(col as i32, row as i32) {
|
||||
dict_to_tile(&dict, tile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── GdClimatePhysics ────────────────────────────────────────────────────
|
||||
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=RefCounted)]
|
||||
pub struct GdClimatePhysics {
|
||||
inner: Option<ClimatePhysics>,
|
||||
base: Base<RefCounted>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl IRefCounted for GdClimatePhysics {
|
||||
fn init(base: Base<RefCounted>) -> Self {
|
||||
Self { inner: None, base }
|
||||
}
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl GdClimatePhysics {
|
||||
/// Initialize with JSON config strings (climate_params, terrain_data, climate_spec).
|
||||
#[func]
|
||||
fn initialize(&mut self, params_json: GString, terrain_json: GString, spec_json: GString) {
|
||||
self.inner = Some(ClimatePhysics::new(
|
||||
¶ms_json.to_string(),
|
||||
&terrain_json.to_string(),
|
||||
&spec_json.to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
/// Run one turn of climate simulation on the given grid.
|
||||
#[func]
|
||||
fn process_step(&mut self, mut grid: Gd<GdGridState>, turn: i64, seed: i64) {
|
||||
match &mut self.inner {
|
||||
Some(physics) => {
|
||||
physics.process_step(&mut grid.bind_mut().inner, turn as u32, seed as u64);
|
||||
}
|
||||
None => {
|
||||
godot_error!("GdClimatePhysics::process_step called before initialize()");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── GdEcologyPhysics ────────────────────────────────────────────────────
|
||||
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=RefCounted)]
|
||||
pub struct GdEcologyPhysics {
|
||||
inner: EcologyPhysics,
|
||||
base: Base<RefCounted>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl IRefCounted for GdEcologyPhysics {
|
||||
fn init(base: Base<RefCounted>) -> Self {
|
||||
Self {
|
||||
inner: EcologyPhysics::new(),
|
||||
base,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl GdEcologyPhysics {
|
||||
/// Run one tick of ecology simulation (flora succession + fauna habitat).
|
||||
#[func]
|
||||
fn process_step(&mut self, mut grid: Gd<GdGridState>) {
|
||||
self.inner.process_step(&mut grid.bind_mut().inner);
|
||||
}
|
||||
}
|
||||
|
||||
// ── GdAtmosphericChemistry ──────────────────────────────────────────────
|
||||
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=RefCounted)]
|
||||
pub struct GdAtmosphericChemistry {
|
||||
base: Base<RefCounted>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl IRefCounted for GdAtmosphericChemistry {
|
||||
fn init(base: Base<RefCounted>) -> Self {
|
||||
Self { base }
|
||||
}
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl GdAtmosphericChemistry {
|
||||
/// Run one step of atmospheric chemistry (O2/CO2/CH4 tracking).
|
||||
#[func]
|
||||
fn step(mut grid: Gd<GdGridState>, spec_json: GString) {
|
||||
let spec_val: serde_json::Value =
|
||||
serde_json::from_str(&spec_json.to_string()).unwrap_or_default();
|
||||
step_atmospheric_chemistry(&mut grid.bind_mut().inner, &spec_val);
|
||||
}
|
||||
}
|
||||
|
||||
// ── GdMapGenerator ──────────────────────────────────────────────────────
|
||||
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=RefCounted)]
|
||||
pub struct GdMapGenerator {
|
||||
inner: Option<MapGenerator>,
|
||||
base: Base<RefCounted>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl IRefCounted for GdMapGenerator {
|
||||
fn init(base: Base<RefCounted>) -> Self {
|
||||
Self { inner: None, base }
|
||||
}
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl GdMapGenerator {
|
||||
/// Initialize with map generation params JSON.
|
||||
#[func]
|
||||
fn initialize(&mut self, params_json: GString) {
|
||||
self.inner = Some(MapGenerator::new(¶ms_json.to_string()));
|
||||
}
|
||||
|
||||
/// Generate a complete map, returning a GdGridState.
|
||||
#[func]
|
||||
fn generate(&self, seed: i64, map_size: GString) -> Gd<GdGridState> {
|
||||
match &self.inner {
|
||||
Some(gen) => {
|
||||
let grid = gen.generate(seed as u64, &map_size.to_string());
|
||||
Gd::from_init_fn(|base| GdGridState { inner: grid, base })
|
||||
}
|
||||
None => {
|
||||
godot_error!("GdMapGenerator::generate called before initialize()");
|
||||
Gd::from_init_fn(|base| GdGridState {
|
||||
inner: GridState::new(0, 0),
|
||||
base,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── GdClimateSpecEval ───────────────────────────────────────────────────
|
||||
|
||||
#[derive(GodotClass)]
|
||||
#[class(base=RefCounted)]
|
||||
pub struct GdClimateSpecEval {
|
||||
base: Base<RefCounted>,
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl IRefCounted for GdClimateSpecEval {
|
||||
fn init(base: Base<RefCounted>) -> Self {
|
||||
Self { base }
|
||||
}
|
||||
}
|
||||
|
||||
#[godot_api]
|
||||
impl GdClimateSpecEval {
|
||||
/// Evaluate a condition string from climate_spec.json.
|
||||
#[func]
|
||||
fn eval_condition(
|
||||
cond: GString,
|
||||
temperature: f64,
|
||||
moisture: f64,
|
||||
elevation: f64,
|
||||
canopy: f64,
|
||||
) -> bool {
|
||||
spec::eval_condition(
|
||||
&cond.to_string(),
|
||||
temperature as f32,
|
||||
moisture as f32,
|
||||
elevation as f32,
|
||||
canopy as f32,
|
||||
)
|
||||
}
|
||||
|
||||
/// Determine the climate-ideal terrain type for a tile.
|
||||
/// Takes tile data as a Dictionary and spec as JSON string.
|
||||
#[func]
|
||||
fn ideal_terrain(tile_dict: Dictionary, spec_json: GString) -> GString {
|
||||
let tile = dict_to_tilestate(&tile_dict);
|
||||
let spec_val: serde_json::Value =
|
||||
serde_json::from_str(&spec_json.to_string()).unwrap_or_default();
|
||||
GString::from(spec::ideal_terrain(&tile, &spec_val))
|
||||
}
|
||||
|
||||
/// Corruption spread multiplier based on tile ley state.
|
||||
#[func]
|
||||
fn ley_channeling_mult(tile_dict: Dictionary, spec_json: GString) -> f64 {
|
||||
let tile = dict_to_tilestate(&tile_dict);
|
||||
let spec_val: serde_json::Value =
|
||||
serde_json::from_str(&spec_json.to_string()).unwrap_or_default();
|
||||
spec::ley_channeling_mult(&tile, &spec_val) as f64
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tile Dictionary conversion helpers ──────────────────────────────────
|
||||
|
||||
fn tile_to_dict(tile: &mc_core::grid::TileState) -> Dictionary {
|
||||
let mut d: Dictionary = Dictionary::new();
|
||||
d.set("col", tile.col as i64);
|
||||
d.set("row", tile.row as i64);
|
||||
d.set("temperature", tile.temperature as f64);
|
||||
d.set("moisture", tile.moisture as f64);
|
||||
d.set("elevation", tile.elevation as f64);
|
||||
d.set("biome_id", GString::from(tile.biome_id.as_str()));
|
||||
d.set("wind_direction", tile.wind_direction as i64);
|
||||
d.set("wind_speed", tile.wind_speed as f64);
|
||||
d.set("sulfate_aerosol", tile.sulfate_aerosol as f64);
|
||||
d.set("quality", tile.quality as i64);
|
||||
d.set("quality_progress", tile.quality_progress as i64);
|
||||
d.set("original_biome_id", GString::from(tile.original_biome_id.as_str()));
|
||||
d.set("ley_line_count", tile.ley_line_count as i64);
|
||||
d.set("ley_school", GString::from(tile.ley_school.as_str()));
|
||||
d.set("reef_health", tile.reef_health as f64);
|
||||
d.set("magic_heat_delta", tile.magic_heat_delta as f64);
|
||||
d.set("magic_moisture_delta", tile.magic_moisture_delta as f64);
|
||||
d.set("is_natural_wonder", tile.is_natural_wonder);
|
||||
d.set("wonder_anchor_strength", tile.wonder_anchor_strength as f64);
|
||||
d.set("wonder_tier", tile.wonder_tier as i64);
|
||||
d.set("canopy_cover", tile.canopy_cover as f64);
|
||||
d.set("undergrowth", tile.undergrowth as f64);
|
||||
d.set("fungi_network", tile.fungi_network as f64);
|
||||
d.set("surface_water", tile.surface_water as f64);
|
||||
d.set("river_source_type", GString::from(tile.river_source_type.as_str()));
|
||||
d.set("is_coastal", tile.is_coastal);
|
||||
d.set("habitat_suitability", tile.habitat_suitability as f64);
|
||||
d.set("aerosol_mitigation", tile.aerosol_mitigation as f64);
|
||||
d.set("resource_id", GString::from(tile.resource_id.as_str()));
|
||||
|
||||
let mut edges: Array<i64> = Array::new();
|
||||
for &e in &tile.river_edges {
|
||||
edges.push(e as i64);
|
||||
}
|
||||
d.set("river_edges", edges);
|
||||
|
||||
d
|
||||
}
|
||||
|
||||
fn dict_to_tile(dict: &Dictionary, tile: &mut mc_core::grid::TileState) {
|
||||
if let Some(v) = dict.get("temperature") { tile.temperature = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("moisture") { tile.moisture = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("elevation") { tile.elevation = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("biome_id") { tile.biome_id = v.to::<GString>().to_string(); }
|
||||
if let Some(v) = dict.get("wind_direction") { tile.wind_direction = v.to::<i64>() as i32; }
|
||||
if let Some(v) = dict.get("wind_speed") { tile.wind_speed = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("sulfate_aerosol") { tile.sulfate_aerosol = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("quality") { tile.quality = v.to::<i64>() as i32; }
|
||||
if let Some(v) = dict.get("quality_progress") { tile.quality_progress = v.to::<i64>() as i32; }
|
||||
if let Some(v) = dict.get("original_biome_id") { tile.original_biome_id = v.to::<GString>().to_string(); }
|
||||
if let Some(v) = dict.get("ley_line_count") { tile.ley_line_count = v.to::<i64>() as i32; }
|
||||
if let Some(v) = dict.get("ley_school") {
|
||||
tile.ley_school = mc_core::grid::LeySchool::from_str(&v.to::<GString>().to_string());
|
||||
}
|
||||
if let Some(v) = dict.get("reef_health") { tile.reef_health = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("magic_heat_delta") { tile.magic_heat_delta = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("magic_moisture_delta") { tile.magic_moisture_delta = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("is_natural_wonder") { tile.is_natural_wonder = v.to::<bool>(); }
|
||||
if let Some(v) = dict.get("wonder_anchor_strength") { tile.wonder_anchor_strength = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("wonder_tier") { tile.wonder_tier = v.to::<i64>() as i32; }
|
||||
if let Some(v) = dict.get("canopy_cover") { tile.canopy_cover = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("undergrowth") { tile.undergrowth = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("fungi_network") { tile.fungi_network = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("surface_water") { tile.surface_water = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("river_source_type") { tile.river_source_type = v.to::<GString>().to_string(); }
|
||||
if let Some(v) = dict.get("is_coastal") { tile.is_coastal = v.to::<bool>(); }
|
||||
if let Some(v) = dict.get("habitat_suitability") { tile.habitat_suitability = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("aerosol_mitigation") { tile.aerosol_mitigation = v.to::<f64>() as f32; }
|
||||
if let Some(v) = dict.get("resource_id") { tile.resource_id = v.to::<GString>().to_string(); }
|
||||
}
|
||||
|
||||
/// Create a TileState from a Dictionary (for spec eval functions that need a full tile).
|
||||
fn dict_to_tilestate(dict: &Dictionary) -> mc_core::grid::TileState {
|
||||
let mut tile = mc_core::grid::TileState::default();
|
||||
dict_to_tile(dict, &mut tile);
|
||||
tile
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue