Update source files

This commit is contained in:
2025-10-13 16:40:21 -05:00
commit 3b2598f24f
178 changed files with 30153 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
[package]
name = "borders-wasm"
version.workspace = true
edition.workspace = true
authors.workspace = true
[lib]
crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
[features]
default = []
[dependencies]
bevy_ecs = { version = "0.17", default-features = false, features = ["std"] }
borders-core = { path = "../borders-core", features = ["ui"] }
console_error_panic_hook = "0.1"
getrandom = { version = "0.3", features = ["wasm_js"] }
gloo-timers = { version = "0.3", features = ["futures"] }
js-sys = "0.3"
lazy_static = "1.5"
serde = { version = "1.0", features = ["derive"] }
serde-wasm-bindgen = "0.6"
serde_json = "1.0"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["registry", "env-filter"] }
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
wasm-tracing = { version = "2.1", features = ["tracing-log"] }
web-time = "1.1"
[package.metadata.cargo-machete]
ignored = ["getrandom", "bevy_ecs"]

View File

@@ -0,0 +1,67 @@
//! WASM-JS bridge for game communication
//!
//! This module provides JavaScript bindings to expose game data and events
//! from the Bevy/WASM game to the React frontend.
use std::sync::{Arc, Mutex};
use wasm_bindgen::prelude::{JsValue, wasm_bindgen};
use borders_core::ui::protocol::{CameraStateUpdate, RenderInputEvent};
// Global state for input handling (needs to be accessible from Bevy systems)
lazy_static::lazy_static! {
static ref INPUT_STATE: Arc<Mutex<borders_core::ui::input::InputState>> =
Arc::new(Mutex::new(borders_core::ui::input::InputState::new()));
static ref CAMERA_STATE: Arc<Mutex<Option<CameraStateUpdate>>> =
Arc::new(Mutex::new(None));
}
/// Handle render input events from the frontend (clicks, keys, hover)
#[wasm_bindgen]
pub fn handle_render_input(event: JsValue) -> Result<(), JsValue> {
let event: RenderInputEvent = serde_wasm_bindgen::from_value(event).map_err(|e| JsValue::from_str(&format!("Failed to deserialize render input: {}", e)))?;
let mut state = INPUT_STATE.lock().map_err(|e| JsValue::from_str(&format!("Failed to lock input state: {}", e)))?;
// TODO: Get actual map width from GameView or TerrainData
let map_width = 2560; // Placeholder
borders_core::ui::handle_render_input(&event, &mut state, map_width).map_err(|e| JsValue::from_str(&e))
}
/// Handle camera state updates from the frontend
#[wasm_bindgen]
pub fn handle_camera_update(state: JsValue) -> Result<(), JsValue> {
let update: CameraStateUpdate = serde_wasm_bindgen::from_value(state).map_err(|e| JsValue::from_str(&format!("Failed to deserialize camera state: {}", e)))?;
let mut camera_state = CAMERA_STATE.lock().map_err(|e| JsValue::from_str(&format!("Failed to lock camera state: {}", e)))?;
borders_core::ui::handle_camera_update(update, &mut camera_state).map_err(|e| JsValue::from_str(&e))
}
/// Get the global input state (for Bevy systems to access)
pub fn get_input_state() -> Arc<Mutex<borders_core::ui::input::InputState>> {
INPUT_STATE.clone()
}
/// Track an analytics event
#[wasm_bindgen]
pub fn track_analytics_event(event: JsValue) -> Result<(), JsValue> {
#[derive(serde::Deserialize)]
struct AnalyticsEventPayload {
event: String,
#[serde(default)]
properties: std::collections::HashMap<String, serde_json::Value>,
}
let payload: AnalyticsEventPayload = serde_wasm_bindgen::from_value(event).map_err(|e| JsValue::from_str(&format!("Failed to deserialize analytics event: {}", e)))?;
let telemetry_event = borders_core::telemetry::TelemetryEvent { event: payload.event, properties: payload.properties };
// Spawn a task to track the event asynchronously
wasm_bindgen_futures::spawn_local(async move {
borders_core::telemetry::track(telemetry_event).await;
});
Ok(())
}

View File

