oxidizer_api 0.4.0

API for writing Kitten Space Agency mods in Rust, loaded by the Oxidizer mod loader
Documentation
//! Runtime for typed bindings generated by `oxidizer --bindgen ksa.rs`.
//!
//! The generated file has a wrapper struct per public KSA.dll type (`ksa::Vehicle(pub Obj)`) with methods for its
//! public methods, properties, fields and constructors. Wrappers deref to their base type and finally to [`Obj`],
//! so base-class members and the by-name API still work. Members are called by metadata token, which is exact but
//! only valid for the KSA.dll build the file was generated from: against another build every call does nothing
//! (and returns null/0) after logging one message, so regenerate after game updates.
//!
//! ```ignore
//! mod ksa; // the generated file
//! let v = ksa::ksa::Program::controlled_vehicle();
//! if !v.is_null() { oxidizer_api::log(&v.id()); }
//! ```

use crate::{Handle, Obj, api, log, read_string};
use std::sync::OnceLock;

pub struct Bindings {
    version: &'static str,
    ok: OnceLock<bool>,
}

impl Bindings {
    pub const fn new(version: &'static str) -> Bindings {
        Bindings { version, ok: OnceLock::new() }
    }

    /// Whether these bindings match the running game.
    pub fn ok(&self) -> bool {
        *self.ok.get_or_init(|| {
            let running = read_string(|b, c| (api().ksa_version)(b, c));
            let ok = running == self.version;
            if !ok {
                log(&format!("typed bindings were generated for another KSA build ({}, running {running}); regenerate them with `oxidizer --bindgen`", self.version));
            }
            ok
        })
    }

    pub fn invoke(&self, this: Handle, token: i32, args: &[&Obj]) -> Obj {
        if !self.ok() {
            return Obj::NULL;
        }
        let hs: Vec<Handle> = args.iter().map(|a| a.handle()).collect();
        unsafe { Obj::from_handle((api().invoke)(this, token, hs.as_ptr(), hs.len())) }
    }

    pub fn field_get(&self, this: Handle, token: i32) -> Obj {
        if !self.ok() {
            return Obj::NULL;
        }
        unsafe { Obj::from_handle((api().field_get)(this, token)) }
    }

    pub fn field_set(&self, this: Handle, token: i32, value: &Obj) {
        if self.ok() {
            (api().field_set)(this, token, value.handle());
        }
    }
}

/// NaN when the value isn't a number (or is null).
pub fn to_f64(o: Obj) -> f64 {
    o.f64().unwrap_or(f64::NAN)
}

/// 0 when the value isn't an integer (or is null).
pub fn to_i64(o: Obj) -> i64 {
    o.i64().unwrap_or(0)
}

pub fn to_bool(o: Obj) -> bool {
    o.f64().is_some_and(|v| v != 0.0)
}