nightshade 0.8.0

A cross-platform data-oriented game engine.
Documentation
use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EngineCommand {
    Log {
        message: String,
    },
    SpawnPrimitive {
        primitive: Primitive,
        x: f32,
        y: f32,
        z: f32,
        request_id: u64,
    },
    DespawnEntity {
        entity_id: u64,
    },
    SetEntityPosition {
        entity_id: u64,
        x: f32,
        y: f32,
        z: f32,
    },
    SetEntityScale {
        entity_id: u64,
        x: f32,
        y: f32,
        z: f32,
    },
    SetEntityRotation {
        entity_id: u64,
        x: f32,
        y: f32,
        z: f32,
        w: f32,
    },
    GetEntityPosition {
        entity_id: u64,
        request_id: u64,
    },
    GetEntityScale {
        entity_id: u64,
        request_id: u64,
    },
    GetEntityRotation {
        entity_id: u64,
        request_id: u64,
    },
    ReadFile {
        path: String,
        request_id: u64,
    },
    LoadTexture {
        path: String,
        request_id: u64,
    },
    LoadPrefab {
        path: String,
        x: f32,
        y: f32,
        z: f32,
        request_id: u64,
    },
    SetEntityMaterial {
        entity_id: u64,
        texture_id: u64,
    },
    SetEntityColor {
        entity_id: u64,
        r: f32,
        g: f32,
        b: f32,
        a: f32,
    },
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EngineEvent {
    KeyPressed {
        key_code: u32,
    },
    KeyReleased {
        key_code: u32,
    },
    MouseMoved {
        x: f32,
        y: f32,
    },
    MouseButtonPressed {
        button: u32,
    },
    MouseButtonReleased {
        button: u32,
    },
    EntitySpawned {
        request_id: u64,
        entity_id: u64,
    },
    EntityPosition {
        request_id: u64,
        entity_id: u64,
        x: f32,
        y: f32,
        z: f32,
    },
    EntityScale {
        request_id: u64,
        entity_id: u64,
        x: f32,
        y: f32,
        z: f32,
    },
    EntityRotation {
        request_id: u64,
        entity_id: u64,
        x: f32,
        y: f32,
        z: f32,
        w: f32,
    },
    EntityNotFound {
        request_id: u64,
        entity_id: u64,
    },
    FrameStart {
        delta_time: f32,
        frame_count: u64,
    },
    FileLoaded {
        request_id: u64,
        data: Vec<u8>,
    },
    FileError {
        request_id: u64,
        error: String,
    },
    TextureLoaded {
        request_id: u64,
        texture_id: u64,
    },
    PrefabLoaded {
        request_id: u64,
        entity_id: u64,
    },
    AssetError {
        request_id: u64,
        error: String,
    },
}

#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
#[repr(u8)]
pub enum Primitive {
    Cube = 1,
    Sphere = 2,
    Cylinder = 3,
    Plane = 4,
    Cone = 5,
}

impl EngineCommand {
    pub fn to_bytes(&self) -> Result<Vec<u8>, postcard::Error> {
        postcard::to_allocvec(self)
    }

    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
        postcard::from_bytes(bytes).ok()
    }
}

impl EngineEvent {
    pub fn to_bytes(&self) -> Result<Vec<u8>, postcard::Error> {
        postcard::to_allocvec(self)
    }

    pub fn from_bytes(bytes: &[u8]) -> Option<Self> {
        postcard::from_bytes(bytes).ok()
    }
}

#[cfg(all(feature = "plugins", target_arch = "wasm32"))]
mod guest {
    use super::{EngineCommand, EngineEvent, Primitive};
    use std::cell::RefCell;
    use std::sync::atomic::{AtomicU64, Ordering};

    unsafe extern "C" {
        fn host_send_command(ptr: *const u8, len: u32);
    }

    static REQUEST_ID_COUNTER: AtomicU64 = AtomicU64::new(1);

    thread_local! {
        static EVENT_BUFFER: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
        static PENDING_EVENTS: RefCell<Vec<EngineEvent>> = const { RefCell::new(Vec::new()) };
    }

    pub fn next_request_id() -> u64 {
        REQUEST_ID_COUNTER.fetch_add(1, Ordering::Relaxed)
    }

    pub fn send_engine_command(command: EngineCommand) {
        let Ok(bytes) = command.to_bytes() else {
            return;
        };
        unsafe {
            host_send_command(bytes.as_ptr(), bytes.len() as u32);
        }
    }

    pub fn log(message: &str) {
        send_engine_command(EngineCommand::Log {
            message: message.to_string(),
        });
    }

