Update source files

This commit is contained in:
2025-10-25 15:20:26 -05:00
commit ec05d52ca9
212 changed files with 32416 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
[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"] }
[package.metadata.cargo-machete]
ignored = ["getrandom", "bevy_ecs"]

View File

@@ -0,0 +1,52 @@
//! WASM-JS bridge for game communication
//!
//! This module provides shared state and utilities for WASM bindings.
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use wasm_bindgen::prelude::{JsValue, wasm_bindgen};
// 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 MAP_DIMENSIONS: Arc<Mutex<Option<(u16, u16)>>> =
Arc::new(Mutex::new(None));
}
/// 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()
}
/// Get the cached map dimensions (for WASM-bindgen functions)
pub fn get_map_dimensions() -> Arc<Mutex<Option<(u16, u16)>>> {
MAP_DIMENSIONS.clone()
}
/// Get the map width from cached dimensions
pub fn get_map_width() -> Option<u16> {
MAP_DIMENSIONS.lock().ok()?.map(|(width, _)| width)
}
/// Track an analytics event (separate from game protocol)
#[wasm_bindgen]
pub fn track_analytics_event(event: JsValue) -> Result<(), JsValue> {
#[derive(serde::Deserialize)]
struct AnalyticsEventPayload {
event: String,
#[serde(default)]
properties: 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,130 @@
pub mod bridge;
pub mod render_bridge;
use bevy_ecs::system::{NonSend, Res};
use std::sync::{Arc, Mutex};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use wasm_bindgen::prelude::*;
/// Type alias for cached map dimensions (width, height)
type MapDimensions = Arc<Mutex<Option<(u16, u16)>>>;
#[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.");
// Initialize telemetry in background (non-blocking)
wasm_bindgen_futures::spawn_local(async {
borders_core::telemetry::init(borders_core::telemetry::TelemetryConfig::default()).await;
borders_core::telemetry::track_session_start().await;
tracing::info!("Telemetry initialized");
});
// Start the game immediately (don't wait for telemetry)
wasm_bindgen_futures::spawn_local(async {
run().await;
});
}
async fn run() {
use borders_core::app::{App, Plugin};
use borders_core::time::Time;
use std::time::Duration;
let mut app = App::new();
// Initialize time tracking
app.insert_resource(Time::new());
// Add core game logic and frontend transport
borders_core::plugin::GamePlugin::new(borders_core::plugin::NetworkMode::Local).build(&mut app);
borders_core::ui::FrontendPlugin::new(render_bridge::WasmTransport).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);
// Insert MapDimensions as NonSend resource (shared with WASM bindings)
let map_dimensions_shared = bridge::get_map_dimensions();
app.insert_non_send_resource(map_dimensions_shared);
// Add system to cache map dimensions
app.add_systems(borders_core::app::Update, cache_map_dimensions_system);
// 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
loop {
// Tick time to measure frame delta
if let Some(mut time) = app.world_mut().get_resource_mut::<Time>() {
time.tick();
}
app.update();
gloo_timers::future::sleep(frame_time).await;
}
}
/// System to cache map dimensions from TerritoryManager for WASM-bindgen functions
fn cache_map_dimensions_system(territory_manager: Option<Res<borders_core::game::TerritoryManager>>, map_dimensions: Option<NonSend<MapDimensions>>) {
let Some(territory_manager) = territory_manager else {
return;
};
let Some(map_dimensions) = map_dimensions else {
return;
};
let Ok(mut dims) = map_dimensions.lock() else {
return;
};
// Get dimensions from TerritoryManager
let current = (territory_manager.width(), territory_manager.height());
// Only update if dimensions changed and are non-zero
if dims.as_ref() != Some(&current) && current != (0, 0) {
*dims = Some(current);
tracing::debug!("Cached map dimensions: {}x{}", current.0, current.1);
}
}

View File

@@ -0,0 +1,124 @@
//! 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 BINARY_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 JSON messages (BackendMessage protocol)
#[wasm_bindgen]
pub fn register_backend_message_callback(callback: js_sys::Function) {
BACKEND_MESSAGE_CALLBACK.with(|cb| {
*cb.borrow_mut() = Some(callback);
});
}
/// Register a callback for binary data (terrain/territory init and deltas)
/// Callback receives (data: Uint8Array, type: "init" | "delta")
#[wasm_bindgen]
pub fn register_binary_callback(callback: js_sys::Function) {
BINARY_CALLBACK.with(|cb| {
*cb.borrow_mut() = Some(callback);
});
}
/// Unified message handler for all frontend->backend communication
/// Handles FrontendMessage (JSON) and RenderInputEvent payloads
#[wasm_bindgen]
pub fn send_message(msg: JsValue) -> Result<(), JsValue> {
// Try to deserialize as FrontendMessage first
if let Ok(message) = serde_wasm_bindgen::from_value::<FrontendMessage>(msg.clone()) {
INBOUND_MESSAGES.with(|messages_cell| {
messages_cell.borrow_mut().push_back(message);
});
return Ok(());
}
// Try to deserialize as RenderInputEvent
if let Ok(event) = serde_wasm_bindgen::from_value::<borders_core::ui::protocol::RenderInputEvent>(msg) {
// Handle render input through existing infrastructure
let input_state = crate::bridge::get_input_state();
let mut state = input_state.lock().map_err(|e| JsValue::from_str(&format!("Failed to lock input state: {}", e)))?;
// Get actual map width from cached dimensions
let map_width = crate::bridge::get_map_width().ok_or_else(|| JsValue::from_str("Map dimensions not yet available"))?;
borders_core::ui::handle_render_input(&event, &mut state, map_width).map_err(|e| JsValue::from_str(&e))?;
return Ok(());
}
Err(JsValue::from_str("Failed to deserialize message as FrontendMessage or RenderInputEvent"))
}
/// WASM-specific frontend transport using JavaScript callbacks
#[derive(Clone)]
pub struct WasmTransport;
impl FrontendTransport for WasmTransport {
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 send_init_binary(&self, data: Vec<u8>) -> Result<(), String> {
BINARY_CALLBACK.with(|cb_cell| {
if let Some(cb) = cb_cell.borrow().as_ref() {
let uint8_array = js_sys::Uint8Array::from(&data[..]);
let type_str = JsValue::from_str("init");
let this = JsValue::null();
if let Err(e) = cb.call2(&this, &uint8_array, &type_str) {
return Err(format!("Binary callback failed: {:?}", e));
}
Ok(())
} else {
Err("No binary callback registered".to_string())
}
})
}
fn send_binary_delta(&self, data: Vec<u8>) -> Result<(), String> {
BINARY_CALLBACK.with(|cb_cell| {
if let Some(cb) = cb_cell.borrow().as_ref() {
let uint8_array = js_sys::Uint8Array::from(&data[..]);
let type_str = JsValue::from_str("delta");
let this = JsValue::null();
if let Err(e) = cb.call2(&this, &uint8_array, &type_str) {
return Err(format!("Binary callback failed: {:?}", e));
}
Ok(())
} else {
Err("No binary callback registered".to_string())
}
})
}
fn try_recv_frontend_message(&self) -> Option<FrontendMessage> {
INBOUND_MESSAGES.with(|messages_cell| messages_cell.borrow_mut().pop_front())
}
}