Tons of fixes
Added movement updates Updated how entities are checked Events sending between packet service all the way to the logic service
This commit is contained in:
@@ -6,8 +6,10 @@ use auth_service::session::session_service_client::SessionServiceClient;
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tonic::transport::Server;
|
||||
use tracing::info;
|
||||
use tracing::{info, error, warn};
|
||||
use utils::logging;
|
||||
use utils::service_discovery::get_kube_service_endpoints_by_dns;
|
||||
|
||||
@@ -40,19 +42,55 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
session_client,
|
||||
};
|
||||
|
||||
// Start gRPC server with graceful shutdown support
|
||||
let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
|
||||
health_reporter.set_serving::<AuthServiceServer<MyAuthService>>().await;
|
||||
|
||||
info!("Authentication Service running on {}", addr);
|
||||
// Create shutdown signal channel
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
|
||||
|
||||
// Start the gRPC server
|
||||
tokio::spawn(
|
||||
Server::builder()
|
||||
let server_task = tokio::spawn(async move {
|
||||
let server = Server::builder()
|
||||
.add_service(health_service)
|
||||
.add_service(AuthServiceServer::new(auth_service))
|
||||
.serve(address),
|
||||
);
|
||||
.serve_with_shutdown(address, async {
|
||||
shutdown_rx.await.ok();
|
||||
info!("Auth service gRPC server shutdown signal received");
|
||||
});
|
||||
|
||||
if let Err(e) = server.await {
|
||||
error!("Auth service gRPC server error: {}", e);
|
||||
} else {
|
||||
info!("Auth service gRPC server shut down gracefully");
|
||||
}
|
||||
});
|
||||
|
||||
info!("Authentication Service running on {}", addr);
|
||||
|
||||
// Wait for shutdown signal
|
||||
info!("Auth service is running. Waiting for shutdown signal...");
|
||||
utils::signal_handler::wait_for_signal().await;
|
||||
|
||||
info!("Shutdown signal received. Beginning graceful shutdown...");
|
||||
|
||||
// Signal the gRPC server to stop accepting new connections
|
||||
if let Err(_) = shutdown_tx.send(()) {
|
||||
warn!("Failed to send shutdown signal to gRPC server (receiver may have been dropped)");
|
||||
}
|
||||
|
||||
// Wait for the gRPC server to finish with a timeout
|
||||
match timeout(Duration::from_secs(30), server_task).await {
|
||||
Ok(result) => {
|
||||
if let Err(e) = result {
|
||||
error!("Auth service gRPC server task failed: {}", e);
|
||||
} else {
|
||||
info!("Auth service shut down successfully");
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
error!("Auth service gRPC server shutdown timed out after 30 seconds");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -11,7 +11,10 @@ use dotenv::dotenv;
|
||||
use std::env;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tracing::Level;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tonic::transport::Server;
|
||||
use tracing::{info, error, warn, Level};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use utils::logging;
|
||||
use utils::service_discovery::get_kube_service_endpoints_by_dns;
|
||||
@@ -42,12 +45,51 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.set_serving::<CharacterServiceServer<MyCharacterService>>()
|
||||
.await;
|
||||
|
||||
tonic::transport::Server::builder()
|
||||
// Create shutdown signal channel
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
|
||||
|
||||
let server_task = tokio::spawn(async move {
|
||||
let server = Server::builder()
|
||||
.add_service(health_service)
|
||||
.add_service(CharacterServiceServer::new(character_service))
|
||||
.serve(address)
|
||||
.await?;
|
||||
.serve_with_shutdown(address, async {
|
||||
shutdown_rx.await.ok();
|
||||
info!("Character service gRPC server shutdown signal received");
|
||||
});
|
||||
|
||||
if let Err(e) = server.await {
|
||||
error!("Character service gRPC server error: {}", e);
|
||||
} else {
|
||||
info!("Character service gRPC server shut down gracefully");
|
||||
}
|
||||
});
|
||||
|
||||
info!("Character Service running on {}", address);
|
||||
|
||||
// Wait for shutdown signal
|
||||
info!("Character service is running. Waiting for shutdown signal...");
|
||||
utils::signal_handler::wait_for_signal().await;
|
||||
|
||||
info!("Shutdown signal received. Beginning graceful shutdown...");
|
||||
|
||||
// Signal the gRPC server to stop accepting new connections
|
||||
if let Err(_) = shutdown_tx.send(()) {
|
||||
warn!("Failed to send shutdown signal to gRPC server (receiver may have been dropped)");
|
||||
}
|
||||
|
||||
// Wait for the gRPC server to finish with a timeout
|
||||
match timeout(Duration::from_secs(30), server_task).await {
|
||||
Ok(result) => {
|
||||
if let Err(e) = result {
|
||||
error!("Character service gRPC server task failed: {}", e);
|
||||
} else {
|
||||
info!("Character service shut down successfully");
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
error!("Character service gRPC server shutdown timed out after 30 seconds");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -93,7 +93,8 @@ services:
|
||||
LOG_LEVEL: "debug"
|
||||
LOGIC_LOG_LEVEL: "debug"
|
||||
# MAP_IDS: "1,2,3,4,5,6,8,9,10,11,15,16,17,18,20,21,22,23,24,25,26,27,28,29,31,32,33,34,35,36,37,40,42,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,70,71,72,73,74,75,76,77,78,79,80"
|
||||
MAP_IDS: "1,2,3,4,5,6,8,9,10,11,15,16,17,18,20"
|
||||
# MAP_IDS: "1,2,3,4,5,6,8,9,10,11,15,16,17,18,20"
|
||||
MAP_IDS: "20"
|
||||
WORLD_SERVICE_NAME: "Athena"
|
||||
service:
|
||||
annotations:
|
||||
|
||||
@@ -109,7 +109,7 @@ impl ChatService for MyChatService {
|
||||
&clients_clone,
|
||||
);
|
||||
}
|
||||
// For other types, we simply broadcast as default.
|
||||
// For other types, we simply broadcast to all as default.
|
||||
_ => {
|
||||
let clients_lock = clients_clone.lock().unwrap();
|
||||
for (id, tx) in clients_lock.iter() {
|
||||
|
||||
@@ -3,6 +3,8 @@ mod chat_channels;
|
||||
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tokio::sync::oneshot;
|
||||
use utils::service_discovery::{get_kube_service_endpoints_by_dns, get_service_endpoints_by_dns};
|
||||
use utils::{health_check, logging};
|
||||
use chat_service::MyChatService;
|
||||
@@ -10,6 +12,7 @@ use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use tonic::transport::Server;
|
||||
use tracing::{info, error, warn};
|
||||
use crate::chat_service::chat::chat_service_server::ChatServiceServer;
|
||||
|
||||
#[tokio::main]
|
||||
@@ -31,21 +34,58 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
guild_channel: Arc::new(chat_channels::guild_chat::GuildChat),
|
||||
};
|
||||
|
||||
// Register service with Consul
|
||||
// Start gRPC server with graceful shutdown support
|
||||
let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
|
||||
health_reporter
|
||||
.set_serving::<ChatServiceServer<MyChatService>>()
|
||||
.await;
|
||||
let address = SocketAddr::new(addr.parse()?, port.parse()?);
|
||||
tokio::spawn(
|
||||
Server::builder()
|
||||
|
||||
// Create shutdown signal channel
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
|
||||
|
||||
let server_task = tokio::spawn(async move {
|
||||
let server = Server::builder()
|
||||
.add_service(chat_service.into_service())
|
||||
.add_service(health_service)
|
||||
.serve(address),
|
||||
);
|
||||
.serve_with_shutdown(address, async {
|
||||
shutdown_rx.await.ok();
|
||||
info!("Chat service gRPC server shutdown signal received");
|
||||
});
|
||||
|
||||
println!("Chat Service listening on {}", address);
|
||||
if let Err(e) = server.await {
|
||||
error!("Chat service gRPC server error: {}", e);
|
||||
} else {
|
||||
info!("Chat service gRPC server shut down gracefully");
|
||||
}
|
||||
});
|
||||
|
||||
info!("Chat Service listening on {}", address);
|
||||
|
||||
// Wait for shutdown signal
|
||||
info!("Chat service is running. Waiting for shutdown signal...");
|
||||
utils::signal_handler::wait_for_signal().await;
|
||||
|
||||
info!("Shutdown signal received. Beginning graceful shutdown...");
|
||||
|
||||
// Signal the gRPC server to stop accepting new connections
|
||||
if let Err(_) = shutdown_tx.send(()) {
|
||||
warn!("Failed to send shutdown signal to gRPC server (receiver may have been dropped)");
|
||||
}
|
||||
|
||||
// Wait for the gRPC server to finish with a timeout
|
||||
match timeout(Duration::from_secs(30), server_task).await {
|
||||
Ok(result) => {
|
||||
if let Err(e) = result {
|
||||
error!("Chat service gRPC server task failed: {}", e);
|
||||
} else {
|
||||
info!("Chat service shut down successfully");
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
error!("Chat service gRPC server shutdown timed out after 30 seconds");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -9,9 +9,10 @@ use std::env;
|
||||
use std::net::SocketAddr;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::{Mutex, oneshot};
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tonic::transport::Server;
|
||||
use tracing::{info, Level};
|
||||
use tracing::{info, error, warn, Level};
|
||||
use tracing_subscriber::EnvFilter;
|
||||
use utils::logging;
|
||||
use utils::redis_cache::RedisCache;
|
||||
@@ -38,22 +39,61 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let db = Arc::new(Database::new(pool, redis_cache));
|
||||
let my_service = MyDatabaseService { db };
|
||||
|
||||
// Start gRPC server with graceful shutdown support
|
||||
let (mut health_reporter, health_service) = tonic_health::server::health_reporter();
|
||||
health_reporter
|
||||
.set_serving::<UserServiceServer<MyDatabaseService>>()
|
||||
.await;
|
||||
|
||||
let address = SocketAddr::new(addr.parse()?, port.parse()?);
|
||||
tokio::spawn(
|
||||
Server::builder()
|
||||
|
||||
// Create shutdown signal channel
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
|
||||
|
||||
let server_task = tokio::spawn(async move {
|
||||
let server = Server::builder()
|
||||
.add_service(health_service)
|
||||
.add_service(UserServiceServer::new(my_service.clone()))
|
||||
.add_service(CharacterDbServiceServer::new(my_service.clone()))
|
||||
.add_service(SessionServiceServer::new(my_service))
|
||||
.serve(address),
|
||||
);
|
||||
.serve_with_shutdown(address, async {
|
||||
shutdown_rx.await.ok();
|
||||
info!("Database service gRPC server shutdown signal received");
|
||||
});
|
||||
|
||||
if let Err(e) = server.await {
|
||||
error!("Database service gRPC server error: {}", e);
|
||||
} else {
|
||||
info!("Database service gRPC server shut down gracefully");
|
||||
}
|
||||
});
|
||||
|
||||
info!("Database Service running on {}", address);
|
||||
|
||||
// Wait for shutdown signal
|
||||
info!("Database service is running. Waiting for shutdown signal...");
|
||||
utils::signal_handler::wait_for_signal().await;
|
||||
|
||||
info!("Shutdown signal received. Beginning graceful shutdown...");
|
||||
|
||||
// Signal the gRPC server to stop accepting new connections
|
||||
if let Err(_) = shutdown_tx.send(()) {
|
||||
warn!("Failed to send shutdown signal to gRPC server (receiver may have been dropped)");
|
||||
}
|
||||
|
||||
// Wait for the gRPC server to finish with a timeout
|
||||
match timeout(Duration::from_secs(30), server_task).await {
|
||||
Ok(result) => {
|
||||
if let Err(e) = result {
|
||||
error!("Database service gRPC server task failed: {}", e);
|
||||
} else {
|
||||
info!("Database service shut down successfully");
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
error!("Database service gRPC server shutdown timed out after 30 seconds");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ fn main() {
|
||||
.build_server(true) // Generate gRPC server code
|
||||
.compile_well_known_types(true)
|
||||
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
|
||||
.compile_protos(&["../proto/game_logic.proto", "../proto/game.proto"], &["../proto"])
|
||||
.compile_protos(&["../proto/game_logic.proto", "../proto/game.proto", "../proto/world.proto"], &["../proto"])
|
||||
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
||||
|
||||
// gRPC Client code
|
||||
@@ -12,6 +12,6 @@ fn main() {
|
||||
.build_server(false) // Generate gRPC client code
|
||||
.compile_well_known_types(true)
|
||||
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
|
||||
.compile_protos(&["../proto/user_db_api.proto", "../proto/auth.proto", "../proto/character.proto", "../proto/character_common.proto", "../proto/chat.proto", "../proto/world.proto"], &["../proto"])
|
||||
.compile_protos(&["../proto/user_db_api.proto", "../proto/auth.proto", "../proto/character.proto", "../proto/character_common.proto", "../proto/chat.proto"], &["../proto"])
|
||||
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Client {
|
||||
client_id: u16,
|
||||
access_level: u16,
|
||||
pub struct Client {
|
||||
pub client_id: u16,
|
||||
pub access_level: u16,
|
||||
pub session_id: String
|
||||
}
|
||||
|
||||
impl Default for Client {
|
||||
fn default() -> Self {
|
||||
Self { client_id: 0, access_level: 1 }
|
||||
Self { client_id: 0, access_level: 1, session_id: "".to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,4 +20,6 @@ impl Client {
|
||||
pub fn get_access_level(&self) -> u16 {
|
||||
self.access_level
|
||||
}
|
||||
|
||||
pub fn get_session_id(&self) -> &str { &self.session_id }
|
||||
}
|
||||
22
game-logic-service/src/components/computed_values.rs
Normal file
22
game-logic-service/src/components/computed_values.rs
Normal file
@@ -0,0 +1,22 @@
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ComputedValues {
|
||||
pub run_speed: f32, // Movement speed in units per second
|
||||
pub walk_speed: f32,
|
||||
pub attack_speed: f32,
|
||||
}
|
||||
|
||||
impl Default for ComputedValues {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
run_speed: 100.0, // Default speed: 100 units per second
|
||||
walk_speed: 50.0,
|
||||
attack_speed: 1.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ComputedValues {
|
||||
pub fn new(run_speed: f32, walk_speed: f32, attack_speed: f32) -> Self {
|
||||
Self { run_speed, walk_speed, attack_speed }
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
#[derive(Debug)]
|
||||
struct Destination {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Destination {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub z: f32,
|
||||
dest: u16,
|
||||
pub dist: f32, // Distance to destination
|
||||
}
|
||||
|
||||
impl Default for Destination {
|
||||
fn default() -> Self {
|
||||
Self { x: 520000.0, y: 520000.0, z: 1.0, dest: 20 }
|
||||
Self { x: 520000.0, y: 520000.0, z: 1.0, dist: 0.0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Destination {
|
||||
pub fn new(x: f32, y: f32, z: f32, distance: f32) -> Self {
|
||||
Self { x, y, z, dist: distance }
|
||||
}
|
||||
}
|
||||
@@ -11,3 +11,4 @@ pub mod item;
|
||||
pub mod stats;
|
||||
pub mod character_graphics;
|
||||
pub mod spawner;
|
||||
pub mod computed_values;
|
||||
@@ -8,14 +8,20 @@ use crate::components::level::Level;
|
||||
use crate::components::life::Life;
|
||||
use crate::components::position::Position;
|
||||
use crate::components::spawner::Spawner;
|
||||
use crate::components::destination::Destination;
|
||||
use crate::components::computed_values::ComputedValues;
|
||||
use crate::components::markers::*;
|
||||
use crate::id_manager::IdManager;
|
||||
use crate::loader::{MobSpawnPoint, Vec3, Zone};
|
||||
use crate::random::{get_random_number_in_range, get_random_point_in_circle};
|
||||
use crate::spatial_grid::SpatialGrid;
|
||||
use crate::world_client::WorldGameLogicServiceImpl;
|
||||
|
||||
pub struct EntityFactory<'a> {
|
||||
pub world: &'a mut World,
|
||||
pub id_manager: Arc<Mutex<IdManager>>
|
||||
pub id_manager: Arc<Mutex<IdManager>>,
|
||||
pub spatial_grid: SpatialGrid,
|
||||
pub world_service: Option<Arc<WorldGameLogicServiceImpl>>,
|
||||
}
|
||||
|
||||
impl<'a> EntityFactory<'a> {
|
||||
@@ -23,10 +29,17 @@ impl<'a> EntityFactory<'a> {
|
||||
pub fn new(world: &'a mut World) -> Self {
|
||||
Self {
|
||||
world,
|
||||
id_manager: Arc::new(Mutex::new(IdManager::new()))
|
||||
id_manager: Arc::new(Mutex::new(IdManager::new())),
|
||||
spatial_grid: SpatialGrid::new(),
|
||||
world_service: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_world_service(mut self, world_service: Arc<WorldGameLogicServiceImpl>) -> Self {
|
||||
self.world_service = Some(world_service);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn load_map(&mut self, map: Zone) {
|
||||
// Load the map data from the database
|
||||
// Spawn all the entities in the map
|
||||
@@ -128,7 +141,11 @@ impl<'a> EntityFactory<'a> {
|
||||
let hp = (base_hp * level.level) as u32;
|
||||
let life = Life::new(hp, hp);
|
||||
|
||||
self.world.spawn((Player, basic_info, level, life, pos.clone()));
|
||||
let entity = self.world.spawn((Player, basic_info, level, life, pos.clone()));
|
||||
|
||||
// Add entity to spatial grid
|
||||
self.spatial_grid.add_entity(entity, &pos);
|
||||
|
||||
debug!("Player spawned at position: {:?}", pos);
|
||||
}
|
||||
|
||||
@@ -147,7 +164,14 @@ impl<'a> EntityFactory<'a> {
|
||||
let hp = (base_hp * level.level) as u32;
|
||||
let life = Life::new(hp, hp);
|
||||
|
||||
self.world.spawn((Npc {id: npc_id, quest_id: 0, angle, event_status: 0}, basic_info, level, life, pos.clone()));
|
||||
let entity = self.world.spawn((Npc {id: npc_id, quest_id: 0, angle, event_status: 0}, basic_info, level, life, pos.clone()));
|
||||
|
||||
// Add entity to spatial grid
|
||||
self.spatial_grid.add_entity(entity, &pos);
|
||||
|
||||
// Send event to nearby clients about the new NPC
|
||||
self.send_npc_spawn_event_to_nearby_clients(npc_id, &pos, angle);
|
||||
|
||||
debug!("Npc spawned at position: {:?}", pos);
|
||||
}
|
||||
|
||||
@@ -171,7 +195,14 @@ impl<'a> EntityFactory<'a> {
|
||||
let spawn_point = Position { x, y, z: pos.z, map_id: pos.map_id, spawn_id: pos.spawn_id };
|
||||
|
||||
// Spawn the mob.
|
||||
let entity = self.world.spawn((Mob {id: mob_id, quest_id: 0}, basic_info, spawn_point.clone()));
|
||||
let entity = self.world.spawn((Mob {id: mob_id, quest_id: 0}, basic_info, level, life, spawn_point.clone()));
|
||||
|
||||
// Add entity to spatial grid
|
||||
self.spatial_grid.add_entity(entity, &spawn_point);
|
||||
|
||||
// Send event to nearby clients about the new mob
|
||||
self.send_mob_spawn_event_to_nearby_clients(id.into(), &spawn_point, mob_id);
|
||||
|
||||
entity
|
||||
}
|
||||
|
||||
@@ -195,57 +226,307 @@ impl<'a> EntityFactory<'a> {
|
||||
pub fn update_spawners(&mut self) {
|
||||
let now = Instant::now();
|
||||
|
||||
// Collect spawner entities to avoid borrow issues.
|
||||
// We need to clone the Position since we use it after.
|
||||
let spawner_data: Vec<(hecs::Entity, Position, Spawner)> = self
|
||||
// Collect spawner entities that need to be processed
|
||||
let spawner_entities: Vec<(hecs::Entity, Position)> = self
|
||||
.world
|
||||
.query::<(&Position, &Spawner)>()
|
||||
.iter()
|
||||
.map(|(entity, (pos, spawner))| {
|
||||
(
|
||||
entity,
|
||||
pos.clone(),
|
||||
Spawner {
|
||||
mob_id: spawner.mob_id,
|
||||
max_mob_count: spawner.max_mob_count,
|
||||
max_spawn_count_at_once: spawner.max_spawn_count_at_once,
|
||||
spawn_rate: spawner.spawn_rate,
|
||||
last_spawn: spawner.last_spawn,
|
||||
spawn_range: spawner.spawn_range,
|
||||
mobs: spawner.mobs.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
})
|
||||
.map(|(entity, (pos, _spawner))| (entity, pos.clone()))
|
||||
.collect();
|
||||
|
||||
// Iterate over each spawner and check if it's time to spawn a mob.
|
||||
for (entity, pos, spawner) in spawner_data {
|
||||
let mut mob_list = spawner.mobs.clone();
|
||||
if mob_list.len() >= spawner.max_mob_count as usize {
|
||||
continue;
|
||||
// Process each spawner individually to avoid borrowing conflicts
|
||||
for (entity, pos) in spawner_entities {
|
||||
// First, check if we need to spawn and collect the data we need
|
||||
let spawn_data = {
|
||||
if let Ok(mut query) = self.world.query_one::<&mut Spawner>(entity) {
|
||||
if let Some(spawner) = query.get() {
|
||||
// Check if spawner has reached max mob count
|
||||
if spawner.mobs.len() >= spawner.max_mob_count as usize {
|
||||
None
|
||||
} else if now.duration_since(spawner.last_spawn) >= spawner.spawn_rate {
|
||||
// Calculate spawn count
|
||||
let spawn_count = get_random_number_in_range(
|
||||
0,
|
||||
min(spawner.max_spawn_count_at_once, spawner.max_mob_count - spawner.mobs.len() as u32)
|
||||
);
|
||||
|
||||
// Store spawner data we need before spawning
|
||||
let mob_id = spawner.mob_id;
|
||||
let spawn_range = spawner.spawn_range;
|
||||
|
||||
// Update spawner timing immediately
|
||||
spawner.last_spawn = now;
|
||||
|
||||
Some((spawn_count, mob_id, spawn_range))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
if now.duration_since(spawner.last_spawn) >= spawner.spawn_rate {
|
||||
let spawn_count = get_random_number_in_range(0, min(spawner.max_spawn_count_at_once, (spawner.max_mob_count - spawner.mobs.len() as u32)));
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// If we have spawn data, spawn the mobs
|
||||
if let Some((spawn_count, mob_id, spawn_range)) = spawn_data {
|
||||
let mut new_mobs = Vec::new();
|
||||
|
||||
// Spawn the mobs
|
||||
for _ in 0..spawn_count {
|
||||
let mob_entity = self.spawn_mob(spawner.mob_id, spawner.spawn_range, pos.clone());
|
||||
|
||||
// Add the mob to the spawner's list of mobs.
|
||||
mob_list.push(mob_entity);
|
||||
|
||||
//TODO: Send a packet to all clients in the area to inform them of the new mob
|
||||
let mob_entity = self.spawn_mob(mob_id, spawn_range, pos.clone());
|
||||
new_mobs.push(mob_entity);
|
||||
}
|
||||
|
||||
// Update the spawner's last_spawn time in the world.
|
||||
let mut query = self.world.query_one::<(&Position, &mut Spawner)>(entity).unwrap();
|
||||
let (_pos, spawner_mut) = query.get().unwrap();
|
||||
spawner_mut.last_spawn = now;
|
||||
spawner_mut.mobs = mob_list;
|
||||
// Update the spawner's mob list
|
||||
if let Ok(mut query) = self.world.query_one::<&mut Spawner>(entity) {
|
||||
if let Some(spawner) = query.get() {
|
||||
spawner.mobs.extend(new_mobs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_nearby_objects(&self, id: Entity) -> Vec<Entity> {
|
||||
debug!("Getting nearby objects for entity {:?}", id);
|
||||
|
||||
// Get the position of the query entity
|
||||
if let Ok(position) = self.world.get::<&Position>(id) {
|
||||
// Use spatial grid to find nearby entities
|
||||
let nearby_entities = self.spatial_grid.get_nearby_entities(
|
||||
self.world,
|
||||
Some(id), // Exclude the query entity itself
|
||||
&*position,
|
||||
position.map_id
|
||||
);
|
||||
|
||||
debug!("Found {} nearby objects for entity {:?} at position ({}, {}, {}) on map {}",
|
||||
nearby_entities.len(), id, position.x, position.y, position.z, position.map_id);
|
||||
|
||||
nearby_entities
|
||||
} else {
|
||||
debug!("Entity {:?} has no position component", id);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run(&mut self) {
|
||||
self.update_spawners();
|
||||
self.update_spatial_grid();
|
||||
}
|
||||
|
||||
/// Updates entity movement based on destinations (like C++ code)
|
||||
/// This should be called every 50ms
|
||||
pub fn update_movement(&mut self, dt_ms: u64) {
|
||||
let dt = Duration::from_millis(dt_ms);
|
||||
|
||||
// Collect entities with Position, Destination, and ComputedValues
|
||||
let moving_entities: Vec<(Entity, Position, Destination, ComputedValues)> = self
|
||||
.world
|
||||
.query::<(&Position, &Destination, &ComputedValues)>()
|
||||
.iter()
|
||||
.map(|(entity, (pos, dest, values))| {
|
||||
(entity, pos.clone(), dest.clone(), values.clone())
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (entity, mut pos, mut dest, values) in moving_entities {
|
||||
let speed = values.run_speed;
|
||||
let ntime_ms = if dest.dist > 0.0 {
|
||||
(1000.0 * dest.dist / speed) as u64
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let ntime = Duration::from_millis(ntime_ms);
|
||||
|
||||
let dx = dest.x - pos.x;
|
||||
let dy = dest.y - pos.y;
|
||||
let distance = (dx * dx + dy * dy).sqrt();
|
||||
dest.dist = distance;
|
||||
|
||||
if ntime <= dt || distance <= 0.0001 {
|
||||
// Reached destination
|
||||
self.update_entity_position(entity, dest.x, dest.y, pos.z);
|
||||
|
||||
// Remove destination component
|
||||
let _ = self.world.remove_one::<Destination>(entity);
|
||||
|
||||
// TODO: check_for_target equivalent
|
||||
debug!("Entity {:?} reached destination ({}, {})", entity, dest.x, dest.y);
|
||||
} else {
|
||||
// Move towards destination
|
||||
let progress = dt.as_millis() as f32 / ntime.as_millis() as f32;
|
||||
let new_x = pos.x + dx * progress;
|
||||
let new_y = pos.y + dy * progress;
|
||||
|
||||
self.update_entity_position(entity, new_x, new_y, pos.z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update an entity's position and spatial grid
|
||||
fn update_entity_position(&mut self, entity: Entity, x: f32, y: f32, z: f32) {
|
||||
if let Ok(mut position) = self.world.get::<&mut Position>(entity) {
|
||||
let old_pos = position.clone();
|
||||
position.x = x;
|
||||
position.y = y;
|
||||
position.z = z;
|
||||
|
||||
// Update spatial grid
|
||||
self.spatial_grid.update_entity_position(entity, &old_pos, &*position);
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the spatial grid with current entity positions
|
||||
/// This should be called periodically to keep the grid in sync
|
||||
pub fn update_spatial_grid(&mut self) {
|
||||
// For now, we rebuild the entire grid each time
|
||||
// In a more optimized implementation, we would track position changes
|
||||
// and only update entities that have moved
|
||||
self.spatial_grid.rebuild_from_world(self.world);
|
||||
}
|
||||
|
||||
/// Send mob spawn event to nearby clients
|
||||
fn send_mob_spawn_event_to_nearby_clients(&self, mob_id: u32, position: &Position, npc_id: u32) {
|
||||
if let Some(world_service) = &self.world_service {
|
||||
let event = world_service.create_mob_spawn_event(mob_id, position.x, position.y, npc_id);
|
||||
|
||||
// Send the event asynchronously
|
||||
let world_service_clone = world_service.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = world_service_clone.send_event(event).await {
|
||||
debug!("Failed to send mob spawn event: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Send NPC spawn event to nearby clients
|
||||
fn send_npc_spawn_event_to_nearby_clients(&self, npc_id: u32, position: &Position, angle: f32) {
|
||||
if let Some(world_service) = &self.world_service {
|
||||
let event = world_service.create_npc_spawn_event(npc_id, position.x, position.y, angle);
|
||||
|
||||
// Send the event asynchronously
|
||||
let world_service_clone = world_service.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = world_service_clone.send_event(event).await {
|
||||
debug!("Failed to send NPC spawn event: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Update player position and check for nearby entity changes
|
||||
pub fn update_player_position(&mut self, session_id: &str, old_pos: Position, new_pos: Position) {
|
||||
// Find the player entity by session_id (we'll need to add session_id to player components)
|
||||
// For now, we'll implement a basic version that checks for nearby changes
|
||||
|
||||
// Get entities that were nearby at old position
|
||||
let old_nearby = self.spatial_grid.get_nearby_entities(
|
||||
self.world,
|
||||
None,
|
||||
&old_pos,
|
||||
old_pos.map_id
|
||||
);
|
||||
|
||||
// Get entities that are nearby at new position
|
||||
let new_nearby = self.spatial_grid.get_nearby_entities(
|
||||
self.world,
|
||||
None,
|
||||
&new_pos,
|
||||
new_pos.map_id
|
||||
);
|
||||
|
||||
// Find entities that entered range (in new_nearby but not in old_nearby)
|
||||
let entered_range: Vec<Entity> = new_nearby.iter()
|
||||
.filter(|entity| !old_nearby.contains(entity))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Find entities that left range (in old_nearby but not in new_nearby)
|
||||
let left_range: Vec<Entity> = old_nearby.iter()
|
||||
.filter(|entity| !new_nearby.contains(entity))
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
// Send spawn events for entities that entered range
|
||||
for entity in entered_range {
|
||||
self.send_entity_spawn_event_to_client(session_id, entity);
|
||||
}
|
||||
|
||||
// Send despawn events for entities that left range
|
||||
for entity in left_range {
|
||||
self.send_entity_despawn_event_to_client(session_id, entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// Send entity spawn event to a specific client
|
||||
fn send_entity_spawn_event_to_client(&self, session_id: &str, entity: Entity) {
|
||||
if let Some(world_service) = &self.world_service {
|
||||
// Get entity information
|
||||
if let (Ok(position), Ok(basic_info)) = (
|
||||
self.world.get::<&Position>(entity),
|
||||
self.world.get::<&BasicInfo>(entity)
|
||||
) {
|
||||
// Determine entity type and send appropriate event
|
||||
if self.world.get::<&Mob>(entity).is_ok() {
|
||||
if let Ok(mob) = self.world.get::<&Mob>(entity) {
|
||||
let event = world_service.create_mob_spawn_event_for_client(
|
||||
session_id,
|
||||
basic_info.id.into(),
|
||||
position.x,
|
||||
position.y,
|
||||
mob.id
|
||||
);
|
||||
|
||||
let world_service_clone = world_service.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = world_service_clone.send_event(event).await {
|
||||
debug!("Failed to send mob spawn event to client: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if self.world.get::<&Npc>(entity).is_ok() {
|
||||
if let Ok(npc) = self.world.get::<&Npc>(entity) {
|
||||
let event = world_service.create_npc_spawn_event_for_client(
|
||||
session_id,
|
||||
basic_info.id.into(),
|
||||
position.x,
|
||||
position.y,
|
||||
npc.angle
|
||||
);
|
||||
|
||||
let world_service_clone = world_service.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = world_service_clone.send_event(event).await {
|
||||
debug!("Failed to send NPC spawn event to client: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
// Note: We don't send player spawn events to avoid showing other players for now
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Send entity despawn event to a specific client
|
||||
fn send_entity_despawn_event_to_client(&self, session_id: &str, entity: Entity) {
|
||||
if let Some(world_service) = &self.world_service {
|
||||
if let Ok(basic_info) = self.world.get::<&BasicInfo>(entity) {
|
||||
let event = world_service.create_object_despawn_event_for_client(
|
||||
session_id,
|
||||
basic_info.id.into()
|
||||
);
|
||||
|
||||
let world_service_clone = world_service.clone();
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = world_service_clone.send_event(event).await {
|
||||
debug!("Failed to send object despawn event to client: {}", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
335
game-logic-service/src/entity_system.rs
Normal file
335
game-logic-service/src/entity_system.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::time::{Duration, Instant};
|
||||
use hecs::{Entity, World};
|
||||
use tracing::debug;
|
||||
use crate::components::basic_info::BasicInfo;
|
||||
use crate::components::destination::Destination;
|
||||
use crate::components::position::Position;
|
||||
use crate::components::markers::*;
|
||||
use crate::components::life::Life;
|
||||
use crate::spatial_grid::SpatialGrid;
|
||||
use crate::id_manager::IdManager;
|
||||
use crate::loader::Zone;
|
||||
use crate::world_client::WorldGameLogicServiceImpl;
|
||||
|
||||
/// EntitySystem manages the game world and provides thread-safe access to entity operations
|
||||
pub struct EntitySystem {
|
||||
world: Arc<Mutex<World>>,
|
||||
spatial_grid: Arc<Mutex<SpatialGrid>>,
|
||||
id_manager: Arc<Mutex<IdManager>>,
|
||||
world_service: Option<Arc<WorldGameLogicServiceImpl>>,
|
||||
last_movement_update: Arc<Mutex<Instant>>,
|
||||
}
|
||||
|
||||
impl EntitySystem {
|
||||
pub fn new() -> Self {
|
||||
let now = Instant::now();
|
||||
Self {
|
||||
world: Arc::new(Mutex::new(World::new())),
|
||||
spatial_grid: Arc::new(Mutex::new(SpatialGrid::new())),
|
||||
id_manager: Arc::new(Mutex::new(IdManager::new())),
|
||||
world_service: None,
|
||||
last_movement_update: Arc::new(Mutex::new(now)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_world_service(mut self, world_service: Arc<WorldGameLogicServiceImpl>) -> Self {
|
||||
self.world_service = Some(world_service);
|
||||
self
|
||||
}
|
||||
|
||||
/// Load a map zone into the world
|
||||
pub fn load_map(&self, zone: Zone) {
|
||||
let mut world = self.world.lock().unwrap();
|
||||
let mut spatial_grid = self.spatial_grid.lock().unwrap();
|
||||
let id_manager = self.id_manager.clone();
|
||||
|
||||
// Create a temporary factory to load the map
|
||||
let mut temp_factory = crate::entity_factory::EntityFactory {
|
||||
world: &mut *world,
|
||||
spatial_grid: std::mem::take(&mut *spatial_grid),
|
||||
id_manager,
|
||||
world_service: self.world_service.clone(),
|
||||
};
|
||||
|
||||
temp_factory.load_map(zone);
|
||||
|
||||
// Put the spatial grid back
|
||||
*spatial_grid = temp_factory.spatial_grid;
|
||||
debug!("Map loaded into EntitySystem");
|
||||
}
|
||||
|
||||
/// Run the entity system update (spawners, spatial grid, etc.)
|
||||
pub fn run(&self) {
|
||||
let now = Instant::now();
|
||||
|
||||
// Check if we should update movement (every 50ms like C++ code)
|
||||
let should_update_movement = {
|
||||
let mut last_movement_update = self.last_movement_update.lock().unwrap();
|
||||
if now.duration_since(*last_movement_update) >= Duration::from_millis(50) {
|
||||
*last_movement_update = now;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
let mut world = self.world.lock().unwrap();
|
||||
let mut spatial_grid = self.spatial_grid.lock().unwrap();
|
||||
let id_manager = self.id_manager.clone();
|
||||
|
||||
// Create a temporary factory to run updates
|
||||
let mut temp_factory = crate::entity_factory::EntityFactory {
|
||||
world: &mut *world,
|
||||
spatial_grid: std::mem::take(&mut *spatial_grid),
|
||||
id_manager,
|
||||
world_service: self.world_service.clone(),
|
||||
};
|
||||
|
||||
// Always update spawners - they have their own timing with spawn_rate
|
||||
temp_factory.update_spawners();
|
||||
|
||||
// Always update spatial grid
|
||||
temp_factory.update_spatial_grid();
|
||||
|
||||
// Update movement every 50ms like C++ code
|
||||
if should_update_movement {
|
||||
temp_factory.update_movement(50); // 50ms delta time
|
||||
}
|
||||
|
||||
// Put the spatial grid back
|
||||
*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<EntityInfo> {
|
||||
let world = self.world.lock().unwrap();
|
||||
let spatial_grid = self.spatial_grid.lock().unwrap();
|
||||
|
||||
// Find the client entity to exclude from results
|
||||
let client_entity = world.query::<(&crate::components::client::Client)>().iter().find_map(|(entity, (client))| {
|
||||
if client.client_id == client_id {
|
||||
Some(entity)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
// Create a temporary position for the query
|
||||
let query_position = Position {
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
map_id,
|
||||
spawn_id: 0,
|
||||
};
|
||||
|
||||
// Get nearby entities using the spatial grid, excluding the client entity
|
||||
let nearby_entities = spatial_grid.get_nearby_entities(
|
||||
&*world,
|
||||
client_entity, // Exclude the client entity itself
|
||||
&query_position,
|
||||
map_id
|
||||
);
|
||||
|
||||
debug!("Found {} nearby entities for client {} at position ({}, {}, {}) on map {}",
|
||||
nearby_entities.len(), client_id, x, y, z, map_id);
|
||||
|
||||
// Convert entities to EntityInfo
|
||||
let mut entity_infos = Vec::new();
|
||||
for entity in nearby_entities {
|
||||
if let Some(info) = self.entity_to_info(&*world, entity) {
|
||||
entity_infos.push(info);
|
||||
}
|
||||
}
|
||||
|
||||
entity_infos
|
||||
}
|
||||
|
||||
/// Get nearby objects at a specific position
|
||||
pub fn get_nearby_objects_at_position(&self, x: f32, y: f32, z: f32, map_id: u16) -> Vec<EntityInfo> {
|
||||
let world = self.world.lock().unwrap();
|
||||
let spatial_grid = self.spatial_grid.lock().unwrap();
|
||||
|
||||
// Create a temporary position for the query
|
||||
let query_position = Position {
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
map_id,
|
||||
spawn_id: 0,
|
||||
};
|
||||
|
||||
// Get nearby entities using the spatial grid
|
||||
let nearby_entities = spatial_grid.get_nearby_entities(
|
||||
&*world,
|
||||
None, // No query entity to exclude
|
||||
&query_position,
|
||||
map_id
|
||||
);
|
||||
|
||||
debug!("Found {} nearby entities at position ({}, {}, {}) on map {}",
|
||||
nearby_entities.len(), x, y, z, map_id);
|
||||
|
||||
// Convert entities to EntityInfo
|
||||
let mut entity_infos = Vec::new();
|
||||
for entity in nearby_entities {
|
||||
if let Some(info) = self.entity_to_info(&*world, entity) {
|
||||
entity_infos.push(info);
|
||||
}
|
||||
}
|
||||
|
||||
entity_infos
|
||||
}
|
||||
|
||||
/// Convert an entity to EntityInfo with all relevant data
|
||||
fn entity_to_info(&self, world: &World, entity: Entity) -> Option<EntityInfo> {
|
||||
// Get position (required)
|
||||
let position = world.get::<&Position>(entity).ok()?;
|
||||
|
||||
// Get basic info (required)
|
||||
let basic_info = world.get::<&BasicInfo>(entity).ok()?;
|
||||
|
||||
// Get life info for HP
|
||||
let (hp, max_hp) = if let Ok(life) = world.get::<&Life>(entity) {
|
||||
(life.get_hp() as i32, life.get_max_hp() as i32)
|
||||
} else {
|
||||
(100, 100)
|
||||
};
|
||||
|
||||
// Determine entity type based on marker components
|
||||
let entity_type = if world.get::<&Player>(entity).is_ok() {
|
||||
EntityType::Player
|
||||
} else if world.get::<&Npc>(entity).is_ok() {
|
||||
EntityType::Npc
|
||||
} else if world.get::<&Mob>(entity).is_ok() {
|
||||
EntityType::Mob
|
||||
} else {
|
||||
return None; // Skip entities without recognized types
|
||||
};
|
||||
|
||||
Some(EntityInfo {
|
||||
id: basic_info.id as i32,
|
||||
entity_type,
|
||||
x: position.x,
|
||||
y: position.y,
|
||||
z: position.z,
|
||||
name: basic_info.name.clone(),
|
||||
hp,
|
||||
max_hp,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a clone of the world for read-only operations
|
||||
pub fn get_world(&self) -> Arc<Mutex<World>> {
|
||||
self.world.clone()
|
||||
}
|
||||
|
||||
pub fn set_destination(&self, client_id: u16, x: f32, y: f32, z: f32) {
|
||||
use crate::components::client::Client;
|
||||
|
||||
let mut world = self.world.lock().unwrap();
|
||||
|
||||
// Find the player entity and get their current position
|
||||
let player_entity = world.query::<(&Client, &Position)>().iter().find_map(|(entity, (client, position))| {
|
||||
if client.client_id == client_id {
|
||||
Some((entity, position.clone()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some((entity, current_pos)) = player_entity {
|
||||
// Calculate distance to new position
|
||||
let distance = ((x - current_pos.x).powi(2) + (y - current_pos.y).powi(2)).sqrt();
|
||||
|
||||
// Create a Destination component for the player
|
||||
let destination = Destination::new(x, y, z, distance);
|
||||
world.insert_one(entity, destination).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// 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) {
|
||||
use crate::components::client::Client;
|
||||
|
||||
let mut world = self.world.lock().unwrap();
|
||||
let mut spatial_grid = self.spatial_grid.lock().unwrap();
|
||||
|
||||
// First, find the player entity
|
||||
let player_entity = {
|
||||
let mut query = world.query::<&Client>();
|
||||
query.iter().find_map(|(entity, client)| {
|
||||
if client.client_id == client_id {
|
||||
Some(entity)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
if let Some(entity) = player_entity {
|
||||
// Get the current position to remove from spatial grid
|
||||
let old_position = {
|
||||
if let Ok(pos) = world.get::<&Position>(entity) {
|
||||
pos.clone()
|
||||
} else {
|
||||
debug!("Player entity {} has no position component", client_id);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Update the spatial grid - remove from old position
|
||||
spatial_grid.remove_entity(entity, &old_position);
|
||||
|
||||
// Update the entity's position component
|
||||
let new_position = Position {
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
map_id,
|
||||
spawn_id: old_position.spawn_id,
|
||||
};
|
||||
|
||||
// Drop the old_position to release the immutable borrow
|
||||
drop(old_position);
|
||||
|
||||
if let Err(e) = world.insert_one(entity, new_position.clone()) {
|
||||
debug!("Failed to update position for player {}: {:?}", client_id, e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Add to spatial grid at new position
|
||||
spatial_grid.add_entity(entity, &new_position);
|
||||
|
||||
debug!("Updated position for player {} to ({}, {}, {})", client_id, x, y, z);
|
||||
} else {
|
||||
debug!("Could not find player entity for client_id: {}", client_id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// Information about an entity that can be sent to clients
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EntityInfo {
|
||||
pub id: i32,
|
||||
pub entity_type: EntityType,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub z: f32,
|
||||
pub name: String,
|
||||
pub hp: i32,
|
||||
pub max_hp: i32,
|
||||
}
|
||||
|
||||
/// Type of entity
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EntityType {
|
||||
Player = 1,
|
||||
Npc = 2,
|
||||
Mob = 3,
|
||||
}
|
||||
@@ -5,27 +5,52 @@ use std::sync::{Arc, Mutex};
|
||||
use tonic::{Request, Response, Status};
|
||||
use tonic::metadata::MetadataMap;
|
||||
use tracing::debug;
|
||||
use crate::entity_system::{EntitySystem, EntityType};
|
||||
|
||||
pub mod game_logic {
|
||||
tonic::include_proto!("game_logic");
|
||||
}
|
||||
|
||||
use game_logic::game_logic_service_server::GameLogicService;
|
||||
use crate::game_logic_service::game_logic::{NearbyObjectsRequest, NearbyObjectsResponse};
|
||||
use crate::game_logic_service::game_logic::{NearbyObjectsRequest, NearbyObjectsResponse, Object};
|
||||
|
||||
pub struct MyGameLogicService {
|
||||
pub map_id: u32,
|
||||
pub entity_system: Arc<EntitySystem>,
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl GameLogicService for MyGameLogicService {
|
||||
async fn get_nearby_objects(&self, request: Request<NearbyObjectsRequest>) -> Result<Response<NearbyObjectsResponse>, Status> {
|
||||
let req = request.into_inner();
|
||||
debug!("{:?}", req);
|
||||
debug!("GetNearbyObjects request: {:?}", req);
|
||||
|
||||
let response = NearbyObjectsResponse {
|
||||
objects: vec![],
|
||||
};
|
||||
// Get nearby entities from the entity system
|
||||
let entity_infos = self.entity_system.get_nearby_objects_for_client(
|
||||
req.client_id as u16,
|
||||
req.x,
|
||||
req.y,
|
||||
req.z,
|
||||
req.map_id as u16,
|
||||
);
|
||||
|
||||
// Convert EntityInfo to proto Object
|
||||
let objects: Vec<Object> = entity_infos
|
||||
.into_iter()
|
||||
.map(|info| Object {
|
||||
id: info.id,
|
||||
r#type: info.entity_type as i32,
|
||||
x: info.x,
|
||||
y: info.y,
|
||||
z: info.z,
|
||||
hp: info.hp,
|
||||
max_hp: info.max_hp,
|
||||
})
|
||||
.collect();
|
||||
|
||||
debug!("Returning {} nearby objects", objects.len());
|
||||
|
||||
let response = NearbyObjectsResponse { objects };
|
||||
Ok(Response::new(response))
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,32 @@
|
||||
pub mod components;
|
||||
mod entity_factory;
|
||||
mod entity_system;
|
||||
mod id_manager;
|
||||
mod loader;
|
||||
mod random;
|
||||
mod game_logic_service;
|
||||
mod game_service;
|
||||
mod world_client;
|
||||
mod spatial_grid;
|
||||
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use hecs::World;
|
||||
use tracing::{debug, error, info};
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tokio::sync::oneshot;
|
||||
use tonic::transport::Server;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use utils::service_discovery::{get_kube_service_endpoints_by_dns, get_service_endpoints_by_dns};
|
||||
use utils::{health_check, logging};
|
||||
|
||||
use loader::load_zone_for_map;
|
||||
use crate::components::position::Position;
|
||||
use crate::entity_factory::EntityFactory;
|
||||
use crate::entity_system::EntitySystem;
|
||||
use crate::world_client::{WorldGameLogicServiceImpl, WorldGameLogicServiceServer};
|
||||
use crate::game_logic_service::{MyGameLogicService, game_logic::game_logic_service_server::GameLogicServiceServer};
|
||||
use crate::game_service::{MyGameService};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
@@ -45,18 +56,65 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// Create world service first
|
||||
let world_game_logic_service_impl = WorldGameLogicServiceImpl::new(map_id);
|
||||
let world_game_logic_service_arc = Arc::new(world_game_logic_service_impl);
|
||||
|
||||
// Create shared entity system with world service
|
||||
let entity_system = Arc::new(EntitySystem::new().with_world_service(world_game_logic_service_arc.clone()));
|
||||
|
||||
// Create world service with entity system for the gRPC server
|
||||
let world_game_logic_service = WorldGameLogicServiceImpl::new(map_id).with_entity_system(entity_system.clone());
|
||||
|
||||
// Create gRPC services
|
||||
let game_logic_service = MyGameLogicService {
|
||||
map_id,
|
||||
entity_system: entity_system.clone(),
|
||||
};
|
||||
let game_service = MyGameService {};
|
||||
|
||||
// Start gRPC server with graceful shutdown support
|
||||
let grpc_addr = format!("{}:{}", addr, port).parse()?;
|
||||
info!("Starting Game Logic Service gRPC server on {}", grpc_addr);
|
||||
|
||||
// Create shutdown signal channels
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
|
||||
let (game_logic_shutdown_tx, mut game_logic_shutdown_rx) = oneshot::channel::<()>();
|
||||
|
||||
let server_task = tokio::spawn(async move {
|
||||
let server = Server::builder()
|
||||
.add_service(GameLogicServiceServer::new(game_logic_service))
|
||||
.add_service(game_service.into_service())
|
||||
.add_service(WorldGameLogicServiceServer::new(world_game_logic_service))
|
||||
.serve_with_shutdown(grpc_addr, async {
|
||||
shutdown_rx.await.ok();
|
||||
info!("gRPC server shutdown signal received");
|
||||
});
|
||||
|
||||
if let Err(e) = server.await {
|
||||
error!("gRPC server error: {}", e);
|
||||
} else {
|
||||
info!("gRPC server shut down gracefully");
|
||||
}
|
||||
});
|
||||
|
||||
let entity_system_clone = entity_system.clone();
|
||||
let game_logic_task = tokio::spawn(async move {
|
||||
let mut loading = true;
|
||||
let mut world = World::new();
|
||||
let mut factory = EntityFactory::new(&mut world);
|
||||
|
||||
loop {
|
||||
// Check for shutdown signal
|
||||
if let Ok(_) = game_logic_shutdown_rx.try_recv() {
|
||||
info!("Game logic task received shutdown signal");
|
||||
break;
|
||||
}
|
||||
|
||||
// Load the map
|
||||
if loading {
|
||||
match load_zone_for_map(file_path, map_id) {
|
||||
Ok(Some(zone)) => {
|
||||
info!("Zone with Map Id {} found:", map_id);
|
||||
factory.load_map(zone);
|
||||
entity_system_clone.load_map(zone);
|
||||
loading = false;
|
||||
}
|
||||
Ok(None) => {
|
||||
@@ -72,14 +130,66 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
|
||||
// Update the world
|
||||
if !loading {
|
||||
factory.run();
|
||||
entity_system_clone.run();
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(1000/30));
|
||||
}
|
||||
info!("Game logic task shut down gracefully");
|
||||
});
|
||||
|
||||
// Register service with Consul
|
||||
// health_check::start_health_check(addr.as_str()).await?;
|
||||
|
||||
// Wait for shutdown signal
|
||||
info!("Game Logic service is running. Waiting for shutdown signal...");
|
||||
utils::signal_handler::wait_for_signal().await;
|
||||
|
||||
info!("Shutdown signal received. Beginning graceful shutdown...");
|
||||
|
||||
// Step 1: Signal the game logic task to stop
|
||||
if let Err(_) = game_logic_shutdown_tx.send(()) {
|
||||
warn!("Failed to send shutdown signal to game logic task (receiver may have been dropped)");
|
||||
}
|
||||
|
||||
// Step 2: Signal the gRPC server to stop accepting new connections
|
||||
if let Err(_) = shutdown_tx.send(()) {
|
||||
warn!("Failed to send shutdown signal to gRPC server (receiver may have been dropped)");
|
||||
}
|
||||
|
||||
// Step 3: Wait for both tasks to finish with timeouts
|
||||
let mut shutdown_errors = Vec::new();
|
||||
|
||||
match timeout(Duration::from_secs(10), game_logic_task).await {
|
||||
Ok(result) => {
|
||||
if let Err(e) = result {
|
||||
error!("Game logic task failed: {}", e);
|
||||
shutdown_errors.push(format!("Game logic task: {}", e));
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
error!("Game logic task shutdown timed out after 10 seconds");
|
||||
shutdown_errors.push("Game logic task: timeout".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
match timeout(Duration::from_secs(30), server_task).await {
|
||||
Ok(result) => {
|
||||
if let Err(e) = result {
|
||||
error!("gRPC server task failed: {}", e);
|
||||
shutdown_errors.push(format!("gRPC server task: {}", e));
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
error!("gRPC server shutdown timed out after 30 seconds");
|
||||
shutdown_errors.push("gRPC server task: timeout".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if shutdown_errors.is_empty() {
|
||||
info!("All components shut down successfully");
|
||||
} else {
|
||||
warn!("Some components failed to shut down cleanly: {:?}", shutdown_errors);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
179
game-logic-service/src/spatial_grid.rs
Normal file
179
game-logic-service/src/spatial_grid.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use hecs::{Entity, World};
|
||||
use crate::components::position::Position;
|
||||
use crate::components::markers::*;
|
||||
use crate::components::basic_info::BasicInfo;
|
||||
|
||||
/// Grid size in world units - each grid cell represents 1000x1000 world units
|
||||
const GRID_SIZE: f32 = 1000.0;
|
||||
|
||||
/// Maximum search radius in grid cells (equivalent to C++ implementation)
|
||||
const MAX_SEARCH_RADIUS: i16 = 10;
|
||||
|
||||
/// Grid coordinate type
|
||||
type GridCoord = (i16, i16);
|
||||
|
||||
/// Spatial grid for efficient nearby entity queries
|
||||
#[derive(Debug)]
|
||||
pub struct SpatialGrid {
|
||||
/// Grid cells containing sets of entities
|
||||
grid: HashMap<GridCoord, HashSet<Entity>>,
|
||||
}
|
||||
|
||||
impl SpatialGrid {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
grid: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert world coordinates to grid coordinates
|
||||
fn world_to_grid(x: f32, y: f32) -> GridCoord {
|
||||
let gx = (x / GRID_SIZE) as i16;
|
||||
let gy = (y / GRID_SIZE) as i16;
|
||||
(gx, gy)
|
||||
}
|
||||
|
||||
/// Add an entity to the spatial grid
|
||||
pub fn add_entity(&mut self, entity: Entity, position: &Position) {
|
||||
let grid_pos = Self::world_to_grid(position.x, position.y);
|
||||
self.grid.entry(grid_pos).or_insert_with(HashSet::new).insert(entity);
|
||||
}
|
||||
|
||||
/// Remove an entity from the spatial grid
|
||||
pub fn remove_entity(&mut self, entity: Entity, position: &Position) {
|
||||
let grid_pos = Self::world_to_grid(position.x, position.y);
|
||||
if let Some(cell) = self.grid.get_mut(&grid_pos) {
|
||||
cell.remove(&entity);
|
||||
// Clean up empty cells
|
||||
if cell.is_empty() {
|
||||
self.grid.remove(&grid_pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Update an entity's position in the grid
|
||||
pub fn update_entity_position(&mut self, entity: Entity, old_position: &Position, new_position: &Position) {
|
||||
let old_grid_pos = Self::world_to_grid(old_position.x, old_position.y);
|
||||
let new_grid_pos = Self::world_to_grid(new_position.x, new_position.y);
|
||||
|
||||
// Only update if the grid position actually changed
|
||||
if old_grid_pos != new_grid_pos {
|
||||
self.remove_entity(entity, old_position);
|
||||
self.add_entity(entity, new_position);
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if two entities are nearby (within MAX_SEARCH_RADIUS grid cells)
|
||||
pub fn is_nearby(&self, pos1: &Position, pos2: &Position) -> bool {
|
||||
let grid_pos1 = Self::world_to_grid(pos1.x, pos1.y);
|
||||
let grid_pos2 = Self::world_to_grid(pos2.x, pos2.y);
|
||||
|
||||
let dx = (grid_pos1.0 - grid_pos2.0).abs();
|
||||
let dy = (grid_pos1.1 - grid_pos2.1).abs();
|
||||
|
||||
dx <= MAX_SEARCH_RADIUS && dy <= MAX_SEARCH_RADIUS
|
||||
}
|
||||
|
||||
/// Get all entities near the given position
|
||||
/// Returns entities within MAX_SEARCH_RADIUS grid cells, excluding the query entity itself
|
||||
pub fn get_nearby_entities(&self, world: &World, query_entity: Option<Entity>, position: &Position, map_id: u16) -> Vec<Entity> {
|
||||
let center_grid = Self::world_to_grid(position.x, position.y);
|
||||
let mut nearby_entities = HashSet::new();
|
||||
|
||||
// Search in a square around the center position
|
||||
for x in (center_grid.0 - MAX_SEARCH_RADIUS)..=(center_grid.0 + MAX_SEARCH_RADIUS) {
|
||||
for y in (center_grid.1 - MAX_SEARCH_RADIUS)..=(center_grid.1 + MAX_SEARCH_RADIUS) {
|
||||
if let Some(cell) = self.grid.get(&(x, y)) {
|
||||
for &entity in cell {
|
||||
// Skip the query entity itself
|
||||
if let Some(query_ent) = query_entity {
|
||||
if entity == query_ent {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Verify the entity is still valid and on the same map
|
||||
if let Ok(entity_pos) = world.get::<&Position>(entity) {
|
||||
if entity_pos.map_id == map_id {
|
||||
nearby_entities.insert(entity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nearby_entities.into_iter().collect()
|
||||
}
|
||||
|
||||
/// Get nearby entities within a specific radius (in world units)
|
||||
pub fn get_nearby_entities_within_radius(&self, world: &World, query_entity: Option<Entity>, position: &Position, map_id: u16, radius: f32) -> Vec<Entity> {
|
||||
let nearby = self.get_nearby_entities(world, query_entity, position, map_id);
|
||||
let radius_squared = radius * radius;
|
||||
|
||||
nearby.into_iter().filter(|&entity| {
|
||||
if let Ok(entity_pos) = world.get::<&Position>(entity) {
|
||||
let dx = entity_pos.x - position.x;
|
||||
let dy = entity_pos.y - position.y;
|
||||
let distance_squared = dx * dx + dy * dy;
|
||||
distance_squared <= radius_squared
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// Rebuild the entire spatial grid from the world state
|
||||
/// This should be called periodically to clean up invalid entities
|
||||
pub fn rebuild_from_world(&mut self, world: &World) {
|
||||
self.grid.clear();
|
||||
|
||||
// Add all entities with positions to the grid
|
||||
for (entity, position) in world.query::<&Position>().iter() {
|
||||
self.add_entity(entity, position);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get statistics about the spatial grid
|
||||
pub fn get_stats(&self) -> (usize, usize) {
|
||||
let cell_count = self.grid.len();
|
||||
let entity_count = self.grid.values().map(|cell| cell.len()).sum();
|
||||
(cell_count, entity_count)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SpatialGrid {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use hecs::World;
|
||||
|
||||
#[test]
|
||||
fn test_world_to_grid() {
|
||||
assert_eq!(SpatialGrid::world_to_grid(0.0, 0.0), (0, 0));
|
||||
assert_eq!(SpatialGrid::world_to_grid(999.0, 999.0), (0, 0));
|
||||
assert_eq!(SpatialGrid::world_to_grid(1000.0, 1000.0), (1, 1));
|
||||
assert_eq!(SpatialGrid::world_to_grid(-1000.0, -1000.0), (-1, -1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_remove_entity() {
|
||||
let mut grid = SpatialGrid::new();
|
||||
let mut world = World::new();
|
||||
|
||||
let pos = Position { x: 500.0, y: 500.0, z: 0.0, map_id: 1, spawn_id: 0 };
|
||||
let entity = world.spawn((pos.clone(),));
|
||||
|
||||
grid.add_entity(entity, &pos);
|
||||
assert_eq!(grid.grid.len(), 1);
|
||||
|
||||
grid.remove_entity(entity, &pos);
|
||||
assert_eq!(grid.grid.len(), 0);
|
||||
}
|
||||
}
|
||||
239
game-logic-service/src/world_client.rs
Normal file
239
game-logic-service/src/world_client.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tonic::{Request, Response, Status, Streaming};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use futures::{Stream, StreamExt};
|
||||
use std::pin::Pin;
|
||||
use crate::entity_system::EntitySystem;
|
||||
|
||||
pub mod world {
|
||||
tonic::include_proto!("world");
|
||||
}
|
||||
|
||||
use world::world_game_logic_service_server::{WorldGameLogicService};
|
||||
|
||||
pub use world::world_game_logic_service_server::WorldGameLogicServiceServer;
|
||||
|
||||
pub struct WorldGameLogicServiceImpl {
|
||||
map_id: u32,
|
||||
event_sender: Arc<Mutex<Option<mpsc::UnboundedSender<world::GameLogicEvent>>>>,
|
||||
event_receiver: Arc<Mutex<Option<mpsc::UnboundedReceiver<world::GameLogicEvent>>>>,
|
||||
entity_system: Option<Arc<EntitySystem>>,
|
||||
}
|
||||
|
||||
impl WorldGameLogicServiceImpl {
|
||||
pub fn new(map_id: u32) -> Self {
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
Self {
|
||||
map_id,
|
||||
event_sender: Arc::new(Mutex::new(Some(tx))),
|
||||
event_receiver: Arc::new(Mutex::new(Some(rx))),
|
||||
entity_system: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_entity_system(mut self, entity_system: Arc<EntitySystem>) -> Self {
|
||||
self.entity_system = Some(entity_system);
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn send_event(&self, event: world::GameLogicEvent) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let sender = self.event_sender.lock().await;
|
||||
if let Some(tx) = sender.as_ref() {
|
||||
tx.send(event)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Event sender not available".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl WorldGameLogicService for WorldGameLogicServiceImpl {
|
||||
type StreamGameEventsStream = Pin<Box<dyn Stream<Item = Result<world::GameLogicEvent, Status>> + Send + Sync + 'static>>;
|
||||
|
||||
async fn stream_game_events(
|
||||
&self,
|
||||
request: Request<Streaming<world::GameLogicEvent>>,
|
||||
) -> Result<Response<Self::StreamGameEventsStream>, Status> {
|
||||
info!("World service connected to game logic for map {}", self.map_id);
|
||||
|
||||
let mut inbound_stream = request.into_inner();
|
||||
let (outbound_tx, outbound_rx) = mpsc::unbounded_channel();
|
||||
|
||||
let map_id = self.map_id;
|
||||
let entity_system = self.entity_system.clone();
|
||||
|
||||
// Handle incoming events from world service
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = inbound_stream.next().await {
|
||||
match event {
|
||||
Ok(game_event) => {
|
||||
debug!("Received event from world service for map {}: {:?}", map_id, game_event);
|
||||
|
||||
// Process the event based on its type
|
||||
match game_event.event {
|
||||
Some(world::game_logic_event::Event::PlayerConnect(connect_event)) => {
|
||||
info!("Player {} connected to map {}", connect_event.session_id, map_id);
|
||||
// Handle player connection logic
|
||||
}
|
||||
Some(world::game_logic_event::Event::PlayerDisconnect(disconnect_event)) => {
|
||||
info!("Player {} disconnected from map {}", disconnect_event.session_id, map_id);
|
||||
// Handle player disconnection logic
|
||||
}
|
||||
Some(world::game_logic_event::Event::PlayerMove(move_event)) => {
|
||||
debug!("Player {} moved to ({}, {}, {}) in map {}",
|
||||
move_event.client_id, move_event.x, move_event.y, move_event.z, map_id);
|
||||
|
||||
// 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::<u16>() {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
debug!("Unhandled event type from world service");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error receiving event from world service for map {}: {}", map_id, e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("World service stream ended for map {}", map_id);
|
||||
});
|
||||
|
||||
// Create outbound stream for sending events to world service
|
||||
let outbound_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(outbound_rx)
|
||||
.map(|event| Ok(event));
|
||||
|
||||
Ok(Response::new(Box::pin(outbound_stream) as Self::StreamGameEventsStream))
|
||||
}
|
||||
}
|
||||
|
||||
impl WorldGameLogicServiceImpl {
|
||||
pub fn create_mob_spawn_event(&self, mob_id: u32, x: f32, y: f32, npc_id: u32) -> world::GameLogicEvent {
|
||||
world::GameLogicEvent {
|
||||
client_ids: vec![], // Will be populated by world service based on nearby players
|
||||
map_id: self.map_id as i32,
|
||||
event: Some(world::game_logic_event::Event::MobSpawn(world::MobSpawnEvent {
|
||||
id: mob_id,
|
||||
pos_x: x,
|
||||
pos_y: y,
|
||||
dest_pos_x: x,
|
||||
dest_pos_y: y,
|
||||
command: 0,
|
||||
target_id: 0,
|
||||
move_mode: 0,
|
||||
hp: 100,
|
||||
team_id: 0,
|
||||
status_flag: 0,
|
||||
npc_id,
|
||||
quest_id: 0,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_npc_spawn_event(&self, npc_id: u32, x: f32, y: f32, angle: f32) -> world::GameLogicEvent {
|
||||
world::GameLogicEvent {
|
||||
client_ids: vec![], // Will be populated by world service based on nearby players
|
||||
map_id: self.map_id as i32,
|
||||
event: Some(world::game_logic_event::Event::NpcSpawn(world::NpcSpawnEvent {
|
||||
id: npc_id,
|
||||
pos_x: x,
|
||||
pos_y: y,
|
||||
dest_pos_x: x,
|
||||
dest_pos_y: y,
|
||||
command: 0,
|
||||
target_id: 0,
|
||||
move_mode: 0,
|
||||
hp: 100,
|
||||
team_id: 0,
|
||||
status_flag: 0,
|
||||
npc_id,
|
||||
quest_id: 0,
|
||||
angle,
|
||||
event_status: 0,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_object_despawn_event(&self, object_id: u32) -> world::GameLogicEvent {
|
||||
world::GameLogicEvent {
|
||||
client_ids: vec![], // Will be populated by world service based on nearby players
|
||||
map_id: self.map_id as i32,
|
||||
event: Some(world::game_logic_event::Event::ObjectDespawn(world::ObjectDespawnEvent {
|
||||
object_id,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
// Client-specific event methods
|
||||
pub fn create_mob_spawn_event_for_client(&self, client_id: &str, mob_id: u32, x: f32, y: f32, npc_id: u32) -> world::GameLogicEvent {
|
||||
world::GameLogicEvent {
|
||||
client_ids: vec![client_id.to_string()],
|
||||
map_id: self.map_id as i32,
|
||||
event: Some(world::game_logic_event::Event::MobSpawn(world::MobSpawnEvent {
|
||||
id: mob_id,
|
||||
pos_x: x,
|
||||
pos_y: y,
|
||||
dest_pos_x: x,
|
||||
dest_pos_y: y,
|
||||
command: 0,
|
||||
target_id: 0,
|
||||
move_mode: 0,
|
||||
hp: 100,
|
||||
team_id: 0,
|
||||
status_flag: 0,
|
||||
npc_id,
|
||||
quest_id: 0,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_npc_spawn_event_for_client(&self, client_id: &str, npc_id: u32, x: f32, y: f32, angle: f32) -> world::GameLogicEvent {
|
||||
world::GameLogicEvent {
|
||||
client_ids: vec![client_id.to_string()],
|
||||
map_id: self.map_id as i32,
|
||||
event: Some(world::game_logic_event::Event::NpcSpawn(world::NpcSpawnEvent {
|
||||
id: npc_id,
|
||||
pos_x: x,
|
||||
pos_y: y,
|
||||
dest_pos_x: x,
|
||||
dest_pos_y: y,
|
||||
command: 0,
|
||||
target_id: 0,
|
||||
move_mode: 0,
|
||||
hp: 100,
|
||||
team_id: 0,
|
||||
status_flag: 0,
|
||||
npc_id,
|
||||
quest_id: 0,
|
||||
angle,
|
||||
event_status: 0,
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_object_despawn_event_for_client(&self, client_id: &str, object_id: u32) -> world::GameLogicEvent {
|
||||
world::GameLogicEvent {
|
||||
client_ids: vec![client_id.to_string()],
|
||||
map_id: self.map_id as i32,
|
||||
event: Some(world::game_logic_event::Event::ObjectDespawn(world::ObjectDespawnEvent {
|
||||
object_id,
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,8 @@ fn main() {
|
||||
"../proto/chat.proto",
|
||||
"../proto/character.proto",
|
||||
"../proto/character_common.proto",
|
||||
"../proto/game.proto"
|
||||
"../proto/game.proto",
|
||||
"../proto/world.proto"
|
||||
],
|
||||
&["../proto"],
|
||||
)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use crate::auth_client::AuthClient;
|
||||
use crate::character_client::CharacterClient;
|
||||
use crate::connection_service::ConnectionService;
|
||||
use crate::world_client::WorldClientManager;
|
||||
use crate::dataconsts::*;
|
||||
use crate::enums;
|
||||
use crate::{character_common, enums};
|
||||
use crate::enums::ItemType;
|
||||
use crate::packet::{send_packet, Packet, PacketPayload};
|
||||
use crate::packet_type::PacketType;
|
||||
@@ -16,6 +17,8 @@ use tokio::sync::Mutex;
|
||||
use tonic::{Code, Status};
|
||||
use tracing::{debug, error, info, warn};
|
||||
use utils::null_string::NullTerminatedString;
|
||||
use crate::character_common::Location;
|
||||
use crate::handlers::character;
|
||||
|
||||
pub(crate) fn string_to_u32(s: &str) -> u32 {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
@@ -240,6 +243,8 @@ pub(crate) async fn handle_select_char_req(
|
||||
character_client: Arc<Mutex<CharacterClient>>,
|
||||
connection_service: Arc<ConnectionService>,
|
||||
connection_id: String,
|
||||
world_client_manager: Arc<WorldClientManager>,
|
||||
world_url: String,
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
use crate::packets::cli_select_char_req::*;
|
||||
use crate::packets::srv_billing_message::*;
|
||||
@@ -436,5 +441,56 @@ pub(crate) async fn handle_select_char_req(
|
||||
send_packet(&mut locked_stream, &response_packet).await?;
|
||||
}
|
||||
|
||||
if let Err(e) = character::establish_world_connection(
|
||||
world_client_manager.clone(),
|
||||
connection_service.clone(),
|
||||
connection_id,
|
||||
world_url.clone(),
|
||||
position,
|
||||
).await {
|
||||
warn!("Failed to establish world connection: {}", e);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn establish_world_connection(
|
||||
world_client_manager: Arc<WorldClientManager>,
|
||||
connection_service: Arc<ConnectionService>,
|
||||
connection_id: String,
|
||||
world_url: String,
|
||||
position: character_common::Location,
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
if let Some(connection_state) = connection_service.get_connection(&connection_id) {
|
||||
if let (Some(session_id), Some(character_id)) = (&connection_state.session_id, connection_state.character_id) {
|
||||
let client_id = connection_id.clone();
|
||||
|
||||
// Connect to world service for this client
|
||||
if let Err(e) = world_client_manager
|
||||
.add_client_connection(
|
||||
session_id.clone(),
|
||||
client_id,
|
||||
&position,
|
||||
world_url,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!("Failed to connect client to world service: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
info!("Connected client {} to world service on map {}", session_id, position.map_id);
|
||||
|
||||
// Start world event handler for this client
|
||||
crate::handlers::world::start_world_event_handler(
|
||||
world_client_manager.clone(),
|
||||
connection_service.clone(),
|
||||
session_id.clone()
|
||||
);
|
||||
} else {
|
||||
warn!("Cannot establish world connection: session_id or character_id not set");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3,3 +3,4 @@ pub mod character;
|
||||
pub mod world;
|
||||
pub mod chat;
|
||||
pub mod chat_client;
|
||||
pub mod world_client;
|
||||
@@ -2,13 +2,14 @@ use crate::character_client::CharacterClient;
|
||||
use crate::connection_service::ConnectionService;
|
||||
use crate::packet::{send_packet, Packet, PacketPayload};
|
||||
use crate::packet_type::PacketType;
|
||||
use crate::world_client::WorldClientManager;
|
||||
use chrono::{Local, Timelike};
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::Mutex;
|
||||
use tonic::transport::Channel;
|
||||
use tracing::debug;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use utils::service_discovery::get_kube_service_endpoints_by_dns;
|
||||
|
||||
fn distance(x1: f64, y1: f64, x2: f64, y2: f64) -> u16 {
|
||||
@@ -21,6 +22,7 @@ pub(crate) async fn handle_change_map_req(
|
||||
character_client: Arc<Mutex<CharacterClient>>,
|
||||
connection_service: Arc<ConnectionService>,
|
||||
connection_id: String,
|
||||
world_client_manager: Arc<WorldClientManager>,
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
use crate::packets::cli_change_map_req::*;
|
||||
use crate::packets::srv_change_map_reply::*;
|
||||
@@ -51,6 +53,16 @@ pub(crate) async fn handle_change_map_req(
|
||||
let now = Local::now();
|
||||
let time_as_u16 = (now.hour() * 100 + now.minute()) as u16;
|
||||
|
||||
// if let Err(e) = world_client_manager
|
||||
// .send_client_map_change_event(&*session_id, request.map_id as u32)
|
||||
// .await
|
||||
// {
|
||||
// warn!("Failed to send map change event to world service: {}", e);
|
||||
// // Don't return error as the map change itself was successful
|
||||
// }
|
||||
//
|
||||
// debug!("Sent map change event for client {} to world service: map {}", session_id, request.map_id);
|
||||
|
||||
let data = SrvChangeMapReply {
|
||||
object_index: client_id,
|
||||
hp: stats.hp as u16,
|
||||
@@ -80,6 +92,7 @@ pub(crate) async fn handle_mouse_cmd_req(
|
||||
packet: Packet,
|
||||
connection_service: Arc<ConnectionService>,
|
||||
connection_id: String,
|
||||
world_client_manager: Arc<WorldClientManager>,
|
||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||
use crate::packets::cli_mouse_cmd::*;
|
||||
use crate::packets::srv_mouse_cmd::*;
|
||||
@@ -89,12 +102,25 @@ pub(crate) async fn handle_mouse_cmd_req(
|
||||
let mut char_id = 0;
|
||||
let mut client_id = 0;
|
||||
let mut character_id_list: Vec<u32> = Vec::new();
|
||||
let mut session_id = "".to_string();
|
||||
if let Some(mut state) = connection_service.get_connection(&connection_id) {
|
||||
char_id = state.character_id.expect("Missing character id in connection state");
|
||||
character_id_list = state.character_list.clone().expect("Missing character id list");
|
||||
client_id = state.client_id;
|
||||
session_id = state.session_id.clone().expect("Missing session id in connection state");
|
||||
}
|
||||
|
||||
if let Err(e) = world_client_manager
|
||||
.send_client_move_event(&*session_id, request.x, request.y, request.z as f32)
|
||||
.await
|
||||
{
|
||||
error!("Failed to send move event to world service: {}", e);
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
debug!("Sent move event for client {} to world service: ({}, {}, {})",
|
||||
session_id, request.x, request.y, request.z);
|
||||
|
||||
let data = SrvMouseCmd {
|
||||
id: client_id,
|
||||
target_id: request.target_id,
|
||||
@@ -112,6 +138,8 @@ pub(crate) async fn handle_mouse_cmd_req(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// World service integration - movement events
|
||||
|
||||
pub(crate) async fn handle_togggle_move_req(
|
||||
packet: Packet,
|
||||
connection_service: Arc<ConnectionService>,
|
||||
@@ -189,3 +217,96 @@ pub(crate) async fn handle_set_animation_req(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// World service integration - movement events
|
||||
|
||||
async fn handle_world_events(
|
||||
world_client_manager: Arc<WorldClientManager>,
|
||||
connection_service: Arc<ConnectionService>,
|
||||
session_id: String,
|
||||
) {
|
||||
info!("Starting world event handler for session {}", session_id);
|
||||
|
||||
loop {
|
||||
match world_client_manager.receive_world_event(&session_id).await {
|
||||
Some(world_event) => {
|
||||
// debug!("Received world event for session {}: {:?}", session_id, world_event);
|
||||
|
||||
// Process the world event and convert to game packets
|
||||
match world_event.event {
|
||||
Some(crate::world_client::world::world_event::Event::NpcSpawn(npc_spawn)) => {
|
||||
debug!("Processing NPC spawn event: {:?}", npc_spawn);
|
||||
// Convert to the appropriate game packet and send to client
|
||||
// This would involve creating a spawn packet and sending it through the connection's writer
|
||||
}
|
||||
Some(crate::world_client::world::world_event::Event::MobSpawn(mob_spawn)) => {
|
||||
debug!("Processing mob spawn event: {:?}", mob_spawn);
|
||||
// Convert to the appropriate game packet and send to client
|
||||
}
|
||||
Some(crate::world_client::world::world_event::Event::ObjectDespawn(despawn)) => {
|
||||
debug!("Processing object despawn event: {:?}", despawn);
|
||||
// Convert to the appropriate game packet and send to client
|
||||
}
|
||||
Some(crate::world_client::world::world_event::Event::NearbyUpdate(nearby_update)) => {
|
||||
debug!("Processing nearby objects update: {} objects", nearby_update.objects.len());
|
||||
// Convert to the appropriate game packet and send to client
|
||||
|
||||
let npcs = nearby_update.objects.iter().filter(|obj| obj.object_type == 2).collect::<Vec<_>>();
|
||||
let mobs = nearby_update.objects.iter().filter(|obj| obj.object_type == 3).collect::<Vec<_>>();
|
||||
|
||||
debug!("There are {} npcs and {} mobs", npcs.len(), mobs.len());
|
||||
debug!("Nearby npcs: {:?}", npcs);
|
||||
debug!("Nearby mobs: {:?}", mobs);
|
||||
|
||||
// if !npcs.is_empty() {
|
||||
// // Send all nearby npcs in a single batch update
|
||||
// let batch_event = SrvNpcChar {
|
||||
// id: 0,
|
||||
// x: 0.0,
|
||||
// y: 0.0,
|
||||
// dest_x: 0.0,
|
||||
// dest_y: 0.0,
|
||||
// command: 0,
|
||||
// target_id: 0,
|
||||
// move_mode: 0,
|
||||
// hp: 0,
|
||||
// team_id: 0,
|
||||
// status_flag: 0,
|
||||
// npc_id: 0,
|
||||
// quest_id: 0,
|
||||
// angle: 0.0,
|
||||
// event_status: 0,
|
||||
// };
|
||||
// let response_packet = Packet::new(PacketType::PakwcNpcChar, &batch_event)?;
|
||||
// if let Some(mut state) = connection_service.get_connection_mut(&session_id) {
|
||||
// let writer_clone = state.writer.clone().unwrap();
|
||||
// let mut locked_stream = writer_clone.lock().await;
|
||||
// send_packet(&mut locked_stream, &response_packet).await?;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
None => {
|
||||
warn!("Received world event with no event data for session {}", session_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
debug!("World event stream ended for session {}", session_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("World event handler ended for session {}", session_id);
|
||||
}
|
||||
|
||||
// Helper function to start world event handling for a client
|
||||
pub fn start_world_event_handler(
|
||||
world_client_manager: Arc<WorldClientManager>,
|
||||
connection_service: Arc<ConnectionService>,
|
||||
session_id: String,
|
||||
) {
|
||||
tokio::spawn(async move {
|
||||
handle_world_events(world_client_manager, connection_service, session_id).await;
|
||||
});
|
||||
}
|
||||
87
packet-service/src/handlers/world_client.rs
Normal file
87
packet-service/src/handlers/world_client.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use tonic::{Request, transport::Channel};
|
||||
use futures::StreamExt;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use std::error::Error;
|
||||
use tracing::{debug, error};
|
||||
|
||||
mod character_common {
|
||||
tonic::include_proto!("character_common");
|
||||
}
|
||||
|
||||
pub mod world {
|
||||
tonic::include_proto!("game");
|
||||
}
|
||||
|
||||
use world::event_service_client::EventServiceClient;
|
||||
use world::GenericEvent;
|
||||
use crate::interceptors::auth_interceptor::AuthInterceptor;
|
||||
|
||||
/// WorldClientHandler encapsulates the bidirectional event stream.
|
||||
/// In addition to providing an API to send messages, it also spawns a
|
||||
/// background task which forwards incoming chat messages through an inbound channel.
|
||||
pub struct WorldClientHandler {
|
||||
outbound_tx: mpsc::Sender<GenericEvent>,
|
||||
/// Inbound messages from the chat service are sent here.
|
||||
pub inbound_rx: Mutex<mpsc::Receiver<GenericEvent>>,
|
||||
}
|
||||
|
||||
impl WorldClientHandler {
|
||||
/// Creates and returns a new WorldClientHandler.
|
||||
///
|
||||
/// * `world_url` - Full URL of the World Service (for example, "http://127.0.0.1:50051")
|
||||
/// * `client_id` - The authenticated client ID to be injected into each request.
|
||||
/// * `session_id` - The authenticated session token to be injected into each request.
|
||||
pub async fn new(
|
||||
world_url: String,
|
||||
client_id: String,
|
||||
session_id: String,
|
||||
) -> Result<Self, Box<dyn Error + Send + Sync>> {
|
||||
// Create a channel to the World Service.
|
||||
let channel = Channel::from_shared(world_url)?.connect().await
|
||||
.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?;
|
||||
let interceptor = AuthInterceptor { client_id, session_id };
|
||||
|
||||
// Create EventService client with interceptor.
|
||||
let mut world_client = EventServiceClient::with_interceptor(channel, interceptor);
|
||||
|
||||
// Create an mpsc channel for outbound messages.
|
||||
let (out_tx, out_rx) = mpsc::channel(32);
|
||||
let outbound_stream = ReceiverStream::new(out_rx);
|
||||
|
||||
// This channel will be used to forward inbound messages to the packet-service.
|
||||
let (in_tx, in_rx) = mpsc::channel(32);
|
||||
|
||||
// Establish the bidirectional chat stream.
|
||||
let request = Request::new(outbound_stream);
|
||||
let mut response = world_client.stream_events(request).await
|
||||
.map_err(|e| Box::new(e) as Box<dyn Error + Send + Sync>)?.into_inner();
|
||||
|
||||
// Spawn a task to continuously receive messages from the Chat Service.
|
||||
// Each received message is sent through the 'in_tx' channel.
|
||||
tokio::spawn(async move {
|
||||
while let Some(result) = response.next().await {
|
||||
match result {
|
||||
Ok(event) => {
|
||||
// Process the event as needed.
|
||||
// debug!("Received event: {:?}", event);
|
||||
if let Err(e) = in_tx.send(event).await {
|
||||
error!("Failed to forward event: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error receiving event stream message: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("Event inbound stream closed");
|
||||
});
|
||||
|
||||
Ok(Self {
|
||||
outbound_tx: out_tx,
|
||||
inbound_rx: Mutex::new(in_rx),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use crate::connection_service::ConnectionService;
|
||||
use crate::metrics::{ACTIVE_CONNECTIONS, PACKETS_RECEIVED};
|
||||
use crate::packet::Packet;
|
||||
use crate::router::PacketRouter;
|
||||
use crate::world_client::WorldClientManager;
|
||||
use dotenv::dotenv;
|
||||
use prometheus::{self, Encoder, TextEncoder};
|
||||
use prometheus_exporter;
|
||||
@@ -16,7 +17,8 @@ use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::{Mutex, Semaphore};
|
||||
use tokio::sync::{Mutex, Semaphore, oneshot};
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tokio::{io, select, signal};
|
||||
use tracing::Level;
|
||||
use tracing::{debug, error, info, warn};
|
||||
@@ -41,6 +43,7 @@ mod router;
|
||||
mod types;
|
||||
mod interceptors;
|
||||
mod id_manager;
|
||||
mod world_client;
|
||||
|
||||
pub mod common {
|
||||
tonic::include_proto!("common");
|
||||
@@ -82,16 +85,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
.get(0)
|
||||
.unwrap()
|
||||
);
|
||||
let world_url = format!(
|
||||
"http://{}",
|
||||
get_kube_service_endpoints_by_dns("world-service", "tcp", "world-service")
|
||||
.await?
|
||||
.get(0)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
// Start health-check endpoint
|
||||
health_check::start_health_check(addr.as_str()).await?;
|
||||
|
||||
let auth_client = Arc::new(Mutex::new(AuthClient::connect(&auth_url).await?));
|
||||
let character_client = Arc::new(Mutex::new(CharacterClient::connect(&character_url).await?));
|
||||
let world_client_manager = Arc::new(WorldClientManager::new());
|
||||
|
||||
let full_addr = format!("{}:{}", &addr, port);
|
||||
|
||||
tokio::spawn(async move {
|
||||
// Create shutdown signal channel for the TCP listener
|
||||
let (tcp_shutdown_tx, mut tcp_shutdown_rx) = oneshot::channel::<()>();
|
||||
|
||||
let tcp_server_task = tokio::spawn(async move {
|
||||
let semaphore = Arc::new(Semaphore::new(MAX_CONCURRENT_CONNECTIONS));
|
||||
let listener = TcpListener::bind(full_addr.clone()).await.unwrap();
|
||||
let buffer_pool = BufferPool::new(BUFFER_POOL_SIZE);
|
||||
@@ -101,12 +115,23 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
auth_client,
|
||||
character_client,
|
||||
connection_service,
|
||||
world_client_manager,
|
||||
world_url,
|
||||
};
|
||||
|
||||
info!("Packet service listening on {}", full_addr);
|
||||
|
||||
loop {
|
||||
let (mut socket, addr) = listener.accept().await.unwrap();
|
||||
tokio::select! {
|
||||
// Check for shutdown signal
|
||||
_ = &mut tcp_shutdown_rx => {
|
||||
info!("TCP server received shutdown signal");
|
||||
break;
|
||||
}
|
||||
// Accept new connections
|
||||
result = listener.accept() => {
|
||||
match result {
|
||||
Ok((mut socket, addr)) => {
|
||||
let packet_router = packet_router.clone();
|
||||
info!("New connection from {}", addr);
|
||||
|
||||
@@ -129,11 +154,43 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
packet_router.connection_service.remove_connection(&connection_id);
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to accept connection: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("TCP server shut down gracefully");
|
||||
});
|
||||
|
||||
let binding = format!("{}:{}", &addr, metrics_port);
|
||||
prometheus_exporter::start(binding.parse().unwrap()).unwrap();
|
||||
|
||||
// Wait for shutdown signal
|
||||
info!("Packet service is running. Waiting for shutdown signal...");
|
||||
utils::signal_handler::wait_for_signal().await;
|
||||
|
||||
info!("Shutdown signal received. Beginning graceful shutdown...");
|
||||
|
||||
// Signal the TCP server to stop accepting new connections
|
||||
if let Err(_) = tcp_shutdown_tx.send(()) {
|
||||
warn!("Failed to send shutdown signal to TCP server (receiver may have been dropped)");
|
||||
}
|
||||
|
||||
// Wait for the TCP server to finish with a timeout
|
||||
match timeout(Duration::from_secs(30), tcp_server_task).await {
|
||||
Ok(result) => {
|
||||
if let Err(e) = result {
|
||||
error!("TCP server task failed: {}", e);
|
||||
} else {
|
||||
info!("Packet service shut down successfully");
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
error!("TCP server shutdown timed out after 30 seconds");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::handlers::*;
|
||||
use crate::metrics::{ACTIVE_CONNECTIONS, PACKETS_RECEIVED, PACKET_PROCESSING_TIME};
|
||||
use crate::packet::Packet;
|
||||
use crate::packet_type::PacketType;
|
||||
use crate::world_client::WorldClientManager;
|
||||
use std::error::Error;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
@@ -19,6 +20,8 @@ pub struct PacketRouter {
|
||||
pub auth_client: Arc<Mutex<AuthClient>>,
|
||||
pub character_client: Arc<Mutex<CharacterClient>>,
|
||||
pub connection_service: Arc<ConnectionService>,
|
||||
pub world_client_manager: Arc<WorldClientManager>,
|
||||
pub world_url: String,
|
||||
}
|
||||
|
||||
impl PacketRouter {
|
||||
@@ -61,6 +64,10 @@ impl PacketRouter {
|
||||
if let Some(state) = self.connection_service.get_connection(&connection_id) {
|
||||
let session_id = state.session_id.unwrap_or_default();
|
||||
if !session_id.is_empty() {
|
||||
// Disconnect from world service
|
||||
self.world_client_manager.remove_client_connection(&session_id).await;
|
||||
|
||||
// Logout from auth service
|
||||
let mut auth_client = self.auth_client.lock().await;
|
||||
auth_client.logout(&session_id).await?;
|
||||
} else {
|
||||
@@ -93,19 +100,18 @@ impl PacketRouter {
|
||||
PacketType::PakcsCharListReq => character::handle_char_list_req(packet, self.character_client.clone(), self.connection_service.clone(), connection_id).await,
|
||||
PacketType::PakcsCreateCharReq => character::handle_create_char_req(packet, self.character_client.clone(), self.connection_service.clone(), connection_id).await,
|
||||
PacketType::PakcsDeleteCharReq => character::handle_delete_char_req(packet, self.character_client.clone(), self.connection_service.clone(), connection_id).await,
|
||||
PacketType::PakcsSelectCharReq => character::handle_select_char_req(packet, self.character_client.clone(), self.connection_service.clone(), connection_id).await,
|
||||
PacketType::PakcsSelectCharReq => character::handle_select_char_req(packet, self.character_client.clone(), self.connection_service.clone(), connection_id, self.world_client_manager.clone(), self.world_url.clone()).await,
|
||||
|
||||
// World Packets
|
||||
PacketType::PakcsChangeMapReq => world::handle_change_map_req(packet, self.character_client.clone(), self.connection_service.clone(), connection_id).await,
|
||||
PacketType::PakcsMouseCmd => world::handle_mouse_cmd_req(packet, self.connection_service.clone(), connection_id).await,
|
||||
// World Packets (enhanced with world service integration)
|
||||
PacketType::PakcsChangeMapReq => world::handle_change_map_req(packet, self.character_client.clone(), self.connection_service.clone(), connection_id, self.world_client_manager.clone()).await,
|
||||
PacketType::PakcsMouseCmd => world::handle_mouse_cmd_req(packet, self.connection_service.clone(), connection_id, self.world_client_manager.clone()).await,
|
||||
PacketType::PakcsToggleMove => world::handle_togggle_move_req(packet, self.connection_service.clone(), connection_id).await,
|
||||
PacketType::PakcsSetAnimation => world::handle_set_animation_req(packet, self.connection_service.clone(), connection_id).await,
|
||||
|
||||
// Chat Packets
|
||||
PacketType::PakcsNormalChat => chat::handle_normal_chat(packet, self.connection_service.clone(), connection_id).await,
|
||||
PacketType::PakcsShoutChat => chat::handle_shout_chat(packet, self.connection_service.clone(), connection_id).await,
|
||||
// 1 => chat::handle_chat(packet).await?,
|
||||
// 2 => movement::handle_movement(packet).await?,
|
||||
|
||||
_ => {
|
||||
warn!("Unhandled packet type: {:?}", packet.packet_type);
|
||||
Ok(())
|
||||
|
||||
335
packet-service/src/world_client.rs
Normal file
335
packet-service/src/world_client.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
use tonic::transport::Channel;
|
||||
use tracing::{debug, error, info, warn};
|
||||
use futures::{Stream, StreamExt};
|
||||
|
||||
pub mod world {
|
||||
tonic::include_proto!("world");
|
||||
}
|
||||
|
||||
pub mod character_common {
|
||||
tonic::include_proto!("character_common");
|
||||
}
|
||||
|
||||
pub mod game {
|
||||
use super::character_common;
|
||||
tonic::include_proto!("game");
|
||||
}
|
||||
|
||||
use world::world_service_client::WorldServiceClient;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WorldClient {
|
||||
client: WorldServiceClient<Channel>,
|
||||
event_sender: Option<mpsc::UnboundedSender<world::ClientEvent>>,
|
||||
}
|
||||
|
||||
impl WorldClient {
|
||||
pub async fn connect(endpoint: &str) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let client = WorldServiceClient::connect(endpoint.to_string()).await?;
|
||||
Ok(WorldClient {
|
||||
client,
|
||||
event_sender: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn get_character(
|
||||
&mut self,
|
||||
token: &str,
|
||||
user_id: &str,
|
||||
char_id: &str,
|
||||
session_id: &str,
|
||||
) -> Result<world::CharacterResponse, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let request = world::CharacterRequest {
|
||||
token: token.to_string(),
|
||||
user_id: user_id.to_string(),
|
||||
char_id: char_id.to_string(),
|
||||
session_id: session_id.to_string(),
|
||||
};
|
||||
|
||||
let response = self.client.get_character(request).await?;
|
||||
Ok(response.into_inner())
|
||||
}
|
||||
|
||||
pub async fn change_map(
|
||||
&mut self,
|
||||
id: i32,
|
||||
x: f32,
|
||||
y: f32,
|
||||
) -> Result<world::ChangeMapResponse, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let request = world::ChangeMapRequest {
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
};
|
||||
|
||||
let response = self.client.change_map(request).await?;
|
||||
Ok(response.into_inner())
|
||||
}
|
||||
|
||||
pub async fn move_character(
|
||||
&mut self,
|
||||
session_id: &str,
|
||||
target_id: u32,
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
) -> Result<world::CharacterMoveResponse, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let request = world::CharacterMoveRequest {
|
||||
session_id: session_id.to_string(),
|
||||
target_id,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
};
|
||||
|
||||
let response = self.client.move_character(request).await?;
|
||||
Ok(response.into_inner())
|
||||
}
|
||||
|
||||
pub async fn get_target_hp(
|
||||
&mut self,
|
||||
session_id: &str,
|
||||
target_id: u32,
|
||||
) -> Result<world::ObjectHpResponse, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let request = world::ObjectHpRequest {
|
||||
session_id: session_id.to_string(),
|
||||
target_id,
|
||||
};
|
||||
|
||||
let response = self.client.get_target_hp(request).await?;
|
||||
Ok(response.into_inner())
|
||||
}
|
||||
|
||||
pub async fn get_nearby_objects(
|
||||
&mut self,
|
||||
session_id: &str,
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
map_id: i32,
|
||||
radius: f32,
|
||||
) -> Result<world::NearbyObjectsResponse, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let request = world::NearbyObjectsRequest {
|
||||
session_id: session_id.to_string(),
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
map_id,
|
||||
radius,
|
||||
};
|
||||
|
||||
let response = self.client.get_nearby_objects(request).await?;
|
||||
Ok(response.into_inner())
|
||||
}
|
||||
|
||||
pub async fn start_client_event_stream(
|
||||
&mut self,
|
||||
outbound_receiver: mpsc::UnboundedReceiver<world::ClientEvent>,
|
||||
) -> Result<mpsc::UnboundedReceiver<world::WorldEvent>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let (inbound_sender, inbound_receiver) = mpsc::unbounded_channel();
|
||||
|
||||
// Create the bidirectional stream
|
||||
let outbound_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(outbound_receiver);
|
||||
|
||||
let response = self.client.stream_client_events(outbound_stream).await?;
|
||||
let mut inbound_stream = response.into_inner();
|
||||
|
||||
// Spawn task to handle incoming events from world service
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = inbound_stream.next().await {
|
||||
match event {
|
||||
Ok(world_event) => {
|
||||
debug!("Received world event: {:?}", world_event);
|
||||
if let Err(e) = inbound_sender.send(world_event) {
|
||||
error!("Failed to forward world event: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error receiving world event: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
info!("World event stream ended");
|
||||
});
|
||||
|
||||
Ok(inbound_receiver)
|
||||
}
|
||||
|
||||
pub fn send_client_event(&self, event: world::ClientEvent) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
if let Some(sender) = &self.event_sender {
|
||||
sender.send(event)?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Event sender not initialized".into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct WorldClientManager {
|
||||
clients: Arc<Mutex<HashMap<String, WorldClient>>>,
|
||||
event_senders: Arc<Mutex<HashMap<String, mpsc::UnboundedSender<world::ClientEvent>>>>,
|
||||
event_receivers: Arc<Mutex<HashMap<String, mpsc::UnboundedReceiver<world::WorldEvent>>>>,
|
||||
}
|
||||
|
||||
impl WorldClientManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
clients: Arc::new(Mutex::new(HashMap::new())),
|
||||
event_senders: Arc::new(Mutex::new(HashMap::new())),
|
||||
event_receivers: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_client_connection(
|
||||
&self,
|
||||
session_id: String,
|
||||
client_id: String,
|
||||
position: &crate::character_common::Location,
|
||||
world_endpoint: String,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut clients = self.clients.lock().await;
|
||||
|
||||
if clients.contains_key(&session_id) {
|
||||
warn!("World client for session {} already exists", session_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut client = WorldClient::connect(&world_endpoint).await?;
|
||||
|
||||
// Set up bidirectional communication
|
||||
let (outbound_sender, outbound_receiver) = mpsc::unbounded_channel();
|
||||
let inbound_receiver = client.start_client_event_stream(outbound_receiver).await?;
|
||||
|
||||
// Send initial connect event
|
||||
let connect_event = world::ClientEvent {
|
||||
session_id: session_id.clone(),
|
||||
client_id: client_id.clone(),
|
||||
map_id: position.map_id as i32,
|
||||
event: Some(world::client_event::Event::Connect(world::ClientConnectEvent {
|
||||
x: position.x as f32,
|
||||
y: position.y as f32,
|
||||
z: 100.0,
|
||||
})),
|
||||
};
|
||||
|
||||
outbound_sender.send(connect_event)?;
|
||||
|
||||
clients.insert(session_id.clone(), client);
|
||||
|
||||
let mut senders = self.event_senders.lock().await;
|
||||
senders.insert(session_id.clone(), outbound_sender);
|
||||
|
||||
let mut receivers = self.event_receivers.lock().await;
|
||||
receivers.insert(session_id.clone(), inbound_receiver);
|
||||
|
||||
info!("Added world client connection for session {} on map {}", session_id, position.map_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_client_connection(&self, session_id: &str) {
|
||||
// Send disconnect event before removing
|
||||
if let Err(e) = self.send_client_disconnect_event(session_id).await {
|
||||
warn!("Failed to send disconnect event for session {}: {}", session_id, e);
|
||||
}
|
||||
|
||||
let mut clients = self.clients.lock().await;
|
||||
clients.remove(session_id);
|
||||
|
||||
let mut senders = self.event_senders.lock().await;
|
||||
senders.remove(session_id);
|
||||
|
||||
let mut receivers = self.event_receivers.lock().await;
|
||||
receivers.remove(session_id);
|
||||
|
||||
info!("Removed world client connection for session {}", session_id);
|
||||
}
|
||||
|
||||
pub async fn send_client_move_event(
|
||||
&self,
|
||||
session_id: &str,
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let senders = self.event_senders.lock().await;
|
||||
if let Some(sender) = senders.get(session_id) {
|
||||
let move_event = world::ClientEvent {
|
||||
session_id: session_id.to_string(),
|
||||
client_id: session_id.to_string(), // Using session_id as client_id for now
|
||||
map_id: 0, // Will be updated by world service
|
||||
event: Some(world::client_event::Event::Move(world::ClientMoveEvent {
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
})),
|
||||
};
|
||||
sender.send(move_event)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_client_map_change_event(&self, session_id: &str, new_map_id: u32) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let senders = self.event_senders.lock().await;
|
||||
if let Some(sender) = senders.get(session_id) {
|
||||
let map_change_event = world::ClientEvent {
|
||||
session_id: session_id.to_string(),
|
||||
client_id: session_id.to_string(),
|
||||
map_id: new_map_id as i32,
|
||||
event: Some(world::client_event::Event::MapChange(world::ClientMapChangeEvent {
|
||||
old_map_id: 0, // We don't track the old map ID in this context
|
||||
new_map_id: new_map_id as i32,
|
||||
x: 0.0, // Default position - could be enhanced to track actual position
|
||||
y: 0.0,
|
||||
z: 0.0,
|
||||
})),
|
||||
};
|
||||
sender.send(map_change_event)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_client_disconnect_event(&self, session_id: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let senders = self.event_senders.lock().await;
|
||||
if let Some(sender) = senders.get(session_id) {
|
||||
let disconnect_event = world::ClientEvent {
|
||||
session_id: session_id.to_string(),
|
||||
client_id: session_id.to_string(),
|
||||
map_id: 0,
|
||||
event: Some(world::client_event::Event::Disconnect(world::ClientDisconnectEvent {})),
|
||||
};
|
||||
sender.send(disconnect_event)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_nearby_objects(
|
||||
&self,
|
||||
session_id: &str,
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
map_id: i32,
|
||||
radius: f32,
|
||||
) -> Result<world::NearbyObjectsResponse, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut clients = self.clients.lock().await;
|
||||
if let Some(client) = clients.get_mut(session_id) {
|
||||
return client.get_nearby_objects(session_id, x, y, z, map_id, radius).await;
|
||||
}
|
||||
Err(format!("No world client found for session {}", session_id).into())
|
||||
}
|
||||
|
||||
pub async fn receive_world_event(&self, session_id: &str) -> Option<world::WorldEvent> {
|
||||
let mut receivers = self.event_receivers.lock().await;
|
||||
if let Some(receiver) = receivers.get_mut(session_id) {
|
||||
receiver.recv().await
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ service GameLogicService {
|
||||
}
|
||||
|
||||
message NearbyObjectsRequest {
|
||||
string session_id = 1;
|
||||
uint32 client_id = 1;
|
||||
float x = 2;
|
||||
float y = 3;
|
||||
float z = 4;
|
||||
@@ -24,4 +24,6 @@ message Object {
|
||||
float x = 3;
|
||||
float y = 4;
|
||||
float z = 5;
|
||||
int32 hp = 6;
|
||||
int32 max_hp = 7;
|
||||
}
|
||||
@@ -7,6 +7,12 @@ service WorldService {
|
||||
rpc ChangeMap(ChangeMapRequest) returns (ChangeMapResponse);
|
||||
rpc MoveCharacter(CharacterMoveRequest) returns (CharacterMoveResponse);
|
||||
rpc GetTargetHp(ObjectHpRequest) returns (ObjectHpResponse);
|
||||
rpc GetNearbyObjects(NearbyObjectsRequest) returns (NearbyObjectsResponse);
|
||||
rpc StreamClientEvents(stream ClientEvent) returns (stream WorldEvent);
|
||||
}
|
||||
|
||||
service WorldGameLogicService {
|
||||
rpc StreamGameEvents(stream GameLogicEvent) returns (stream GameLogicEvent);
|
||||
}
|
||||
|
||||
message CharacterRequest {
|
||||
@@ -66,3 +72,154 @@ message ObjectHpResponse {
|
||||
uint32 target_id = 1;
|
||||
int32 hp = 2;
|
||||
}
|
||||
|
||||
message NearbyObjectsRequest {
|
||||
string session_id = 1;
|
||||
float x = 2;
|
||||
float y = 3;
|
||||
float z = 4;
|
||||
int32 map_id = 5;
|
||||
float radius = 6;
|
||||
}
|
||||
|
||||
message NearbyObjectsResponse {
|
||||
repeated WorldObject objects = 1;
|
||||
}
|
||||
|
||||
message WorldObject {
|
||||
uint32 id = 1;
|
||||
int32 object_type = 2;
|
||||
float x = 3;
|
||||
float y = 4;
|
||||
float z = 5;
|
||||
int32 map_id = 6;
|
||||
string name = 7;
|
||||
int32 hp = 8;
|
||||
int32 max_hp = 9;
|
||||
}
|
||||
|
||||
message ClientEvent {
|
||||
string session_id = 1;
|
||||
string client_id = 2;
|
||||
int32 map_id = 3;
|
||||
|
||||
oneof event {
|
||||
ClientConnectEvent connect = 4;
|
||||
ClientDisconnectEvent disconnect = 5;
|
||||
ClientMoveEvent move = 6;
|
||||
ClientMapChangeEvent map_change = 7;
|
||||
}
|
||||
}
|
||||
|
||||
message ClientConnectEvent {
|
||||
float x = 1;
|
||||
float y = 2;
|
||||
float z = 3;
|
||||
}
|
||||
|
||||
message ClientDisconnectEvent {
|
||||
// Empty for now
|
||||
}
|
||||
|
||||
message ClientMoveEvent {
|
||||
float x = 1;
|
||||
float y = 2;
|
||||
float z = 3;
|
||||
}
|
||||
|
||||
message ClientMapChangeEvent {
|
||||
int32 old_map_id = 1;
|
||||
int32 new_map_id = 2;
|
||||
float x = 3;
|
||||
float y = 4;
|
||||
float z = 5;
|
||||
}
|
||||
|
||||
message WorldEvent {
|
||||
repeated string client_ids = 1;
|
||||
|
||||
oneof event {
|
||||
NpcSpawnEvent npc_spawn = 2;
|
||||
MobSpawnEvent mob_spawn = 3;
|
||||
ObjectDespawnEvent object_despawn = 4;
|
||||
NearbyObjectsUpdate nearby_update = 5;
|
||||
}
|
||||
}
|
||||
|
||||
message GameLogicEvent {
|
||||
repeated string client_ids = 1;
|
||||
int32 map_id = 2;
|
||||
|
||||
oneof event {
|
||||
NpcSpawnEvent npc_spawn = 3;
|
||||
MobSpawnEvent mob_spawn = 4;
|
||||
ObjectDespawnEvent object_despawn = 5;
|
||||
PlayerMoveEvent player_move = 6;
|
||||
PlayerConnectEvent player_connect = 7;
|
||||
PlayerDisconnectEvent player_disconnect = 8;
|
||||
}
|
||||
}
|
||||
|
||||
message NpcSpawnEvent {
|
||||
uint32 id = 1;
|
||||
float pos_x = 2;
|
||||
float pos_y = 3;
|
||||
float dest_pos_x = 4;
|
||||
float dest_pos_y = 5;
|
||||
int32 command = 6;
|
||||
uint32 target_id = 7;
|
||||
int32 move_mode = 8;
|
||||
int32 hp = 9;
|
||||
int32 team_id = 10;
|
||||
int32 status_flag = 11;
|
||||
uint32 npc_id = 12;
|
||||
uint32 quest_id = 13;
|
||||
float angle = 14;
|
||||
int32 event_status = 15;
|
||||
}
|
||||
|
||||
message MobSpawnEvent {
|
||||
uint32 id = 1;
|
||||
float pos_x = 2;
|
||||
float pos_y = 3;
|
||||
float dest_pos_x = 4;
|
||||
float dest_pos_y = 5;
|
||||
int32 command = 6;
|
||||
uint32 target_id = 7;
|
||||
int32 move_mode = 8;
|
||||
int32 hp = 9;
|
||||
int32 team_id = 10;
|
||||
int32 status_flag = 11;
|
||||
uint32 npc_id = 12;
|
||||
uint32 quest_id = 13;
|
||||
}
|
||||
|
||||
message ObjectDespawnEvent {
|
||||
uint32 object_id = 1;
|
||||
}
|
||||
|
||||
message PlayerMoveEvent {
|
||||
string session_id = 1;
|
||||
string client_id = 2;
|
||||
float x = 3;
|
||||
float y = 4;
|
||||
float z = 5;
|
||||
}
|
||||
|
||||
message PlayerConnectEvent {
|
||||
string session_id = 1;
|
||||
string client_id = 2;
|
||||
int32 map_id = 3;
|
||||
float x = 4;
|
||||
float y = 5;
|
||||
float z = 6;
|
||||
}
|
||||
|
||||
message PlayerDisconnectEvent {
|
||||
string session_id = 1;
|
||||
string client_id = 2;
|
||||
}
|
||||
|
||||
message NearbyObjectsUpdate {
|
||||
repeated WorldObject objects = 1;
|
||||
}
|
||||
|
||||
@@ -7,10 +7,15 @@ edition = "2021"
|
||||
utils = { path = "../utils" }
|
||||
dotenv = "0.15"
|
||||
tokio = { version = "1.41.1", features = ["full"] }
|
||||
tokio-stream = "0.1"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0.140"
|
||||
kube = { version = "1.1.0", features = ["runtime", "derive"] }
|
||||
k8s-openapi = { version = "0.25.0", features = ["latest"] }
|
||||
tracing = "0.1.41"
|
||||
tonic = "0.12"
|
||||
prost = "0.13"
|
||||
futures = "0.3"
|
||||
|
||||
[build-dependencies]
|
||||
tonic-build = "0.12.3"
|
||||
|
||||
@@ -69,6 +69,36 @@ The service can be configured using environment variables:
|
||||
- `LISTEN_ADDR`: The address to listen on (default: "0.0.0.0")
|
||||
- `SERVICE_PORT`: The port to listen on (default: "50054")
|
||||
- `LOG_LEVEL`: Logging level (default: "info")
|
||||
- `MAP_IDS`: Comma-separated list of map IDs to manage (e.g., "42,43,44,45")
|
||||
- `WORLD_SERVICE_NAME`: Name of the world service instance (default: "default-service")
|
||||
|
||||
### Game Logic Connection Retry Configuration
|
||||
|
||||
The world service includes comprehensive retry logic for connecting to game logic instances that may not be ready immediately:
|
||||
|
||||
#### Connection Info Retrieval Retry
|
||||
- `CONNECTION_INFO_MAX_RETRIES`: Maximum number of retry attempts for getting pod connection info (default: 3)
|
||||
- `CONNECTION_INFO_INITIAL_DELAY_MS`: Initial delay between connection info retries in milliseconds (default: 2000)
|
||||
- `CONNECTION_INFO_MAX_DELAY_MS`: Maximum delay between connection info retries in milliseconds (default: 10000)
|
||||
|
||||
#### gRPC Client Connection Retry
|
||||
- `GAME_LOGIC_MAX_RETRIES`: Maximum number of retry attempts for gRPC connections (default: 5)
|
||||
- `GAME_LOGIC_INITIAL_DELAY_MS`: Initial delay between gRPC connection retries in milliseconds (default: 500)
|
||||
- `GAME_LOGIC_MAX_DELAY_MS`: Maximum delay between gRPC connection retries in milliseconds (default: 10000)
|
||||
|
||||
Both retry mechanisms use exponential backoff, doubling the delay after each failed attempt up to the maximum delay. The service will continue starting even if some game logic instances fail to connect, allowing for partial functionality.
|
||||
|
||||
### Kubernetes Service Creation
|
||||
|
||||
The world service automatically creates both Pods and Services for each game logic instance:
|
||||
|
||||
- **Pod**: Contains the game logic service container
|
||||
- **Service**: Provides stable networking and service discovery
|
||||
- Type: ClusterIP
|
||||
- Port: 50056 (gRPC)
|
||||
- Selector: Matches specific instance by app, map_id, and instance labels
|
||||
|
||||
The world service connects to game logic instances using Kubernetes Service DNS names (e.g., `world-default-service-42-service.default.svc.cluster.local`) instead of direct Pod IPs, providing better reliability and following Kubernetes best practices.
|
||||
|
||||
## Running the Service
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ fn main() {
|
||||
tonic_build::configure()
|
||||
.build_server(true) // Generate gRPC server code
|
||||
.compile_well_known_types(true)
|
||||
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
|
||||
.compile_protos(&["../proto/world.proto", "../proto/game.proto"], &["../proto"])
|
||||
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
||||
|
||||
@@ -11,6 +10,6 @@ fn main() {
|
||||
tonic_build::configure()
|
||||
.build_server(false) // Generate gRPC client code
|
||||
.compile_well_known_types(true)
|
||||
.compile_protos(&["../proto/user_db_api.proto", "../proto/auth.proto", "../proto/character.proto", "../proto/character_common.proto", "../proto/chat.proto"], &["../proto"])
|
||||
.compile_protos(&["../proto/user_db_api.proto", "../proto/auth.proto", "../proto/character.proto", "../proto/character_common.proto", "../proto/chat.proto", "../proto/game_logic.proto", "../proto/game.proto"], &["../proto"])
|
||||
.unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e));
|
||||
}
|
||||
|
||||
267
world-service/src/game_logic_client.rs
Normal file
267
world-service/src/game_logic_client.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, Mutex};
|
||||
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 world::world_game_logic_service_client::WorldGameLogicServiceClient;
|
||||
use game_logic::game_logic_service_client::GameLogicServiceClient;
|
||||
|
||||
pub struct GameLogicClientManager {
|
||||
clients: Arc<Mutex<HashMap<u32, GameLogicClient>>>,
|
||||
}
|
||||
|
||||
pub struct GameLogicClient {
|
||||
pub map_id: u32,
|
||||
pub endpoint: String,
|
||||
pub service_client: Option<GameLogicServiceClient<Channel>>,
|
||||
pub world_client: Option<WorldGameLogicServiceClient<Channel>>,
|
||||
pub event_sender: Option<mpsc::UnboundedSender<world::GameLogicEvent>>,
|
||||
}
|
||||
|
||||
impl GameLogicClientManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
clients: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get retry configuration from environment variables with sensible defaults
|
||||
fn get_retry_config() -> (u32, Duration, Duration) {
|
||||
let max_retries = std::env::var("GAME_LOGIC_MAX_RETRIES")
|
||||
.unwrap_or_else(|_| "3".to_string())
|
||||
.parse::<u32>()
|
||||
.unwrap_or(5);
|
||||
|
||||
let initial_delay_ms = std::env::var("GAME_LOGIC_INITIAL_DELAY_MS")
|
||||
.unwrap_or_else(|_| "500".to_string())
|
||||
.parse::<u64>()
|
||||
.unwrap_or(500);
|
||||
|
||||
let max_delay_ms = std::env::var("GAME_LOGIC_MAX_DELAY_MS")
|
||||
.unwrap_or_else(|_| "10000".to_string())
|
||||
.parse::<u64>()
|
||||
.unwrap_or(10000);
|
||||
|
||||
(
|
||||
max_retries,
|
||||
Duration::from_millis(initial_delay_ms),
|
||||
Duration::from_millis(max_delay_ms),
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn add_client(&self, map_id: u32, endpoint: String) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let (max_retries, initial_delay, max_delay) = Self::get_retry_config();
|
||||
self.add_client_with_retry(map_id, endpoint, max_retries, initial_delay, max_delay).await
|
||||
}
|
||||
|
||||
pub async fn add_client_with_retry(
|
||||
&self,
|
||||
map_id: u32,
|
||||
endpoint: String,
|
||||
max_retries: u32,
|
||||
initial_delay: Duration,
|
||||
max_delay: Duration,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut clients = self.clients.lock().await;
|
||||
|
||||
if clients.contains_key(&map_id) {
|
||||
warn!("Game logic client for map {} already exists", map_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Release the lock before attempting connections
|
||||
drop(clients);
|
||||
|
||||
let mut client = GameLogicClient {
|
||||
map_id,
|
||||
endpoint: endpoint.clone(),
|
||||
service_client: None,
|
||||
world_client: None,
|
||||
event_sender: None,
|
||||
};
|
||||
|
||||
// Retry logic for connecting to the game logic service
|
||||
let mut delay = initial_delay;
|
||||
let mut last_error = None;
|
||||
|
||||
for attempt in 0..=max_retries {
|
||||
match GameLogicServiceClient::connect(endpoint.clone()).await {
|
||||
Ok(service_client) => {
|
||||
client.service_client = Some(service_client);
|
||||
debug!("Connected to game logic service for map {} at {} (attempt {})", map_id, endpoint, attempt + 1);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
last_error = Some(e);
|
||||
if attempt < max_retries {
|
||||
warn!("Failed to connect to game logic service for map {} at {} (attempt {}): {}. Retrying in {:?}...",
|
||||
map_id, endpoint, attempt + 1, last_error.as_ref().unwrap(), delay);
|
||||
sleep(delay).await;
|
||||
delay = std::cmp::min(delay * 2, max_delay);
|
||||
} else {
|
||||
error!("Failed to connect to game logic service for map {} at {} after {} attempts: {}",
|
||||
map_id, endpoint, max_retries + 1, last_error.as_ref().unwrap());
|
||||
return Err(last_error.unwrap().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset delay for the second connection
|
||||
delay = initial_delay;
|
||||
last_error = None;
|
||||
|
||||
// Retry logic for connecting to the world-game-logic service
|
||||
for attempt in 0..=max_retries {
|
||||
match WorldGameLogicServiceClient::connect(endpoint.clone()).await {
|
||||
Ok(world_client) => {
|
||||
client.world_client = Some(world_client);
|
||||
debug!("Connected to game logic world service for map {} at {} (attempt {})", map_id, endpoint, attempt + 1);
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
last_error = Some(e);
|
||||
if attempt < max_retries {
|
||||
warn!("Failed to connect to game logic world service for map {} at {} (attempt {}): {}. Retrying in {:?}...",
|
||||
map_id, endpoint, attempt + 1, last_error.as_ref().unwrap(), delay);
|
||||
sleep(delay).await;
|
||||
delay = std::cmp::min(delay * 2, max_delay);
|
||||
} else {
|
||||
error!("Failed to connect to game logic world service for map {} at {} after {} attempts: {}",
|
||||
map_id, endpoint, max_retries + 1, last_error.as_ref().unwrap());
|
||||
return Err(last_error.unwrap().into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-acquire the lock and insert the client
|
||||
let mut clients = self.clients.lock().await;
|
||||
clients.insert(map_id, client);
|
||||
debug!("Successfully added game logic client for map {} at {}", map_id, endpoint);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_client(&self, map_id: u32) {
|
||||
let mut clients = self.clients.lock().await;
|
||||
if let Some(_client) = clients.remove(&map_id) {
|
||||
debug!("Removed game logic client for map {}", map_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_nearby_objects(
|
||||
&self,
|
||||
map_id: u32,
|
||||
client_id: u32,
|
||||
x: f32,
|
||||
y: f32,
|
||||
z: f32,
|
||||
) -> Result<game_logic::NearbyObjectsResponse, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut clients = self.clients.lock().await;
|
||||
|
||||
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,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
map_id: map_id as i32,
|
||||
};
|
||||
|
||||
let response = service_client.get_nearby_objects(request).await?;
|
||||
return Ok(response.into_inner());
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("No game logic client found for map {}", map_id).into())
|
||||
}
|
||||
|
||||
pub async fn start_event_stream(
|
||||
&self,
|
||||
map_id: u32,
|
||||
outbound_receiver: mpsc::UnboundedReceiver<world::GameLogicEvent>,
|
||||
) -> Result<mpsc::UnboundedReceiver<world::GameLogicEvent>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let mut clients = self.clients.lock().await;
|
||||
|
||||
if let Some(client) = clients.get_mut(&map_id) {
|
||||
if let Some(mut world_client) = client.world_client.take() {
|
||||
let (inbound_sender, inbound_receiver) = mpsc::unbounded_channel();
|
||||
|
||||
// Create the bidirectional stream
|
||||
let outbound_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(outbound_receiver);
|
||||
|
||||
let response = world_client.stream_game_events(outbound_stream).await?;
|
||||
let mut inbound_stream = response.into_inner();
|
||||
|
||||
// Spawn task to handle incoming events
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = inbound_stream.next().await {
|
||||
match event {
|
||||
Ok(game_event) => {
|
||||
debug!("Received event from game logic for map {}: {:?}", map_id, game_event);
|
||||
if let Err(e) = inbound_sender.send(game_event) {
|
||||
error!("Failed to forward event from game logic for map {}: {}", map_id, e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error receiving event from game logic for map {}: {}", map_id, e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
debug!("Event stream from game logic for map {} ended", map_id);
|
||||
});
|
||||
|
||||
return Ok(inbound_receiver);
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("No world client found for map {}", map_id).into())
|
||||
}
|
||||
|
||||
pub async fn send_event(&self, map_id: u32, event: world::GameLogicEvent) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let clients = self.clients.lock().await;
|
||||
|
||||
if let Some(client) = clients.get(&map_id) {
|
||||
if let Some(sender) = &client.event_sender {
|
||||
sender.send(event)?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(format!("No event sender found for map {}", map_id).into())
|
||||
}
|
||||
|
||||
pub async fn list_connected_maps(&self) -> Vec<u32> {
|
||||
let clients = self.clients.lock().await;
|
||||
clients.keys().cloned().collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl GameLogicClient {
|
||||
pub async fn connect(map_id: u32, endpoint: String) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
|
||||
let service_client = GameLogicServiceClient::connect(endpoint.clone()).await?;
|
||||
let world_client = WorldGameLogicServiceClient::connect(endpoint.clone()).await?;
|
||||
|
||||
Ok(Self {
|
||||
map_id,
|
||||
endpoint,
|
||||
service_client: Some(service_client),
|
||||
world_client: Some(world_client),
|
||||
event_sender: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ use kube::{
|
||||
api::{Api, PostParams, DeleteParams},
|
||||
Client,
|
||||
};
|
||||
use k8s_openapi::api::core::v1::Pod;
|
||||
use k8s_openapi::api::core::v1::{Pod, Service};
|
||||
use serde_json::json;
|
||||
use std::error::Error;
|
||||
use tokio::time::{sleep, Duration, Instant};
|
||||
@@ -36,8 +36,8 @@ impl K8sOrchestrator {
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new game-logic Pod with the given `instance_name` and container `image`.
|
||||
/// Adjust the pod manifest as needed for your game-logic container.
|
||||
/// Creates a new game-logic Pod and Service with the given `instance_name` and container `image`.
|
||||
/// This creates both the Pod and a corresponding Service for proper networking.
|
||||
pub async fn create_game_logic_instance(
|
||||
&self,
|
||||
instance_name: &str,
|
||||
@@ -55,7 +55,8 @@ impl K8sOrchestrator {
|
||||
"name": instance_name,
|
||||
"labels": {
|
||||
"app": "game-logic",
|
||||
"map_id": map_id_str
|
||||
"map_id": map_id_str,
|
||||
"instance": instance_name
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
@@ -96,9 +97,55 @@ impl K8sOrchestrator {
|
||||
|
||||
// Create the Pod in Kubernetes.
|
||||
let created_pod = pods.create(&PostParams::default(), &pod).await?;
|
||||
|
||||
// Create a corresponding Service for the Pod
|
||||
self.create_service_for_instance(instance_name, map_id).await?;
|
||||
|
||||
Ok(created_pod)
|
||||
}
|
||||
|
||||
/// Creates a Kubernetes Service for the game-logic instance
|
||||
async fn create_service_for_instance(
|
||||
&self,
|
||||
instance_name: &str,
|
||||
map_id: u32,
|
||||
) -> Result<Service, Box<dyn Error>> {
|
||||
let services: Api<Service> = Api::namespaced(self.client.clone(), &self.namespace);
|
||||
|
||||
let map_id_str = map_id.to_string();
|
||||
let service_name = format!("{}-service", instance_name);
|
||||
|
||||
let service_manifest = json!({
|
||||
"apiVersion": "v1",
|
||||
"kind": "Service",
|
||||
"metadata": {
|
||||
"name": service_name,
|
||||
"labels": {
|
||||
"app": "game-logic",
|
||||
"map_id": map_id_str
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"selector": {
|
||||
"app": "game-logic",
|
||||
"map_id": map_id_str,
|
||||
"instance": instance_name
|
||||
},
|
||||
"ports": [{
|
||||
"name": "grpc",
|
||||
"port": 50056,
|
||||
"targetPort": 50056,
|
||||
"protocol": "TCP"
|
||||
}],
|
||||
"type": "ClusterIP"
|
||||
}
|
||||
});
|
||||
|
||||
let service: Service = serde_json::from_value(service_manifest)?;
|
||||
let created_service = services.create(&PostParams::default(), &service).await?;
|
||||
Ok(created_service)
|
||||
}
|
||||
|
||||
/// Retrieves the updated Pod object for a given instance name.
|
||||
pub async fn get_instance(&self, instance_name: &str)
|
||||
-> Result<Pod, Box<dyn Error>> {
|
||||
@@ -107,41 +154,49 @@ impl K8sOrchestrator {
|
||||
Ok(pod)
|
||||
}
|
||||
|
||||
/// Checks the status of the game-logic Pod and returns its gRPC connection info.
|
||||
/// It attempts to determine the port from the pod's container spec (searching for a port
|
||||
/// named "grpc"). If not found, it falls back to the default port 50051.
|
||||
/// Gets connection info for the game-logic instance using the Kubernetes Service.
|
||||
/// This provides a stable endpoint that doesn't change when pods restart.
|
||||
pub async fn get_connection_info(&self, instance_name: &str)
|
||||
-> Result<Option<ConnectionInfo>, Box<dyn Error>>
|
||||
{
|
||||
// Check if the pod is ready first
|
||||
let pod = self.get_instance(instance_name).await?;
|
||||
if let Some(status) = pod.status {
|
||||
if let Some(pod_ip) = status.pod_ip {
|
||||
// Try to extract the container port dynamically.
|
||||
if let Some(spec) = pod.spec {
|
||||
if let Some(status) = &pod.status {
|
||||
// Check if pod is running and ready
|
||||
if status.phase.as_ref().map_or(false, |phase| phase == "Running") {
|
||||
// Use the Service DNS name instead of Pod IP
|
||||
let service_name = format!("{}-service", instance_name);
|
||||
let service_dns = format!("{}.{}.svc.cluster.local", service_name, self.namespace);
|
||||
|
||||
// Get the port from the pod spec or use default
|
||||
let port = if let Some(spec) = &pod.spec {
|
||||
if let Some(container) = spec.containers.first() {
|
||||
if let Some(ports) = &container.ports {
|
||||
// Look for a port with the name "grpc"
|
||||
if let Some(grpc_port) = ports.iter().find(|p| {
|
||||
p.name.as_ref().map_or(false, |n| n == "grpc")
|
||||
}) {
|
||||
grpc_port.container_port as u16
|
||||
} else if let Some(first_port) = ports.first() {
|
||||
first_port.container_port as u16
|
||||
} else {
|
||||
50056 // Default port
|
||||
}
|
||||
} else {
|
||||
50056 // Default port
|
||||
}
|
||||
} else {
|
||||
50056 // Default port
|
||||
}
|
||||
} else {
|
||||
50056 // Default port
|
||||
};
|
||||
|
||||
return Ok(Some(ConnectionInfo {
|
||||
ip: pod_ip,
|
||||
port: grpc_port.container_port as u16,
|
||||
ip: service_dns,
|
||||
port,
|
||||
}));
|
||||
}
|
||||
// Or use the first container port if no named port was found.
|
||||
if let Some(first_port) = ports.first() {
|
||||
return Ok(Some(ConnectionInfo {
|
||||
ip: pod_ip,
|
||||
port: first_port.container_port as u16,
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Use fallback port if no port information is available.
|
||||
return Ok(Some(ConnectionInfo { ip: pod_ip, port: 50051 }));
|
||||
}
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
@@ -174,12 +229,19 @@ impl K8sOrchestrator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Shuts down (deletes) the game-logic Pod with the given name.
|
||||
/// Shuts down (deletes) the game-logic Pod and Service with the given name.
|
||||
pub async fn shutdown_instance(&self, instance_name: &str)
|
||||
-> Result<(), Box<dyn Error>> {
|
||||
let pods: Api<Pod> = Api::namespaced(self.client.clone(), &self.namespace);
|
||||
// DeleteParams::default() is sufficient for a forceful deletion.
|
||||
let services: Api<Service> = Api::namespaced(self.client.clone(), &self.namespace);
|
||||
|
||||
// Delete the Pod
|
||||
pods.delete(instance_name, &DeleteParams::default()).await?;
|
||||
|
||||
// Delete the corresponding Service
|
||||
let service_name = format!("{}-service", instance_name);
|
||||
services.delete(&service_name, &DeleteParams::default()).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,21 @@
|
||||
mod k8s_orchestrator;
|
||||
mod world_service;
|
||||
mod game_logic_client;
|
||||
|
||||
use dotenv::dotenv;
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{sleep, Duration, timeout};
|
||||
use tokio::sync::oneshot;
|
||||
use tonic::transport::Server;
|
||||
use utils::service_discovery::{get_kube_service_endpoints_by_dns, get_service_endpoints_by_dns};
|
||||
use utils::{health_check, logging};
|
||||
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;
|
||||
|
||||
fn get_service_name() -> String {
|
||||
env::var("WORLD_SERVICE_NAME").unwrap_or_else(|_| "default-service".to_string())
|
||||
@@ -21,6 +31,70 @@ fn get_map_ids() -> Vec<u32> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Get connection retry configuration from environment variables with sensible defaults
|
||||
fn get_connection_retry_config() -> (u32, Duration, Duration) {
|
||||
let max_retries = env::var("CONNECTION_INFO_MAX_RETRIES")
|
||||
.unwrap_or_else(|_| "3".to_string())
|
||||
.parse::<u32>()
|
||||
.unwrap_or(3);
|
||||
|
||||
let initial_delay_ms = env::var("CONNECTION_INFO_INITIAL_DELAY_MS")
|
||||
.unwrap_or_else(|_| "2000".to_string())
|
||||
.parse::<u64>()
|
||||
.unwrap_or(2000);
|
||||
|
||||
let max_delay_ms = env::var("CONNECTION_INFO_MAX_DELAY_MS")
|
||||
.unwrap_or_else(|_| "10000".to_string())
|
||||
.parse::<u64>()
|
||||
.unwrap_or(10000);
|
||||
|
||||
(
|
||||
max_retries,
|
||||
Duration::from_millis(initial_delay_ms),
|
||||
Duration::from_millis(max_delay_ms),
|
||||
)
|
||||
}
|
||||
|
||||
/// Retry wrapper for getting connection info with exponential backoff
|
||||
async fn get_connection_info_with_retry(
|
||||
orchestrator: &K8sOrchestrator,
|
||||
instance_name: &str,
|
||||
poll_timeout_secs: u64,
|
||||
max_retries: u32,
|
||||
initial_delay: Duration,
|
||||
max_delay: Duration,
|
||||
) -> Result<crate::k8s_orchestrator::ConnectionInfo, String> {
|
||||
let mut delay = initial_delay;
|
||||
let mut last_error_msg = String::new();
|
||||
|
||||
for attempt in 0..=max_retries {
|
||||
match orchestrator.poll_connection_info(instance_name, poll_timeout_secs).await {
|
||||
Ok(conn_info) => {
|
||||
if attempt > 0 {
|
||||
info!("Successfully retrieved connection info for {} instance after {} attempts", instance_name, attempt + 1);
|
||||
}
|
||||
return Ok(conn_info);
|
||||
}
|
||||
Err(e) => {
|
||||
last_error_msg = e.to_string();
|
||||
if attempt < max_retries {
|
||||
warn!("Failed to retrieve connection info for {} instance (attempt {}): {}. Retrying in {:?}...",
|
||||
instance_name, attempt + 1, last_error_msg, delay);
|
||||
sleep(delay).await;
|
||||
delay = std::cmp::min(delay * 2, max_delay);
|
||||
} else {
|
||||
error!("Failed to retrieve connection info for {} instance after {} attempts: {}",
|
||||
instance_name, max_retries + 1, last_error_msg);
|
||||
return Err(last_error_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This should never be reached due to the loop logic above
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
dotenv().ok();
|
||||
@@ -49,7 +123,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
Err(e) => {
|
||||
if e.to_string().contains("AlreadyExists") {
|
||||
warn!("Game-logic instance already exists: {}", e);
|
||||
debug!("Game-logic instance already exists: {}", e);
|
||||
// No reason to return an error here.
|
||||
//TODO: We may want to check to make sure the pod is working correctly.
|
||||
} else {
|
||||
@@ -60,45 +134,228 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
}
|
||||
}
|
||||
|
||||
for instance_name in instance_names.clone() {
|
||||
match orchestrator.poll_connection_info(&instance_name, 30).await {
|
||||
Ok(conn_info) => {
|
||||
// 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_game_logic_service = MyWorldGameLogicService::new(world_service_shared.clone());
|
||||
|
||||
// Get retry configuration for connection info
|
||||
let (conn_max_retries, conn_initial_delay, conn_max_delay) = get_connection_retry_config();
|
||||
|
||||
// Connect to game logic instances in parallel using futures::future::join_all
|
||||
info!("Connecting to {} game logic instances in parallel...", map_ids.len());
|
||||
|
||||
// Create an Arc to share the orchestrator across tasks
|
||||
let orchestrator = Arc::new(orchestrator);
|
||||
|
||||
let connection_futures: Vec<_> = map_ids.iter().zip(instance_names.clone())
|
||||
.map(|(map_id, instance_name)| {
|
||||
let orchestrator = orchestrator.clone();
|
||||
let game_logic_manager = game_logic_manager.clone();
|
||||
let map_id = *map_id;
|
||||
let instance_name = instance_name.clone();
|
||||
|
||||
async move {
|
||||
// Handle the connection info retrieval and convert error to String immediately
|
||||
let conn_info = match get_connection_info_with_retry(
|
||||
&orchestrator,
|
||||
&instance_name,
|
||||
30, // poll timeout in seconds (existing behavior)
|
||||
conn_max_retries,
|
||||
conn_initial_delay,
|
||||
conn_max_delay
|
||||
).await {
|
||||
Ok(info) => info,
|
||||
Err(e) => {
|
||||
let error_msg = format!("Map {}: Connection info retrieval failed - {}", map_id, e);
|
||||
error!("Error retrieving connection info for {} instance after retries: {}", instance_name, e);
|
||||
warn!("Skipping map {} due to connection info retrieval failure", map_id);
|
||||
return Err(error_msg);
|
||||
}
|
||||
};
|
||||
|
||||
debug!("Successfully retrieved connection info for {} instance: {:?}", instance_name, conn_info);
|
||||
//TODO: Store the connection info for later use.
|
||||
let endpoint = format!("http://{}:{}", conn_info.ip, conn_info.port);
|
||||
info!("Attempting to connect to game logic service for map {} at endpoint: {}", map_id, endpoint);
|
||||
|
||||
// Try to resolve the DNS name to see if it's reachable
|
||||
match tokio::net::lookup_host(&format!("{}:{}", conn_info.ip, conn_info.port)).await {
|
||||
Ok(addrs) => {
|
||||
let resolved_addrs: Vec<_> = addrs.collect();
|
||||
info!("DNS resolution successful for {}: {:?}", conn_info.ip, resolved_addrs);
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error retrieving connection info for {} instance: {}", instance_name, e);
|
||||
return Err(e);
|
||||
warn!("DNS resolution failed for {}: {}", conn_info.ip, e);
|
||||
}
|
||||
}
|
||||
|
||||
// Give the service a moment to be ready
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
|
||||
|
||||
// Try alternative endpoint formats if the full DNS name doesn't work
|
||||
let alternative_endpoints = vec![
|
||||
// endpoint.clone(), // this one doesn't seem to work
|
||||
format!("http://{}-service:{}", instance_name, conn_info.port), // Short service name
|
||||
format!("http://{}:{}", conn_info.ip.split('.').next().unwrap_or(&conn_info.ip), conn_info.port), // Just service name without domain
|
||||
];
|
||||
|
||||
let mut connection_successful = false;
|
||||
let mut last_connection_error = String::new();
|
||||
|
||||
// Try each endpoint format
|
||||
for (i, test_endpoint) in alternative_endpoints.iter().enumerate() {
|
||||
debug!("Trying endpoint format {}: {}", i + 1, test_endpoint);
|
||||
match game_logic_manager.add_client(map_id, test_endpoint.clone()).await {
|
||||
Ok(()) => {
|
||||
info!("Successfully connected to game logic service for map {} using endpoint: {}", map_id, test_endpoint);
|
||||
connection_successful = true;
|
||||
break;
|
||||
}
|
||||
Err(e) => {
|
||||
debug!("Failed to connect using endpoint {}: {}", test_endpoint, e);
|
||||
last_connection_error = format!("Endpoint {}: {}", test_endpoint, e);
|
||||
if i < alternative_endpoints.len() - 1 {
|
||||
debug!("Trying next endpoint format...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if connection_successful {
|
||||
Ok(map_id)
|
||||
} else {
|
||||
error!("Failed to connect to game logic service for map {} using any endpoint format. Last error: {}", map_id, last_connection_error);
|
||||
Err(format!("Map {}: Failed to connect using any endpoint format - {}", map_id, last_connection_error))
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Wait for all connection attempts to complete
|
||||
let connection_results = futures::future::join_all(connection_futures).await;
|
||||
|
||||
// Process connection results
|
||||
let mut successful_connections = 0;
|
||||
let mut failed_connections = Vec::new();
|
||||
|
||||
for result in connection_results {
|
||||
match result {
|
||||
Ok(map_id) => {
|
||||
successful_connections += 1;
|
||||
debug!("Connection for map {} completed successfully", map_id);
|
||||
}
|
||||
Err(error_msg) => {
|
||||
failed_connections.push(error_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Game logic connection summary: {} successful, {} failed",
|
||||
successful_connections, failed_connections.len());
|
||||
|
||||
if !failed_connections.is_empty() {
|
||||
warn!("Failed connections: {:?}", failed_connections);
|
||||
}
|
||||
|
||||
if successful_connections == 0 {
|
||||
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);
|
||||
}
|
||||
|
||||
// Set the gRPC server address
|
||||
let addr = env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0".to_string());
|
||||
let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "50054".to_string());
|
||||
let db_url = format!(
|
||||
"http://{}",
|
||||
get_kube_service_endpoints_by_dns("database-service", "tcp", "database-service")
|
||||
.await?
|
||||
.get(0)
|
||||
.unwrap()
|
||||
);
|
||||
let chat_service = format!(
|
||||
"http://{}",
|
||||
get_kube_service_endpoints_by_dns("chat-service", "tcp", "chat-service")
|
||||
.await?
|
||||
.get(0)
|
||||
.unwrap()
|
||||
);
|
||||
// let db_url = format!(
|
||||
// "http://{}",
|
||||
// get_kube_service_endpoints_by_dns("database-service", "tcp", "database-service")
|
||||
// .await?
|
||||
// .get(0)
|
||||
// .unwrap()
|
||||
// );
|
||||
// let chat_service = format!(
|
||||
// "http://{}",
|
||||
// get_kube_service_endpoints_by_dns("chat-service", "tcp", "chat-service")
|
||||
// .await?
|
||||
// .get(0)
|
||||
// .unwrap()
|
||||
// );
|
||||
|
||||
// Start gRPC server with graceful shutdown support
|
||||
let grpc_addr = format!("{}:{}", addr, port).parse()?;
|
||||
info!("Starting World Service gRPC server on {}", grpc_addr);
|
||||
|
||||
// Create shutdown signal channel
|
||||
let (shutdown_tx, shutdown_rx) = oneshot::channel::<()>();
|
||||
|
||||
let server_task = tokio::spawn(async move {
|
||||
let server = Server::builder()
|
||||
.add_service(WorldServiceServer::new(world_service))
|
||||
.add_service(WorldGameLogicServiceServer::new(world_game_logic_service))
|
||||
.serve_with_shutdown(grpc_addr, async {
|
||||
shutdown_rx.await.ok();
|
||||
debug!("gRPC server shutdown signal received");
|
||||
});
|
||||
|
||||
if let Err(e) = server.await {
|
||||
error!("gRPC server error: {}", e);
|
||||
} else {
|
||||
debug!("gRPC server shut down gracefully");
|
||||
}
|
||||
});
|
||||
|
||||
// Register service with Consul
|
||||
health_check::start_health_check(addr.as_str()).await?;
|
||||
|
||||
// Wait for shutdown signal
|
||||
info!("World service is running. Waiting for shutdown signal...");
|
||||
utils::signal_handler::wait_for_signal().await;
|
||||
|
||||
// Shutdown all game-logic instances
|
||||
let instances: Vec<_> = instance_names.iter().map(|instance_name| orchestrator.shutdown_instance(instance_name)).collect();
|
||||
for instance in instances {
|
||||
instance.await?;
|
||||
info!("Shutdown signal received. Beginning graceful shutdown...");
|
||||
|
||||
// Step 1: Signal the gRPC server to stop accepting new connections
|
||||
if let Err(_) = shutdown_tx.send(()) {
|
||||
warn!("Failed to send shutdown signal to gRPC server (receiver may have been dropped)");
|
||||
}
|
||||
|
||||
// Step 2: Wait for the gRPC server to finish with a timeout
|
||||
match timeout(Duration::from_secs(30), server_task).await {
|
||||
Ok(result) => {
|
||||
if let Err(e) = result {
|
||||
error!("gRPC server task failed: {}", e);
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
error!("gRPC server shutdown timed out after 30 seconds");
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Shutdown all game-logic instances
|
||||
info!("Shutting down {} game-logic instances...", instance_names.len());
|
||||
let mut shutdown_errors = Vec::new();
|
||||
|
||||
for instance_name in &instance_names {
|
||||
match timeout(Duration::from_secs(10), orchestrator.shutdown_instance(instance_name)).await {
|
||||
Ok(Ok(())) => {
|
||||
info!("Successfully shut down game-logic instance: {}", instance_name);
|
||||
}
|
||||
Ok(Err(e)) => {
|
||||
error!("Failed to shutdown game-logic instance {}: {}", instance_name, e);
|
||||
shutdown_errors.push(format!("Instance {}: {}", instance_name, e));
|
||||
}
|
||||
Err(_) => {
|
||||
error!("Timeout shutting down game-logic instance: {}", instance_name);
|
||||
shutdown_errors.push(format!("Instance {}: timeout", instance_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if shutdown_errors.is_empty() {
|
||||
info!("All components shut down successfully");
|
||||
} else {
|
||||
warn!("Some components failed to shut down cleanly: {:?}", shutdown_errors);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
559
world-service/src/world_service.rs
Normal file
559
world-service/src/world_service.rs
Normal file
@@ -0,0 +1,559 @@
|
||||
use futures::{Stream, StreamExt};
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
use std::sync::{Arc, Mutex};
|
||||
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 world::world_service_server::WorldService;
|
||||
use world::world_game_logic_service_server::WorldGameLogicService;
|
||||
use world::*;
|
||||
|
||||
pub struct MyWorldService {
|
||||
pub client_connections: Arc<Mutex<HashMap<String, ClientConnection>>>,
|
||||
pub game_logic_connections: Arc<Mutex<HashMap<u32, GameLogicConnection>>>,
|
||||
pub game_logic_manager: Option<Arc<GameLogicClientManager>>,
|
||||
}
|
||||
|
||||
pub struct ClientConnection {
|
||||
pub session_id: String,
|
||||
pub client_id: String,
|
||||
pub map_id: i32,
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub z: f32,
|
||||
pub sender: mpsc::UnboundedSender<WorldEvent>,
|
||||
}
|
||||
|
||||
pub struct GameLogicConnection {
|
||||
pub map_id: u32,
|
||||
pub sender: mpsc::UnboundedSender<world::GameLogicEvent>,
|
||||
}
|
||||
|
||||
impl MyWorldService {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
client_connections: Arc::new(Mutex::new(HashMap::new())),
|
||||
game_logic_connections: Arc::new(Mutex::new(HashMap::new())),
|
||||
game_logic_manager: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_with_game_logic_manager(game_logic_manager: Arc<GameLogicClientManager>) -> Self {
|
||||
Self {
|
||||
client_connections: Arc::new(Mutex::new(HashMap::new())),
|
||||
game_logic_connections: Arc::new(Mutex::new(HashMap::new())),
|
||||
game_logic_manager: Some(game_logic_manager),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_client_connection(&self, session_id: String, client_id: String, map_id: i32, sender: mpsc::UnboundedSender<WorldEvent>) {
|
||||
let mut connections = self.client_connections.lock().unwrap();
|
||||
connections.insert(session_id.clone(), ClientConnection {
|
||||
session_id,
|
||||
client_id,
|
||||
map_id,
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
z: 0.0,
|
||||
sender,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn remove_client_connection(&self, session_id: &str) {
|
||||
let mut connections = self.client_connections.lock().unwrap();
|
||||
connections.remove(session_id);
|
||||
}
|
||||
|
||||
pub fn add_game_logic_connection(&self, map_id: u32, sender: mpsc::UnboundedSender<world::GameLogicEvent>) {
|
||||
let mut connections = self.game_logic_connections.lock().unwrap();
|
||||
connections.insert(map_id, GameLogicConnection {
|
||||
map_id,
|
||||
sender,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn remove_game_logic_connection(&self, map_id: u32) {
|
||||
let mut connections = self.game_logic_connections.lock().unwrap();
|
||||
connections.remove(&map_id);
|
||||
}
|
||||
|
||||
pub fn broadcast_to_clients_in_map(&self, map_id: i32, event: WorldEvent) {
|
||||
let connections = self.client_connections.lock().unwrap();
|
||||
for connection in connections.values() {
|
||||
if connection.map_id == map_id {
|
||||
if let Err(e) = connection.sender.send(event.clone()) {
|
||||
warn!("Failed to send event to client {}: {}", connection.client_id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_to_game_logic(&self, map_id: u32, event: world::GameLogicEvent) {
|
||||
let connections = self.game_logic_connections.lock().unwrap();
|
||||
if let Some(connection) = connections.get(&map_id) {
|
||||
if let Err(e) = connection.sender.send(event) {
|
||||
warn!("Failed to send event to game logic for map {}: {}", map_id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_nearby_objects_for_client(&self, session_id: &str, x: f32, y: f32, z: f32, map_id: i32, radius: f32) -> Vec<WorldObject> {
|
||||
// This is a placeholder implementation
|
||||
// In a real implementation, you would query the game logic service or maintain a spatial index
|
||||
debug!("Getting nearby objects for client {} at ({}, {}, {}) in map {} with radius {}",
|
||||
session_id, x, y, z, map_id, radius);
|
||||
|
||||
// Return empty list for now - this will be populated by game logic service
|
||||
vec![]
|
||||
}
|
||||
|
||||
/// Send nearby objects to a connecting client
|
||||
pub async fn send_nearby_objects_to_client(&self, session_id: &str, client_id: &str, x: f32, y: f32, z: f32, map_id: i32) {
|
||||
debug!("Sending nearby objects to client {} at ({}, {}, {}) in map {}", client_id, x, y, z, map_id);
|
||||
|
||||
// Get the client connection to send events
|
||||
let client_sender = {
|
||||
let connections = self.client_connections.lock().unwrap();
|
||||
connections.get(session_id).map(|conn| conn.sender.clone())
|
||||
};
|
||||
|
||||
let Some(sender) = client_sender else {
|
||||
warn!("No client connection found for session {}", session_id);
|
||||
return;
|
||||
};
|
||||
|
||||
// Get nearby objects from game logic service if available
|
||||
if let Some(game_logic_manager) = &self.game_logic_manager {
|
||||
// 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::<u32>().unwrap_or(0)).unwrap_or(0)
|
||||
};
|
||||
|
||||
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());
|
||||
|
||||
if !response.objects.is_empty() {
|
||||
// Convert game logic objects to WorldObjects for batch sending
|
||||
let world_objects: Vec<WorldObject> = response.objects
|
||||
.into_iter()
|
||||
.filter_map(|obj| {
|
||||
// Filter out players for now, only include NPCs and Mobs
|
||||
match obj.r#type {
|
||||
1 => {
|
||||
// Player - skip for now
|
||||
debug!("Skipping player object {} for nearby objects", obj.id);
|
||||
None
|
||||
}
|
||||
2 | 3 => {
|
||||
// NPC or Mob - include in batch
|
||||
Some(WorldObject {
|
||||
id: obj.id as u32,
|
||||
object_type: obj.r#type,
|
||||
x: obj.x,
|
||||
y: obj.y,
|
||||
z: obj.z,
|
||||
map_id: map_id,
|
||||
name: format!("Object_{}", obj.id), // Default name
|
||||
hp: obj.hp, // Default HP
|
||||
max_hp: obj.max_hp, // Default max HP
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
debug!("Unknown object type {} for object {}", obj.r#type, obj.id);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if !world_objects.is_empty() {
|
||||
// Send all nearby objects in a single batch update
|
||||
let batch_event = WorldEvent {
|
||||
client_ids: vec![client_id.to_string()],
|
||||
event: Some(world_event::Event::NearbyUpdate(NearbyObjectsUpdate {
|
||||
objects: world_objects.clone(),
|
||||
})),
|
||||
};
|
||||
|
||||
if let Err(e) = sender.send(batch_event) {
|
||||
warn!("Failed to send nearby objects batch update to client {}: {}", client_id, e);
|
||||
} else {
|
||||
debug!("Sent {} nearby objects in batch to client {}", world_objects.len(), client_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Failed to get nearby objects from game logic service for client {}: {}", client_id, e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("No game logic manager available, skipping nearby objects");
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a game logic object to a WorldEvent based on its type
|
||||
fn convert_game_logic_object_to_world_event(&self, obj: &client_game_logic::Object, client_id: &str) -> Option<WorldEvent> {
|
||||
// Object types: 1 = Player, 2 = NPC, 3 = Mob (based on common MMORPG conventions)
|
||||
match obj.r#type {
|
||||
2 => {
|
||||
// NPC
|
||||
Some(WorldEvent {
|
||||
client_ids: vec![client_id.to_string()],
|
||||
event: Some(world_event::Event::NpcSpawn(NpcSpawnEvent {
|
||||
id: obj.id as u32,
|
||||
pos_x: obj.x,
|
||||
pos_y: obj.y,
|
||||
dest_pos_x: obj.x,
|
||||
dest_pos_y: obj.y,
|
||||
command: 0,
|
||||
target_id: 0,
|
||||
move_mode: 0,
|
||||
hp: 100, // Default HP
|
||||
team_id: 0,
|
||||
status_flag: 0,
|
||||
npc_id: obj.id as u32,
|
||||
quest_id: 0,
|
||||
angle: 0.0,
|
||||
event_status: 0,
|
||||
})),
|
||||
})
|
||||
}
|
||||
3 => {
|
||||
// Mob
|
||||
Some(WorldEvent {
|
||||
client_ids: vec![client_id.to_string()],
|
||||
event: Some(world_event::Event::MobSpawn(MobSpawnEvent {
|
||||
id: obj.id as u32,
|
||||
pos_x: obj.x,
|
||||
pos_y: obj.y,
|
||||
dest_pos_x: obj.x,
|
||||
dest_pos_y: obj.y,
|
||||
command: 0,
|
||||
target_id: 0,
|
||||
move_mode: 0,
|
||||
hp: 100, // Default HP
|
||||
team_id: 0,
|
||||
status_flag: 0,
|
||||
npc_id: obj.id as u32,
|
||||
quest_id: 0,
|
||||
})),
|
||||
})
|
||||
}
|
||||
1 => {
|
||||
// Player - for now we don't send player spawn events to other players on connect
|
||||
// This would typically be handled differently (e.g., through a separate player list)
|
||||
debug!("Skipping player object {} for nearby objects", obj.id);
|
||||
None
|
||||
}
|
||||
_ => {
|
||||
debug!("Unknown object type {} for object {}", obj.r#type, obj.id);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl WorldService for MyWorldService {
|
||||
async fn get_character(&self, request: Request<CharacterRequest>) -> Result<Response<CharacterResponse>, Status> {
|
||||
let req = request.into_inner();
|
||||
debug!("GetCharacter request: {:?}", req);
|
||||
|
||||
let response = CharacterResponse {
|
||||
count: 1,
|
||||
};
|
||||
Ok(Response::new(response))
|
||||
}
|
||||
|
||||
async fn change_map(&self, request: Request<ChangeMapRequest>) -> Result<Response<ChangeMapResponse>, Status> {
|
||||
let req = request.into_inner();
|
||||
debug!("ChangeMap request: {:?}", req);
|
||||
|
||||
let response = ChangeMapResponse {
|
||||
id: req.id,
|
||||
map_id: 1,
|
||||
x: req.x,
|
||||
y: req.y,
|
||||
move_mode: 0,
|
||||
ride_mode: 0,
|
||||
};
|
||||
Ok(Response::new(response))
|
||||
}
|
||||
|
||||
async fn move_character(&self, request: Request<CharacterMoveRequest>) -> Result<Response<CharacterMoveResponse>, Status> {
|
||||
let req = request.into_inner();
|
||||
debug!("MoveCharacter request: {:?}", req);
|
||||
|
||||
// Update client position
|
||||
{
|
||||
let mut connections = self.client_connections.lock().unwrap();
|
||||
if let Some(connection) = connections.get_mut(&req.session_id) {
|
||||
connection.x = req.x;
|
||||
connection.y = req.y;
|
||||
connection.z = req.z;
|
||||
}
|
||||
}
|
||||
|
||||
let response = CharacterMoveResponse {
|
||||
id: 1,
|
||||
target_id: req.target_id as i32,
|
||||
distance: 0,
|
||||
x: req.x,
|
||||
y: req.y,
|
||||
z: req.z,
|
||||
};
|
||||
Ok(Response::new(response))
|
||||
}
|
||||
|
||||
async fn get_target_hp(&self, request: Request<ObjectHpRequest>) -> Result<Response<ObjectHpResponse>, Status> {
|
||||
let req = request.into_inner();
|
||||
debug!("GetTargetHp request: {:?}", req);
|
||||
|
||||
let response = ObjectHpResponse {
|
||||
target_id: req.target_id,
|
||||
hp: 100, // Placeholder
|
||||
};
|
||||
Ok(Response::new(response))
|
||||
}
|
||||
|
||||
async fn get_nearby_objects(&self, request: Request<NearbyObjectsRequest>) -> Result<Response<NearbyObjectsResponse>, Status> {
|
||||
let req = request.into_inner();
|
||||
debug!("GetNearbyObjects request: {:?}", req);
|
||||
|
||||
let objects = self.get_nearby_objects_for_client(&req.session_id, req.x, req.y, req.z, req.map_id, req.radius);
|
||||
|
||||
let response = NearbyObjectsResponse {
|
||||
objects,
|
||||
};
|
||||
Ok(Response::new(response))
|
||||
}
|
||||
|
||||
type StreamClientEventsStream = Pin<Box<dyn Stream<Item = Result<WorldEvent, Status>> + Send + Sync + 'static>>;
|
||||
|
||||
async fn stream_client_events(
|
||||
&self,
|
||||
request: Request<Streaming<ClientEvent>>,
|
||||
) -> Result<Response<Self::StreamClientEventsStream>, Status> {
|
||||
trace!("New client stream connection established");
|
||||
|
||||
let mut inbound_stream = request.into_inner();
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
|
||||
let client_connections = self.client_connections.clone();
|
||||
let game_logic_connections = self.game_logic_connections.clone();
|
||||
let game_logic_manager = self.game_logic_manager.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut current_session_id: Option<String> = None;
|
||||
|
||||
while let Some(event) = inbound_stream.next().await {
|
||||
match event {
|
||||
Ok(client_event) => {
|
||||
debug!("Received client event: {:?}", client_event);
|
||||
|
||||
match client_event.event {
|
||||
Some(client_event::Event::Connect(connect_event)) => {
|
||||
debug!("Client {} connected to map {} at position ({}, {}, {})",
|
||||
client_event.client_id, client_event.map_id,
|
||||
connect_event.x, connect_event.y, connect_event.z);
|
||||
|
||||
let session_id = client_event.session_id.clone();
|
||||
let client_id = client_event.client_id.clone();
|
||||
let map_id = client_event.map_id;
|
||||
let x = connect_event.x;
|
||||
let y = connect_event.y;
|
||||
let z = connect_event.z;
|
||||
|
||||
// Track the session ID for cleanup
|
||||
current_session_id = Some(session_id.clone());
|
||||
|
||||
// Add client connection to the HashMap with position from connect event
|
||||
{
|
||||
let mut connections = client_connections.lock().unwrap();
|
||||
connections.insert(session_id.clone(), ClientConnection {
|
||||
session_id: session_id.clone(),
|
||||
client_id: client_id.clone(),
|
||||
map_id,
|
||||
x,
|
||||
y,
|
||||
z,
|
||||
sender: tx.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
// Send all nearby objects to the client
|
||||
// Create a service instance with the proper game logic manager
|
||||
let world_service = MyWorldService {
|
||||
client_connections: client_connections.clone(),
|
||||
game_logic_connections: game_logic_connections.clone(),
|
||||
game_logic_manager: game_logic_manager.clone(),
|
||||
};
|
||||
|
||||
// Send nearby objects to the connecting client
|
||||
world_service.send_nearby_objects_to_client(&session_id, &client_id, x, y, z, map_id).await;
|
||||
}
|
||||
Some(client_event::Event::Disconnect(_)) => {
|
||||
debug!("Client {} disconnected", client_event.client_id);
|
||||
// Handle client disconnection
|
||||
}
|
||||
Some(client_event::Event::Move(move_event)) => {
|
||||
debug!("Client {} moved to ({}, {}, {})", client_event.client_id, move_event.x, move_event.y, move_event.z);
|
||||
|
||||
// Update client position
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Send PlayerMoveEvent to game logic service
|
||||
let player_move_event = world::GameLogicEvent {
|
||||
client_ids: vec![],
|
||||
map_id: client_event.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(),
|
||||
x: move_event.x,
|
||||
y: move_event.y,
|
||||
z: move_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_move_event) {
|
||||
warn!("Failed to send player move event to game logic for map {}: {}", client_event.map_id, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(client_event::Event::MapChange(map_change_event)) => {
|
||||
debug!("Client {} changed from map {} to map {}", client_event.client_id, map_change_event.old_map_id, map_change_event.new_map_id);
|
||||
// Handle map change
|
||||
}
|
||||
None => {
|
||||
warn!("Received client event with no event data");
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error receiving client event: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up client connection when stream ends
|
||||
if let Some(session_id) = current_session_id {
|
||||
let mut connections = client_connections.lock().unwrap();
|
||||
connections.remove(&session_id);
|
||||
debug!("Client connection {} removed from connections", session_id);
|
||||
}
|
||||
|
||||
trace!("Client event stream ended");
|
||||
});
|
||||
|
||||
let outbound_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(rx).map(|msg| Ok(msg));
|
||||
Ok(Response::new(Box::pin(outbound_stream) as Self::StreamClientEventsStream))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MyWorldGameLogicService {
|
||||
pub world_service: Arc<MyWorldService>,
|
||||
}
|
||||
|
||||
impl MyWorldGameLogicService {
|
||||
pub fn new(world_service: Arc<MyWorldService>) -> Self {
|
||||
Self {
|
||||
world_service,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tonic::async_trait]
|
||||
impl WorldGameLogicService for MyWorldGameLogicService {
|
||||
type StreamGameEventsStream = Pin<Box<dyn Stream<Item = Result<world::GameLogicEvent, Status>> + Send + Sync + 'static>>;
|
||||
|
||||
async fn stream_game_events(
|
||||
&self,
|
||||
request: Request<Streaming<world::GameLogicEvent>>,
|
||||
) -> Result<Response<Self::StreamGameEventsStream>, Status> {
|
||||
trace!("New game logic stream connection established");
|
||||
|
||||
let mut inbound_stream = request.into_inner();
|
||||
let (tx, rx) = mpsc::unbounded_channel();
|
||||
|
||||
let world_service = self.world_service.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = inbound_stream.next().await {
|
||||
match event {
|
||||
Ok(game_event) => {
|
||||
debug!("Received game event: {:?}", game_event);
|
||||
|
||||
// Convert game logic events to world events and broadcast to relevant clients
|
||||
match game_event.event {
|
||||
Some(world::game_logic_event::Event::NpcSpawn(npc_spawn)) => {
|
||||
let _world_event = WorldEvent {
|
||||
client_ids: game_event.client_ids.clone(),
|
||||
event: Some(world_event::Event::NpcSpawn(npc_spawn)),
|
||||
};
|
||||
// Broadcast to clients - for now broadcast to all clients in the map
|
||||
// In a real implementation, you'd determine the map from the event
|
||||
// world_service.broadcast_to_clients_in_map(game_event.map_id, world_event);
|
||||
}
|
||||
Some(world::game_logic_event::Event::MobSpawn(mob_spawn)) => {
|
||||
let _world_event = WorldEvent {
|
||||
client_ids: game_event.client_ids.clone(),
|
||||
event: Some(world_event::Event::MobSpawn(mob_spawn)),
|
||||
};
|
||||
// Broadcast to clients
|
||||
}
|
||||
Some(world::game_logic_event::Event::ObjectDespawn(despawn)) => {
|
||||
let _world_event = WorldEvent {
|
||||
client_ids: game_event.client_ids.clone(),
|
||||
event: Some(world_event::Event::ObjectDespawn(despawn)),
|
||||
};
|
||||
// Broadcast to clients
|
||||
}
|
||||
_ => {
|
||||
debug!("Unhandled game logic event type");
|
||||
}
|
||||
}
|
||||
|
||||
// Echo the event back for now
|
||||
if let Err(e) = tx.send(game_event) {
|
||||
error!("Failed to send game event: {:?}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Error receiving game event: {:?}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
trace!("Game logic event stream ended");
|
||||
});
|
||||
|
||||
let outbound_stream = tokio_stream::wrappers::UnboundedReceiverStream::new(rx).map(|msg| Ok(msg));
|
||||
Ok(Response::new(Box::pin(outbound_stream) as Self::StreamGameEventsStream))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user