wormhole-engine 0.1.0

A portable, no-editor game engine with Rust core and Crystal scripting
Documentation
mod window;
mod renderer;
mod timer;
mod script_host;
mod ffi_bridge;
mod input;
mod camera;
mod texture;

use winit::{
    event::{DeviceEvent, Event, WindowEvent},
    event_loop::{ControlFlow, EventLoop},
    keyboard::{KeyCode, PhysicalKey},
    window::CursorGrabMode,
};
use renderer::Renderer;
use timer::Timer;
use script_host::ScriptHost;
use input::InputState;
use camera::Camera;
use std::path::PathBuf;
use libloading::Library;

fn main() {
    env_logger::init();
    
    // Initialize FFI bridge for Crystal scripts (creates its own InputState)
    let engine_state = ffi_bridge::init_bridge();
    
    // Register the engine state with the library's static (so Crystal can access it)
    // We clone the EngineState (which clones the Arcs, so they share the same data),
    // then register a pointer to it with the library
    unsafe {
        if let Ok(lib) = libloading::Library::new("target/debug/libwormhole.dylib") {
            if let Ok(register_fn) = lib.get::<extern "C" fn(*mut ffi_bridge::EngineState)>(b"engine_register_state_ptr") {
                let cloned_state = engine_state.clone(); // Clone the Arcs (they share data)
                let state_ptr = Box::into_raw(Box::new(cloned_state));
                register_fn(state_ptr);
            }
        }
    }
    
    let event_loop = EventLoop::new().unwrap();
    event_loop.set_control_flow(ControlFlow::Poll);
    
    let mut window_state = window::WindowState::new(&event_loop);
    let window = &window_state.window;
    
    // Grab cursor and hide it for mouse look
    window.set_cursor_grab(CursorGrabMode::Locked)
        .or_else(|_| window.set_cursor_grab(CursorGrabMode::Confined))
        .expect("Failed to grab cursor");
    window.set_cursor_visible(false);
    
    let mut renderer = pollster::block_on(Renderer::new(window));
    
    // Camera pointer will be registered from within the closure (after renderer is moved)
    
    let mut timer = Timer::new();
    
    // Try to load script library (optional for now)
    let script_host = match find_script_library() {
        Some(path) => {
            match ScriptHost::load(&path) {
                Ok(host) => {
                    println!("Loaded script library from: {:?}", path);
                    host.call_init();
                    Some(host)
                }
                Err(e) => {
                    eprintln!("Failed to load script library: {}", e);
                    eprintln!("Continuing without script...");
                    None
                }
            }
        }
        None => {
            println!("No script library found, continuing without script...");
            None
        }
    };

    let window_id = renderer.window_id();
    let engine_state = engine_state; // Move into closure
    let input_state_ref = engine_state.input_state.clone();
    
    // Register camera pointer AFTER renderer is moved into closure
    // We need to do this from within the closure, but we can't access renderer before it's moved
    // So we'll register it on the first redraw event
    let mut camera_registered = false;
    
    event_loop.run(move |event, elwt| {
        match event {
            // Handle device events for relative mouse movement (works with cursor grab)
            Event::DeviceEvent { event: DeviceEvent::MouseMotion { delta }, .. } => {
                if let Ok(mut input) = input_state_ref.lock() {
                    // delta is (x, y) relative movement
                    let current_pos = input.mouse_position();
                    input.on_cursor_moved(current_pos.0 + delta.0 as f32, current_pos.1 + delta.1 as f32);
                }
            }
            Event::WindowEvent {
                ref event,
                window_id: id,
            } if id == window_id => {
                match event {
                    WindowEvent::CloseRequested => {
                        elwt.exit();
                    }
                    WindowEvent::Resized(physical_size) => {
                        renderer.resize(physical_size.width, physical_size.height);
                    }
                    WindowEvent::KeyboardInput { event, .. } => {
                        if let PhysicalKey::Code(key_code) = event.physical_key {
                            // Handle ESC key to exit
                            if key_code == KeyCode::Escape && event.state == winit::event::ElementState::Pressed {
                                elwt.exit();
                            }
                            
                            if let Ok(mut input) = input_state_ref.lock() {
                                match event.state {
                                    winit::event::ElementState::Pressed => {
                                        input.on_key_pressed(key_code);
                                    }
                                    winit::event::ElementState::Released => {
                                        input.on_key_released(key_code);
                                    }
                                }
                            }
                        }
                    }
                    WindowEvent::CursorMoved { position, .. } => {
                        if let Ok(mut input) = input_state_ref.lock() {
                            input.on_cursor_moved(position.x as f32, position.y as f32);
                        }
                    }
                    WindowEvent::MouseInput { state, button, .. } => {
                        if let Ok(mut input) = input_state_ref.lock() {
                            match state {
                                winit::event::ElementState::Pressed => {
                                    input.on_mouse_button_pressed(*button);
                                }
                                winit::event::ElementState::Released => {
                                    input.on_mouse_button_released(*button);
                                }
                            }
                        }
                    }
                    WindowEvent::RedrawRequested => {
                        // Register camera pointer on first redraw (after renderer is moved into closure)
                        if !camera_registered {
                            unsafe {
                                if let Ok(lib) = Library::new("target/debug/libwormhole.dylib") {
                                    if let Ok(register_fn) = lib.get::<extern "C" fn(*mut Camera)>(b"engine_register_camera_ptr") {
                                        let camera_ptr: *mut Camera = std::ptr::addr_of_mut!(renderer.camera);
                                        eprintln!("DEBUG: Registering camera pointer from closure: {:p}", camera_ptr);
                                        register_fn(camera_ptr);
                                    }
                                }
                            }
                            camera_registered = true;
                        }
                        
                        let delta = timer.delta_time();
                        
                        // Call script update if available (reads mouse_delta before clearing)
                        if let Some(ref script) = script_host {
                            script.call_update(delta);
                        }
                        
                        // Clear frame-specific state AFTER script has read it
                        if let Ok(mut input) = input_state_ref.lock() {
                            input.begin_frame();
                        }
                        
                        // Get rotation from FFI bridge (set by Crystal script)
                        let rotation = ffi_bridge::get_rotation(&engine_state);
                        renderer.set_rotation(rotation);
                        
                        match renderer.render() {
                            Ok(_) => {}
                            Err(wgpu::SurfaceError::Lost) => {
                                // Surface lost, will be reconfigured on next render
                            }
                            Err(wgpu::SurfaceError::OutOfMemory) => {
                                eprintln!("Out of memory!");
                                elwt.exit();
                            }
                            Err(e) => {
                                eprintln!("Render error: {:?}", e);
                            }
                        }
                        
                        // Update FPS in window title  
                        if let Some(fps) = timer.update_fps() {
                            window.set_title(&format!("Wormhole - FPS: {}", fps));
                        }
                    }
                    _ => {}
                }
            }
            Event::AboutToWait => {
                window.request_redraw();
            }
            _ => {}
        }
    }).unwrap();
}

fn find_script_library() -> Option<PathBuf> {
    // First, try to find library in same directory as executable (for packaged games)
    if let Ok(exe_path) = std::env::current_exe() {
        if let Some(exe_dir) = exe_path.parent() {
            let exe_dir_candidates = [
                exe_dir.join("libgame.dylib"),  // macOS
                exe_dir.join("libgame.so"),     // Linux
                exe_dir.join("game.dll"),       // Windows
            ];
            
            for candidate in &exe_dir_candidates {
                if candidate.exists() {
                    return Some(candidate.clone());
                }
            }
        }
    }
    
    // Fallback: try common development locations
    let candidates = [
        "libgame.dylib",  // macOS
        "target/debug/libgame.dylib",
        "target/release/libgame.dylib",
        "examples/test-game/libgame.dylib",
        "libgame.so",  // Linux
        "target/debug/libgame.so",
        "target/release/libgame.so",
        "examples/test-game/libgame.so",
        "game.dll",  // Windows
        "target/debug/game.dll",
        "target/release/game.dll",
        "examples/test-game/game.dll",
    ];
    
    for candidate in &candidates {
        if PathBuf::from(candidate).exists() {
            return Some(PathBuf::from(candidate));
        }
    }
    
    None
}