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 }