diff --git a/Cargo.toml b/Cargo.toml index 05ed338..4629772 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "chat-service", "character-service", "database-service", + "game-logic-service", "packet-service", "world-service", "utils", diff --git a/game-logic-service/Cargo.toml b/game-logic-service/Cargo.toml new file mode 100644 index 0000000..97a7ec5 --- /dev/null +++ b/game-logic-service/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "game-logic-service" +version = "0.1.0" +edition = "2021" + +[dependencies] +utils = { path = "../utils" } +dotenv = "0.15" +tokio = { version = "1.45.1", features = ["full"] } +serde = { version = "1.0", features = ["derive"] } +hecs = { version = "0.10.5", features = ["serde", "default", "std"] } +serde_json = "1.0.140" +tracing = "0.1.41" +tracing-subscriber = { version = "0.3.19", features = ["env-filter", "chrono"] } +rand = "0.8.5" +tonic = "0.12.3" +prost = "0.13.4" +futures = "0.3.31" +tokio-stream = "0.1.17" +warp = "0.3.7" + +[build-dependencies] +tonic-build = "0.12.3" diff --git a/game-logic-service/Dockerfile b/game-logic-service/Dockerfile new file mode 100644 index 0000000..d4cb22b --- /dev/null +++ b/game-logic-service/Dockerfile @@ -0,0 +1,25 @@ +FROM rust:alpine AS builder +LABEL authors="raven" + +RUN apk add --no-cache musl-dev libressl-dev protobuf-dev + +WORKDIR /usr/src/utils +COPY ./utils . + +WORKDIR /usr/src/proto +COPY ./proto . + +WORKDIR /usr/src/game-logic-service +COPY ./game-logic-service . + +RUN cargo build --release + +FROM alpine:3 + +RUN apk add --no-cache libssl3 libgcc + +COPY --from=builder /usr/src/game-logic-service/target/release/game-logic-service /usr/local/bin/game-logic-service + +EXPOSE 50056 + +CMD ["game-logic-service"] \ No newline at end of file diff --git a/game-logic-service/build.rs b/game-logic-service/build.rs new file mode 100644 index 0000000..c3263d8 --- /dev/null +++ b/game-logic-service/build.rs @@ -0,0 +1,17 @@ +fn main() { + // gRPC Server code + tonic_build::configure() + .build_server(true) // Generate gRPC server code + .compile_well_known_types(true) + .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") + .compile_protos(&["../proto/game_logic.proto", "../proto/game.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"]) + .unwrap_or_else(|e| panic!("Failed to compile protos {:?}", e)); +} diff --git a/game-logic-service/src/components/basic_info.rs b/game-logic-service/src/components/basic_info.rs new file mode 100644 index 0000000..4add3f9 --- /dev/null +++ b/game-logic-service/src/components/basic_info.rs @@ -0,0 +1,20 @@ + +#[derive(Debug)] +pub struct BasicInfo { + pub name: String, + pub id: u16, + pub tag: u32, + pub team_id: u32, + pub job: u16, + pub stat_points: u32, + pub skill_points: u32, + pub pk_flag: u16, + pub stone: u8, + pub char_id: u32, +} + +impl Default for BasicInfo { + fn default() -> Self { + Self { name: "TEST".to_string(), id: 0, tag: 0, team_id: 0, job: 0, stat_points: 0, skill_points: 0, pk_flag: 0, stone: 0, char_id: 0 } + } +} \ No newline at end of file diff --git a/game-logic-service/src/components/character_graphics.rs b/game-logic-service/src/components/character_graphics.rs new file mode 100644 index 0000000..22111a0 --- /dev/null +++ b/game-logic-service/src/components/character_graphics.rs @@ -0,0 +1,31 @@ + +#[derive(Debug)] +struct CharacterGraphics { + race: u8, + hair: u8, + face: u8, +} + +impl Default for CharacterGraphics { + fn default() -> Self { + Self { race: 0, hair: 0, face: 0 } + } +} + +impl CharacterGraphics { + pub fn new(race: u8, hair: u8, face: u8) -> Self { + Self { race, hair, face } + } + + pub fn get_race(&self) -> u8 { + self.race + } + + pub fn get_hair(&self) -> u8 { + self.hair + } + + pub fn get_face(&self) -> u8 { + self.face + } +} \ No newline at end of file diff --git a/game-logic-service/src/components/client.rs b/game-logic-service/src/components/client.rs new file mode 100644 index 0000000..94e337b --- /dev/null +++ b/game-logic-service/src/components/client.rs @@ -0,0 +1,22 @@ + +#[derive(Debug)] +struct Client { + client_id: u16, + access_level: u16, +} + +impl Default for Client { + fn default() -> Self { + Self { client_id: 0, access_level: 1 } + } +} + +impl Client { + pub fn get_client_id(&self) -> u16 { + self.client_id + } + + pub fn get_access_level(&self) -> u16 { + self.access_level + } +} \ No newline at end of file diff --git a/game-logic-service/src/components/destination.rs b/game-logic-service/src/components/destination.rs new file mode 100644 index 0000000..f233ee2 --- /dev/null +++ b/game-logic-service/src/components/destination.rs @@ -0,0 +1,13 @@ +#[derive(Debug)] +struct Destination { + pub x: f32, + pub y: f32, + pub z: f32, + dest: u16, +} + +impl Default for Destination { + fn default() -> Self { + Self { x: 520000.0, y: 520000.0, z: 1.0, dest: 20 } + } +} \ No newline at end of file diff --git a/game-logic-service/src/components/item.rs b/game-logic-service/src/components/item.rs new file mode 100644 index 0000000..c5cacc6 --- /dev/null +++ b/game-logic-service/src/components/item.rs @@ -0,0 +1,30 @@ +#[derive(Debug)] +pub struct Item { + pub is_created: bool, + pub is_zuly: bool, + pub life: f32, + pub durability: u8, + pub has_socket: bool, + pub is_appraised: bool, + pub grade: u8, + pub count: u32, + pub gem_option: u16, + pub price: u32, +} + +impl Default for Item { + fn default() -> Self { + Self { + is_created: false, + is_zuly: false, + life: 0.0, + durability: 0, + has_socket: false, + is_appraised: false, + grade: 0, + count: 0, + gem_option: 0, + price: 0, + } + } +} \ No newline at end of file diff --git a/game-logic-service/src/components/level.rs b/game-logic-service/src/components/level.rs new file mode 100644 index 0000000..72b77a2 --- /dev/null +++ b/game-logic-service/src/components/level.rs @@ -0,0 +1,34 @@ +#[derive(Debug)] +pub(crate) struct Level { + pub level: u16, + pub xp: u64, + pub penalty_xp: u64, +} + +impl Default for Level { + fn default() -> Self { + Self { level: 1, xp: 0, penalty_xp: 0 } + } +} + +impl Level { + pub fn get_level(&self) -> u16 { + self.level + } + + pub fn get_xp(&self) -> u64 { + self.xp + } + + pub fn get_penalty_xp(&self) -> u64 { + self.penalty_xp + } + + pub fn add_xp(&mut self, amount: u64) { + self.xp += amount; + } + + pub fn add_penalty_xp(&mut self, amount: u64) { + self.penalty_xp += amount; + } +} diff --git a/game-logic-service/src/components/life.rs b/game-logic-service/src/components/life.rs new file mode 100644 index 0000000..0a999b2 --- /dev/null +++ b/game-logic-service/src/components/life.rs @@ -0,0 +1,40 @@ + +#[derive(Debug)] +pub(crate) struct Life { + hp: u32, + max_hp: u32, +} + +impl Default for Life { + fn default() -> Self { + Self { hp: 100, max_hp: 100 } + } +} + +impl Life { + pub fn new(hp: u32, max_hp: u32) -> Self { + Self { hp, max_hp } + } + + pub fn get_hp(&self) -> u32 { + self.hp + } + + pub fn get_max_hp(&self) -> u32 { + self.max_hp + } + + pub fn take_damage(&mut self, damage: u32) { + self.hp = self.hp.saturating_sub(damage); + if self.hp <= 0 { + self.hp = 0; + } + } + + pub fn heal(&mut self, amount: u32) { + self.hp = self.hp.saturating_add(amount); + if self.hp > self.max_hp { + self.hp = self.max_hp; + } + } +} \ No newline at end of file diff --git a/game-logic-service/src/components/magic.rs b/game-logic-service/src/components/magic.rs new file mode 100644 index 0000000..dee6d80 --- /dev/null +++ b/game-logic-service/src/components/magic.rs @@ -0,0 +1,36 @@ + +#[derive(Debug)] +struct Magic { + mp: u32, + max_mp: u32, +} + +impl Default for Magic { + fn default() -> Self { + Self { mp: 55, max_mp: 55 } + } +} + +impl Magic { + pub fn get_mp(&self) -> u32 { + self.mp + } + + pub fn get_max_mp(&self) -> u32 { + self.max_mp + } + + pub fn use_mp(&mut self, amount: u32) { + self.mp = self.mp.saturating_sub(amount); + if self.mp <= 0 { + self.mp = 0; + } + } + + pub fn restore_mp(&mut self, amount: u32) { + self.mp = self.mp.saturating_add(amount); + if self.mp > self.max_mp { + self.mp = self.max_mp; + } + } +} \ No newline at end of file diff --git a/game-logic-service/src/components/markers.rs b/game-logic-service/src/components/markers.rs new file mode 100644 index 0000000..91c6f6e --- /dev/null +++ b/game-logic-service/src/components/markers.rs @@ -0,0 +1,28 @@ +use hecs::Entity; + +#[derive(Debug)] +pub struct Mob { + pub id: u32, + pub quest_id: u32, +} + +#[derive(Debug)] +pub struct Npc { + pub id: u32, + pub quest_id: u32, + pub angle: f32, + pub event_status: u16, +} + +#[derive(Debug)] +pub struct Player; + +#[derive(Debug)] +pub struct Pet { + pub owner: Entity, +} + +#[derive(Debug)] +pub struct PlayerSpawn { + pub point_type: u32 +} \ No newline at end of file diff --git a/game-logic-service/src/components/mod.rs b/game-logic-service/src/components/mod.rs new file mode 100644 index 0000000..a3ed367 --- /dev/null +++ b/game-logic-service/src/components/mod.rs @@ -0,0 +1,13 @@ +pub mod markers; +pub mod position; +pub mod basic_info; +pub mod destination; +pub mod target; +pub mod level; +pub mod life; +pub mod magic; +pub mod client; +pub mod item; +pub mod stats; +pub mod character_graphics; +pub mod spawner; \ No newline at end of file diff --git a/game-logic-service/src/components/position.rs b/game-logic-service/src/components/position.rs new file mode 100644 index 0000000..b20e70f --- /dev/null +++ b/game-logic-service/src/components/position.rs @@ -0,0 +1,20 @@ +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct Position { + #[serde(rename = "X")] + pub x: f32, + #[serde(rename = "Y")] + pub y: f32, + #[serde(rename = "Z")] + pub z: f32, + pub map_id: u16, + pub spawn_id: u16, +} + +impl Default for Position { + fn default() -> Self { + // Set the default position to (520000.0, 520000.0) + Self { x: 520000.0, y: 520000.0, z: 1.0, map_id: 20, spawn_id: 1 } + } +} \ No newline at end of file diff --git a/game-logic-service/src/components/spawner.rs b/game-logic-service/src/components/spawner.rs new file mode 100644 index 0000000..13661cf --- /dev/null +++ b/game-logic-service/src/components/spawner.rs @@ -0,0 +1,60 @@ +use std::time::{Duration, Instant}; +use hecs::Entity; + +#[derive(Debug)] +pub struct Spawner { + /// The ID of the mob to be spawned. + pub mob_id: u32, + /// The maximum number of mobs that can be spawned. + pub max_mob_count: u32, + /// The maximum number of mobs that can be spawned at once. + pub max_spawn_count_at_once: u32, + /// The range within which the spawner should generate a mob. + pub spawn_range: u32, + /// The frequency in seconds at which the spawner should generate a mob. + pub spawn_rate: Duration, + /// The last time a mob was spawned. + pub last_spawn: Instant, + /// The list of mobs that have been spawned by this spawner. + pub mobs: Vec, +} + +impl Default for Spawner { + fn default() -> Self { + Self { + mob_id: 0, + max_mob_count: 1, + max_spawn_count_at_once: 1, + spawn_rate: Duration::from_secs(10), + last_spawn: Instant::now(), + spawn_range: 100, + mobs: Vec::new(), + } + } +} + +impl Spawner { + pub fn get_mob_id(&self) -> u32 { + self.mob_id + } + + pub fn get_max_mob_count(&self) -> u32 { + self.max_mob_count + } + + pub fn get_max_spawn_count_at_once(&self) -> u32 { + self.max_spawn_count_at_once + } + + pub fn get_spawn_rate(&self) -> Duration { + self.spawn_rate + } + + pub fn get_last_spawn(&self) -> Instant { + self.last_spawn + } + + pub fn get_spawn_range(&self) -> u32 { + self.spawn_range + } +} \ No newline at end of file diff --git a/game-logic-service/src/components/stats.rs b/game-logic-service/src/components/stats.rs new file mode 100644 index 0000000..8ada7a2 --- /dev/null +++ b/game-logic-service/src/components/stats.rs @@ -0,0 +1,51 @@ +#[derive(Debug)] +pub struct Stats { + strength: u16, + dexterity: u16, + intelligence: u16, + constitution: u16, + charisma: u16, + sense: u16, + head_size: u8, + body_size: u8, +} + +impl Default for Stats { + fn default() -> Self { + Self { strength: 10, dexterity: 10, intelligence: 10, constitution: 10, charisma: 10, sense: 10, head_size: 1, body_size: 1 } + } +} + +impl Stats { + pub fn get_strength(&self) -> u16 { + self.strength + } + + pub fn get_dexterity(&self) -> u16 { + self.dexterity + } + + pub fn get_intelligence(&self) -> u16 { + self.intelligence + } + + pub fn get_constitution(&self) -> u16 { + self.constitution + } + + pub fn get_charisma(&self) -> u16 { + self.charisma + } + + pub fn get_sense(&self) -> u16 { + self.sense + } + + pub fn get_head_size(&self) -> u8 { + self.head_size + } + + pub fn get_body_size(&self) -> u8 { + self.body_size + } +} diff --git a/game-logic-service/src/components/target.rs b/game-logic-service/src/components/target.rs new file mode 100644 index 0000000..ba83ce2 --- /dev/null +++ b/game-logic-service/src/components/target.rs @@ -0,0 +1,20 @@ +use hecs::{Entity}; + +#[derive(Debug)] +struct Target { + target: Entity, +} + +impl Target { + pub fn new(target: Entity) -> Self { + Self { target } + } + + pub fn get_target(&self) -> Entity { + self.target + } + + pub fn set_target(&mut self, target: Entity) { + self.target = target; + } +} \ No newline at end of file diff --git a/game-logic-service/src/entity_factory.rs b/game-logic-service/src/entity_factory.rs new file mode 100644 index 0000000..ed7f55e --- /dev/null +++ b/game-logic-service/src/entity_factory.rs @@ -0,0 +1,251 @@ +use std::cmp::min; +use hecs::{Entity, World}; +use std::time::{Duration, Instant}; +use std::sync::{Arc, Mutex}; +use tracing::debug; +use crate::components::basic_info::BasicInfo; +use crate::components::level::Level; +use crate::components::life::Life; +use crate::components::position::Position; +use crate::components::spawner::Spawner; +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}; + +pub struct EntityFactory<'a> { + pub world: &'a mut World, + pub id_manager: Arc> +} + +impl<'a> EntityFactory<'a> { + /// Creates a new factory. + pub fn new(world: &'a mut World) -> Self { + Self { + world, + id_manager: Arc::new(Mutex::new(IdManager::new())) + } + } + + pub fn load_map(&mut self, map: Zone) { + // Load the map data from the database + // Spawn all the entities in the map + + if let Some(mob_spawn_points) = map.mob_spawn_points { + for spawner in mob_spawn_points { + // Process the mob spawner. + spawner + .normal_spawn_points + .unwrap_or_default() + .into_iter() + .chain(spawner.tactical_spawn_points.unwrap_or_default().into_iter()) + .for_each(|spawn_point| { + // debug!("Spawn Point: {:?}", spawn_point); + self.spawn_mob_spawner( + spawn_point.monster_id, + spawner.limit, + spawn_point.count, + Position { + x: spawner.position.x as f32, + y: spawner.position.y as f32, + z: spawner.position.z as f32, + map_id: map.id as u16, + spawn_id: 0, + }, + Duration::from_secs(spawner.interval as u64), + spawner.range, + ); + }); + } + } + + if let Some(npc_spawn_points) = map.npc_spawn_points { + for npc in npc_spawn_points { + // Process the npc spawn point. + // debug!("NPC ID: {}", npc.id); + // debug!("Dialog ID: {}", npc.dialog_id); + // debug!("Position: x = {}, y = {}, z = {}", + // npc.position.x, npc.position.y, npc.position.z); + self.spawn_npc(npc.id, Position { + x: npc.position.x as f32, + y: npc.position.y as f32, + z: npc.position.z as f32, + map_id: map.id as u16, + spawn_id: 0, + }, + npc.angle); + } + } + + if let Some(spawn_points) = map.spawn_points { + for spawn_point in spawn_points { + // Process the spawn point. + // debug!("Player Spawn Point Type: {}", spawn_point.point_type); + // debug!("Position: x = {}, y = {}, z = {}", + // spawn_point.position.x, spawn_point.position.y, spawn_point.position.z); + self.create_player_spawn_point(spawn_point.point_type, Position { + x: spawn_point.position.x as f32, + y: spawn_point.position.y as f32, + z: spawn_point.position.z as f32, + map_id: map.id as u16, + spawn_id: 0, + }); + } + } + + if let Some(warp_points) = map.warp_points { + for warp_point in warp_points { + // Process the warp point. + // debug!("Warp Point Alias: {}", warp_point.alias); + // debug!("Destination Gate ID: {}", warp_point.destination_gate_id); + // debug!("Destination: x = {}, y = {}, z = {}", + // warp_point.destination.x, warp_point.destination.y, warp_point.destination.z); + // debug!("Map ID: {}", warp_point.map_id); + // debug!("Min Position: x = {}, y = {}, z = {}", + // warp_point.min_position.x, warp_point.min_position.y, warp_point.min_position.z); + // debug!("Max Position: x = {}, y = {}, z = {}", + // warp_point.max_position.x, warp_point.max_position.y, warp_point.max_position.z); + } + } + } + + pub fn create_player_spawn_point(&mut self, point_type: u32, pos: Position) { + self.world.spawn((PlayerSpawn {point_type},pos.clone())); + debug!("Player spawn point created at position: {:?}", pos); + } + + pub fn spawn_player(&mut self, pos: Position) { + let id = self.id_manager.lock().unwrap().get_free_id(); + let basic_info = BasicInfo { + name: "Player".to_string(), + id: id, + tag: id as u32, + ..Default::default() + }; + let level = Level { level: 1, ..Default::default() }; + + let base_hp = 100; + let hp = (base_hp * level.level) as u32; + let life = Life::new(hp, hp); + + self.world.spawn((Player, basic_info, level, life, pos.clone())); + debug!("Player spawned at position: {:?}", pos); + } + + /// Spawns a npc at the specified position. + pub fn spawn_npc(&mut self, npc_id: u32, pos: Position, angle: f32) { + let id = self.id_manager.lock().unwrap().get_free_id(); + let basic_info = BasicInfo { + name: "NPC".to_string(), + id: id, + tag: id as u32, + ..Default::default() + }; + let level = Level { level: 1, ..Default::default() }; + + let base_hp = 100; + 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())); + debug!("Npc spawned at position: {:?}", pos); + } + + /// Spawns a mob at the specified position. + pub fn spawn_mob(&mut self, mob_id: u32, spawn_range: u32, pos: Position) -> Entity { + let id = self.id_manager.lock().unwrap().get_free_id(); + let basic_info = BasicInfo { + name: "MOB".to_string(), + id: id, + tag: id as u32, + ..Default::default() + }; + + let level = Level { level: 1, ..Default::default() }; + + let base_hp = 100; + let hp = (base_hp * level.level) as u32; + let life = Life::new(hp, hp); + + let (x, y) = get_random_point_in_circle((pos.x, pos.y), spawn_range as f32); + 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())); + entity + } + + /// Spawns a spawner at the specified position with the given spawn rate. + pub fn spawn_mob_spawner(&mut self, mob_id: u32, max_mob_count: u32, max_spawn_count_at_once: u32, pos: Position, spawn_rate: Duration, spawn_range: u32) { + let spawner = Spawner { + mob_id, + spawn_rate, + spawn_range, + max_mob_count, + max_spawn_count_at_once, + ..Default::default() + }; + self.world.spawn((spawner, pos.clone())); + } + + /// Updates all spawner entities in the world. + /// + /// If the spawn interval has elapsed, a mob will be spawned and the spawner's + /// 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 + .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() + }, + ) + }) + .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 + } + + // 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 run(&mut self) { + self.update_spawners(); + } +} \ No newline at end of file diff --git a/game-logic-service/src/game_logic_service.rs b/game-logic-service/src/game_logic_service.rs new file mode 100644 index 0000000..c107201 --- /dev/null +++ b/game-logic-service/src/game_logic_service.rs @@ -0,0 +1,31 @@ +use futures::{Stream, StreamExt}; +use std::collections::HashMap; +use std::pin::Pin; +use std::sync::{Arc, Mutex}; +use tonic::{Request, Response, Status}; +use tonic::metadata::MetadataMap; +use tracing::debug; + +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}; + +pub struct MyGameLogicService { + pub map_id: u32, +} + +#[tonic::async_trait] +impl GameLogicService for MyGameLogicService { + async fn get_nearby_objects(&self, request: Request) -> Result, Status> { + let req = request.into_inner(); + debug!("{:?}", req); + + let response = NearbyObjectsResponse { + objects: vec![], + }; + Ok(Response::new(response)) + } +} \ No newline at end of file diff --git a/game-logic-service/src/game_service.rs b/game-logic-service/src/game_service.rs new file mode 100644 index 0000000..872afbe --- /dev/null +++ b/game-logic-service/src/game_service.rs @@ -0,0 +1,69 @@ +use std::pin::Pin; +use futures::{Stream, StreamExt}; +use tonic::{Request, Response, Status, Streaming}; +use tracing::{info, error}; +use tokio_stream::wrappers::ReceiverStream; +use tokio::sync::mpsc; + +pub mod character_common { + tonic::include_proto!("character_common"); +} +pub mod game { + tonic::include_proto!("game"); +} + +use game::event_service_server::{EventService, EventServiceServer}; +use game::GenericEvent; + +pub struct MyGameService {} + +impl MyGameService { + pub fn into_service(self) -> EventServiceServer { + EventServiceServer::new(self) + } +} + +#[tonic::async_trait] +impl EventService for MyGameService { + type StreamEventsStream = + Pin> + Send + Sync + 'static>>; + + async fn stream_events( + &self, + request: Request>, + ) -> Result, Status> { + info!("Received connection from world service without authentication"); + + // Extract the inbound stream. + let mut inbound_stream = request.into_inner(); + + // Create a channel for sending outgoing events. + let (tx, rx) = mpsc::channel(32); + + // Spawn a task to handle processing of inbound events. + let tx_clone = tx.clone(); + tokio::spawn(async move { + while let Some(event) = inbound_stream.next().await { + match event { + Ok(ev) => { + info!("Received event: {:?}", ev); + // Process the event as needed. + // For demonstration, simply echo the event back. + if let Err(err) = tx_clone.send(ev).await { + error!("Failed forwarding event: {:?}", err); + } + } + Err(e) => { + error!("Error receiving event: {:?}", e); + break; + } + } + } + info!("Inbound event stream ended."); + }); + + // Wrap the receiver in a stream and return it. + let outbound_stream = ReceiverStream::new(rx).map(|msg| Ok(msg)); + Ok(Response::new(Box::pin(outbound_stream) as Self::StreamEventsStream)) + } +} \ No newline at end of file diff --git a/game-logic-service/src/id_manager.rs b/game-logic-service/src/id_manager.rs new file mode 100644 index 0000000..2283da9 --- /dev/null +++ b/game-logic-service/src/id_manager.rs @@ -0,0 +1,61 @@ +use std::collections::HashSet; + +#[derive(Clone, Debug)] +pub struct IdManager { + free_ids: HashSet, + max_id: u16, // the first ID is 1 +} + +impl IdManager { + /// Creates a new IdManager with no free IDs and starting ID of 1. + pub fn new() -> Self { + Self { + free_ids: HashSet::new(), + max_id: 1, + } + } + + /// Retrieves an available ID. + /// + /// If any are available in the free_ids set, returns one of them. + /// Otherwise, returns a fresh ID and increments max_id. + pub fn get_free_id(&mut self) -> u16 { + if let Some(&id) = self.free_ids.iter().next() { + self.free_ids.remove(&id); + id + } else { + let id = self.max_id; + self.max_id += 1; + id + } + } + + /// Releases an ID, making it available for reuse. + pub fn release_id(&mut self, id: u16) { + self.free_ids.insert(id); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_id_manager() { + let mut manager = IdManager::new(); + + let id1 = manager.get_free_id(); + let id2 = manager.get_free_id(); + assert_eq!(id1, 1); + assert_eq!(id2, 2); + + // Release id1 and then get a free id which should return id1 + manager.release_id(id1); + let id3 = manager.get_free_id(); + assert_eq!(id3, id1); + + // Next free id should be id3 (old id2 was already used) + let id4 = manager.get_free_id(); + assert_eq!(id4, 3); + } +} \ No newline at end of file diff --git a/game-logic-service/src/loader.rs b/game-logic-service/src/loader.rs new file mode 100644 index 0000000..0d7da3e --- /dev/null +++ b/game-logic-service/src/loader.rs @@ -0,0 +1,160 @@ +// src/loader.rs + +use serde::Deserialize; +use serde_json::Value; +use std::{ + error::Error, + fs::File, + io::BufReader, +}; +use tracing::info; + +/// A 3D vector type used for positions, scales, etc. +#[derive(Debug, Deserialize)] +pub struct Vec3 { + #[serde(rename = "X")] + pub x: f64, + #[serde(rename = "Y")] + pub y: f64, + #[serde(rename = "Z")] + pub z: f64, +} + +/// Top-level zone definition. +#[derive(Debug, Deserialize)] +pub struct Zone { + #[serde(rename = "Id")] + pub id: u32, + #[serde(rename = "SpawnPoints")] + pub spawn_points: Option>, + #[serde(rename = "NpcSpawnPoints")] + pub npc_spawn_points: Option>, + #[serde(rename = "MobSpawnPoints")] + pub mob_spawn_points: Option>, + #[serde(rename = "WarpPoints")] + pub warp_points: Option>, +} + +/// A simple spawn point with a type and a position. +#[derive(Debug, Deserialize)] +pub struct SpawnPoint { + #[serde(rename = "Type")] + pub point_type: u32, + #[serde(rename = "Position")] + pub position: Vec3, +} + +/// An NPC spawn point. +#[derive(Debug, Deserialize)] +pub struct NpcSpawnPoint { + #[serde(rename = "Id")] + pub id: u32, + #[serde(rename = "DialogId")] + pub dialog_id: u32, + #[serde(rename = "Position")] + pub position: Vec3, + #[serde(rename = "Angle")] + pub angle: f32, +} + +#[derive(Debug, Deserialize)] +pub struct SpawnPointDefinition { + #[serde(rename = "Name")] + pub name: String, + #[serde(rename = "Monster")] + pub monster_id: u32, + #[serde(rename = "Count")] + pub count: u32, +} + +/// A mob spawn point. +#[derive(Debug, Deserialize)] +pub struct MobSpawnPoint { + #[serde(rename = "SpawnName")] + pub spawn_name: String, + #[serde(rename = "NormalSpawnPoints")] + pub normal_spawn_points: Option>, + #[serde(rename = "TacticalSpawnPoints")] + pub tactical_spawn_points: Option>, + #[serde(rename = "Interval")] + pub interval: u32, + #[serde(rename = "Limit")] + pub limit: u32, + #[serde(rename = "Range")] + pub range: u32, + #[serde(rename = "TacticalVariable")] + pub tactical_variable: u32, + #[serde(rename = "Name")] + pub name: String, + #[serde(rename = "ObjectType")] + pub object_type: u32, + #[serde(rename = "ObjectID")] + pub object_id: u32, + #[serde(rename = "MapPosition")] + pub map_position: Option, + #[serde(rename = "Position")] + pub position: Vec3, + #[serde(rename = "Rotation")] + pub rotation: Rotation, + #[serde(rename = "Scale")] + pub scale: Vec3, +} + +/// A simple map position. +#[derive(Debug, Deserialize)] +pub struct MapPosition { + #[serde(rename = "X")] + pub x: i32, + #[serde(rename = "Y")] + pub y: i32, +} + +/// A simple rotation structure. +#[derive(Debug, Deserialize)] +pub struct Rotation { + #[serde(rename = "W")] + pub w: f64, +} + +/// A warp point. +#[derive(Debug, Deserialize)] +pub struct WarpPoint { + #[serde(rename = "Alias")] + pub alias: String, + #[serde(rename = "DestinationGateId")] + pub destination_gate_id: u32, + #[serde(rename = "Destination")] + pub destination: Vec3, + #[serde(rename = "MapId")] + pub map_id: u32, + #[serde(rename = "MinPosition")] + pub min_position: Vec3, + #[serde(rename = "MaxPosition")] + pub max_position: Vec3, +} + +/// Loads only the zone (from a JSON file) with a top-level `Id` matching the given `map_id`. +/// +/// The JSON file is assumed to be an array of zones. +pub fn load_zone_for_map( + file_path: &str, + map_id: u32, +) -> Result, Box> { + let file = File::open(file_path)?; + let reader = BufReader::new(file); + // Deserialize the file as a JSON value first. + let value: Value = serde_json::from_reader(reader)?; + // Ensure the top-level element is an array. + let zones = value + .as_array() + .ok_or("Expected JSON array at top level")?; + + for zone_value in zones { + // Deserialize each zone. + let zone: Zone = serde_json::from_value(zone_value.clone())?; + if zone.id == map_id { + return Ok(Some(zone)); + } + } + Ok(None) +} \ No newline at end of file diff --git a/game-logic-service/src/main.rs b/game-logic-service/src/main.rs new file mode 100644 index 0000000..4cf738c --- /dev/null +++ b/game-logic-service/src/main.rs @@ -0,0 +1,85 @@ +pub mod components; +mod entity_factory; +mod id_manager; +mod loader; +mod random; +mod game_logic_service; +mod game_service; + +use dotenv::dotenv; +use std::env; +use hecs::World; +use tracing::{debug, error, info}; +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; + +#[tokio::main] +async fn main() -> Result<(), Box> { + dotenv().ok(); + let app_name = env!("CARGO_PKG_NAME"); + logging::setup_logging(app_name, &["game_logic_service", "health_check"]); + + + let map_id = env::var("MAP_ID").unwrap_or_else(|_| "20".to_string()).parse::()?; + let file_path = "/opt/data/zone_data.json"; + + // // Set the gRPC server address + let addr = env::var("LISTEN_ADDR").unwrap_or_else(|_| "0.0.0.0".to_string()); + let port = env::var("SERVICE_PORT").unwrap_or_else(|_| "50056".to_string()); + let db_url = format!( + "http://{}", + get_kube_service_endpoints_by_dns("database-service", "tcp", "database-service") + .await? + .get(0) + .unwrap() + ); + let chat_service = format!( + "http://{}", + get_kube_service_endpoints_by_dns("chat-service", "tcp", "chat-service") + .await? + .get(0) + .unwrap() + ); + + let game_logic_task = tokio::spawn(async move { + let mut loading = true; + let mut world = World::new(); + let mut factory = EntityFactory::new(&mut world); + + loop { + // 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); + loading = false; + } + Ok(None) => { + error!("No zone found for Map Id {}", map_id); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + Err(e) => { + error!("Error loading zone: {}", e); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + } + } + + // Update the world + if !loading { + factory.run(); + } + std::thread::sleep(std::time::Duration::from_millis(1000/30)); + } + }); + + // Register service with Consul + // health_check::start_health_check(addr.as_str()).await?; + utils::signal_handler::wait_for_signal().await; + Ok(()) +} diff --git a/game-logic-service/src/random.rs b/game-logic-service/src/random.rs new file mode 100644 index 0000000..9bdb1a2 --- /dev/null +++ b/game-logic-service/src/random.rs @@ -0,0 +1,37 @@ +use rand::Rng; + +pub fn get_random_number() -> u32 { + rand::thread_rng().gen_range(0..1000000) +} + +pub fn get_random_number_in_range(min: u32, max: u32) -> u32 { + rand::thread_rng().gen_range(min..=max) +} + +pub fn get_random_number_in_range_f32(min: f32, max: f32) -> f32 { + rand::thread_rng().gen_range(min..=max) +} + +pub fn get_random_number_in_range_f64(min: f64, max: f64) -> f64 { + rand::thread_rng().gen_range(min..=max) +} + +pub fn get_random_bool() -> bool { + rand::thread_rng().gen_bool(0.5) +} + +pub fn get_random_item_from_vec(vec: &Vec) -> Option<&T> { + if vec.is_empty() { + return None; + } + let index = rand::thread_rng().gen_range(0..vec.len()); + vec.get(index) +} + +pub fn get_random_point_in_circle(center: (f32, f32), radius: f32) -> (f32, f32) { + let angle = rand::thread_rng().gen_range(0.0..std::f32::consts::PI * 2.0); + let distance = rand::thread_rng().gen_range(0.0..radius); + let x = center.0 + distance * angle.cos(); + let y = center.1 + distance * angle.sin(); + (x, y) +} diff --git a/packet-service/build.rs b/packet-service/build.rs index 6a69942..c5dc5c8 100644 --- a/packet-service/build.rs +++ b/packet-service/build.rs @@ -10,6 +10,7 @@ fn main() { "../proto/chat.proto", "../proto/character.proto", "../proto/character_common.proto", + "../proto/game.proto" ], &["../proto"], ) diff --git a/proto/game.proto b/proto/game.proto new file mode 100644 index 0000000..a0ca5c7 --- /dev/null +++ b/proto/game.proto @@ -0,0 +1,167 @@ +syntax = "proto3"; + +import "google/protobuf/empty.proto"; +//google.protobuf.Empty +import "character_common.proto"; + +package game; + +// Define a mob spawn event. +message MobSpawnEvent { + uint32 id = 1; + float pos_x = 2; + float pos_y = 3; + float dest_pos_x = 4; + float dest_pos_y = 5; + uint32 command = 6; + uint32 target_id = 7; + uint32 move_mode = 8; + int32 hp = 9; + int32 team_id = 10; + uint32 status_flag = 11; + uint32 npc_id = 12; + uint32 quest_id = 13; +} + +message NpcSpawnEvent { + uint32 id = 1; + float pos_x = 2; + float pos_y = 3; + float dest_pos_x = 4; + float dest_pos_y = 5; + uint32 command = 6; + uint32 target_id = 7; + uint32 move_mode = 8; + int32 hp = 9; + int32 team_id = 10; + uint32 status_flag = 11; + uint32 npc_id = 12; + uint32 quest_id = 13; + float angle = 14; + uint32 event_status = 15; +} + +message PlayerSpawnEvent { + uint32 id = 1; + float pos_x = 2; + float pos_y = 3; + float dest_pos_x = 4; + float dest_pos_y = 5; + uint32 command = 6; + uint32 target_id = 7; + uint32 move_mode = 8; + int32 hp = 9; + int32 team_id = 10; + uint32 status_flag = 11; + uint32 race = 12; + uint32 run_speed = 13; + uint32 atk_speed = 14; + uint32 weight_rate = 15; + uint32 face = 16; + uint32 hair = 17; + repeated character_common.EquippedItem inventory = 18; + repeated character_common.ItemHeader bullets = 19; + repeated character_common.EquippedItem riding_items = 20; + uint32 job = 21; + uint32 level = 22; + uint32 z = 23; + uint32 sub_flag = 24; + string name = 25; + string other_name = 26; +} + +message ObjectDespawnEvent { + uint32 object_id = 1; +} + +// Define a player connect event. +message PlayerConnectEvent { + string player_id = 1; +} + +// Define a player disconnect event. +message PlayerDisconnectEvent { + string player_id = 1; +} + +message PlayerMoveEvent { + string player_id = 1; + float pos_x = 2; + float pos_y = 3; +} + +message PlayerAttackEvent { + string player_id = 1; + uint32 target_id = 2; +} + +message PlayerSkillEvent { + string player_id = 1; + uint32 skill_id = 2; + uint32 target_id = 3; +} + +message MobAttackEvent { + uint32 mob_id = 1; + uint32 target_id = 2; +} + +message MobSkillEvent { + uint32 mob_id = 1; + uint32 skill_id = 2; + uint32 target_id = 3; +} + +message DamageEvent { + uint32 source_id = 1; + uint32 target_id = 2; + uint32 damage = 3; + uint32 action = 4; + float x = 5; + float y = 6; + character_common.Item item = 7; + uint32 id = 8; + uint32 owner_id = 9; +} + +message DropItemEvent { + float x = 1; + float y = 2; + character_common.Item item = 3; + uint32 id = 4; + uint32 owner_id = 5; + uint32 time_to_live = 6; +} + +message PlayerLevelUpEvent { + string player_id = 1; + uint32 level = 2; + uint32 exp = 3; + uint32 stat_points = 4; + uint32 skill_points = 5; +} + +// Define a generic event using oneof for the different types. +message GenericEvent { + repeated string clients = 1; // List of clients to get this event. + + oneof event { + MobSpawnEvent mob_spawn = 2; + NpcSpawnEvent npc_spawn = 3; + PlayerSpawnEvent player_spawn = 4; + PlayerMoveEvent player_move = 5; + PlayerAttackEvent player_attack = 6; + PlayerSkillEvent player_skill = 7; + MobAttackEvent mob_attack = 8; + MobSkillEvent mob_skill = 9; + DamageEvent damage = 10; + ObjectDespawnEvent object_despawn = 11; + DropItemEvent drop_item = 12; + PlayerLevelUpEvent player_level_up = 13; + } +} + +// gRPC service definition for a streaming RPC. +service EventService { + rpc StreamEvents(stream GenericEvent) returns (stream GenericEvent); +} \ No newline at end of file diff --git a/proto/game_logic.proto b/proto/game_logic.proto index 49d90df..4adb9f8 100644 --- a/proto/game_logic.proto +++ b/proto/game_logic.proto @@ -3,50 +3,25 @@ syntax = "proto3"; package game_logic; service GameLogicService { - rpc GetCharacter(CharacterRequest) returns (CharacterResponse); - rpc MoveCharacter(CharacterMoveRequest) returns (CharacterMoveResponse); - rpc GetTargetHp(ObjectHpRequest) returns (ObjectHpResponse); + rpc GetNearbyObjects(NearbyObjectsRequest) returns (NearbyObjectsResponse); } -message CharacterRequest { - string token = 1; - string user_id = 2; - string char_id = 3; - string session_id = 4; -} - -message CharacterResponse { - int32 count = 1; -} - -message CharacterMoveRequest { +message NearbyObjectsRequest { string session_id = 1; - uint32 target_id = 2; + float x = 2; + float y = 3; + float z = 4; + int32 map_id = 5; +} + +message NearbyObjectsResponse { + repeated Object objects = 1; +} + +message Object { + int32 id = 1; + int32 type_ = 2; float x = 3; float y = 4; float z = 5; -} - -message CharacterMoveResponse { - int32 id = 1; - int32 target_id = 2; - int32 distance = 3; - float x = 4; - float y = 5; - float z = 6; -} - -message AttackRequest { - string session_id = 1; - uint32 target_id = 2; -} - -message ObjectHpRequest { - string session_id = 1; - uint32 target_id = 2; -} - -message ObjectHpResponse { - uint32 target_id = 1; - int32 hp = 2; -} +} \ No newline at end of file diff --git a/scripts/build_and_push.py b/scripts/build_and_push.py index 1a49c3d..2ed36dc 100644 --- a/scripts/build_and_push.py +++ b/scripts/build_and_push.py @@ -7,6 +7,7 @@ images = [ "chat-service", "character-service", "database-service", + "game-logic-service", "packet-service", "world-service" ] @@ -15,6 +16,7 @@ dockerfile_paths = [ "../chat-service/Dockerfile", "../character-service/Dockerfile", "../database-service/Dockerfile", + "../game-logic-service/Dockerfile", "../packet-service/Dockerfile", "../world-service/Dockerfile", ] diff --git a/world-service/build.rs b/world-service/build.rs index 41ce197..e60cd86 100644 --- a/world-service/build.rs +++ b/world-service/build.rs @@ -4,13 +4,13 @@ fn main() { .build_server(true) // Generate gRPC server code .compile_well_known_types(true) .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") - .compile_protos(&["../proto/world.proto"], &["../proto"]) + .compile_protos(&["../proto/world.proto", "../proto/game.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) - .compile_protos(&["../proto/user_db_api.proto", "../proto/auth.proto", "../proto/character.proto", "../proto/character_common.proto", "../proto/chat.proto", "../proto/game_logic.proto"], &["../proto"]) + .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)); }