chore: honestly fixed so much and forgot to commit
Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
@@ -17,11 +17,25 @@ use uuid::Uuid;
|
||||
#[derive(Resource)]
|
||||
pub struct ControlSocketPath(pub String);
|
||||
|
||||
/// Resource holding the shutdown sender for control socket
|
||||
#[derive(Resource)]
|
||||
pub struct ControlSocketShutdown(Option<Sender<()>>);
|
||||
|
||||
pub fn cleanup_control_socket(
|
||||
mut exit_events: MessageReader<bevy::app::AppExit>,
|
||||
socket_path: Option<Res<ControlSocketPath>>,
|
||||
shutdown: Option<Res<ControlSocketShutdown>>,
|
||||
) {
|
||||
for _ in exit_events.read() {
|
||||
// Send shutdown signal to control socket thread
|
||||
if let Some(ref shutdown_res) = shutdown {
|
||||
if let Some(ref sender) = shutdown_res.0 {
|
||||
info!("Sending shutdown signal to control socket");
|
||||
let _ = sender.send(());
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up socket file
|
||||
if let Some(ref path) = socket_path {
|
||||
info!("Cleaning up control socket at {}", path.0);
|
||||
let _ = std::fs::remove_file(&path.0);
|
||||
@@ -87,6 +101,10 @@ pub fn start_control_socket_system(
|
||||
let app_queue = AppCommandQueue::new();
|
||||
commands.insert_resource(app_queue.clone());
|
||||
|
||||
// Create shutdown channel
|
||||
let (shutdown_tx, shutdown_rx) = unbounded::<()>();
|
||||
commands.insert_resource(ControlSocketShutdown(Some(shutdown_tx)));
|
||||
|
||||
// Clone bridge and queue for the async task
|
||||
let bridge = bridge.clone();
|
||||
let queue = app_queue;
|
||||
@@ -109,14 +127,25 @@ pub fn start_control_socket_system(
|
||||
}
|
||||
};
|
||||
|
||||
// Accept connections in a loop
|
||||
// Accept connections in a loop with shutdown support
|
||||
loop {
|
||||
match listener.accept().await {
|
||||
Ok((mut stream, _addr)) => {
|
||||
let bridge = bridge.clone();
|
||||
tokio::select! {
|
||||
// Check for shutdown signal
|
||||
_ = tokio::task::spawn_blocking({
|
||||
let rx = shutdown_rx.clone();
|
||||
move || rx.try_recv()
|
||||
}) => {
|
||||
info!("Control socket received shutdown signal");
|
||||
break;
|
||||
}
|
||||
// Accept new connection
|
||||
result = listener.accept() => {
|
||||
match result {
|
||||
Ok((mut stream, _addr)) => {
|
||||
let bridge = bridge.clone();
|
||||
|
||||
let queue_clone = queue.clone();
|
||||
tokio::spawn(async move {
|
||||
let queue_clone = queue.clone();
|
||||
tokio::spawn(async move {
|
||||
// Read command length
|
||||
let mut len_buf = [0u8; 4];
|
||||
if let Err(e) = stream.read_exact(&mut len_buf).await {
|
||||
@@ -155,12 +184,15 @@ pub fn start_control_socket_system(
|
||||
error!("Failed to send response: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to accept connection: {}", e);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to accept connection: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("Control socket server shut down cleanly");
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -270,4 +302,7 @@ async fn send_response(
|
||||
|
||||
// No-op stubs for iOS and release builds
|
||||
#[cfg(any(target_os = "ios", not(debug_assertions)))]
|
||||
pub fn start_control_socket_system() {}
|
||||
pub fn start_control_socket_system(mut commands: Commands) {
|
||||
// Insert empty shutdown resource for consistency
|
||||
commands.insert_resource(ControlSocketShutdown(None));
|
||||
}
|
||||
|
||||
@@ -1,27 +1,37 @@
|
||||
//! Cube entity management
|
||||
|
||||
use bevy::prelude::*;
|
||||
use libmarathon::{
|
||||
networking::{
|
||||
NetworkEntityMap,
|
||||
NetworkedEntity,
|
||||
NetworkedSelection,
|
||||
NetworkedTransform,
|
||||
NodeVectorClock,
|
||||
Synced,
|
||||
},
|
||||
persistence::Persisted,
|
||||
};
|
||||
use serde::{
|
||||
Deserialize,
|
||||
Serialize,
|
||||
};
|
||||
use libmarathon::networking::{NetworkEntityMap, Synced};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Marker component for the replicated cube
|
||||
#[derive(Component, Reflect, Debug, Clone, Copy, Default, Serialize, Deserialize)]
|
||||
#[reflect(Component)]
|
||||
pub struct CubeMarker;
|
||||
///
|
||||
/// This component contains all the data needed for rendering a cube.
|
||||
/// The `#[synced]` attribute automatically handles network synchronization.
|
||||
#[macros::synced]
|
||||
pub struct CubeMarker {
|
||||
/// RGB color values (0.0 to 1.0)
|
||||
pub color_r: f32,
|
||||
pub color_g: f32,
|
||||
pub color_b: f32,
|
||||
pub size: f32,
|
||||
}
|
||||
|
||||
impl CubeMarker {
|
||||
pub fn with_color(color: Color, size: f32) -> Self {
|
||||
let [r, g, b, _] = color.to_linear().to_f32_array();
|
||||
Self {
|
||||
color_r: r,
|
||||
color_g: g,
|
||||
color_b: b,
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(&self) -> Color {
|
||||
Color::srgb(self.color_r, self.color_g, self.color_b)
|
||||
}
|
||||
}
|
||||
|
||||
/// Message to spawn a new cube at a specific position
|
||||
#[derive(Message)]
|
||||
@@ -39,10 +49,33 @@ pub struct CubePlugin;
|
||||
|
||||
impl Plugin for CubePlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.register_type::<CubeMarker>()
|
||||
.add_message::<SpawnCubeEvent>()
|
||||
app.add_message::<SpawnCubeEvent>()
|
||||
.add_message::<DeleteCubeEvent>()
|
||||
.add_systems(Update, (handle_spawn_cube, handle_delete_cube));
|
||||
.add_systems(Update, (
|
||||
handle_spawn_cube,
|
||||
handle_delete_cube,
|
||||
add_cube_rendering_system, // Custom rendering!
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// Custom rendering system - detects Added<CubeMarker> and adds mesh/material
|
||||
fn add_cube_rendering_system(
|
||||
mut commands: Commands,
|
||||
query: Query<(Entity, &CubeMarker), Added<CubeMarker>>,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
for (entity, cube) in &query {
|
||||
commands.entity(entity).insert((
|
||||
Mesh3d(meshes.add(Cuboid::new(cube.size, cube.size, cube.size))),
|
||||
MeshMaterial3d(materials.add(StandardMaterial {
|
||||
base_color: cube.color(), // Use the color() helper method
|
||||
perceptual_roughness: 0.7,
|
||||
metallic: 0.3,
|
||||
..default()
|
||||
})),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,42 +83,16 @@ impl Plugin for CubePlugin {
|
||||
fn handle_spawn_cube(
|
||||
mut commands: Commands,
|
||||
mut messages: MessageReader<SpawnCubeEvent>,
|
||||
mut meshes: Option<ResMut<Assets<Mesh>>>,
|
||||
mut materials: Option<ResMut<Assets<StandardMaterial>>>,
|
||||
node_clock: Res<NodeVectorClock>,
|
||||
) {
|
||||
for event in messages.read() {
|
||||
let entity_id = Uuid::new_v4();
|
||||
let node_id = node_clock.node_id;
|
||||
info!("Spawning cube at {:?}", event.position);
|
||||
|
||||
info!("Spawning cube {} at {:?}", entity_id, event.position);
|
||||
|
||||
let mut entity = commands.spawn((
|
||||
CubeMarker,
|
||||
commands.spawn((
|
||||
CubeMarker::with_color(Color::srgb(0.8, 0.3, 0.6), 1.0),
|
||||
Transform::from_translation(event.position),
|
||||
GlobalTransform::default(),
|
||||
// Networking
|
||||
NetworkedEntity::with_id(entity_id, node_id),
|
||||
NetworkedTransform,
|
||||
NetworkedSelection::default(),
|
||||
// Persistence
|
||||
Persisted::with_id(entity_id),
|
||||
// Sync marker
|
||||
Synced,
|
||||
Synced, // Auto-adds NetworkedEntity, Persisted, NetworkedTransform
|
||||
));
|
||||
|
||||
// Only add rendering components if assets are available (non-headless mode)
|
||||
if let (Some(ref mut meshes), Some(ref mut materials)) = (meshes.as_mut(), materials.as_mut()) {
|
||||
entity.insert((
|
||||
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
|
||||
MeshMaterial3d(materials.add(StandardMaterial {
|
||||
base_color: Color::srgb(0.8, 0.3, 0.6),
|
||||
perceptual_roughness: 0.7,
|
||||
metallic: 0.3,
|
||||
..default()
|
||||
})),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,8 +104,14 @@ fn handle_delete_cube(
|
||||
) {
|
||||
for event in messages.read() {
|
||||
if let Some(bevy_entity) = entity_map.get_entity(event.entity_id) {
|
||||
info!("Deleting cube {}", event.entity_id);
|
||||
commands.entity(bevy_entity).despawn();
|
||||
info!("Marking cube {} for deletion", event.entity_id);
|
||||
// Add ToDelete marker - the handle_local_deletions_system will:
|
||||
// 1. Increment vector clock
|
||||
// 2. Create Delete operation
|
||||
// 3. Record tombstone
|
||||
// 4. Broadcast deletion to peers
|
||||
// 5. Despawn entity locally
|
||||
commands.entity(bevy_entity).insert(libmarathon::networking::ToDelete);
|
||||
} else {
|
||||
warn!("Attempted to delete unknown cube {}", event.entity_id);
|
||||
}
|
||||
|
||||
@@ -43,11 +43,10 @@ fn render_debug_ui(
|
||||
// Node information
|
||||
if let Some(clock) = &node_clock {
|
||||
ui.label(format!("Node ID: {}", &clock.node_id.to_string()[..8]));
|
||||
// Show the current node's clock value (timestamp)
|
||||
let current_timestamp =
|
||||
clock.clock.clocks.get(&clock.node_id).copied().unwrap_or(0);
|
||||
ui.label(format!("Clock: {}", current_timestamp));
|
||||
ui.label(format!("Known nodes: {}", clock.clock.clocks.len()));
|
||||
// Show the sum of all timestamps (total operations across all nodes)
|
||||
let total_ops: u64 = clock.clock.timestamps.values().sum();
|
||||
ui.label(format!("Clock: {} (total ops)", total_ops));
|
||||
ui.label(format!("Known nodes: {}", clock.clock.node_count()));
|
||||
} else {
|
||||
ui.label("Node: Not initialized");
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ fn poll_engine_events(
|
||||
let events = (*bridge).poll_events();
|
||||
|
||||
if !events.is_empty() {
|
||||
debug!("Polling {} engine events", events.len());
|
||||
for event in events {
|
||||
match event {
|
||||
EngineEvent::NetworkingInitializing { session_id, status } => {
|
||||
@@ -113,10 +114,17 @@ fn poll_engine_events(
|
||||
}
|
||||
EngineEvent::PeerJoined { node_id } => {
|
||||
info!("Peer joined: {}", node_id);
|
||||
|
||||
// Initialize peer in vector clock so it shows up in UI immediately
|
||||
node_clock.clock.timestamps.entry(node_id).or_insert(0);
|
||||
|
||||
// TODO(Phase 3.3): Trigger sync
|
||||
}
|
||||
EngineEvent::PeerLeft { node_id } => {
|
||||
info!("Peer left: {}", node_id);
|
||||
|
||||
// Remove peer from vector clock
|
||||
node_clock.clock.timestamps.remove(&node_id);
|
||||
}
|
||||
EngineEvent::LockAcquired { entity_id, holder } => {
|
||||
debug!("Lock acquired: entity={}, holder={}", entity_id, holder);
|
||||
@@ -165,20 +173,15 @@ fn poll_engine_events(
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle app exit to stop networking immediately
|
||||
/// Handle app exit - send shutdown signal to EngineCore
|
||||
fn handle_app_exit(
|
||||
mut exit_events: MessageReader<bevy::app::AppExit>,
|
||||
bridge: Res<EngineBridge>,
|
||||
current_session: Res<CurrentSession>,
|
||||
) {
|
||||
for _ in exit_events.read() {
|
||||
// If networking is active, send stop command
|
||||
// Don't wait - the task will be aborted when the runtime shuts down
|
||||
if current_session.session.state == SessionState::Active
|
||||
|| current_session.session.state == SessionState::Joining {
|
||||
info!("App exiting, aborting networking immediately");
|
||||
bridge.send_command(EngineCommand::StopNetworking);
|
||||
// Don't sleep - just let the app exit. The tokio runtime will clean up.
|
||||
}
|
||||
info!("App exiting - sending Shutdown command to EngineCore");
|
||||
bridge.send_command(EngineCommand::Shutdown);
|
||||
// The EngineCore will receive the Shutdown command and gracefully exit
|
||||
// its event loop, allowing the tokio runtime thread to complete
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,14 @@ use bevy::prelude::*;
|
||||
use libmarathon::{
|
||||
engine::GameAction,
|
||||
platform::input::InputController,
|
||||
networking::{EntityLockRegistry, NetworkedEntity, NetworkedSelection, NodeVectorClock},
|
||||
networking::{
|
||||
EntityLockRegistry, LocalSelection, NetworkedEntity,
|
||||
NodeVectorClock,
|
||||
},
|
||||
};
|
||||
|
||||
use crate::cube::CubeMarker;
|
||||
|
||||
use super::event_buffer::InputEventBuffer;
|
||||
|
||||
pub struct InputHandlerPlugin;
|
||||
@@ -16,7 +21,9 @@ pub struct InputHandlerPlugin;
|
||||
impl Plugin for InputHandlerPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<InputControllerResource>()
|
||||
.add_systems(Update, handle_game_actions);
|
||||
// handle_game_actions updates selection - must run before release_locks_on_deselection_system
|
||||
.add_systems(Update, handle_game_actions.before(libmarathon::networking::release_locks_on_deselection_system))
|
||||
.add_systems(PostUpdate, update_lock_visuals);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,9 +53,10 @@ fn to_bevy_vec2(v: glam::Vec2) -> bevy::math::Vec2 {
|
||||
fn handle_game_actions(
|
||||
input_buffer: Res<InputEventBuffer>,
|
||||
mut controller_res: ResMut<InputControllerResource>,
|
||||
mut lock_registry: ResMut<EntityLockRegistry>,
|
||||
lock_registry: Res<EntityLockRegistry>,
|
||||
node_clock: Res<NodeVectorClock>,
|
||||
mut cube_query: Query<(&NetworkedEntity, &mut Transform, &mut NetworkedSelection), With<crate::cube::CubeMarker>>,
|
||||
mut selection: ResMut<LocalSelection>,
|
||||
mut cube_query: Query<(&NetworkedEntity, &mut Transform), With<crate::cube::CubeMarker>>,
|
||||
camera_query: Query<(&Camera, &GlobalTransform)>,
|
||||
window_query: Query<&Window>,
|
||||
) {
|
||||
@@ -65,14 +73,23 @@ fn handle_game_actions(
|
||||
for action in all_actions {
|
||||
match action {
|
||||
GameAction::SelectEntity { position } => {
|
||||
apply_select_entity(
|
||||
// Do raycasting to find which entity (if any) was clicked
|
||||
let entity_id = raycast_entity(
|
||||
position,
|
||||
&mut lock_registry,
|
||||
node_id,
|
||||
&mut cube_query,
|
||||
&cube_query,
|
||||
&camera_query,
|
||||
&window_query,
|
||||
);
|
||||
|
||||
// Update selection
|
||||
// The release_locks_on_deselection_system will automatically handle lock changes
|
||||
selection.clear();
|
||||
if let Some(id) = entity_id {
|
||||
selection.insert(id);
|
||||
info!("Selected entity {}", id);
|
||||
} else {
|
||||
info!("Deselected all entities");
|
||||
}
|
||||
}
|
||||
|
||||
GameAction::MoveEntity { delta } => {
|
||||
@@ -98,32 +115,32 @@ fn handle_game_actions(
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply SelectEntity action - raycast to find clicked cube and select it
|
||||
fn apply_select_entity(
|
||||
/// Raycast to find which entity was clicked
|
||||
///
|
||||
/// Returns the network ID of the closest entity hit by the ray, or None if nothing was hit.
|
||||
fn raycast_entity(
|
||||
position: glam::Vec2,
|
||||
lock_registry: &mut EntityLockRegistry,
|
||||
node_id: uuid::Uuid,
|
||||
cube_query: &mut Query<(&NetworkedEntity, &mut Transform, &mut NetworkedSelection), With<crate::cube::CubeMarker>>,
|
||||
cube_query: &Query<(&NetworkedEntity, &mut Transform), With<crate::cube::CubeMarker>>,
|
||||
camera_query: &Query<(&Camera, &GlobalTransform)>,
|
||||
window_query: &Query<&Window>,
|
||||
) {
|
||||
) -> Option<uuid::Uuid> {
|
||||
// Get the camera and window
|
||||
let Ok((camera, camera_transform)) = camera_query.single() else {
|
||||
return;
|
||||
return None;
|
||||
};
|
||||
let Ok(window) = window_query.single() else {
|
||||
return;
|
||||
return None;
|
||||
};
|
||||
|
||||
// Convert screen position to world ray
|
||||
let Some(ray) = screen_to_world_ray(position, camera, camera_transform, window) else {
|
||||
return;
|
||||
return None;
|
||||
};
|
||||
|
||||
// Find the closest cube hit by the ray
|
||||
let mut closest_hit: Option<(uuid::Uuid, f32)> = None;
|
||||
|
||||
for (networked, transform, _) in cube_query.iter() {
|
||||
for (networked, transform) in cube_query.iter() {
|
||||
// Test ray against cube AABB (1x1x1 cube)
|
||||
if let Some(distance) = ray_aabb_intersection(
|
||||
ray.origin,
|
||||
@@ -137,31 +154,7 @@ fn apply_select_entity(
|
||||
}
|
||||
}
|
||||
|
||||
// If we hit a cube, clear all selections and select this one
|
||||
if let Some((hit_entity_id, _)) = closest_hit {
|
||||
// Clear all previous selections and locks
|
||||
for (networked, _, mut selection) in cube_query.iter_mut() {
|
||||
selection.clear();
|
||||
lock_registry.release(networked.network_id, node_id);
|
||||
}
|
||||
|
||||
// Select and lock the clicked cube
|
||||
for (networked, _, mut selection) in cube_query.iter_mut() {
|
||||
if networked.network_id == hit_entity_id {
|
||||
selection.add(hit_entity_id);
|
||||
let _ = lock_registry.try_acquire(hit_entity_id, node_id);
|
||||
info!("Selected cube {}", hit_entity_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Clicked on empty space - deselect all
|
||||
for (networked, _, mut selection) in cube_query.iter_mut() {
|
||||
selection.clear();
|
||||
lock_registry.release(networked.network_id, node_id);
|
||||
}
|
||||
info!("Deselected all cubes");
|
||||
}
|
||||
closest_hit.map(|(entity_id, _)| entity_id)
|
||||
}
|
||||
|
||||
/// Apply MoveEntity action to locked cubes
|
||||
@@ -169,12 +162,12 @@ fn apply_move_entity(
|
||||
delta: glam::Vec2,
|
||||
lock_registry: &EntityLockRegistry,
|
||||
node_id: uuid::Uuid,
|
||||
cube_query: &mut Query<(&NetworkedEntity, &mut Transform, &mut NetworkedSelection), With<crate::cube::CubeMarker>>,
|
||||
cube_query: &mut Query<(&NetworkedEntity, &mut Transform), With<crate::cube::CubeMarker>>,
|
||||
) {
|
||||
let bevy_delta = to_bevy_vec2(delta);
|
||||
let sensitivity = 0.01; // Scale factor
|
||||
|
||||
for (networked, mut transform, _) in cube_query.iter_mut() {
|
||||
for (networked, mut transform) in cube_query.iter_mut() {
|
||||
if lock_registry.is_locked_by(networked.network_id, node_id, node_id) {
|
||||
transform.translation.x += bevy_delta.x * sensitivity;
|
||||
transform.translation.y -= bevy_delta.y * sensitivity; // Invert Y for screen coords
|
||||
@@ -187,12 +180,12 @@ fn apply_rotate_entity(
|
||||
delta: glam::Vec2,
|
||||
lock_registry: &EntityLockRegistry,
|
||||
node_id: uuid::Uuid,
|
||||
cube_query: &mut Query<(&NetworkedEntity, &mut Transform, &mut NetworkedSelection), With<crate::cube::CubeMarker>>,
|
||||
cube_query: &mut Query<(&NetworkedEntity, &mut Transform), With<crate::cube::CubeMarker>>,
|
||||
) {
|
||||
let bevy_delta = to_bevy_vec2(delta);
|
||||
let sensitivity = 0.01;
|
||||
|
||||
for (networked, mut transform, _) in cube_query.iter_mut() {
|
||||
for (networked, mut transform) in cube_query.iter_mut() {
|
||||
if lock_registry.is_locked_by(networked.network_id, node_id, node_id) {
|
||||
let rotation_x = Quat::from_rotation_y(bevy_delta.x * sensitivity);
|
||||
let rotation_y = Quat::from_rotation_x(-bevy_delta.y * sensitivity);
|
||||
@@ -206,11 +199,11 @@ fn apply_move_depth(
|
||||
delta: f32,
|
||||
lock_registry: &EntityLockRegistry,
|
||||
node_id: uuid::Uuid,
|
||||
cube_query: &mut Query<(&NetworkedEntity, &mut Transform, &mut NetworkedSelection), With<crate::cube::CubeMarker>>,
|
||||
cube_query: &mut Query<(&NetworkedEntity, &mut Transform), With<crate::cube::CubeMarker>>,
|
||||
) {
|
||||
let sensitivity = 0.1;
|
||||
|
||||
for (networked, mut transform, _) in cube_query.iter_mut() {
|
||||
for (networked, mut transform) in cube_query.iter_mut() {
|
||||
if lock_registry.is_locked_by(networked.network_id, node_id, node_id) {
|
||||
transform.translation.z += delta * sensitivity;
|
||||
}
|
||||
@@ -221,9 +214,9 @@ fn apply_move_depth(
|
||||
fn apply_reset_entity(
|
||||
lock_registry: &EntityLockRegistry,
|
||||
node_id: uuid::Uuid,
|
||||
cube_query: &mut Query<(&NetworkedEntity, &mut Transform, &mut NetworkedSelection), With<crate::cube::CubeMarker>>,
|
||||
cube_query: &mut Query<(&NetworkedEntity, &mut Transform), With<crate::cube::CubeMarker>>,
|
||||
) {
|
||||
for (networked, mut transform, _) in cube_query.iter_mut() {
|
||||
for (networked, mut transform) in cube_query.iter_mut() {
|
||||
if lock_registry.is_locked_by(networked.network_id, node_id, node_id) {
|
||||
transform.translation = Vec3::ZERO;
|
||||
transform.rotation = Quat::IDENTITY;
|
||||
@@ -317,3 +310,38 @@ fn ray_aabb_intersection(
|
||||
Some(tmin)
|
||||
}
|
||||
}
|
||||
|
||||
/// System to update visual appearance based on lock state
|
||||
///
|
||||
/// Color scheme:
|
||||
/// - Green: Locked by us (we can edit)
|
||||
/// - Blue: Locked by someone else (they can edit, we can't)
|
||||
/// - Pink: Not locked (nobody is editing)
|
||||
fn update_lock_visuals(
|
||||
lock_registry: Res<EntityLockRegistry>,
|
||||
node_clock: Res<NodeVectorClock>,
|
||||
mut cubes: Query<(&NetworkedEntity, &mut MeshMaterial3d<StandardMaterial>), With<CubeMarker>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
for (networked, material_handle) in cubes.iter_mut() {
|
||||
let entity_id = networked.network_id;
|
||||
|
||||
// Determine color based on lock state
|
||||
let node_id = node_clock.node_id;
|
||||
let color = if lock_registry.is_locked_by(entity_id, node_id, node_id) {
|
||||
// Locked by us - green
|
||||
Color::srgb(0.3, 0.8, 0.3)
|
||||
} else if lock_registry.is_locked(entity_id, node_id) {
|
||||
// Locked by someone else - blue
|
||||
Color::srgb(0.3, 0.5, 0.9)
|
||||
} else {
|
||||
// Not locked - default pink
|
||||
Color::srgb(0.8, 0.3, 0.6)
|
||||
};
|
||||
|
||||
// Update material color
|
||||
if let Some(mat) = materials.get_mut(&material_handle.0) {
|
||||
mat.base_color = color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ pub mod debug_ui;
|
||||
pub mod engine_bridge;
|
||||
pub mod input;
|
||||
pub mod rendering;
|
||||
pub mod session_ui;
|
||||
pub mod setup;
|
||||
|
||||
pub use cube::CubeMarker;
|
||||
|
||||
@@ -28,6 +28,22 @@ struct Args {
|
||||
/// Path to the control socket (Unix domain socket)
|
||||
#[arg(long, default_value = "/tmp/marathon-control.sock")]
|
||||
control_socket: String,
|
||||
|
||||
/// Log level (trace, debug, info, warn, error)
|
||||
#[arg(long, default_value = "info")]
|
||||
log_level: String,
|
||||
|
||||
/// Path to log file (relative to current directory)
|
||||
#[arg(long, default_value = "marathon.log")]
|
||||
log_file: String,
|
||||
|
||||
/// Disable log file output (console only)
|
||||
#[arg(long, default_value = "false")]
|
||||
no_log_file: bool,
|
||||
|
||||
/// Disable console output (file only)
|
||||
#[arg(long, default_value = "false")]
|
||||
no_console: bool,
|
||||
}
|
||||
|
||||
mod camera;
|
||||
@@ -36,7 +52,6 @@ mod cube;
|
||||
mod debug_ui;
|
||||
mod engine_bridge;
|
||||
mod rendering;
|
||||
mod selection;
|
||||
mod session;
|
||||
mod session_ui;
|
||||
mod setup;
|
||||
@@ -49,7 +64,6 @@ mod input;
|
||||
use camera::*;
|
||||
use cube::*;
|
||||
use rendering::*;
|
||||
use selection::*;
|
||||
use session::*;
|
||||
use session_ui::*;
|
||||
|
||||
@@ -84,13 +98,86 @@ fn main() {
|
||||
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
{
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
tracing_subscriber::EnvFilter::from_default_env()
|
||||
.add_directive("wgpu=error".parse().unwrap())
|
||||
.add_directive("naga=warn".parse().unwrap()),
|
||||
)
|
||||
.init();
|
||||
use tracing_subscriber::prelude::*;
|
||||
|
||||
// Parse log level from args
|
||||
let default_level = args.log_level.parse::<tracing::Level>()
|
||||
.unwrap_or_else(|_| {
|
||||
eprintln!("Invalid log level '{}', using 'info'", args.log_level);
|
||||
tracing::Level::INFO
|
||||
});
|
||||
|
||||
// Build filter with default level and quieter dependencies
|
||||
let filter = tracing_subscriber::EnvFilter::from_default_env()
|
||||
.add_directive(default_level.into())
|
||||
.add_directive("wgpu=error".parse().unwrap())
|
||||
.add_directive("naga=warn".parse().unwrap());
|
||||
|
||||
// Build subscriber based on combination of flags
|
||||
match (args.no_console, args.no_log_file) {
|
||||
(false, false) => {
|
||||
// Both console and file
|
||||
let console_layer = tracing_subscriber::fmt::layer()
|
||||
.with_writer(std::io::stdout);
|
||||
|
||||
let log_path = std::path::PathBuf::from(&args.log_file);
|
||||
let log_dir = log_path.parent().unwrap_or_else(|| std::path::Path::new("."));
|
||||
let log_filename = log_path.file_name().unwrap().to_str().unwrap();
|
||||
let file_appender = tracing_appender::rolling::never(log_dir, log_filename);
|
||||
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
|
||||
std::mem::forget(_guard);
|
||||
let file_layer = tracing_subscriber::fmt::layer()
|
||||
.with_writer(non_blocking)
|
||||
.with_ansi(false);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(console_layer)
|
||||
.with(file_layer)
|
||||
.init();
|
||||
|
||||
eprintln!(">>> Logs written to: {} and console", args.log_file);
|
||||
}
|
||||
(false, true) => {
|
||||
// Console only
|
||||
let console_layer = tracing_subscriber::fmt::layer()
|
||||
.with_writer(std::io::stdout);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(console_layer)
|
||||
.init();
|
||||
|
||||
eprintln!(">>> Console logging only (no log file)");
|
||||
}
|
||||
(true, false) => {
|
||||
// File only
|
||||
let log_path = std::path::PathBuf::from(&args.log_file);
|
||||
let log_dir = log_path.parent().unwrap_or_else(|| std::path::Path::new("."));
|
||||
let log_filename = log_path.file_name().unwrap().to_str().unwrap();
|
||||
let file_appender = tracing_appender::rolling::never(log_dir, log_filename);
|
||||
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
|
||||
std::mem::forget(_guard);
|
||||
let file_layer = tracing_subscriber::fmt::layer()
|
||||
.with_writer(non_blocking)
|
||||
.with_ansi(false);
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.with(file_layer)
|
||||
.init();
|
||||
|
||||
eprintln!(">>> Logs written to: {} (console disabled)", args.log_file);
|
||||
}
|
||||
(true, true) => {
|
||||
// Neither - warn but initialize anyway
|
||||
tracing_subscriber::registry()
|
||||
.with(filter)
|
||||
.init();
|
||||
|
||||
eprintln!(">>> Warning: Both console and file logging disabled!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
eprintln!(">>> Tracing subscriber initialized");
|
||||
@@ -213,7 +300,7 @@ fn main() {
|
||||
app.add_plugins(CameraPlugin);
|
||||
app.add_plugins(RenderingPlugin);
|
||||
app.add_plugins(input::InputHandlerPlugin);
|
||||
app.add_plugins(SelectionPlugin);
|
||||
// SelectionPlugin removed - InputHandlerPlugin already handles selection via GameActions
|
||||
app.add_plugins(DebugUiPlugin);
|
||||
app.add_plugins(SessionUiPlugin);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
use bevy::prelude::*;
|
||||
use libmarathon::{
|
||||
debug_ui::{egui, EguiContexts, EguiPrimaryContextPass},
|
||||
engine::{EngineBridge, EngineCommand},
|
||||
engine::{EngineBridge, EngineCommand, NetworkingInitStatus},
|
||||
networking::{CurrentSession, NodeVectorClock, SessionId, SessionState},
|
||||
};
|
||||
|
||||
@@ -15,10 +15,16 @@ pub struct SessionUiPlugin;
|
||||
impl Plugin for SessionUiPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.init_resource::<SessionUiState>()
|
||||
.init_resource::<NetworkingStatus>()
|
||||
.add_systems(EguiPrimaryContextPass, session_ui_panel);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct NetworkingStatus {
|
||||
pub latest_status: Option<NetworkingInitStatus>,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
struct SessionUiState {
|
||||
join_code_input: String,
|
||||
@@ -31,6 +37,7 @@ fn session_ui_panel(
|
||||
current_session: Res<CurrentSession>,
|
||||
node_clock: Option<Res<NodeVectorClock>>,
|
||||
bridge: Res<EngineBridge>,
|
||||
networking_status: Res<NetworkingStatus>,
|
||||
) {
|
||||
// Log session state for debugging
|
||||
debug!("Session UI: state={:?}, id={}",
|
||||
@@ -45,65 +52,107 @@ fn session_ui_panel(
|
||||
.default_pos([320.0, 10.0])
|
||||
.default_width(280.0)
|
||||
.show(ctx, |ui| {
|
||||
// Check if networking is active based on session state
|
||||
if current_session.session.state == SessionState::Active {
|
||||
// ONLINE MODE: Networking is active
|
||||
ui.heading("Session (Online)");
|
||||
ui.separator();
|
||||
// Display UI based on session state
|
||||
match current_session.session.state {
|
||||
SessionState::Active => {
|
||||
// ONLINE MODE: Networking is active
|
||||
ui.heading("Session (Online)");
|
||||
ui.separator();
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Code:");
|
||||
ui.code(current_session.session.id.to_code());
|
||||
if ui.small_button("📋").clicked() {
|
||||
// TODO: Copy to clipboard (requires clipboard API)
|
||||
info!("Session code: {}", current_session.session.id.to_code());
|
||||
}
|
||||
});
|
||||
|
||||
ui.label(format!("State: {:?}", current_session.session.state));
|
||||
|
||||
if let Some(clock) = node_clock.as_ref() {
|
||||
ui.label(format!("Connected nodes: {}", clock.clock.clocks.len()));
|
||||
}
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Stop networking button
|
||||
if ui.button("🔌 Stop Networking").clicked() {
|
||||
info!("Stopping networking");
|
||||
bridge.send_command(EngineCommand::StopNetworking);
|
||||
}
|
||||
} else {
|
||||
// OFFLINE MODE: Networking not started or disconnected
|
||||
ui.heading("Offline Mode");
|
||||
ui.separator();
|
||||
|
||||
ui.label("World is running offline");
|
||||
ui.label("Vector clock is tracking changes");
|
||||
|
||||
if let Some(clock) = node_clock.as_ref() {
|
||||
let current_seq = clock.clock.clocks.get(&clock.node_id).copied().unwrap_or(0);
|
||||
ui.label(format!("Local sequence: {}", current_seq));
|
||||
}
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Start networking button
|
||||
if ui.button("🌐 Start Networking").clicked() {
|
||||
info!("Starting networking (will create new session)");
|
||||
// Generate a new session ID on the fly
|
||||
let new_session_id = libmarathon::networking::SessionId::new();
|
||||
info!("New session code: {}", new_session_id.to_code());
|
||||
bridge.send_command(EngineCommand::StartNetworking {
|
||||
session_id: new_session_id,
|
||||
ui.horizontal(|ui| {
|
||||
ui.label("Code:");
|
||||
ui.code(current_session.session.id.to_code());
|
||||
if ui.small_button("📋").clicked() {
|
||||
// TODO: Copy to clipboard (requires clipboard API)
|
||||
info!("Session code: {}", current_session.session.id.to_code());
|
||||
}
|
||||
});
|
||||
|
||||
ui.label(format!("State: {:?}", current_session.session.state));
|
||||
|
||||
if let Some(clock) = node_clock.as_ref() {
|
||||
ui.label(format!("Connected nodes: {}", clock.clock.node_count()));
|
||||
}
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Stop networking button
|
||||
if ui.button("🔌 Stop Networking").clicked() {
|
||||
info!("Stopping networking");
|
||||
bridge.send_command(EngineCommand::StopNetworking);
|
||||
}
|
||||
}
|
||||
SessionState::Joining => {
|
||||
// INITIALIZING: Networking is starting up
|
||||
ui.heading("Connecting...");
|
||||
ui.separator();
|
||||
|
||||
ui.add_space(5.0);
|
||||
// Display initialization status
|
||||
if let Some(ref status) = networking_status.latest_status {
|
||||
match status {
|
||||
NetworkingInitStatus::CreatingEndpoint => {
|
||||
ui.label("⏳ Creating network endpoint...");
|
||||
}
|
||||
NetworkingInitStatus::EndpointReady => {
|
||||
ui.label("✓ Network endpoint ready");
|
||||
}
|
||||
NetworkingInitStatus::DiscoveringPeers { session_code, attempt } => {
|
||||
ui.label(format!("🔍 Discovering peers for session {}", session_code));
|
||||
ui.label(format!(" Attempt {}/3...", attempt));
|
||||
}
|
||||
NetworkingInitStatus::PeersFound { count } => {
|
||||
ui.label(format!("✓ Found {} peer(s)!", count));
|
||||
}
|
||||
NetworkingInitStatus::NoPeersFound => {
|
||||
ui.label("ℹ No existing peers found");
|
||||
ui.label(" (Creating new session)");
|
||||
}
|
||||
NetworkingInitStatus::PublishingToDHT => {
|
||||
ui.label("📡 Publishing to DHT...");
|
||||
}
|
||||
NetworkingInitStatus::InitializingGossip => {
|
||||
ui.label("🔧 Initializing gossip protocol...");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ui.label("⏳ Initializing...");
|
||||
}
|
||||
|
||||
// Join existing session button
|
||||
if ui.button("➕ Join Session").clicked() {
|
||||
ui_state.show_join_dialog = true;
|
||||
ui.add_space(10.0);
|
||||
ui.label("Please wait...");
|
||||
}
|
||||
_ => {
|
||||
// OFFLINE MODE: Networking not started or disconnected
|
||||
ui.heading("Offline Mode");
|
||||
ui.separator();
|
||||
|
||||
ui.label("World is running offline");
|
||||
ui.label("Vector clock is tracking changes");
|
||||
|
||||
if let Some(clock) = node_clock.as_ref() {
|
||||
let current_seq = clock.clock.timestamps.get(&clock.node_id).copied().unwrap_or(0);
|
||||
ui.label(format!("Local sequence: {}", current_seq));
|
||||
}
|
||||
|
||||
ui.add_space(10.0);
|
||||
|
||||
// Start networking button
|
||||
if ui.button("🌐 Start Networking").clicked() {
|
||||
info!("Starting networking (will create new session)");
|
||||
// Generate a new session ID on the fly
|
||||
let new_session_id = libmarathon::networking::SessionId::new();
|
||||
info!("New session code: {}", new_session_id.to_code());
|
||||
bridge.send_command(EngineCommand::StartNetworking {
|
||||
session_id: new_session_id,
|
||||
});
|
||||
}
|
||||
|
||||
ui.add_space(5.0);
|
||||
|
||||
// Join existing session button
|
||||
if ui.button("➕ Join Session").clicked() {
|
||||
ui_state.show_join_dialog = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -114,7 +163,10 @@ fn session_ui_panel(
|
||||
.collapsible(false)
|
||||
.show(ctx, |ui| {
|
||||
ui.label("Enter session code (abc-def-123):");
|
||||
ui.text_edit_singleline(&mut ui_state.join_code_input);
|
||||
let text_edit = ui.text_edit_singleline(&mut ui_state.join_code_input);
|
||||
|
||||
// Auto-focus the text input when dialog opens
|
||||
text_edit.request_focus();
|
||||
|
||||
ui.add_space(5.0);
|
||||
ui.label("Note: Joining requires app restart");
|
||||
|
||||
Reference in New Issue
Block a user