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:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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,
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user