@@ -0,0 +1,98 @@
pub mod bridge;
pub mod render_bridge;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use wasm_bindgen::prelude::*;
#[wasm_bindgen(start)]
pub fn main() {
// Set up panic hook for better error messages in the browser
console_error_panic_hook::set_once();
// Initialize tracing for WASM (outputs to browser console)
// Debug builds: debug level for our crates, info for dependencies
// Release builds: warn level for our crates, error for dependencies
#[cfg(debug_assertions)]
let level_filter = "borders_core=debug,borders_protocol=debug,borders_wasm=debug,info";
#[cfg(not(debug_assertions))]
let level_filter = "borders_core=warn,borders_protocol=warn,borders_wasm=warn,error";
if let Err(e) = tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(level_filter))
.with(wasm_tracing::WasmLayer::new(
wasm_tracing::WasmLayerConfig::new()
.set_show_fields(true)
.set_report_logs_in_timings(true)
.set_console_config(wasm_tracing::ConsoleConfig::ReportWithConsoleColor)
// Only show origin (filename, line number) in debug builds
.set_show_origin(true)
.clone(),
))
.try_init()
{
eprintln!("Failed to initialize tracing: {}", e);
}
// Log build information
tracing::info!("Iron Borders v{}", borders_core::build_info::VERSION);
tracing::info!("Git: {} | Built: {}", borders_core::build_info::git_commit_short(), borders_core::build_info::BUILD_TIME);
tracing::info!("© 2025 Ryan Walters. All Rights Reserved.");
// Start the Bevy app
wasm_bindgen_futures::spawn_local(async {
// Initialize telemetry (async to load user ID from IndexedDB)
borders_core::telemetry::init(borders_core::telemetry::TelemetryConfig::default()).await;
borders_core::telemetry::track_session_start().await;
tracing::info!("Telemetry initialized");
run().await;
});
}
async fn run() {
use borders_core::app::{App, Plugin};
use borders_core::time::Time;
use std::time::Duration;
use web_time::Instant;
let mut app = App::new();
// Initialize time tracking
app.insert_resource(Time::new());
// Add core game logic and frontend transport
borders_core::GamePlugin::new(borders_core::plugin::NetworkMode::Local).build(&mut app);
borders_core::ui::FrontendPlugin::new(render_bridge::WasmRenderBridgeTransport).build(&mut app);
// Insert InputState as NonSend resource (shared with WASM bindings)
let input_state_shared = bridge::get_input_state();
app.insert_non_send_resource(input_state_shared);
// Run startup systems
app.run_startup();
// Finish app setup
app.finish();
app.cleanup();
// Manual update loop at 60 FPS (worker-compatible)
let frame_time = Duration::from_millis(16); // ~60 FPS
let mut last_frame_time = Instant::now();
loop {
let frame_start = Instant::now();
// Update time resource with delta from PREVIOUS frame
let delta = frame_start.duration_since(last_frame_time);
if let Some(mut time) = app.world_mut().get_resource_mut::<Time>() {
time.update(delta);
}
app.update();
last_frame_time = frame_start;
gloo_timers::future::sleep(frame_time).await;
}
}

View File

@@ -0,0 +1,68 @@
//! WASM-specific frontend transport using JavaScript callbacks
//!
//! This module provides the WASM implementation of FrontendTransport,
//! sending render messages and UI events through JavaScript callbacks to the browser frontend.
use std::cell::RefCell;
use std::collections::VecDeque;
use borders_core::ui::FrontendTransport;
use borders_core::ui::protocol::{BackendMessage, FrontendMessage};
use wasm_bindgen::prelude::*;
// Thread-local storage for callbacks and inbound messages
thread_local! {
static BACKEND_MESSAGE_CALLBACK: RefCell<Option<js_sys::Function>> = const { RefCell::new(None) };
static INBOUND_MESSAGES: RefCell<VecDeque<FrontendMessage>> = const { RefCell::new(VecDeque::new()) };
}
/// Register a callback for backend messages (all messages from backend to frontend)
#[wasm_bindgen]
pub fn register_backend_message_callback(callback: js_sys::Function) {
BACKEND_MESSAGE_CALLBACK.with(|cb| {
*cb.borrow_mut() = Some(callback);
});
}
/// Send a frontend message from JavaScript to the game core
#[wasm_bindgen]
pub fn send_frontend_message(msg: JsValue) -> Result<(), JsValue> {
let message: FrontendMessage = serde_wasm_bindgen::from_value(msg).map_err(|e| JsValue::from_str(&format!("Failed to deserialize frontend message: {}", e)))?;
INBOUND_MESSAGES.with(|messages_cell| {
messages_cell.borrow_mut().push_back(message);
});
Ok(())
}
/// WASM-specific frontend transport using JavaScript callbacks
#[derive(Clone)]
pub struct WasmRenderBridgeTransport;
impl FrontendTransport for WasmRenderBridgeTransport {
fn send_backend_message(&self, message: &BackendMessage) -> Result<(), String> {
BACKEND_MESSAGE_CALLBACK.with(|cb_cell| {
if let Some(cb) = cb_cell.borrow().as_ref() {
match serde_wasm_bindgen::to_value(message) {
Ok(js_payload) => {
let this = JsValue::null();
if let Err(e) = cb.call1(&this, &js_payload) {
return Err(format!("Backend message callback failed: {:?}", e));
}
Ok(())
}
Err(e) => Err(format!("Failed to serialize backend message: {}", e)),
}
} else {
Err("No backend message callback registered".to_string())
}
})
}
fn try_recv_frontend_message(&self) -> Option<FrontendMessage> {
INBOUND_MESSAGES.with(|messages_cell| messages_cell.borrow_mut().pop_front())
}
// WASM uses structured messages only, no binary streaming
}