Add spawn/delete commands, fix session state and entity broadcast
- marathonctl now supports spawn/delete entity commands - Fixed session state bug (was transitioning to Left every 5s) - Fixed entity broadcast to detect Added<NetworkedEntity> - Added AppCommandQueue pattern for app-level control commands References: #131, #132
This commit is contained in:
@@ -6,27 +6,90 @@
|
||||
|
||||
use anyhow::Result;
|
||||
use bevy::prelude::*;
|
||||
use crossbeam_channel::{Receiver, Sender, unbounded};
|
||||
use libmarathon::{
|
||||
engine::{EngineBridge, EngineCommand},
|
||||
networking::{ControlCommand, ControlResponse, SessionId},
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Resource holding the control socket path
|
||||
#[derive(Resource)]
|
||||
pub struct ControlSocketPath(pub String);
|
||||
|
||||
pub fn cleanup_control_socket(
|
||||
mut exit_events: MessageReader<bevy::app::AppExit>,
|
||||
socket_path: Option<Res<ControlSocketPath>>,
|
||||
) {
|
||||
for _ in exit_events.read() {
|
||||
if let Some(ref path) = socket_path {
|
||||
info!("Cleaning up control socket at {}", path.0);
|
||||
let _ = std::fs::remove_file(&path.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Commands that can be sent from the control socket to the app
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum AppCommand {
|
||||
SpawnEntity {
|
||||
entity_type: String,
|
||||
position: Vec3,
|
||||
},
|
||||
DeleteEntity {
|
||||
entity_id: Uuid,
|
||||
},
|
||||
}
|
||||
|
||||
/// Queue for app-level commands from control socket
|
||||
#[derive(Resource, Clone)]
|
||||
pub struct AppCommandQueue {
|
||||
sender: Sender<AppCommand>,
|
||||
receiver: Receiver<AppCommand>,
|
||||
}
|
||||
|
||||
impl AppCommandQueue {
|
||||
pub fn new() -> Self {
|
||||
let (sender, receiver) = unbounded();
|
||||
Self { sender, receiver }
|
||||
}
|
||||
|
||||
pub fn send(&self, command: AppCommand) {
|
||||
let _ = self.sender.send(command);
|
||||
}
|
||||
|
||||
pub fn try_recv(&self) -> Option<AppCommand> {
|
||||
self.receiver.try_recv().ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AppCommandQueue {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Startup system to launch the control socket server
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
#[cfg(debug_assertions)]
|
||||
pub fn start_control_socket_system(socket_path_res: Res<ControlSocketPath>, bridge: Res<EngineBridge>) {
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
pub fn start_control_socket_system(
|
||||
mut commands: Commands,
|
||||
socket_path_res: Res<ControlSocketPath>,
|
||||
bridge: Res<EngineBridge>,
|
||||
) {
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::net::UnixListener;
|
||||
|
||||
let socket_path = socket_path_res.0.clone();
|
||||
info!("Starting control socket at {}", socket_path);
|
||||
|
||||
// Clone bridge for the async task
|
||||
// Create app command queue
|
||||
let app_queue = AppCommandQueue::new();
|
||||
commands.insert_resource(app_queue.clone());
|
||||
|
||||
// Clone bridge and queue for the async task
|
||||
let bridge = bridge.clone();
|
||||
let queue = app_queue;
|
||||
|
||||
// Spawn tokio runtime in background thread
|
||||
std::thread::spawn(move || {
|
||||
@@ -52,6 +115,7 @@ pub fn start_control_socket_system(socket_path_res: Res<ControlSocketPath>, brid
|
||||
Ok((mut stream, _addr)) => {
|
||||
let bridge = bridge.clone();
|
||||
|
||||
let queue_clone = queue.clone();
|
||||
tokio::spawn(async move {
|
||||
// Read command length
|
||||
let mut len_buf = [0u8; 4];
|
||||
@@ -84,7 +148,7 @@ pub fn start_control_socket_system(socket_path_res: Res<ControlSocketPath>, brid
|
||||
info!("Received control command: {:?}", command);
|
||||
|
||||
// Handle command
|
||||
let response = handle_command(command, &bridge).await;
|
||||
let response = handle_command(command, &bridge, &queue_clone).await;
|
||||
|
||||
// Send response
|
||||
if let Err(e) = send_response(&mut stream, response).await {
|
||||
@@ -104,7 +168,11 @@ pub fn start_control_socket_system(socket_path_res: Res<ControlSocketPath>, brid
|
||||
/// Handle a control command and generate a response
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
#[cfg(debug_assertions)]
|
||||
async fn handle_command(command: ControlCommand, bridge: &EngineBridge) -> ControlResponse {
|
||||
async fn handle_command(
|
||||
command: ControlCommand,
|
||||
bridge: &EngineBridge,
|
||||
app_queue: &AppCommandQueue,
|
||||
) -> ControlResponse {
|
||||
match command {
|
||||
ControlCommand::JoinSession { session_code } => {
|
||||
match SessionId::from_code(&session_code) {
|
||||
@@ -129,12 +197,58 @@ async fn handle_command(command: ControlCommand, bridge: &EngineBridge) -> Contr
|
||||
}
|
||||
}
|
||||
|
||||
ControlCommand::SpawnEntity { entity_type, position } => {
|
||||
app_queue.send(AppCommand::SpawnEntity {
|
||||
entity_type,
|
||||
position: Vec3::from_array(position),
|
||||
});
|
||||
ControlResponse::Ok {
|
||||
message: "Entity spawn command queued".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
ControlCommand::DeleteEntity { entity_id } => {
|
||||
app_queue.send(AppCommand::DeleteEntity { entity_id });
|
||||
ControlResponse::Ok {
|
||||
message: format!("Entity delete command queued for {}", entity_id),
|
||||
}
|
||||
}
|
||||
|
||||
_ => ControlResponse::Error {
|
||||
error: format!("Command {:?} not yet implemented", command),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// System to process app commands from the control socket
|
||||
pub fn process_app_commands(
|
||||
queue: Option<Res<AppCommandQueue>>,
|
||||
mut spawn_cube_writer: MessageWriter<crate::cube::SpawnCubeEvent>,
|
||||
mut delete_cube_writer: MessageWriter<crate::cube::DeleteCubeEvent>,
|
||||
) {
|
||||
let Some(queue) = queue else { return };
|
||||
|
||||
while let Some(command) = queue.try_recv() {
|
||||
match command {
|
||||
AppCommand::SpawnEntity { entity_type, position } => {
|
||||
match entity_type.as_str() {
|
||||
"cube" => {
|
||||
info!("Spawning cube at {:?}", position);
|
||||
spawn_cube_writer.write(crate::cube::SpawnCubeEvent { position });
|
||||
}
|
||||
_ => {
|
||||
warn!("Unknown entity type: {}", entity_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
AppCommand::DeleteEntity { entity_id } => {
|
||||
info!("Deleting entity {}", entity_id);
|
||||
delete_cube_writer.write(crate::cube::DeleteCubeEvent { entity_id });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send a response back through the Unix socket
|
||||
#[cfg(not(target_os = "ios"))]
|
||||
#[cfg(debug_assertions)]
|
||||
|
||||
Reference in New Issue
Block a user