Fix egui session panel sync with engine state
The session UI panel was not updating when networking started via control socket because poll_engine_events used commands.insert_resource which queued updates until end of schedule. The UI could run before the command was applied. Fixed by: - Initialize CurrentSession at startup in offline state (session.rs) - Use direct ResMut mutation in poll_engine_events (engine_bridge.rs) - Check session.state instead of resource existence (session_ui.rs) This ensures Bevy's change detection triggers immediately when engine events update the session state. Refs #131, #132 Signed-off-by: Sienna Meridian Satterwhite <sienna@r3t.io>
This commit is contained in:
@@ -7,7 +7,7 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use libmarathon::{
|
use libmarathon::{
|
||||||
engine::{EngineBridge, EngineCommand, EngineEvent},
|
engine::{EngineBridge, EngineCommand, EngineEvent},
|
||||||
networking::{CurrentSession, NetworkedEntity, NodeVectorClock, Session, SessionState, VectorClock},
|
networking::{CurrentSession, NetworkedEntity, NodeVectorClock, Session, SessionState},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct EngineBridgePlugin;
|
pub struct EngineBridgePlugin;
|
||||||
@@ -42,9 +42,8 @@ fn detect_changes_and_tick(
|
|||||||
/// 1. Polls all available events from the EngineBridge
|
/// 1. Polls all available events from the EngineBridge
|
||||||
/// 2. Dispatches them to update Bevy resources and state
|
/// 2. Dispatches them to update Bevy resources and state
|
||||||
fn poll_engine_events(
|
fn poll_engine_events(
|
||||||
mut commands: Commands,
|
|
||||||
bridge: Res<EngineBridge>,
|
bridge: Res<EngineBridge>,
|
||||||
mut current_session: Option<ResMut<CurrentSession>>,
|
mut current_session: ResMut<CurrentSession>,
|
||||||
mut node_clock: ResMut<NodeVectorClock>,
|
mut node_clock: ResMut<NodeVectorClock>,
|
||||||
) {
|
) {
|
||||||
let events = (*bridge).poll_events();
|
let events = (*bridge).poll_events();
|
||||||
@@ -53,70 +52,77 @@ fn poll_engine_events(
|
|||||||
for event in events {
|
for event in events {
|
||||||
match event {
|
match event {
|
||||||
EngineEvent::NetworkingStarted { session_id, node_id } => {
|
EngineEvent::NetworkingStarted { session_id, node_id } => {
|
||||||
info!("🌐 Networking started: session={}, node={}",
|
info!("Networking started: session={}, node={}",
|
||||||
session_id.to_code(), node_id);
|
session_id.to_code(), node_id);
|
||||||
|
|
||||||
// Create session if it doesn't exist
|
// Update session to use the new session ID and set state to Active
|
||||||
if current_session.is_none() {
|
current_session.session = Session::new(session_id.clone());
|
||||||
let mut session = Session::new(session_id.clone());
|
current_session.session.state = SessionState::Active;
|
||||||
session.state = SessionState::Active;
|
info!("Updated CurrentSession to Active: {}", session_id.to_code());
|
||||||
commands.insert_resource(CurrentSession::new(session, VectorClock::new()));
|
|
||||||
info!("Created new session resource: {}", session_id.to_code());
|
|
||||||
} else if let Some(ref mut session) = current_session {
|
|
||||||
// Update existing session state to Active
|
|
||||||
session.session.state = SessionState::Active;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update node ID in clock
|
// Update node ID in clock
|
||||||
node_clock.node_id = node_id;
|
node_clock.node_id = node_id;
|
||||||
}
|
}
|
||||||
EngineEvent::NetworkingFailed { error } => {
|
EngineEvent::NetworkingFailed { error } => {
|
||||||
error!("❌ Networking failed: {}", error);
|
error!("Networking failed: {}", error);
|
||||||
|
|
||||||
// Keep session state as Created (if session exists)
|
// Keep session state as Created
|
||||||
if let Some(ref mut session) = current_session {
|
current_session.session.state = SessionState::Created;
|
||||||
session.session.state = SessionState::Created;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
EngineEvent::NetworkingStopped => {
|
EngineEvent::NetworkingStopped => {
|
||||||
info!("🔌 Networking stopped");
|
info!("Networking stopped");
|
||||||
|
|
||||||
// Update session state to Disconnected (if session exists)
|
// Update session state to Disconnected
|
||||||
if let Some(ref mut session) = current_session {
|
current_session.session.state = SessionState::Disconnected;
|
||||||
session.session.state = SessionState::Disconnected;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
EngineEvent::PeerJoined { node_id } => {
|
EngineEvent::PeerJoined { node_id } => {
|
||||||
info!("👋 Peer joined: {}", node_id);
|
info!("Peer joined: {}", node_id);
|
||||||
// TODO(Phase 3.3): Trigger sync
|
// TODO(Phase 3.3): Trigger sync
|
||||||
}
|
}
|
||||||
EngineEvent::PeerLeft { node_id } => {
|
EngineEvent::PeerLeft { node_id } => {
|
||||||
info!("👋 Peer left: {}", node_id);
|
info!("Peer left: {}", node_id);
|
||||||
}
|
}
|
||||||
EngineEvent::LockAcquired { entity_id, holder } => {
|
EngineEvent::LockAcquired { entity_id, holder } => {
|
||||||
debug!("🔒 Lock acquired: entity={}, holder={}", entity_id, holder);
|
debug!("Lock acquired: entity={}, holder={}", entity_id, holder);
|
||||||
// TODO(Phase 3.4): Update lock visuals
|
// TODO(Phase 3.4): Update lock visuals
|
||||||
}
|
}
|
||||||
EngineEvent::LockReleased { entity_id } => {
|
EngineEvent::LockReleased { entity_id } => {
|
||||||
debug!("🔓 Lock released: entity={}", entity_id);
|
debug!("Lock released: entity={}", entity_id);
|
||||||
// TODO(Phase 3.4): Update lock visuals
|
// TODO(Phase 3.4): Update lock visuals
|
||||||
}
|
}
|
||||||
|
EngineEvent::ClockTicked { sequence, clock: _ } => {
|
||||||
|
debug!("Clock ticked: sequence={}", sequence);
|
||||||
|
// Clock tick confirmed - no action needed
|
||||||
|
}
|
||||||
|
EngineEvent::SessionJoined { session_id } => {
|
||||||
|
info!("Session joined: {}", session_id.to_code());
|
||||||
|
// Update session state
|
||||||
|
current_session.session.state = SessionState::Joining;
|
||||||
|
}
|
||||||
|
EngineEvent::SessionLeft => {
|
||||||
|
info!("Session left");
|
||||||
|
// Update session state
|
||||||
|
current_session.session.state = SessionState::Left;
|
||||||
|
}
|
||||||
|
EngineEvent::EntitySpawned { entity_id, position, rotation, version: _ } => {
|
||||||
|
debug!("Entity spawned: id={}, pos={:?}, rot={:?}", entity_id, position, rotation);
|
||||||
|
// TODO: Spawn entity in Bevy
|
||||||
|
}
|
||||||
|
EngineEvent::EntityUpdated { entity_id, position, rotation, version: _ } => {
|
||||||
|
debug!("Entity updated: id={}, pos={:?}, rot={:?}", entity_id, position, rotation);
|
||||||
|
// TODO: Update entity in Bevy
|
||||||
|
}
|
||||||
|
EngineEvent::EntityDeleted { entity_id, version: _ } => {
|
||||||
|
debug!("Entity deleted: id={}", entity_id);
|
||||||
|
// TODO: Delete entity in Bevy
|
||||||
|
}
|
||||||
EngineEvent::LockDenied { entity_id, current_holder } => {
|
EngineEvent::LockDenied { entity_id, current_holder } => {
|
||||||
debug!("⛔ Lock denied: entity={}, holder={}", entity_id, current_holder);
|
debug!("Lock denied: entity={}, current_holder={}", entity_id, current_holder);
|
||||||
// TODO(Phase 3.4): Show visual feedback
|
// TODO: Show lock denied feedback
|
||||||
}
|
}
|
||||||
EngineEvent::LockExpired { entity_id } => {
|
EngineEvent::LockExpired { entity_id } => {
|
||||||
debug!("⏰ Lock expired: entity={}", entity_id);
|
debug!("Lock expired: entity={}", entity_id);
|
||||||
// TODO(Phase 3.4): Update lock visuals
|
// TODO: Update lock visuals
|
||||||
}
|
|
||||||
EngineEvent::ClockTicked { sequence, clock } => {
|
|
||||||
debug!("🕐 Clock ticked to {}", sequence);
|
|
||||||
|
|
||||||
// Update the NodeVectorClock resource with the new clock state
|
|
||||||
node_clock.clock = clock;
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
debug!("Unhandled engine event: {:?}", event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,17 +6,17 @@
|
|||||||
use bevy::prelude::*;
|
use bevy::prelude::*;
|
||||||
use libmarathon::{
|
use libmarathon::{
|
||||||
networking::{
|
networking::{
|
||||||
EntityLockRegistry, NetworkEntityMap, NodeVectorClock, VectorClock,
|
CurrentSession, EntityLockRegistry, NetworkEntityMap, NodeVectorClock, Session, VectorClock,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
/// Initialize offline resources on app startup
|
/// Initialize offline resources on app startup
|
||||||
///
|
///
|
||||||
/// This sets up the vector clock and networking-related resources, but does NOT
|
/// This sets up the vector clock and networking-related resources.
|
||||||
/// create a session. Sessions only exist when networking is active.
|
/// Creates an offline CurrentSession that will be updated when networking starts.
|
||||||
pub fn initialize_offline_resources(world: &mut World) {
|
pub fn initialize_offline_resources(world: &mut World) {
|
||||||
info!("Initializing offline resources (no session yet)...");
|
info!("Initializing offline resources...");
|
||||||
|
|
||||||
// Create node ID (persists for this app instance)
|
// Create node ID (persists for this app instance)
|
||||||
let node_id = Uuid::new_v4();
|
let node_id = Uuid::new_v4();
|
||||||
@@ -32,5 +32,11 @@ pub fn initialize_offline_resources(world: &mut World) {
|
|||||||
world.insert_resource(NetworkEntityMap::default());
|
world.insert_resource(NetworkEntityMap::default());
|
||||||
world.insert_resource(EntityLockRegistry::default());
|
world.insert_resource(EntityLockRegistry::default());
|
||||||
|
|
||||||
info!("Offline resources initialized (vector clock ready)");
|
// Create offline session (will be updated when networking starts)
|
||||||
|
// This ensures CurrentSession resource always exists for UI binding
|
||||||
|
let offline_session_id = libmarathon::networking::SessionId::new();
|
||||||
|
let offline_session = Session::new(offline_session_id);
|
||||||
|
world.insert_resource(CurrentSession::new(offline_session, VectorClock::new()));
|
||||||
|
|
||||||
|
info!("Offline resources initialized (vector clock ready, session created in offline state)");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use bevy::prelude::*;
|
|||||||
use libmarathon::{
|
use libmarathon::{
|
||||||
debug_ui::{egui, EguiContexts, EguiPrimaryContextPass},
|
debug_ui::{egui, EguiContexts, EguiPrimaryContextPass},
|
||||||
engine::{EngineBridge, EngineCommand},
|
engine::{EngineBridge, EngineCommand},
|
||||||
networking::{CurrentSession, NodeVectorClock, SessionId},
|
networking::{CurrentSession, NodeVectorClock, SessionId, SessionState},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct SessionUiPlugin;
|
pub struct SessionUiPlugin;
|
||||||
@@ -28,10 +28,15 @@ struct SessionUiState {
|
|||||||
fn session_ui_panel(
|
fn session_ui_panel(
|
||||||
mut contexts: EguiContexts,
|
mut contexts: EguiContexts,
|
||||||
mut ui_state: ResMut<SessionUiState>,
|
mut ui_state: ResMut<SessionUiState>,
|
||||||
current_session: Option<Res<CurrentSession>>,
|
current_session: Res<CurrentSession>,
|
||||||
node_clock: Option<Res<NodeVectorClock>>,
|
node_clock: Option<Res<NodeVectorClock>>,
|
||||||
bridge: Res<EngineBridge>,
|
bridge: Res<EngineBridge>,
|
||||||
) {
|
) {
|
||||||
|
// Log session state for debugging
|
||||||
|
debug!("Session UI: state={:?}, id={}",
|
||||||
|
current_session.session.state,
|
||||||
|
current_session.session.id.to_code());
|
||||||
|
|
||||||
let Ok(ctx) = contexts.ctx_mut() else {
|
let Ok(ctx) = contexts.ctx_mut() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
@@ -40,21 +45,22 @@ fn session_ui_panel(
|
|||||||
.default_pos([320.0, 10.0])
|
.default_pos([320.0, 10.0])
|
||||||
.default_width(280.0)
|
.default_width(280.0)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
if let Some(session) = current_session.as_ref() {
|
// Check if networking is active based on session state
|
||||||
// ONLINE MODE: Session exists, networking is active
|
if current_session.session.state == SessionState::Active {
|
||||||
|
// ONLINE MODE: Networking is active
|
||||||
ui.heading("Session (Online)");
|
ui.heading("Session (Online)");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label("Code:");
|
ui.label("Code:");
|
||||||
ui.code(session.session.id.to_code());
|
ui.code(current_session.session.id.to_code());
|
||||||
if ui.small_button("📋").clicked() {
|
if ui.small_button("📋").clicked() {
|
||||||
// TODO: Copy to clipboard (requires clipboard API)
|
// TODO: Copy to clipboard (requires clipboard API)
|
||||||
info!("Session code: {}", session.session.id.to_code());
|
info!("Session code: {}", current_session.session.id.to_code());
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.label(format!("State: {:?}", session.session.state));
|
ui.label(format!("State: {:?}", current_session.session.state));
|
||||||
|
|
||||||
if let Some(clock) = node_clock.as_ref() {
|
if let Some(clock) = node_clock.as_ref() {
|
||||||
ui.label(format!("Connected nodes: {}", clock.clock.clocks.len()));
|
ui.label(format!("Connected nodes: {}", clock.clock.clocks.len()));
|
||||||
@@ -68,7 +74,7 @@ fn session_ui_panel(
|
|||||||
bridge.send_command(EngineCommand::StopNetworking);
|
bridge.send_command(EngineCommand::StopNetworking);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// OFFLINE MODE: No session, networking not started
|
// OFFLINE MODE: Networking not started or disconnected
|
||||||
ui.heading("Offline Mode");
|
ui.heading("Offline Mode");
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user