Added initial game logic service

This commit is contained in:
2025-06-24 14:10:27 -04:00
parent 4c91fe3557
commit f75782885b
30 changed files with 1366 additions and 43 deletions

View File

@@ -5,6 +5,7 @@ members = [
"chat-service",
"character-service",
"database-service",
"game-logic-service",
"packet-service",
"world-service",
"utils",

View File

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

View File

@@ -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"]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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<NearbyObjectsRequest>) -> Result<Response<NearbyObjectsResponse>, Status> {
let req = request.into_inner();
debug!("{:?}", req);
let response = NearbyObjectsResponse {
objects: vec![],
};
Ok(Response::new(response))
}
}

View File

@@ -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<Self> {
EventServiceServer::new(self)
}
}
#[tonic::async_trait]
impl EventService for MyGameService {
type StreamEventsStream =
Pin<Box<dyn Stream<Item = Result<GenericEvent, Status>> + Send + Sync + 'static>>;
async fn stream_events(
&self,
request: Request<Streaming<GenericEvent>>,
) -> Result<Response<Self::StreamEventsStream>, 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))
}
}

View File

@@ -0,0 +1,61 @@
use std::collections::HashSet;
#[derive(Clone, Debug)]
pub struct IdManager {
free_ids: HashSet<u16>,
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);
}
}

View File

@@ -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<Vec<SpawnPoint>>,
#[serde(rename = "NpcSpawnPoints")]
pub npc_spawn_points: Option<Vec<NpcSpawnPoint>>,
#[serde(rename = "MobSpawnPoints")]
pub mob_spawn_points: Option<Vec<MobSpawnPoint>>,
#[serde(rename = "WarpPoints")]
pub warp_points: Option<Vec<WarpPoint>>,
}
/// 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<Vec<SpawnPointDefinition>>,
#[serde(rename = "TacticalSpawnPoints")]
pub tactical_spawn_points: Option<Vec<SpawnPointDefinition>>,
#[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<MapPosition>,
#[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<Option<Zone>, Box<dyn Error>> {
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)
}

View File

@@ -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<dyn std::error::Error>> {
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::<u32>()?;
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(())
}

View File

@@ -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<T>(vec: &Vec<T>) -> 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)
}

View File

@@ -10,6 +10,7 @@ fn main() {
"../proto/chat.proto",
"../proto/character.proto",
"../proto/character_common.proto",
"../proto/game.proto"
],
&["../proto"],
)

167
proto/game.proto Normal file
View File

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

View File

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

View File

@@ -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",
]

View File

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