1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
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
}