/ rust / dynostic_ffi / src / lib.rs
lib.rs
   1  //! C ABI layer for DYNOSTIC.
   2  //!
   3  //! This crate exposes a minimal, stable interface intended for LuaJIT FFI.
   4  
   5  #![allow(clippy::not_unsafe_ptr_arg_deref)]
   6  
   7  use dynostic_core::{
   8      generate_ed25519_keypair, sha256_hex, AiConfig, AiHeatmapKind, ContentPack, DynosticEvent,
   9      EncounterSpec, Engine, HazardKind, PackSignatureStatus, Pos, StatusKind, TileKind, TileLink,
  10      TileTrigger, VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH,
  11  };
  12  use std::ffi::CString;
  13  use std::os::raw::c_char;
  14  
  15  /// Opaque handle type for the front-end.
  16  pub type DynosticEngine = Engine;
  17  
  18  const ABI_VERSION_MAJOR: u32 = 1;
  19  const ABI_VERSION_MINOR: u32 = 0;
  20  const ABI_VERSION_PATCH: u32 = 0;
  21  
  22  fn catch_unwind_or<T: Copy>(default: T, f: impl FnOnce() -> T) -> T {
  23      match std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)) {
  24          Ok(v) => v,
  25          Err(_) => default,
  26      }
  27  }
  28  
  29  fn catch_unwind_void(f: impl FnOnce()) {
  30      let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(f));
  31  }
  32  
  33  fn bytes_from_raw<'a>(data: *const u8, len: u32) -> Option<&'a [u8]> {
  34      if data.is_null() || len == 0 {
  35          return None;
  36      }
  37      unsafe { Some(std::slice::from_raw_parts(data, len as usize)) }
  38  }
  39  
  40  fn str_from_raw<'a>(data: *const u8, len: u32) -> Option<&'a str> {
  41      let bytes = bytes_from_raw(data, len)?;
  42      std::str::from_utf8(bytes).ok()
  43  }
  44  
  45  fn tile_kind_from_u8(kind: u8) -> Option<TileKind> {
  46      match kind {
  47          0 => Some(TileKind::Floor),
  48          1 => Some(TileKind::Wall),
  49          2 => Some(TileKind::Cover),
  50          _ => None,
  51      }
  52  }
  53  
  54  fn hazard_kind_from_u8(kind: u8) -> Option<HazardKind> {
  55      match kind {
  56          1 => Some(HazardKind::Fire),
  57          2 => Some(HazardKind::Smoke),
  58          _ => None,
  59      }
  60  }
  61  
  62  fn status_kind_from_u8(kind: u8) -> Option<StatusKind> {
  63      match kind {
  64          1 => Some(StatusKind::Poison),
  65          2 => Some(StatusKind::Burn),
  66          3 => Some(StatusKind::Stun),
  67          4 => Some(StatusKind::Shield),
  68          _ => None,
  69      }
  70  }
  71  
  72  fn ai_heatmap_kind_from_u8(kind: u8) -> Option<AiHeatmapKind> {
  73      match kind {
  74          1 => Some(AiHeatmapKind::Danger),
  75          2 => Some(AiHeatmapKind::Sound),
  76          3 => Some(AiHeatmapKind::Visibility),
  77          4 => Some(AiHeatmapKind::Cover),
  78          _ => None,
  79      }
  80  }
  81  
  82  #[no_mangle]
  83  pub extern "C" fn dynostic_version_major() -> u32 {
  84      VERSION_MAJOR
  85  }
  86  
  87  #[no_mangle]
  88  pub extern "C" fn dynostic_version_minor() -> u32 {
  89      VERSION_MINOR
  90  }
  91  
  92  #[no_mangle]
  93  pub extern "C" fn dynostic_version_patch() -> u32 {
  94      VERSION_PATCH
  95  }
  96  
  97  #[no_mangle]
  98  pub extern "C" fn dynostic_abi_version_major() -> u32 {
  99      ABI_VERSION_MAJOR
 100  }
 101  
 102  #[no_mangle]
 103  pub extern "C" fn dynostic_abi_version_minor() -> u32 {
 104      ABI_VERSION_MINOR
 105  }
 106  
 107  #[no_mangle]
 108  pub extern "C" fn dynostic_abi_version_patch() -> u32 {
 109      ABI_VERSION_PATCH
 110  }
 111  
 112  #[no_mangle]
 113  pub extern "C" fn dynostic_abi_compatible(required_major: u32, required_minor: u32) -> u32 {
 114      if ABI_VERSION_MAJOR == required_major && ABI_VERSION_MINOR == required_minor {
 115          1
 116      } else {
 117          0
 118      }
 119  }
 120  
 121  #[no_mangle]
 122  pub extern "C" fn dynostic_create(seed: u64) -> *mut DynosticEngine {
 123      catch_unwind_or(std::ptr::null_mut(), || {
 124          Box::into_raw(Box::new(Engine::new(seed)))
 125      })
 126  }
 127  
 128  #[no_mangle]
 129  pub extern "C" fn dynostic_destroy(engine: *mut DynosticEngine) {
 130      catch_unwind_void(|| {
 131          if engine.is_null() {
 132              return;
 133          }
 134          unsafe {
 135              drop(Box::from_raw(engine));
 136          }
 137      });
 138  }
 139  
 140  #[no_mangle]
 141  pub extern "C" fn dynostic_step_ms(engine: *mut DynosticEngine, dt_ms: u32) {
 142      catch_unwind_void(|| unsafe {
 143          let Some(e) = engine.as_mut() else { return };
 144          e.step_ms(dt_ms);
 145      });
 146  }
 147  
 148  #[no_mangle]
 149  pub extern "C" fn dynostic_tick(engine: *const DynosticEngine) -> u64 {
 150      catch_unwind_or(0, || unsafe {
 151          engine.as_ref().map(|e| e.tick()).unwrap_or(0)
 152      })
 153  }
 154  
 155  #[no_mangle]
 156  pub extern "C" fn dynostic_pos_x(engine: *const DynosticEngine) -> i32 {
 157      catch_unwind_or(0, || unsafe {
 158          engine.as_ref().map(|e| e.pos().0).unwrap_or(0)
 159      })
 160  }
 161  
 162  #[no_mangle]
 163  pub extern "C" fn dynostic_pos_y(engine: *const DynosticEngine) -> i32 {
 164      catch_unwind_or(0, || unsafe {
 165          engine.as_ref().map(|e| e.pos().1).unwrap_or(0)
 166      })
 167  }
 168  
 169  #[no_mangle]
 170  pub extern "C" fn dynostic_phase(engine: *const DynosticEngine) -> u8 {
 171      catch_unwind_or(0, || unsafe {
 172          engine.as_ref().map(|e| e.phase().as_u8()).unwrap_or(0)
 173      })
 174  }
 175  
 176  #[no_mangle]
 177  pub extern "C" fn dynostic_plans_len(engine: *const DynosticEngine) -> u32 {
 178      catch_unwind_or(0, || unsafe {
 179          engine
 180              .as_ref()
 181              .map(|e| e.planned_intents_len().min(u32::MAX as usize) as u32)
 182              .unwrap_or(0)
 183      })
 184  }
 185  
 186  #[no_mangle]
 187  pub extern "C" fn dynostic_plan_move(
 188      engine: *mut DynosticEngine,
 189      entity_id: u32,
 190      x: i32,
 191      y: i32,
 192  ) -> u32 {
 193      catch_unwind_or(0, || unsafe {
 194          engine
 195              .as_mut()
 196              .map(|e| e.plan_move(entity_id, Pos::new(x, y)) as u32)
 197              .unwrap_or(0)
 198      })
 199  }
 200  
 201  #[no_mangle]
 202  pub extern "C" fn dynostic_plan_wait(engine: *mut DynosticEngine, entity_id: u32) -> u32 {
 203      catch_unwind_or(0, || unsafe {
 204          engine
 205              .as_mut()
 206              .map(|e| e.plan_wait(entity_id) as u32)
 207              .unwrap_or(0)
 208      })
 209  }
 210  
 211  #[no_mangle]
 212  pub extern "C" fn dynostic_plan_attack(
 213      engine: *mut DynosticEngine,
 214      entity_id: u32,
 215      target_id: u32,
 216  ) -> u32 {
 217      catch_unwind_or(0, || unsafe {
 218          engine
 219              .as_mut()
 220              .map(|e| e.plan_attack(entity_id, target_id) as u32)
 221              .unwrap_or(0)
 222      })
 223  }
 224  
 225  #[no_mangle]
 226  pub extern "C" fn dynostic_plan_use(
 227      engine: *mut DynosticEngine,
 228      entity_id: u32,
 229      ability_id: u32,
 230      x: i32,
 231      y: i32,
 232  ) -> u32 {
 233      catch_unwind_or(0, || unsafe {
 234          engine
 235              .as_mut()
 236              .map(|e| e.plan_use(entity_id, ability_id, Pos::new(x, y)) as u32)
 237              .unwrap_or(0)
 238      })
 239  }
 240  
 241  #[no_mangle]
 242  pub extern "C" fn dynostic_ai_plan(engine: *mut DynosticEngine, team_id: u32) -> u32 {
 243      catch_unwind_or(0, || unsafe {
 244          engine
 245              .as_mut()
 246              .map(|e| e.auto_plan_ai(team_id as u8))
 247              .unwrap_or(0)
 248      })
 249  }
 250  
 251  #[no_mangle]
 252  pub extern "C" fn dynostic_set_ai_config(
 253      engine: *mut DynosticEngine,
 254      aggression: i32,
 255      risk_tolerance: i32,
 256      focus_fire: i32,
 257      vision_range: u32,
 258  ) -> u32 {
 259      catch_unwind_or(0, || unsafe {
 260          let Some(e) = engine.as_mut() else { return 0 };
 261          e.set_ai_config(AiConfig {
 262              aggression,
 263              risk_tolerance,
 264              focus_fire,
 265              vision_range,
 266          });
 267          1
 268      })
 269  }
 270  
 271  #[no_mangle]
 272  pub extern "C" fn dynostic_load_abilities_json(
 273      engine: *mut DynosticEngine,
 274      data: *const u8,
 275      len: u32,
 276  ) -> u32 {
 277      catch_unwind_or(0, || unsafe {
 278          let Some(e) = engine.as_mut() else { return 0 };
 279          if data.is_null() {
 280              return 0;
 281          }
 282          let slice = std::slice::from_raw_parts(data, len as usize);
 283          let Ok(text) = std::str::from_utf8(slice) else {
 284              return 0;
 285          };
 286          e.load_abilities_from_json(text).is_ok() as u32
 287      })
 288  }
 289  
 290  #[no_mangle]
 291  pub extern "C" fn dynostic_load_reactions_json(
 292      engine: *mut DynosticEngine,
 293      data: *const u8,
 294      len: u32,
 295  ) -> u32 {
 296      catch_unwind_or(0, || unsafe {
 297          let Some(e) = engine.as_mut() else { return 0 };
 298          if data.is_null() {
 299              return 0;
 300          }
 301          let slice = std::slice::from_raw_parts(data, len as usize);
 302          let Ok(text) = std::str::from_utf8(slice) else {
 303              return 0;
 304          };
 305          e.load_reactions_from_json(text).is_ok() as u32
 306      })
 307  }
 308  
 309  #[no_mangle]
 310  pub extern "C" fn dynostic_load_director_json(
 311      engine: *mut DynosticEngine,
 312      data: *const u8,
 313      len: u32,
 314  ) -> u32 {
 315      catch_unwind_or(0, || unsafe {
 316          let Some(e) = engine.as_mut() else { return 0 };
 317          if data.is_null() {
 318              return 0;
 319          }
 320          let slice = std::slice::from_raw_parts(data, len as usize);
 321          let Ok(text) = std::str::from_utf8(slice) else {
 322              return 0;
 323          };
 324          e.load_director_from_json(text).is_ok() as u32
 325      })
 326  }
 327  
 328  #[no_mangle]
 329  pub extern "C" fn dynostic_load_ai_json(
 330      engine: *mut DynosticEngine,
 331      data: *const u8,
 332      len: u32,
 333  ) -> u32 {
 334      catch_unwind_or(0, || unsafe {
 335          let Some(e) = engine.as_mut() else { return 0 };
 336          if data.is_null() {
 337              return 0;
 338          }
 339          let slice = std::slice::from_raw_parts(data, len as usize);
 340          let Ok(text) = std::str::from_utf8(slice) else {
 341              return 0;
 342          };
 343          e.load_ai_from_json(text).is_ok() as u32
 344      })
 345  }
 346  
 347  #[no_mangle]
 348  pub extern "C" fn dynostic_generate_encounter(
 349      engine: *mut DynosticEngine,
 350      seed: u64,
 351      width: i32,
 352      height: i32,
 353      wall_density: u8,
 354      hazard_count: u32,
 355      team_a: u32,
 356      team_b: u32,
 357      max_hp: i32,
 358      armor: i32,
 359  ) -> u32 {
 360      catch_unwind_or(0, || unsafe {
 361          let Some(e) = engine.as_mut() else { return 0 };
 362          let spec = EncounterSpec {
 363              width,
 364              height,
 365              wall_density,
 366              hazard_count,
 367              team_a,
 368              team_b,
 369              max_hp,
 370              armor,
 371          };
 372          e.generate_encounter(seed, spec) as u32
 373      })
 374  }
 375  
 376  #[no_mangle]
 377  pub extern "C" fn dynostic_clear_plans(engine: *mut DynosticEngine) {
 378      catch_unwind_void(|| unsafe {
 379          let Some(e) = engine.as_mut() else { return };
 380          e.clear_plans();
 381      });
 382  }
 383  
 384  #[no_mangle]
 385  pub extern "C" fn dynostic_commit(engine: *mut DynosticEngine) -> u32 {
 386      catch_unwind_or(0, || unsafe {
 387          engine.as_mut().map(|e| e.commit()).unwrap_or(0)
 388      })
 389  }
 390  
 391  #[no_mangle]
 392  pub extern "C" fn dynostic_grid_width(engine: *const DynosticEngine) -> i32 {
 393      catch_unwind_or(0, || unsafe {
 394          engine
 395              .as_ref()
 396              .map(|e| e.world().grid().width())
 397              .unwrap_or(0)
 398      })
 399  }
 400  
 401  #[no_mangle]
 402  pub extern "C" fn dynostic_grid_height(engine: *const DynosticEngine) -> i32 {
 403      catch_unwind_or(0, || unsafe {
 404          engine
 405              .as_ref()
 406              .map(|e| e.world().grid().height())
 407              .unwrap_or(0)
 408      })
 409  }
 410  
 411  #[no_mangle]
 412  pub extern "C" fn dynostic_tile_kind(engine: *const DynosticEngine, x: i32, y: i32) -> u8 {
 413      catch_unwind_or(1, || unsafe {
 414          engine
 415              .as_ref()
 416              .map(|e| e.world().grid().tile(x, y) as u8)
 417              .unwrap_or(1)
 418      })
 419  }
 420  
 421  #[no_mangle]
 422  pub extern "C" fn dynostic_tile_height(engine: *const DynosticEngine, x: i32, y: i32) -> i32 {
 423      catch_unwind_or(0, || unsafe {
 424          engine
 425              .as_ref()
 426              .map(|e| e.world().tile_height(x, y))
 427              .unwrap_or(0)
 428      })
 429  }
 430  
 431  #[no_mangle]
 432  pub extern "C" fn dynostic_hazard_kind(engine: *const DynosticEngine, x: i32, y: i32) -> u8 {
 433      catch_unwind_or(0, || unsafe {
 434          engine
 435              .as_ref()
 436              .and_then(|e| e.world().hazard_kind(x, y))
 437              .map(|kind| kind.as_u8())
 438              .unwrap_or(0)
 439      })
 440  }
 441  
 442  #[no_mangle]
 443  pub extern "C" fn dynostic_hazard_duration(engine: *const DynosticEngine, x: i32, y: i32) -> u32 {
 444      catch_unwind_or(0, || unsafe {
 445          engine
 446              .as_ref()
 447              .map(|e| e.world().hazard_duration(x, y))
 448              .unwrap_or(0)
 449      })
 450  }
 451  
 452  #[no_mangle]
 453  pub extern "C" fn dynostic_set_tile_kind(
 454      engine: *mut DynosticEngine,
 455      x: i32,
 456      y: i32,
 457      kind: u8,
 458  ) -> u32 {
 459      catch_unwind_or(0, || unsafe {
 460          let Some(e) = engine.as_mut() else { return 0 };
 461          let Some(kind) = tile_kind_from_u8(kind) else {
 462              return 0;
 463          };
 464          e.editor_set_tile_kind(x, y, kind) as u32
 465      })
 466  }
 467  
 468  #[no_mangle]
 469  pub extern "C" fn dynostic_set_tile_height(
 470      engine: *mut DynosticEngine,
 471      x: i32,
 472      y: i32,
 473      height: i32,
 474  ) -> u32 {
 475      catch_unwind_or(0, || unsafe {
 476          let Some(e) = engine.as_mut() else { return 0 };
 477          e.editor_set_tile_height(x, y, height) as u32
 478      })
 479  }
 480  
 481  #[no_mangle]
 482  pub extern "C" fn dynostic_set_hazard(
 483      engine: *mut DynosticEngine,
 484      x: i32,
 485      y: i32,
 486      kind: u8,
 487      duration: u32,
 488  ) -> u32 {
 489      catch_unwind_or(0, || unsafe {
 490          let Some(e) = engine.as_mut() else { return 0 };
 491          let Some(kind) = hazard_kind_from_u8(kind) else {
 492              return 0;
 493          };
 494          e.editor_set_hazard(Pos::new(x, y), kind, duration) as u32
 495      })
 496  }
 497  
 498  #[no_mangle]
 499  pub extern "C" fn dynostic_clear_hazard(engine: *mut DynosticEngine, x: i32, y: i32) -> u32 {
 500      catch_unwind_or(0, || unsafe {
 501          let Some(e) = engine.as_mut() else { return 0 };
 502          e.editor_clear_hazard(Pos::new(x, y)) as u32
 503      })
 504  }
 505  
 506  #[no_mangle]
 507  pub extern "C" fn dynostic_tile_triggers_json(
 508      engine: *const DynosticEngine,
 509      x: i32,
 510      y: i32,
 511  ) -> *mut c_char {
 512      catch_unwind_or(std::ptr::null_mut(), || unsafe {
 513          let Some(e) = engine.as_ref() else {
 514              return std::ptr::null_mut();
 515          };
 516          let triggers = e.world().tile_triggers(x, y).unwrap_or(&[]);
 517          let Ok(output) = serde_json::to_string(triggers) else {
 518              return std::ptr::null_mut();
 519          };
 520          CString::new(output)
 521              .ok()
 522              .map(|value| value.into_raw())
 523              .unwrap_or(std::ptr::null_mut())
 524      })
 525  }
 526  
 527  #[no_mangle]
 528  pub extern "C" fn dynostic_set_tile_triggers_json(
 529      engine: *mut DynosticEngine,
 530      x: i32,
 531      y: i32,
 532      data: *const u8,
 533      len: u32,
 534  ) -> u32 {
 535      catch_unwind_or(0, || unsafe {
 536          let Some(e) = engine.as_mut() else { return 0 };
 537          let Some(text) = str_from_raw(data, len) else {
 538              return 0;
 539          };
 540          let Ok(triggers) = serde_json::from_str::<Vec<TileTrigger>>(text) else {
 541              return 0;
 542          };
 543          e.editor_set_tile_triggers(x, y, triggers) as u32
 544      })
 545  }
 546  
 547  #[no_mangle]
 548  pub extern "C" fn dynostic_clear_tile_triggers(engine: *mut DynosticEngine, x: i32, y: i32) -> u32 {
 549      catch_unwind_or(0, || unsafe {
 550          let Some(e) = engine.as_mut() else { return 0 };
 551          e.editor_clear_tile_triggers(x, y) as u32
 552      })
 553  }
 554  
 555  #[no_mangle]
 556  pub extern "C" fn dynostic_tile_links_json(
 557      engine: *const DynosticEngine,
 558      x: i32,
 559      y: i32,
 560  ) -> *mut c_char {
 561      catch_unwind_or(std::ptr::null_mut(), || unsafe {
 562          let Some(e) = engine.as_ref() else {
 563              return std::ptr::null_mut();
 564          };
 565          let links = e.world().tile_links(x, y).unwrap_or(&[]);
 566          let Ok(output) = serde_json::to_string(links) else {
 567              return std::ptr::null_mut();
 568          };
 569          CString::new(output)
 570              .ok()
 571              .map(|value| value.into_raw())
 572              .unwrap_or(std::ptr::null_mut())
 573      })
 574  }
 575  
 576  #[no_mangle]
 577  pub extern "C" fn dynostic_set_tile_links_json(
 578      engine: *mut DynosticEngine,
 579      x: i32,
 580      y: i32,
 581      data: *const u8,
 582      len: u32,
 583  ) -> u32 {
 584      catch_unwind_or(0, || unsafe {
 585          let Some(e) = engine.as_mut() else { return 0 };
 586          let Some(text) = str_from_raw(data, len) else {
 587              return 0;
 588          };
 589          let Ok(links) = serde_json::from_str::<Vec<TileLink>>(text) else {
 590              return 0;
 591          };
 592          e.editor_set_tile_links(x, y, links) as u32
 593      })
 594  }
 595  
 596  #[no_mangle]
 597  pub extern "C" fn dynostic_clear_tile_links(engine: *mut DynosticEngine, x: i32, y: i32) -> u32 {
 598      catch_unwind_or(0, || unsafe {
 599          let Some(e) = engine.as_mut() else { return 0 };
 600          e.editor_clear_tile_links(x, y) as u32
 601      })
 602  }
 603  
 604  #[no_mangle]
 605  pub extern "C" fn dynostic_entities_len(engine: *const DynosticEngine) -> u32 {
 606      catch_unwind_or(0, || unsafe {
 607          engine
 608              .as_ref()
 609              .map(|e| e.world().entities_len().min(u32::MAX as usize) as u32)
 610              .unwrap_or(0)
 611      })
 612  }
 613  
 614  #[no_mangle]
 615  pub extern "C" fn dynostic_entity_id(engine: *const DynosticEngine, index: u32) -> u32 {
 616      catch_unwind_or(0, || unsafe {
 617          engine
 618              .as_ref()
 619              .and_then(|e| e.world().entity(index as usize))
 620              .map(|entity| entity.id())
 621              .unwrap_or(0)
 622      })
 623  }
 624  
 625  #[no_mangle]
 626  pub extern "C" fn dynostic_entity_x(engine: *const DynosticEngine, index: u32) -> i32 {
 627      catch_unwind_or(0, || unsafe {
 628          engine
 629              .as_ref()
 630              .and_then(|e| e.world().entity(index as usize))
 631              .map(|entity| entity.pos().0)
 632              .unwrap_or(0)
 633      })
 634  }
 635  
 636  #[no_mangle]
 637  pub extern "C" fn dynostic_entity_y(engine: *const DynosticEngine, index: u32) -> i32 {
 638      catch_unwind_or(0, || unsafe {
 639          engine
 640              .as_ref()
 641              .and_then(|e| e.world().entity(index as usize))
 642              .map(|entity| entity.pos().1)
 643              .unwrap_or(0)
 644      })
 645  }
 646  
 647  #[no_mangle]
 648  pub extern "C" fn dynostic_entity_hp(engine: *const DynosticEngine, index: u32) -> i32 {
 649      catch_unwind_or(0, || unsafe {
 650          engine
 651              .as_ref()
 652              .and_then(|e| e.world().entity(index as usize))
 653              .map(|entity| entity.hp())
 654              .unwrap_or(0)
 655      })
 656  }
 657  
 658  #[no_mangle]
 659  pub extern "C" fn dynostic_entity_max_hp(engine: *const DynosticEngine, index: u32) -> i32 {
 660      catch_unwind_or(0, || unsafe {
 661          engine
 662              .as_ref()
 663              .and_then(|e| e.world().entity(index as usize))
 664              .map(|entity| entity.max_hp())
 665              .unwrap_or(0)
 666      })
 667  }
 668  
 669  #[no_mangle]
 670  pub extern "C" fn dynostic_entity_armor(engine: *const DynosticEngine, index: u32) -> i32 {
 671      catch_unwind_or(0, || unsafe {
 672          engine
 673              .as_ref()
 674              .and_then(|e| e.world().entity(index as usize))
 675              .map(|entity| entity.armor())
 676              .unwrap_or(0)
 677      })
 678  }
 679  
 680  #[no_mangle]
 681  pub extern "C" fn dynostic_entity_team(engine: *const DynosticEngine, index: u32) -> u8 {
 682      catch_unwind_or(0, || unsafe {
 683          engine
 684              .as_ref()
 685              .and_then(|e| e.world().entity(index as usize))
 686              .map(|entity| entity.team())
 687              .unwrap_or(0)
 688      })
 689  }
 690  
 691  #[no_mangle]
 692  pub extern "C" fn dynostic_entity_alive(engine: *const DynosticEngine, index: u32) -> u32 {
 693      catch_unwind_or(0, || unsafe {
 694          engine
 695              .as_ref()
 696              .and_then(|e| e.world().entity(index as usize))
 697              .map(|entity| entity.is_alive() as u32)
 698              .unwrap_or(0)
 699      })
 700  }
 701  
 702  #[no_mangle]
 703  pub extern "C" fn dynostic_entity_status_len(engine: *const DynosticEngine, index: u32) -> u32 {
 704      catch_unwind_or(0, || unsafe {
 705          engine
 706              .as_ref()
 707              .and_then(|e| e.world().entity(index as usize))
 708              .map(|entity| entity.statuses().len().min(u32::MAX as usize) as u32)
 709              .unwrap_or(0)
 710      })
 711  }
 712  
 713  #[no_mangle]
 714  pub extern "C" fn dynostic_entity_status_kind(
 715      engine: *const DynosticEngine,
 716      index: u32,
 717      status_index: u32,
 718  ) -> u8 {
 719      catch_unwind_or(0, || unsafe {
 720          engine
 721              .as_ref()
 722              .and_then(|e| e.world().entity(index as usize))
 723              .and_then(|entity| entity.statuses().get(status_index as usize))
 724              .map(|status| status.kind().as_u8())
 725              .unwrap_or(0)
 726      })
 727  }
 728  
 729  #[no_mangle]
 730  pub extern "C" fn dynostic_entity_status_duration(
 731      engine: *const DynosticEngine,
 732      index: u32,
 733      status_index: u32,
 734  ) -> u32 {
 735      catch_unwind_or(0, || unsafe {
 736          engine
 737              .as_ref()
 738              .and_then(|e| e.world().entity(index as usize))
 739              .and_then(|entity| entity.statuses().get(status_index as usize))
 740              .map(|status| status.duration())
 741              .unwrap_or(0)
 742      })
 743  }
 744  
 745  #[no_mangle]
 746  pub extern "C" fn dynostic_detection_state(
 747      engine: *const DynosticEngine,
 748      observer_team: u8,
 749      target_id: u32,
 750  ) -> u8 {
 751      catch_unwind_or(0, || unsafe {
 752          engine
 753              .as_ref()
 754              .map(|e| e.world().detection_state(observer_team, target_id).as_u8())
 755              .unwrap_or(0)
 756      })
 757  }
 758  
 759  #[no_mangle]
 760  pub extern "C" fn dynostic_detection_pos_x(
 761      engine: *const DynosticEngine,
 762      observer_team: u8,
 763      target_id: u32,
 764  ) -> i32 {
 765      catch_unwind_or(0, || unsafe {
 766          engine
 767              .as_ref()
 768              .and_then(|e| e.world().detection_last_pos(observer_team, target_id))
 769              .map(|pos| pos.x)
 770              .unwrap_or(0)
 771      })
 772  }
 773  
 774  #[no_mangle]
 775  pub extern "C" fn dynostic_detection_pos_y(
 776      engine: *const DynosticEngine,
 777      observer_team: u8,
 778      target_id: u32,
 779  ) -> i32 {
 780      catch_unwind_or(0, || unsafe {
 781          engine
 782              .as_ref()
 783              .and_then(|e| e.world().detection_last_pos(observer_team, target_id))
 784              .map(|pos| pos.y)
 785              .unwrap_or(0)
 786      })
 787  }
 788  
 789  #[no_mangle]
 790  pub extern "C" fn dynostic_spawn_entity(
 791      engine: *mut DynosticEngine,
 792      x: i32,
 793      y: i32,
 794      team: u8,
 795      max_hp: i32,
 796      armor: i32,
 797  ) -> u32 {
 798      catch_unwind_or(0, || unsafe {
 799          let Some(e) = engine.as_mut() else { return 0 };
 800          e.editor_spawn_entity(Pos::new(x, y), team, max_hp, armor)
 801      })
 802  }
 803  
 804  #[no_mangle]
 805  pub extern "C" fn dynostic_remove_entity(engine: *mut DynosticEngine, entity_id: u32) -> u32 {
 806      catch_unwind_or(0, || unsafe {
 807          let Some(e) = engine.as_mut() else { return 0 };
 808          e.editor_remove_entity(entity_id) as u32
 809      })
 810  }
 811  
 812  #[no_mangle]
 813  pub extern "C" fn dynostic_move_entity(
 814      engine: *mut DynosticEngine,
 815      entity_id: u32,
 816      x: i32,
 817      y: i32,
 818  ) -> u32 {
 819      catch_unwind_or(0, || unsafe {
 820          let Some(e) = engine.as_mut() else { return 0 };
 821          e.editor_move_entity(entity_id, Pos::new(x, y)) as u32
 822      })
 823  }
 824  
 825  #[no_mangle]
 826  pub extern "C" fn dynostic_set_entity_hp(
 827      engine: *mut DynosticEngine,
 828      entity_id: u32,
 829      hp: i32,
 830  ) -> u32 {
 831      catch_unwind_or(0, || unsafe {
 832          let Some(e) = engine.as_mut() else { return 0 };
 833          e.editor_set_entity_hp(entity_id, hp) as u32
 834      })
 835  }
 836  
 837  #[no_mangle]
 838  pub extern "C" fn dynostic_set_entity_team(
 839      engine: *mut DynosticEngine,
 840      entity_id: u32,
 841      team: u8,
 842  ) -> u32 {
 843      catch_unwind_or(0, || unsafe {
 844          let Some(e) = engine.as_mut() else { return 0 };
 845          e.editor_set_entity_team(entity_id, team) as u32
 846      })
 847  }
 848  
 849  #[no_mangle]
 850  pub extern "C" fn dynostic_set_entity_armor(
 851      engine: *mut DynosticEngine,
 852      entity_id: u32,
 853      armor: i32,
 854  ) -> u32 {
 855      catch_unwind_or(0, || unsafe {
 856          let Some(e) = engine.as_mut() else { return 0 };
 857          e.editor_set_entity_armor(entity_id, armor) as u32
 858      })
 859  }
 860  
 861  #[no_mangle]
 862  pub extern "C" fn dynostic_apply_status(
 863      engine: *mut DynosticEngine,
 864      entity_id: u32,
 865      kind: u8,
 866      duration: u32,
 867  ) -> u32 {
 868      catch_unwind_or(0, || unsafe {
 869          let Some(e) = engine.as_mut() else { return 0 };
 870          let Some(kind) = status_kind_from_u8(kind) else {
 871              return 0;
 872          };
 873          e.editor_apply_status(entity_id, kind, duration) as u32
 874      })
 875  }
 876  
 877  #[no_mangle]
 878  pub extern "C" fn dynostic_clear_status(
 879      engine: *mut DynosticEngine,
 880      entity_id: u32,
 881      kind: u8,
 882  ) -> u32 {
 883      catch_unwind_or(0, || unsafe {
 884          let Some(e) = engine.as_mut() else { return 0 };
 885          let Some(kind) = status_kind_from_u8(kind) else {
 886              return 0;
 887          };
 888          e.editor_clear_status(entity_id, kind) as u32
 889      })
 890  }
 891  
 892  #[no_mangle]
 893  pub extern "C" fn dynostic_clear_statuses(engine: *mut DynosticEngine, entity_id: u32) -> u32 {
 894      catch_unwind_or(0, || unsafe {
 895          let Some(e) = engine.as_mut() else { return 0 };
 896          e.editor_clear_statuses(entity_id)
 897      })
 898  }
 899  
 900  #[no_mangle]
 901  pub extern "C" fn dynostic_events_len(engine: *const DynosticEngine) -> u32 {
 902      catch_unwind_or(0, || unsafe {
 903          engine
 904              .as_ref()
 905              .map(|e| e.events().len().min(u32::MAX as usize) as u32)
 906              .unwrap_or(0)
 907      })
 908  }
 909  
 910  /// Pointer is valid until the next `dynostic_step_ms` call on the same engine.
 911  #[no_mangle]
 912  pub extern "C" fn dynostic_events_ptr(engine: *const DynosticEngine) -> *const DynosticEvent {
 913      catch_unwind_or(std::ptr::null(), || unsafe {
 914          engine
 915              .as_ref()
 916              .map(|e| e.events().as_ptr())
 917              .unwrap_or(std::ptr::null())
 918      })
 919  }
 920  
 921  #[no_mangle]
 922  pub extern "C" fn dynostic_debug_traces_json(engine: *const DynosticEngine) -> *mut c_char {
 923      catch_unwind_or(std::ptr::null_mut(), || unsafe {
 924          let Some(e) = engine.as_ref() else {
 925              return std::ptr::null_mut();
 926          };
 927          let Ok(json) = e.debug_traces_json() else {
 928              return std::ptr::null_mut();
 929          };
 930          CString::new(json)
 931              .ok()
 932              .map(|value| value.into_raw())
 933              .unwrap_or(std::ptr::null_mut())
 934      })
 935  }
 936  
 937  #[no_mangle]
 938  pub extern "C" fn dynostic_ai_heatmap_json(
 939      engine: *mut DynosticEngine,
 940      kind: u8,
 941      team: u8,
 942  ) -> *mut c_char {
 943      catch_unwind_or(std::ptr::null_mut(), || unsafe {
 944          let Some(e) = engine.as_mut() else {
 945              return std::ptr::null_mut();
 946          };
 947          let Some(kind) = ai_heatmap_kind_from_u8(kind) else {
 948              return std::ptr::null_mut();
 949          };
 950          let Ok(json) = e.ai_heatmap_json(kind, team) else {
 951              return std::ptr::null_mut();
 952          };
 953          CString::new(json)
 954              .ok()
 955              .map(|value| value.into_raw())
 956              .unwrap_or(std::ptr::null_mut())
 957      })
 958  }
 959  
 960  #[no_mangle]
 961  pub extern "C" fn dynostic_save_snapshot_json(engine: *const DynosticEngine) -> *mut c_char {
 962      catch_unwind_or(std::ptr::null_mut(), || unsafe {
 963          let Some(e) = engine.as_ref() else {
 964              return std::ptr::null_mut();
 965          };
 966          let Ok(json) = e.save_snapshot_json() else {
 967              return std::ptr::null_mut();
 968          };
 969          CString::new(json)
 970              .ok()
 971              .map(|value| value.into_raw())
 972              .unwrap_or(std::ptr::null_mut())
 973      })
 974  }
 975  
 976  #[no_mangle]
 977  pub extern "C" fn dynostic_load_snapshot_json(
 978      engine: *mut DynosticEngine,
 979      data: *const u8,
 980      len: u32,
 981  ) -> u32 {
 982      catch_unwind_or(0, || unsafe {
 983          let Some(e) = engine.as_mut() else { return 0 };
 984          let Some(json) = str_from_raw(data, len) else {
 985              return 0;
 986          };
 987          e.load_snapshot_json(json).is_ok() as u32
 988      })
 989  }
 990  
 991  #[no_mangle]
 992  pub extern "C" fn dynostic_sha256_hex(data: *const u8, len: u32) -> *mut c_char {
 993      catch_unwind_or(std::ptr::null_mut(), || {
 994          let Some(bytes) = bytes_from_raw(data, len) else {
 995              return std::ptr::null_mut();
 996          };
 997          let hex = sha256_hex(bytes);
 998          CString::new(hex)
 999              .ok()
1000              .map(|value| value.into_raw())
1001              .unwrap_or(std::ptr::null_mut())
1002      })
1003  }
1004  
1005  #[no_mangle]
1006  pub extern "C" fn dynostic_pack_signature_status(data: *const u8, len: u32) -> u8 {
1007      catch_unwind_or(PackSignatureStatus::Invalid.as_u8(), || {
1008          let Some(json) = str_from_raw(data, len) else {
1009              return PackSignatureStatus::Invalid.as_u8();
1010          };
1011          let Ok(pack) = ContentPack::from_json(json) else {
1012              return PackSignatureStatus::Invalid.as_u8();
1013          };
1014          pack.signature_status().as_u8()
1015      })
1016  }
1017  
1018  #[no_mangle]
1019  pub extern "C" fn dynostic_pack_sign_json(
1020      data: *const u8,
1021      len: u32,
1022      secret_key: *const u8,
1023      secret_len: u32,
1024  ) -> *mut c_char {
1025      catch_unwind_or(std::ptr::null_mut(), || {
1026          let Some(json) = str_from_raw(data, len) else {
1027              return std::ptr::null_mut();
1028          };
1029          let Some(secret_b64) = str_from_raw(secret_key, secret_len) else {
1030              return std::ptr::null_mut();
1031          };
1032          let Ok(pack) = ContentPack::from_json(json) else {
1033              return std::ptr::null_mut();
1034          };
1035          let Ok(signature) = pack.sign_ed25519(secret_b64) else {
1036              return std::ptr::null_mut();
1037          };
1038          let output = format!(
1039              "{{\"algorithm\":\"{}\",\"public_key\":\"{}\",\"signature\":\"{}\"}}",
1040              signature.algorithm, signature.public_key, signature.signature
1041          );
1042          CString::new(output)
1043              .ok()
1044              .map(|value| value.into_raw())
1045              .unwrap_or(std::ptr::null_mut())
1046      })
1047  }
1048  
1049  #[no_mangle]
1050  pub extern "C" fn dynostic_ed25519_keypair_json() -> *mut c_char {
1051      catch_unwind_or(std::ptr::null_mut(), || {
1052          let Ok((public_key, secret_key)) = generate_ed25519_keypair() else {
1053              return std::ptr::null_mut();
1054          };
1055          let output = format!(
1056              "{{\"public_key\":\"{}\",\"secret_key\":\"{}\"}}",
1057              public_key, secret_key
1058          );
1059          CString::new(output)
1060              .ok()
1061              .map(|value| value.into_raw())
1062              .unwrap_or(std::ptr::null_mut())
1063      })
1064  }
1065  
1066  #[no_mangle]
1067  pub extern "C" fn dynostic_string_free(value: *mut c_char) {
1068      catch_unwind_void(|| {
1069          if value.is_null() {
1070              return;
1071          }
1072          unsafe {
1073              drop(CString::from_raw(value));
1074          }
1075      });
1076  }