    pub fn spawn_primitive(primitive: Primitive, x: f32, y: f32, z: f32) -> u64 {
        let request_id = next_request_id();
        send_engine_command(EngineCommand::SpawnPrimitive {
            primitive,
            x,
            y,
            z,
            request_id,
        });
        request_id
    }

    pub fn spawn_cube(x: f32, y: f32, z: f32) -> u64 {
        spawn_primitive(Primitive::Cube, x, y, z)
    }

    pub fn spawn_sphere(x: f32, y: f32, z: f32) -> u64 {
        spawn_primitive(Primitive::Sphere, x, y, z)
    }

    pub fn spawn_cylinder(x: f32, y: f32, z: f32) -> u64 {
        spawn_primitive(Primitive::Cylinder, x, y, z)
    }

    pub fn spawn_plane(x: f32, y: f32, z: f32) -> u64 {
        spawn_primitive(Primitive::Plane, x, y, z)
    }

    pub fn spawn_cone(x: f32, y: f32, z: f32) -> u64 {
        spawn_primitive(Primitive::Cone, x, y, z)
    }

    pub fn despawn_entity(entity_id: u64) {
        send_engine_command(EngineCommand::DespawnEntity { entity_id });
    }

    pub fn set_entity_position(entity_id: u64, x: f32, y: f32, z: f32) {
        send_engine_command(EngineCommand::SetEntityPosition { entity_id, x, y, z });
    }

    pub fn set_entity_scale(entity_id: u64, x: f32, y: f32, z: f32) {
        send_engine_command(EngineCommand::SetEntityScale { entity_id, x, y, z });
    }

    pub fn set_entity_rotation(entity_id: u64, x: f32, y: f32, z: f32, w: f32) {
        send_engine_command(EngineCommand::SetEntityRotation {
            entity_id,
            x,
            y,
            z,
            w,
        });
    }

    pub fn get_entity_position(entity_id: u64) -> u64 {
        let request_id = next_request_id();
        send_engine_command(EngineCommand::GetEntityPosition {
            entity_id,
            request_id,
        });
        request_id
    }

    pub fn get_entity_scale(entity_id: u64) -> u64 {
        let request_id = next_request_id();
        send_engine_command(EngineCommand::GetEntityScale {
            entity_id,
            request_id,
        });
        request_id
    }

    pub fn get_entity_rotation(entity_id: u64) -> u64 {
        let request_id = next_request_id();
        send_engine_command(EngineCommand::GetEntityRotation {
            entity_id,
            request_id,
        });
        request_id
    }

    pub fn read_file(path: &str) -> u64 {
        let request_id = next_request_id();
        send_engine_command(EngineCommand::ReadFile {
            path: path.to_string(),
            request_id,
        });
        request_id
    }

    pub fn load_texture(path: &str) -> u64 {
        let request_id = next_request_id();
        send_engine_command(EngineCommand::LoadTexture {
            path: path.to_string(),
            request_id,
        });
        request_id
    }

    pub fn load_prefab(path: &str, x: f32, y: f32, z: f32) -> u64 {
        let request_id = next_request_id();
        send_engine_command(EngineCommand::LoadPrefab {
            path: path.to_string(),
            x,
            y,
            z,
            request_id,
        });
        request_id
    }

    pub fn set_entity_material(entity_id: u64, texture_id: u64) {
        send_engine_command(EngineCommand::SetEntityMaterial {
            entity_id,
            texture_id,
        });
    }

    pub fn set_entity_color(entity_id: u64, r: f32, g: f32, b: f32, a: f32) {
        send_engine_command(EngineCommand::SetEntityColor {
            entity_id,
            r,
            g,
            b,
            a,
        });
    }

    #[unsafe(no_mangle)]
    pub extern "C" fn plugin_alloc(size: u32) -> *mut u8 {
        EVENT_BUFFER.with_borrow_mut(|buffer| {
            buffer.resize(size as usize, 0);
            buffer.as_mut_ptr()
        })
    }

    #[unsafe(no_mangle)]
    pub extern "C" fn plugin_receive_event(ptr: *const u8, len: u32) {
        let bytes = unsafe { std::slice::from_raw_parts(ptr, len as usize) };
        if let Some(event) = EngineEvent::from_bytes(bytes) {
            PENDING_EVENTS.with_borrow_mut(|events| {
                events.push(event);
            });
        }
    }

    pub fn drain_engine_events() -> Vec<EngineEvent> {
        PENDING_EVENTS.with_borrow_mut(|events| std::mem::take(events))
    }
}

#[cfg(all(feature = "plugins", target_arch = "wasm32"))]
pub use guest::*;