diff --git a/game-logic-service/src/components/character_graphics.rs b/game-logic-service/src/components/character_graphics.rs index 22111a0..b8272c1 100644 --- a/game-logic-service/src/components/character_graphics.rs +++ b/game-logic-service/src/components/character_graphics.rs @@ -1,6 +1,6 @@ #[derive(Debug)] -struct CharacterGraphics { +pub(crate) struct CharacterGraphics { race: u8, hair: u8, face: u8, diff --git a/game-logic-service/src/components/client.rs b/game-logic-service/src/components/client.rs index 663a80e..9955746 100644 --- a/game-logic-service/src/components/client.rs +++ b/game-logic-service/src/components/client.rs @@ -1,20 +1,20 @@ #[derive(Debug)] pub struct Client { - pub client_id: u16, + pub client_id: String, pub access_level: u16, pub session_id: String } impl Default for Client { fn default() -> Self { - Self { client_id: 0, access_level: 1, session_id: "".to_string() } + Self { client_id: "".to_string(), access_level: 1, session_id: "".to_string() } } } impl Client { - pub fn get_client_id(&self) -> u16 { - self.client_id + pub fn get_client_id(&self) -> &str { + &self.client_id } pub fn get_access_level(&self) -> u16 { diff --git a/game-logic-service/src/entity_system.rs b/game-logic-service/src/entity_system.rs index 9b92857..2fba64d 100644 --- a/game-logic-service/src/entity_system.rs +++ b/game-logic-service/src/entity_system.rs @@ -4,7 +4,9 @@ use hecs::{Entity, World}; use tracing::debug; use crate::components::basic_info::BasicInfo; use crate::components::destination::Destination; +use crate::components::level::Level; use crate::components::position::Position; +use crate::components::character_graphics::CharacterGraphics; use crate::components::markers::*; use crate::components::life::Life; use crate::spatial_grid::SpatialGrid; @@ -101,8 +103,8 @@ impl EntitySystem { *spatial_grid = temp_factory.spatial_grid; } - /// Get nearby objects for a specific client - pub fn get_nearby_objects_for_client(&self, client_id: u16, x: f32, y: f32, z: f32, map_id: u16) -> Vec { + /// Get nearby objects for a specific client (excluding the client itself) + pub fn get_nearby_objects_for_client(&self, client_id: &str, x: f32, y: f32, z: f32, map_id: u16) -> Vec { let world = self.world.lock().unwrap(); let spatial_grid = self.spatial_grid.lock().unwrap(); @@ -146,7 +148,7 @@ impl EntitySystem { entity_infos } - /// Get nearby objects at a specific position + /// Get all nearby objects at a specific position pub fn get_nearby_objects_at_position(&self, x: f32, y: f32, z: f32, map_id: u16) -> Vec { let world = self.world.lock().unwrap(); let spatial_grid = self.spatial_grid.lock().unwrap(); @@ -208,6 +210,46 @@ impl EntitySystem { return None; // Skip entities without recognized types }; + // get the extra entity data based on the type + let extra_data = match entity_type { + EntityType::Player => { + let player = world.get::<&Player>(entity).ok()?; + let level = world.get::<&Level>(entity).ok()?; + let character_graphics = world.get::<&CharacterGraphics>(entity).ok()?; + ExtraEntityData { + player: Some(PlayerData { + level: level.get_level() as u32, + class: basic_info.job as u32, + race: character_graphics.get_race() as u32, + }), + npc: None, + mob: None, + } + }, + EntityType::Npc => { + let npc = world.get::<&Npc>(entity).ok()?; + ExtraEntityData { + player: None, + npc: Some(NpcData { + quest_id: npc.quest_id, + angle: npc.angle as u32, + event_status: npc.event_status as u32, + }), + mob: None, + } + }, + EntityType::Mob => { + let mob = world.get::<&Mob>(entity).ok()?; + ExtraEntityData { + player: None, + npc: None, + mob: Some(MobData { + quest_id: mob.quest_id, + }), + } + } + }; + Some(EntityInfo { id: basic_info.id as i32, entity_type, @@ -217,6 +259,7 @@ impl EntitySystem { name: basic_info.name.clone(), hp, max_hp, + extra_data, }) } @@ -225,7 +268,7 @@ impl EntitySystem { self.world.clone() } - pub fn set_destination(&self, client_id: u16, x: f32, y: f32, z: f32) { + pub fn set_destination(&self, client_id: &str, x: f32, y: f32, z: f32) { use crate::components::client::Client; let mut world = self.world.lock().unwrap(); @@ -252,7 +295,7 @@ impl EntitySystem { /// Update player position and check for nearby entity changes - pub fn update_player_position(&self, client_id: u16, x: f32, y: f32, z: f32, map_id: u16) { + pub fn update_player_position(&self, client_id: &str, x: f32, y: f32, z: f32, map_id: u16) { use crate::components::client::Client; let mut world = self.world.lock().unwrap(); @@ -324,6 +367,33 @@ pub struct EntityInfo { pub name: String, pub hp: i32, pub max_hp: i32, + pub extra_data: ExtraEntityData, +} + +#[derive(Debug, Clone)] +pub struct ExtraEntityData { + pub player: Option, + pub npc: Option, + pub mob: Option, +} + +#[derive(Debug, Clone)] +pub struct PlayerData { + pub level: u32, + pub class: u32, + pub race: u32, +} + +#[derive(Debug, Clone)] +pub struct NpcData { + pub quest_id: u32, + pub angle: u32, + pub event_status: u32, +} + +#[derive(Debug, Clone)] +pub struct MobData { + pub quest_id: u32, } /// Type of entity diff --git a/game-logic-service/src/game_logic_service.rs b/game-logic-service/src/game_logic_service.rs index 28fe440..3e66478 100644 --- a/game-logic-service/src/game_logic_service.rs +++ b/game-logic-service/src/game_logic_service.rs @@ -27,7 +27,7 @@ impl GameLogicService for MyGameLogicService { // Get nearby entities from the entity system let entity_infos = self.entity_system.get_nearby_objects_for_client( - req.client_id as u16, + &req.client_id, req.x, req.y, req.z, @@ -35,6 +35,7 @@ impl GameLogicService for MyGameLogicService { ); // Convert EntityInfo to proto Object + // TODO: we also need to grab the extra data from the entity and add it to the objects let objects: Vec = entity_infos .into_iter() .map(|info| Object { diff --git a/game-logic-service/src/world_client.rs b/game-logic-service/src/world_client.rs index b5e805f..028b953 100644 --- a/game-logic-service/src/world_client.rs +++ b/game-logic-service/src/world_client.rs @@ -63,6 +63,7 @@ impl WorldGameLogicService for WorldGameLogicServiceImpl { let map_id = self.map_id; let entity_system = self.entity_system.clone(); + let outbound_tx_clone = outbound_tx.clone(); // Handle incoming events from world service tokio::spawn(async move { @@ -76,10 +77,38 @@ impl WorldGameLogicService for WorldGameLogicServiceImpl { Some(world::game_logic_event::Event::PlayerConnect(connect_event)) => { info!("Player {} connected to map {}", connect_event.session_id, map_id); // Handle player connection logic + if let Some(entity_system) = &entity_system { + + let world = entity_system.get_world(); + let mut world_lock = world.lock().unwrap(); + + //TODO: add an entity for the player if it doesn't exist + // entity_system.spawn_player(crate::components::position::Position { + // x: connect_event.x, + // y: connect_event.y, + // z: connect_event.z, + // map_id: map_id as u16, + // spawn_id: 0, + // }); + + // Find the player entity and update session_id using UUID client_id + let mut query = world_lock.query::<&mut crate::components::client::Client>(); + for (entity, mut client) in query.iter() { + if client.client_id == connect_event.client_id { + client.session_id = connect_event.session_id.clone(); + debug!("Updated session_id for client_id {} to {}", connect_event.client_id, connect_event.session_id); + break; + } + } + } } Some(world::game_logic_event::Event::PlayerDisconnect(disconnect_event)) => { info!("Player {} disconnected from map {}", disconnect_event.session_id, map_id); - // Handle player disconnection logic + // Handle player disconnection logic - for now just log it + // TODO: Remove player entity from the world when disconnect handling is implemented + if let Some(entity_system) = &entity_system { + // entity_system.remove_player(&disconnect_event.session_id); + } } Some(world::game_logic_event::Event::PlayerMove(move_event)) => { debug!("Player {} moved to ({}, {}, {}) in map {}", @@ -87,18 +116,13 @@ impl WorldGameLogicService for WorldGameLogicServiceImpl { // Handle player movement logic - update position and check for nearby changes if let Some(entity_system) = &entity_system { - // Parse client_id from string to u16 - if let Ok(client_id) = move_event.client_id.parse::() { - entity_system.update_player_position( - client_id, - move_event.x, - move_event.y, - move_event.z, - map_id as u16 - ); - } else { - debug!("Failed to parse client_id: {}", move_event.client_id); - } + entity_system.update_player_position( + &move_event.client_id, + move_event.x, + move_event.y, + move_event.z, + map_id as u16 + ); } } _ => { @@ -115,6 +139,35 @@ impl WorldGameLogicService for WorldGameLogicServiceImpl { info!("World service stream ended for map {}", map_id); }); + // Send an initial connection acknowledgment event to keep the stream alive + let initial_event = world::GameLogicEvent { + client_ids: vec![], + map_id: map_id as i32, + event: None, // Empty event just to establish the stream + }; + + if let Err(e) = outbound_tx.send(initial_event) { + error!("Failed to send initial event for map {}: {}", map_id, e); + } + + // Spawn a task to periodically check for spawner events and send them + let entity_system_spawner = self.entity_system.clone(); + let _outbound_tx_spawner = outbound_tx.clone(); + let _spawner_map_id = map_id; + tokio::spawn(async move { + let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(1)); + loop { + interval.tick().await; + + if let Some(ref _entity_system) = entity_system_spawner { + // Check for any spawner events that need to be sent + // For now, this keeps the task alive and ready for future spawner events + // Maybe we want to use a pub sub channel for spawner events in the future + // See entity_factory.rs ln 285 for where we would send the events + } + } + }); + // Create outbound stream for sending events to world service let outbound_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(outbound_rx) .map(|event| Ok(event)); diff --git a/proto/game_logic.proto b/proto/game_logic.proto index 4ea81a1..521972f 100644 --- a/proto/game_logic.proto +++ b/proto/game_logic.proto @@ -7,7 +7,7 @@ service GameLogicService { } message NearbyObjectsRequest { - uint32 client_id = 1; + string client_id = 1; float x = 2; float y = 3; float z = 4; diff --git a/world-service/src/game_logic_client.rs b/world-service/src/game_logic_client.rs index c9bef4c..85af81a 100644 --- a/world-service/src/game_logic_client.rs +++ b/world-service/src/game_logic_client.rs @@ -5,14 +5,7 @@ use tokio::time::{sleep, Duration}; use tonic::transport::Channel; use tracing::{debug, error, info, warn}; use futures::StreamExt; - -pub mod world { - tonic::include_proto!("world"); -} - -pub mod game_logic { - tonic::include_proto!("game_logic"); -} +use crate::proto::{world, game_logic}; use world::world_game_logic_service_client::WorldGameLogicServiceClient; use game_logic::game_logic_service_client::GameLogicServiceClient; @@ -163,7 +156,7 @@ impl GameLogicClientManager { pub async fn get_nearby_objects( &self, map_id: u32, - client_id: u32, + client_id: &str, x: f32, y: f32, z: f32, @@ -173,7 +166,7 @@ impl GameLogicClientManager { if let Some(client) = clients.get_mut(&map_id) { if let Some(service_client) = &mut client.service_client { let request = game_logic::NearbyObjectsRequest { - client_id, + client_id: client_id.parse().unwrap(), x, y, z, diff --git a/world-service/src/main.rs b/world-service/src/main.rs index 61c953d..d58833c 100644 --- a/world-service/src/main.rs +++ b/world-service/src/main.rs @@ -1,12 +1,14 @@ mod k8s_orchestrator; mod world_service; mod game_logic_client; +mod proto; use dotenv::dotenv; use std::env; use std::sync::Arc; use tokio::time::{sleep, Duration, timeout}; -use tokio::sync::oneshot; +use tokio::sync::{mpsc, oneshot}; +use tokio_stream::StreamExt; use tonic::transport::Server; use utils::service_discovery::{get_kube_service_endpoints_by_dns, get_service_endpoints_by_dns}; use utils::{health_check, logging}; @@ -14,8 +16,7 @@ use tracing::{debug, error, info, warn}; use crate::k8s_orchestrator::K8sOrchestrator; use crate::world_service::{MyWorldService, MyWorldGameLogicService}; use crate::game_logic_client::GameLogicClientManager; -use world_service::world::world_service_server::WorldServiceServer; -use world_service::world::world_game_logic_service_server::WorldGameLogicServiceServer; +use crate::proto::world::{GameLogicEvent, world_service_server::WorldServiceServer, world_game_logic_service_server::WorldGameLogicServiceServer}; fn get_service_name() -> String { env::var("WORLD_SERVICE_NAME").unwrap_or_else(|_| "default-service".to_string()) @@ -137,7 +138,7 @@ async fn main() -> Result<(), Box> { // Create game logic client manager and world service let game_logic_manager = Arc::new(GameLogicClientManager::new()); let world_service_shared = Arc::new(MyWorldService::new_with_game_logic_manager(game_logic_manager.clone())); - let world_service = MyWorldService::new_with_game_logic_manager(game_logic_manager.clone()); + let world_service = world_service_shared.clone(); let world_game_logic_service = MyWorldGameLogicService::new(world_service_shared.clone()); // Get retry configuration for connection info @@ -262,6 +263,50 @@ async fn main() -> Result<(), Box> { error!("No game logic instances could be connected! Service may not function properly."); } else { info!("World service startup completed with {} active game logic connections", successful_connections); + + // Start bidirectional streaming for all connected game logic services + debug!("Starting bidirectional event streams for {} game logic connections...", successful_connections); + let connected_maps = game_logic_manager.list_connected_maps().await; + + for map_id in connected_maps { + // Create channels for bidirectional communication + let (outbound_tx, outbound_rx): (mpsc::UnboundedSender, mpsc::UnboundedReceiver) = mpsc::unbounded_channel(); + + // Start the event stream + match game_logic_manager.start_event_stream(map_id, outbound_rx).await { + Ok(inbound_rx) => { + debug!("Successfully started event stream for map {}", map_id); + + // Add the game logic connection to the world service + world_service_shared.add_game_logic_connection(map_id, outbound_tx.clone()); + + // Spawn a task to handle incoming events from game logic + let world_service_clone = world_service_shared.clone(); + let game_logic_manager_clone = game_logic_manager.clone(); + tokio::spawn(async move { + let mut inbound_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(inbound_rx); + while let Some(event) = inbound_stream.next().await { + match &event.event { + _ => { + debug!("Received event from game logic service for map {}: {:?}", map_id, event); + // TODO: Process the event and potentially broadcast to clients + } + } + } + warn!("Event stream from game logic service for map {} ended", map_id); + + // TODO: Implement reconnection logic here + // For now, just log the disconnection + error!("Game logic stream for map {} disconnected, reconnection not yet implemented", map_id); + }); + } + Err(e) => { + error!("Failed to start event stream for map {}: {}", map_id, e); + } + } + } + + debug!("Event stream initialization completed"); } // Set the gRPC server address @@ -291,7 +336,7 @@ async fn main() -> Result<(), Box> { let server_task = tokio::spawn(async move { let server = Server::builder() - .add_service(WorldServiceServer::new(world_service)) + .add_service(WorldServiceServer::new((*world_service).clone())) .add_service(WorldGameLogicServiceServer::new(world_game_logic_service)) .serve_with_shutdown(grpc_addr, async { shutdown_rx.await.ok(); diff --git a/world-service/src/proto.rs b/world-service/src/proto.rs new file mode 100644 index 0000000..125afe0 --- /dev/null +++ b/world-service/src/proto.rs @@ -0,0 +1,9 @@ +// Shared proto definitions to avoid duplication across modules + +pub mod world { + tonic::include_proto!("world"); +} + +pub mod game_logic { + tonic::include_proto!("game_logic"); +} diff --git a/world-service/src/world_service.rs b/world-service/src/world_service.rs index 1cef7f0..b749dd8 100644 --- a/world-service/src/world_service.rs +++ b/world-service/src/world_service.rs @@ -6,15 +6,9 @@ use tonic::{Request, Response, Status, Streaming}; use tracing::{debug, error, info, trace, warn}; use tokio::sync::mpsc; use tokio_stream::wrappers::ReceiverStream; -use crate::game_logic_client::{GameLogicClientManager, game_logic as client_game_logic}; - -pub mod world { - tonic::include_proto!("world"); -} - -pub mod game_logic { - tonic::include_proto!("game_logic"); -} +use crate::game_logic_client::GameLogicClientManager; +use crate::proto::game_logic as client_game_logic; +use crate::proto::{world, game_logic}; use world::world_service_server::WorldService; use world::world_game_logic_service_server::WorldGameLogicService; @@ -26,6 +20,16 @@ pub struct MyWorldService { pub game_logic_manager: Option>, } +impl Clone for MyWorldService { + fn clone(&self) -> Self { + Self { + client_connections: self.client_connections.clone(), + game_logic_connections: self.game_logic_connections.clone(), + game_logic_manager: self.game_logic_manager.clone(), + } + } +} + pub struct ClientConnection { pub session_id: String, pub client_id: String, @@ -139,10 +143,10 @@ impl MyWorldService { // Get client_id from the client connection let client_id = { let connections = self.client_connections.lock().unwrap(); - connections.get(session_id).map(|conn| conn.client_id.parse::().unwrap_or(0)).unwrap_or(0) + connections.get(session_id).map(|conn| conn.client_id.clone()).unwrap_or("".to_string()) }; - match game_logic_manager.get_nearby_objects(map_id as u32, client_id, x, y, z).await { + match game_logic_manager.get_nearby_objects(map_id as u32, &client_id, x, y, z).await { Ok(response) => { debug!("Received {} nearby objects from game logic service", response.objects.len()); @@ -396,6 +400,29 @@ impl WorldService for MyWorldService { }); } + { + let player_connect_event = world::GameLogicEvent { + client_ids: vec![], + map_id: client_event.map_id, + event: Some(world::game_logic_event::Event::PlayerConnect(world::PlayerConnectEvent { + session_id: client_event.session_id.clone(), + client_id: client_event.client_id.clone(), + map_id: client_event.map_id, + x: connect_event.x, + y: connect_event.y, + z: connect_event.z, + })), + }; + + // Send to game logic service for the appropriate map + let game_logic_connections = game_logic_connections.lock().unwrap(); + if let Some(connection) = game_logic_connections.get(&(client_event.map_id as u32)) { + if let Err(e) = connection.sender.send(player_connect_event) { + warn!("Failed to send player connect event to game logic for map {}: {}", client_event.map_id, e); + } + } + } + // Send all nearby objects to the client // Create a service instance with the proper game logic manager let world_service = MyWorldService { @@ -415,19 +442,26 @@ impl WorldService for MyWorldService { debug!("Client {} moved to ({}, {}, {})", client_event.client_id, move_event.x, move_event.y, move_event.z); // Update client position - { + let actual_map_id = { let mut connections = client_connections.lock().unwrap(); if let Some(connection) = connections.get_mut(&client_event.session_id) { connection.x = move_event.x; connection.y = move_event.y; connection.z = move_event.z; + connection.map_id + } else { + warn!("No connection found for session_id: {}", client_event.session_id); + return; // Skip processing if no connection found } - } + }; + + debug!("Using actual map_id {} for client {} (client_event.map_id was {})", + actual_map_id, client_event.client_id, client_event.map_id); // Send PlayerMoveEvent to game logic service let player_move_event = world::GameLogicEvent { client_ids: vec![], - map_id: client_event.map_id, + map_id: actual_map_id, event: Some(world::game_logic_event::Event::PlayerMove(world::PlayerMoveEvent { session_id: client_event.session_id.clone(), client_id: client_event.client_id.clone(), @@ -439,10 +473,15 @@ impl WorldService for MyWorldService { // Send to game logic service for the appropriate map let game_logic_connections = game_logic_connections.lock().unwrap(); - if let Some(connection) = game_logic_connections.get(&(client_event.map_id as u32)) { + let target_map_id = actual_map_id as u32; + + if let Some(connection) = game_logic_connections.get(&target_map_id) { if let Err(e) = connection.sender.send(player_move_event) { warn!("Failed to send player move event to game logic for map {}: {}", client_event.map_id, e); } + } else { + warn!("No game logic connection found for map {} (target_map_id: {}).", + actual_map_id, target_map_id); } } Some(client_event::Event::MapChange(map_change_event)) => {