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:
2025-06-25 12:30:07 -04:00
parent f75782885b
commit d906cd8d64
34 changed files with 3550 additions and 186 deletions

View File

@@ -4,14 +4,14 @@ 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
tonic_build::configure()
.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));
}

View File

@@ -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 }
}

View 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 }
}
}

View File

@@ -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 }
}
}

View File

@@ -10,4 +10,5 @@ pub mod client;
pub mod item;
pub mod stats;
pub mod character_graphics;
pub mod spawner;
pub mod spawner;
pub mod computed_values;

View File

@@ -8,24 +8,37 @@ 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> {
/// Creates a new factory.
pub fn new(world: &'a mut World) -> Self {
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
@@ -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
}
@@ -194,58 +225,308 @@ impl<'a> EntityFactory<'a> {
/// last spawn timestamp is updated.
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;
}
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)));
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
// 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
}
} 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(mob_id, spawn_range, pos.clone());
new_mobs.push(mob_entity);
}
// 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);
}
}
// 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;
}
}
}
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);
}
});
}
}
}
}

View 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,
}

View File

@@ -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))
}
}

View File

@@ -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(())
}

View 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);
}
}

View 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,
})),
}
}
}