This commit is contained in:
xeon 2024-09-11 19:37:46 +03:00
parent cfb00eaf29
commit 099ac9f871
127 changed files with 207074 additions and 1 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
/target
/hotpatch.toml
/configserver.toml
/loginserver.toml
/gateway.toml
/gameserver.toml
/shorekeeper-protocol/generated

2687
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

61
Cargo.toml Normal file
View file

@ -0,0 +1,61 @@
[workspace]
members = ["common", "config-server", "hotpatch-server", "login-server", "gateway-server", "gateway-server/kcp", "shorekeeper-database", "shorekeeper-http", "shorekeeper-protokey", "shorekeeper-protocol", "shorekeeper-protocol/shorekeeper-protocol-derive", "game-server", "shorekeeper-network", "shorekeeper-data"]
resolver = "2"
[workspace.package]
version = "0.1.0"
[workspace.dependencies]
# Framework
tokio = { version = "1.39.3", features = ["full"] }
axum = "0.7.5"
axum-server = "0.7.1"
zeromq = { version = "0.4.0", default-features = false, features = ["tokio-runtime", "tcp-transport"] }
# Database
sqlx = { version = "0.8.2", features = ["postgres", "runtime-tokio-rustls"] }
# Cryptography
aes = "0.8.4"
cbc = { version = "0.1.2", features = ["alloc"] }
cipher = "0.4.4"
rand = "0.8.5"
rsa = { version = "0.9.6", features = ["pem"] }
# Serialization
serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.128"
toml = "0.8.19"
prost = "0.13.2"
prost-build = "0.13.2"
# Utility
anyhow = "1.0.86"
thiserror = "1.0.63"
paste = "1.0.15"
rbase64 = "2.0.3"
dashmap = "6.1.0"
hex = "0.4.3"
byteorder = "1.5.0"
crc32fast = "1.4.2"
# Tracing
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
# Internal
kcp = { path = "gateway-server/kcp" }
common = { path = "common/" }
shorekeeper-http = { path = "shorekeeper-http/" }
shorekeeper-data = { path = "shorekeeper-data/" }
shorekeeper-database = { path = "shorekeeper-database/" }
shorekeeper-network = { path = "shorekeeper-network/" }
shorekeeper-protocol = { path = "shorekeeper-protocol/" }
shorekeeper-protocol-derive = { path = "shorekeeper-protocol/shorekeeper-protocol-derive" }
shorekeeper-protokey = { path = "shorekeeper-protokey/" }
[profile.release]
strip = true # Automatically strip symbols from the binary.
lto = true # Link-time optimization.
opt-level = 3 # Optimize for speed.
codegen-units = 1 # Maximum size reduction optimizations.

View file

@ -1,3 +1,59 @@
# Shorekeeper
Wuthering Waves server emulator written in Rust
![Screenshot](https://git.xeondev.com/Shorekeeper/Shorekeeper/raw/branch/master/screenshot.png)
## About
**Shorekeeper is an open-source Wuthering Waves server emulator written in Rust**. The goal of this project is to ensure a clean, easy-to-understand code environment. Shorekeeper uses **tokio** for asynchronous networking operations, **axum** as http framework and **ZeroMQ** for communication between servers. It also implements **performant and extensible ECS** for emulation of the game world.
## Getting started
#### Requirements
- [Rust](https://www.rust-lang.org/tools/install)
- [PostgreSQL](https://www.postgresql.org/download/)
- [Protoc](https://github.com/protocolbuffers/protobuf/releases) (for protobuf codegen)
#### Setup
##### a) building from sources
```sh
git clone https://git.xeondev.com/Shorekeeper/Shorekeeper.git
cd Shorekeeper
cargo run --bin config-server
cargo run --bin hotpatch-server
cargo run --bin login-server
cargo run --bin gateway-server
cargo run --bin game-server
```
##### b) using pre-built binaries
Navigate to the [Releases](https://git.xeondev.com/Shorekeeper/Shorekeeper/releases)
page and download the latest release for your platform.<br>
Launch all servers: `config-server`, `hotpatch-server`, `login-server`, `gateway-server`, `game-server`
##### NOTE: you don't have to install Rust and Protoc if you're going to use pre-built binaries, although the preferred way is building from sources.<br>We don't provide any support for pre-built binaries.
#### Configuration
You should configure each server using their own config files. They're being created in current working directory upon first startup.
##### Database section
You have to specify credentials for **PostgreSQL**<br>
###### An example of database configuration:
```
[database]
host = "localhost:5432"
user_name = "postgres"
password = ""
db_name = "shorekeeper"
```
##### NOTE: don't forget to create database with specified `db_name` (default: `shorekeeper`). For example, you can do so with PgAdmin.
#### Data
The data files: Logic JSON collections (`assets/logic/json`) and config/hotpatch indexes (`assets/config`, `assets/hotpatch`) are included in this repository. Keep in mind that you need to have the `assets` subdirectory in current working directory.
#### Connecting
You have to download client of Wuthering Waves Beta 1.3, apply the [shorekeeper-patch](https://git.xeondev.com/xeon/shorekeeper-patch/releases) and add necessary `.pak` files, which you can get here: [shorekeeper-pak](https://git.xeondev.com/Shorekeeper/shorekeeper-pak)
### Troubleshooting
[Visit our discord](https://discord.gg/reversedrooms) if you have any questions/issues
### Support
If you want to support this project, feel free to [send a tip via boosty](https://boosty.to/xeondev/donate)

52
assets/config/index.json Normal file
View file

@ -0,0 +1,52 @@
{
"default": {
"CdnUrl": [
{
"url": "http://127.0.0.1:10002/prod/client/",
"weight": "2323"
}
],
"SecondaryUrl": [],
"PriceRatio": 1,
"SpeedRatio": 1,
"GachaUrl": {
"GachaRecord": "http://127.0.0.1:10001/gacha/record",
"GachaPoolDetail": "http://127.0.0.1:10001/gacha/detail"
},
"LogReport": {
"name": "pioneer-upload-log-1319073642",
"region": "dev-reversedrooms"
},
"PackageUpdateDescUrl": {
"MainUrl": "http://127.0.0.1:10001/force_update/UpdateDesc.html",
"SubUrl": "http://127.0.0.1:10001/force_update/UpdateDesc.html"
},
"PackageUpdateUrl": {
"MainUrl": "http://127.0.0.1:10001/force_update/UpdateJs.html",
"SubUrl": "http://127.0.0.1:10001/force_update/UpdateJs.html"
},
"TDCfg": {
"AppID": "3e2e647670b7498fa645eb9574f78c2c",
"URL": "http://127.0.0.1:10001/TDCfg"
},
"GmOpen": false,
"IosAuditFirstDownloadTip": false,
"NoticeUrl": "http://127.0.0.1:10001/notice",
"MixUri": "rODM5DcqOhYsIOtsEuZWNGFa2guZgl57",
"ResUri": "rODM5DcqOhYsIOtsEuZWNGFa2guZgl57",
"LoginServers": [
{
"id": "f9e0fc655c1931bc03ad976e9fc14473",
"ip": "http://127.0.0.1:5500",
"name": "Shorekeeper"
}
],
"PrivateServers": {
"enable": false,
"serverUrl": ""
}
},
"p1": {
"IosAuditFirstDownloadTip": false
}
}

View file

@ -0,0 +1,60 @@
{
"PackageVersion": "1.3.0",
"LauncherVersion": "1.3.9",
"ResourceVersion": "1.3.9",
"LauncherIndexSha1": {
"1.3.1": "90FDF17EA0B4015D43C344CB7229E76AB32549DD",
"1.3.2": "C9A587AB1FA6CA57CD23E0FB3F0103BFDCAA8E37",
"1.3.3": "1C7AF02F13DBE69637DB43039E2FFB8C9AD9A04B",
"1.3.4": "DA50F315041E216568A7713074C6475F6AB4530E",
"1.3.5": "EA9C6F6D5E920F47F96D8F8BC366A4CED62A0346",
"1.3.6": "8CA7E6573A52B16CFAA29E996D389918B6829E7A",
"1.3.7": "FCAAED58E5983027A82F52C350418CCE7BD531D2",
"1.3.8": "91D6231B3F4C9A6605B79E23D0C02F9790DD6BCF",
"1.3.9": "B36BD648AB2A637A4E087B7B115A6CCBDAEBDF9A"
},
"ResourceIndexSha1": {
"1.3.1": "2D635E549EB6F99659571D72741B62249473A77A",
"1.3.2": "C5814A80EA3E7D80D4CFBCD884D1FD158BF0AD9D",
"1.3.3": "1E0F05333B09A9215B4AA5C437BFC7DC4014E348",
"1.3.4": "6155D492540A99ECF0DA06D2B7EEBFE36231FBC2",
"1.3.5": "1E60C8F60CA1AAA9955441B4F4265C8288B95F33",
"1.3.6": "AA10A8DD1025D5033E291060C686B816513ADCAD",
"1.3.7": "A9881305EBD3DC5A6892D49BDAF540F56EE56232",
"1.3.8": "261CA25DAD6877DF3C57DA39947130867FCC09CE",
"1.3.9": "88A9E40631FC1C11A91A61CB3F4BE8C13C5E2BD3"
},
"ChangeList": "2333675",
"CompatibleChangeLists": [],
"Versions": [
{
"Name": "en",
"Version": "1.3.0",
"IndexSha1": {
"1.3.0": "6FB5B66EF8B3EECBBBEBE74A82BC23E3FC35450B"
}
},
{
"Name": "ja",
"Version": "1.3.0",
"IndexSha1": {
"1.3.0": "E4DA1960DB36CE8166C042AD8B9AF98C1A9119F3"
}
},
{
"Name": "ko",
"Version": "1.3.0",
"IndexSha1": {
"1.3.0": "498B379E95FC617385CCD832B8C359FA5AC220CE"
}
},
{
"Name": "zh",
"Version": "1.3.0",
"IndexSha1": {
"1.3.0": "CC58C357A80E7B3846264918197FC3ECAA1FE190"
}
}
],
"UpdateTime": 1725869509
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

11
common/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "common"
edition = "2021"
version.workspace = true
[dependencies]
tracing.workspace = true
tracing-subscriber.workspace = true
toml.workspace = true
serde.workspace = true

18
common/src/config_util.rs Normal file
View file

@ -0,0 +1,18 @@
use serde::de::DeserializeOwned;
pub trait TomlConfig: DeserializeOwned {
const DEFAULT_TOML: &str;
}
pub fn load_or_create<'a, C>(path: &str) -> C
where
C: DeserializeOwned + TomlConfig,
{
std::fs::read_to_string(path).map_or_else(
|_| {
std::fs::write(path, C::DEFAULT_TOML).unwrap();
toml::from_str(C::DEFAULT_TOML).unwrap()
},
|data| toml::from_str(&data).unwrap(),
)
}

4
common/src/lib.rs Normal file
View file

@ -0,0 +1,4 @@
pub mod config_util;
pub mod logging;
pub mod splash;
pub mod time_util;

8
common/src/logging.rs Normal file
View file

@ -0,0 +1,8 @@
use tracing::Level;
pub fn init(max_level: Level) {
tracing_subscriber::fmt()
.with_max_level(max_level)
.with_target(false)
.init();
}

3
common/src/splash.rs Normal file
View file

@ -0,0 +1,3 @@
pub fn print_splash() {
println!(" _____ __ __ \n / ___// /_ ____ ________ / /_____ ___ ____ ___ _____\n \\__ \\/ __ \\/ __ \\/ ___/ _ \\/ //_/ _ \\/ _ \\/ __ \\/ _ \\/ ___/\n ___/ / / / / /_/ / / / __/ ,< / __/ __/ /_/ / __/ / \n/____/_/ /_/\\____/_/ \\___/_/|_|\\___/\\___/ .___/\\___/_/ \n /_/ ");
}

15
common/src/time_util.rs Normal file
View file

@ -0,0 +1,15 @@
use std::time::{SystemTime, UNIX_EPOCH};
pub fn unix_timestamp() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs() as u64
}
pub fn unix_timestamp_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis() as u64
}

22
config-server/Cargo.toml Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "config-server"
edition = "2021"
version.workspace = true
[dependencies]
# Framework
tokio.workspace = true
shorekeeper-http.workspace = true
# Serialization
serde.workspace = true
toml.workspace = true
# Util
anyhow.workspace = true
# Tracing
tracing.workspace = true
# Internal
common.workspace = true

View file

@ -0,0 +1,6 @@
[network]
http_addr = "0.0.0.0:10001"
[encryption]
key = "t+AEu5SGdpz06tomonajLMau9AJgmyTvVhz9VtGf1+0="
iv = "fprc5lBWADQB7tim0R2JxQ=="

45
config-server/src/main.rs Normal file
View file

@ -0,0 +1,45 @@
use std::fs;
use std::sync::LazyLock;
use anyhow::Result;
use common::config_util::{self, TomlConfig};
use serde::Deserialize;
use shorekeeper_http::{
config::{AesSettings, NetworkSettings},
Application,
};
#[derive(Deserialize)]
pub struct ServerConfig {
pub network: NetworkSettings,
pub encryption: AesSettings,
}
impl TomlConfig for ServerConfig {
const DEFAULT_TOML: &str = include_str!("../configserver.default.toml");
}
#[tokio::main]
async fn main() -> Result<()> {
static CONFIG: LazyLock<ServerConfig> =
LazyLock::new(|| config_util::load_or_create("configserver.toml"));
::common::splash::print_splash();
::common::logging::init(::tracing::Level::DEBUG);
Application::new()
.get("/index.json", get_index)
.with_encryption(&CONFIG.encryption)
.serve(&CONFIG.network)
.await?;
Ok(())
}
async fn get_index() -> &'static str {
static INDEX: LazyLock<String> =
LazyLock::new(|| fs::read_to_string("assets/config/index.json").unwrap());
&*INDEX
}

28
game-server/Cargo.toml Normal file
View file

@ -0,0 +1,28 @@
[package]
name = "game-server"
edition = "2021"
version.workspace = true
[dependencies]
# Framework
tokio.workspace = true
# Serialization
serde.workspace = true
# Util
anyhow.workspace = true
thiserror.workspace = true
paste.workspace = true
dashmap.workspace = true
hex.workspace = true
# Tracing
tracing.workspace = true
# Internal
common.workspace = true
shorekeeper-data.workspace = true
shorekeeper-database.workspace = true
shorekeeper-network.workspace = true
shorekeeper-protocol.workspace = true

View file

@ -0,0 +1,13 @@
service_id = 2
[database]
host = "localhost:5432"
user_name = "postgres"
password = ""
db_name = "shorekeeper"
[service_end_point]
addr = "tcp://127.0.0.1:10004"
[gateway_end_point]
addr = "tcp://127.0.0.1:10003"

16
game-server/src/config.rs Normal file
View file

@ -0,0 +1,16 @@
use common::config_util::TomlConfig;
use serde::Deserialize;
use shorekeeper_database::DatabaseSettings;
use shorekeeper_network::config::ServiceEndPoint;
#[derive(Deserialize)]
pub struct ServiceConfig {
pub service_id: u32,
pub database: DatabaseSettings,
pub service_end_point: ServiceEndPoint,
pub gateway_end_point: ServiceEndPoint,
}
impl TomlConfig for ServiceConfig {
const DEFAULT_TOML: &str = include_str!("../gameserver.default.toml");
}

View file

@ -0,0 +1,22 @@
use std::sync::OnceLock;
use shorekeeper_network::{config::ServiceEndPoint, ServiceClient, ServiceMessage};
static CLIENT: OnceLock<ServiceClient> = OnceLock::new();
pub fn init(own_service_id: u32, gateway_end_point: &'static ServiceEndPoint) {
if CLIENT.get().is_some() {
tracing::error!("gateway_connection: already initialized");
return;
}
let _ = CLIENT.set(ServiceClient::new(own_service_id, gateway_end_point));
}
pub async fn push_message(message: ServiceMessage) {
CLIENT.get().unwrap().push(message).await
}
pub fn push_message_sync(message: ServiceMessage) {
CLIENT.get().unwrap().push_sync(message)
}

View file

@ -0,0 +1,201 @@
use std::collections::HashMap;
use shorekeeper_data::BasePropertyData;
use shorekeeper_protocol::{
entity_component_pb::ComponentPb, AttrData, AttributeComponentPb, EAttributeType,
EntityComponentPb, LivingStatus,
};
use crate::logic::ecs::component::Component;
pub struct Attribute {
pub attr_map: HashMap<EAttributeType, (i32, i32)>,
}
macro_rules! impl_from_data {
($($name:ident),*) => {
pub fn from_data(base_property: &BasePropertyData) -> Self {
Self {
attr_map: HashMap::from([$(
::paste::paste!((EAttributeType::[<$name:camel>], (base_property.$name, 0))),
)*]),
}
}
};
}
impl Component for Attribute {
fn set_pb_data(&self, pb: &mut shorekeeper_protocol::EntityPb) {
pb.living_status = self
.is_alive()
.then_some(LivingStatus::Alive)
.unwrap_or(LivingStatus::Dead)
.into();
pb.component_pbs.push(EntityComponentPb {
component_pb: Some(ComponentPb::AttributeComponent(AttributeComponentPb {
attr_data: self
.attr_map
.iter()
.map(|(ty, (base, incr))| AttrData {
attribute_type: (*ty).into(),
base_value: *base,
increment: *incr,
})
.collect(),
hardness_mode_id: 0,
rage_mode_id: 0,
})),
})
}
}
impl Attribute {
pub fn is_alive(&self) -> bool {
self.attr_map
.get(&EAttributeType::Life)
.copied()
.unwrap_or_default()
.0
> 0
}
impl_from_data!(
lv,
life_max,
life,
sheild,
sheild_damage_change,
sheild_damage_reduce,
atk,
crit,
crit_damage,
def,
energy_efficiency,
cd_reduse,
reaction_efficiency,
damage_change_normal_skill,
damage_change,
damage_reduce,
damage_change_auto,
damage_change_cast,
damage_change_ultra,
damage_change_qte,
damage_change_phys,
damage_change_element1,
damage_change_element2,
damage_change_element3,
damage_change_element4,
damage_change_element5,
damage_change_element6,
damage_resistance_phys,
damage_resistance_element1,
damage_resistance_element2,
damage_resistance_element3,
damage_resistance_element4,
damage_resistance_element5,
damage_resistance_element6,
heal_change,
healed_change,
damage_reduce_phys,
damage_reduce_element1,
damage_reduce_element2,
damage_reduce_element3,
damage_reduce_element4,
damage_reduce_element5,
damage_reduce_element6,
reaction_change1,
reaction_change2,
reaction_change3,
reaction_change4,
reaction_change5,
reaction_change6,
reaction_change7,
reaction_change8,
reaction_change9,
reaction_change10,
reaction_change11,
reaction_change12,
reaction_change13,
reaction_change14,
reaction_change15,
energy_max,
energy,
special_energy_1_max,
special_energy_1,
special_energy_2_max,
special_energy_2,
special_energy_3_max,
special_energy_3,
special_energy_4_max,
special_energy_4,
strength_max,
strength,
strength_recover,
strength_punish_time,
strength_run,
strength_swim,
strength_fast_swim,
hardness_max,
hardness,
hardness_recover,
hardness_punish_time,
hardness_change,
hardness_reduce,
rage_max,
rage,
rage_recover,
rage_punish_time,
rage_change,
rage_reduce,
tough_max,
tough,
tough_recover,
tough_change,
tough_reduce,
tough_recover_delay_time,
element_power1,
element_power2,
element_power3,
element_power4,
element_power5,
element_power6,
special_damage_change,
strength_fast_climb_cost,
element_property_type,
weak_time,
ignore_def_rate,
ignore_damage_resistance_phys,
ignore_damage_resistance_element1,
ignore_damage_resistance_element2,
ignore_damage_resistance_element3,
ignore_damage_resistance_element4,
ignore_damage_resistance_element5,
ignore_damage_resistance_element6,
skill_tough_ratio,
strength_climb_jump,
strength_gliding,
mass,
braking_friction_factor,
gravity_scale,
speed_ratio,
damage_change_phantom,
auto_attack_speed,
cast_attack_speed,
status_build_up_1_max,
status_build_up_1,
status_build_up_2_max,
status_build_up_2,
status_build_up_3_max,
status_build_up_3,
status_build_up_4_max,
status_build_up_4,
status_build_up_5_max,
status_build_up_5,
paralysis_time_max,
paralysis_time,
paralysis_time_recover,
element_energy_max,
element_energy
);
}

View file

@ -0,0 +1,15 @@
use shorekeeper_protocol::EntityConfigType;
use crate::logic::ecs::component::Component;
pub struct EntityConfig {
pub config_id: i32,
pub config_type: EntityConfigType,
}
impl Component for EntityConfig {
fn set_pb_data(&self, pb: &mut shorekeeper_protocol::EntityPb) {
pb.config_id = self.config_id;
pb.config_type = self.config_type.into();
}
}

View file

@ -0,0 +1,15 @@
mod attribute;
mod entity_config;
mod movement;
mod owner_player;
mod player_entity_marker;
mod position;
mod visibility;
pub use attribute::Attribute;
pub use entity_config::EntityConfig;
pub use movement::Movement;
pub use owner_player::OwnerPlayer;
pub use player_entity_marker::PlayerEntityMarker;
pub use position::Position;
pub use visibility::Visibility;

View file

@ -0,0 +1,14 @@
use std::collections::VecDeque;
use shorekeeper_protocol::MoveReplaySample;
use crate::logic::ecs::component::Component;
#[derive(Default)]
pub struct Movement {
pub pending_movement_vec: VecDeque<MoveReplaySample>,
}
impl Component for Movement {
fn set_pb_data(&self, _: &mut shorekeeper_protocol::EntityPb) {}
}

View file

@ -0,0 +1,9 @@
use crate::logic::ecs::component::Component;
pub struct OwnerPlayer(pub i32);
impl Component for OwnerPlayer {
fn set_pb_data(&self, pb: &mut shorekeeper_protocol::EntityPb) {
pb.player_id = self.0;
}
}

View file

@ -0,0 +1,11 @@
use shorekeeper_protocol::EEntityType;
use crate::logic::ecs::component::Component;
pub struct PlayerEntityMarker;
impl Component for PlayerEntityMarker {
fn set_pb_data(&self, pb: &mut shorekeeper_protocol::EntityPb) {
pb.entity_type = EEntityType::Player.into();
}
}

View file

@ -0,0 +1,11 @@
use crate::logic::{ecs::component::Component, math::Transform};
pub struct Position(pub Transform);
impl Component for Position {
fn set_pb_data(&self, pb: &mut shorekeeper_protocol::EntityPb) {
pb.pos = Some(self.0.get_position_protobuf());
pb.rot = Some(self.0.get_rotation_protobuf());
pb.init_pos = Some(self.0.get_position_protobuf());
}
}

View file

@ -0,0 +1,9 @@
use crate::logic::ecs::component::Component;
pub struct Visibility(pub bool);
impl Component for Visibility {
fn set_pb_data(&self, pb: &mut shorekeeper_protocol::EntityPb) {
pb.is_visible = self.0;
}
}

View file

@ -0,0 +1,37 @@
use shorekeeper_protocol::EntityPb;
use crate::logic::components::*;
macro_rules! impl_component_container {
($($comp:ident;)*) => {
pub enum ComponentContainer {
$(
$comp($comp),
)*
}
impl ComponentContainer {
pub fn set_pb_data(&self, pb: &mut shorekeeper_protocol::EntityPb) {
match self {
$(
Self::$comp(comp) => comp.set_pb_data(pb),
)*
}
}
}
};
}
impl_component_container! {
Position;
EntityConfig;
OwnerPlayer;
Visibility;
Attribute;
PlayerEntityMarker;
Movement;
}
pub trait Component {
fn set_pb_data(&self, pb: &mut EntityPb);
}

View file

@ -0,0 +1,57 @@
use std::{cell::RefCell, collections::HashSet};
use super::component::ComponentContainer;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct Entity(i64);
pub struct EntityBuilder<'comp>(Entity, &'comp mut Vec<RefCell<ComponentContainer>>);
#[derive(Default)]
pub struct EntityManager {
entity_id_counter: i64,
active_entity_set: HashSet<Entity>,
}
impl EntityManager {
pub fn create(&mut self) -> Entity {
self.entity_id_counter += 1;
let entity = Entity(self.entity_id_counter);
self.active_entity_set.insert(entity);
entity
}
pub fn get(&self, id: i64) -> Option<Entity> {
self.active_entity_set.get(&Entity(id)).copied()
}
#[expect(dead_code)]
pub fn remove(&mut self, entity: Entity) -> bool {
self.active_entity_set.remove(&entity)
}
}
impl<'comp> EntityBuilder<'comp> {
pub fn builder(
entity: Entity,
components: &'comp mut Vec<RefCell<ComponentContainer>>,
) -> Self {
Self(entity, components)
}
pub fn with(self, component: ComponentContainer) -> Self {
self.1.push(RefCell::new(component));
self
}
pub fn build(self) -> Entity {
self.0
}
}
impl From<Entity> for i64 {
fn from(value: Entity) -> Self {
value.0
}
}

View file

@ -0,0 +1,64 @@
pub mod component;
pub mod entity;
pub mod world;
// Query specified components from all entities
#[macro_export]
macro_rules! query_with {
($world:expr, $($comp:ident),*) => {
$world.components().iter().filter(|(_, comps)| {
$(comps.iter().any(|comp| matches!(&*comp.borrow(), ComponentContainer::$comp(_))) && )* true
})
.map(|(e, comps)| {
(*e,
$(
comps.iter().find_map(|comp| {
let r = comp.try_borrow_mut().ok()?;
if matches!(&*r, ComponentContainer::$comp(_)) {
Some(::std::cell::RefMut::map(r, |r| {
let ComponentContainer::$comp(comp_inner) = r else { unreachable!() };
comp_inner
}))
}
else {
None
}
}).unwrap(),
)*
)
})
.collect::<Vec<_>>()
};
}
#[macro_export]
macro_rules! ident_as_none {
($t:ident) => {
None
};
}
// Query components of specified entity
#[macro_export]
macro_rules! query_components {
($world:expr, $entity_id:expr, $($comp:ident),*) => {
$world.components().iter().find(|(id, _)| $entity_id == i64::from(**id))
.map(|(_, comps)| {
($(
comps.iter().find_map(|comp| {
let r = comp.try_borrow_mut().ok()?;
if matches!(&*r, ComponentContainer::$comp(_)) {
Some(::std::cell::RefMut::map(r, |r| {
let ComponentContainer::$comp(comp_inner) = r else { unreachable!() };
comp_inner
}))
}
else {
None
}
}),
)*)
})
.unwrap_or_else(|| ($( crate::ident_as_none!($comp), )*))
};
}

View file

@ -0,0 +1,58 @@
use std::cell::{RefCell, RefMut};
use std::collections::hash_map::{Keys, Values};
use std::collections::HashMap;
use crate::logic::player::InWorldPlayer;
use super::component::ComponentContainer;
use super::entity::{Entity, EntityBuilder, EntityManager};
pub struct World {
components: HashMap<Entity, Vec<RefCell<ComponentContainer>>>,
entity_manager: EntityManager,
in_world_players: HashMap<i32, InWorldPlayer>, // joined players metadata
}
impl World {
pub fn new() -> Self {
Self {
components: HashMap::new(),
entity_manager: EntityManager::default(),
in_world_players: HashMap::new(),
}
}
pub fn create_entity(&mut self) -> EntityBuilder {
let entity = self.entity_manager.create();
EntityBuilder::builder(entity, self.components.entry(entity).or_insert(Vec::new()))
}
pub fn is_in_world(&self, entity_id: i64) -> bool {
self.entity_manager.get(entity_id).is_some()
}
pub fn components(&self) -> &HashMap<Entity, Vec<RefCell<ComponentContainer>>> {
&self.components
}
pub fn get_entity_components(&self, entity: Entity) -> Vec<RefMut<ComponentContainer>> {
let Some(components) = self.components.get(&entity) else {
return Vec::with_capacity(0);
};
components.iter().map(|rc| rc.borrow_mut()).collect()
}
pub fn player_ids(&self) -> Keys<'_, i32, InWorldPlayer> {
self.in_world_players.keys()
}
pub fn players(&self) -> Values<'_, i32, InWorldPlayer> {
self.in_world_players.values()
}
pub fn set_in_world_player_data(&mut self, in_world_player: InWorldPlayer) {
self.in_world_players
.insert(in_world_player.player_id, in_world_player);
}
}

View file

@ -0,0 +1,80 @@
mod scene;
pub use scene::*;
use shorekeeper_protocol::message::Message;
macro_rules! handle_request {
($($name:ident;)*) => {
fn handle_request(player: &mut super::player::Player, mut msg: Message) {
use ::shorekeeper_protocol::{MessageID, Protobuf};
::paste::paste! {
match msg.get_message_id() {
$(
::shorekeeper_protocol::[<$name Request>]::MESSAGE_ID => {
let Ok(request) = ::shorekeeper_protocol::[<$name Request>]::decode(&*msg.remove_payload()) else {
tracing::debug!("failed to decode {}, player_id: {}", stringify!([<$name Request>]), player.basic_info.id);
return;
};
tracing::debug!("logic: processing request {}", stringify!([<$name Request>]));
let mut response = ::shorekeeper_protocol::[<$name Response>]::default();
[<on_ $name:snake _request>](player, request, &mut response);
player.respond(response, msg.get_rpc_id());
},
)*
unhandled => ::tracing::warn!("can't find handler for request with message_id={unhandled}")
}
}
}
};
}
macro_rules! handle_push {
($($name:ident;)*) => {
fn handle_push(player: &mut super::player::Player, mut msg: Message) {
use ::shorekeeper_protocol::{MessageID, Protobuf};
::paste::paste! {
match msg.get_message_id() {
$(
::shorekeeper_protocol::[<$name Push>]::MESSAGE_ID => {
let Ok(push) = ::shorekeeper_protocol::[<$name Push>]::decode(&*msg.remove_payload()) else {
tracing::debug!("failed to decode {}, player_id: {}", stringify!([<$name Push>]), player.basic_info.id);
return;
};
[<on_ $name:snake _push>](player, push);
},
)*
unhandled => ::tracing::warn!("can't find handler for push with message_id={unhandled}")
}
}
}
};
}
handle_request! {
UpdateSceneDate;
EntityActive;
EntityOnLanded;
}
handle_push! {
MovePackage;
}
pub fn handle_logic_message(player: &mut super::player::Player, msg: Message) {
match msg {
Message::Request { .. } => handle_request(player, msg),
Message::Push { .. } => handle_push(player, msg),
_ => tracing::warn!(
"handle_logic_message: wrong message type: {}, message_id: {}, player_id: {}",
msg.get_message_type(),
msg.get_message_id(),
player.basic_info.id,
),
}
}

View file

@ -0,0 +1,77 @@
use shorekeeper_protocol::{
EntityActiveRequest, EntityActiveResponse, EntityOnLandedRequest, EntityOnLandedResponse,
ErrorCode, MovePackagePush, UpdateSceneDateRequest, UpdateSceneDateResponse,
};
use crate::{logic::ecs::component::ComponentContainer, logic::player::Player, query_components};
pub fn on_update_scene_date_request(
_player: &Player,
_request: UpdateSceneDateRequest,
response: &mut UpdateSceneDateResponse,
) {
response.error_code = ErrorCode::Success.into();
}
pub fn on_entity_active_request(
player: &Player,
request: EntityActiveRequest,
response: &mut EntityActiveResponse,
) {
let world = player.world.borrow();
if !world.is_in_world(request.entity_id) {
tracing::debug!(
"EntityActiveRequest: entity with id {} doesn't exist, player_id: {}",
request.entity_id,
player.basic_info.id
);
return;
};
if let Some(position) = query_components!(world, request.entity_id, Position).0 {
// TODO: proper entity "activation" logic
response.pos = Some(position.0.get_position_protobuf());
response.rot = Some(position.0.get_rotation_protobuf());
}
response.component_pbs = Vec::new(); // not implemented
response.error_code = ErrorCode::Success.into();
}
pub fn on_entity_on_landed_request(
_: &Player,
request: EntityOnLandedRequest,
_: &mut EntityOnLandedResponse,
) {
tracing::debug!(
"EntityOnLandedRequest: entity with id {} landed",
request.entity_id
);
}
pub fn on_move_package_push(player: &mut Player, push: MovePackagePush) {
let world = player.world.borrow();
for moving_entity in push.moving_entities {
if !world.is_in_world(moving_entity.entity_id) {
tracing::debug!(
"MovePackage: entity with id {} doesn't exist",
moving_entity.entity_id
);
continue;
}
let Some(mut movement) = query_components!(world, moving_entity.entity_id, Movement).0
else {
tracing::warn!(
"MovePackage: entity {} doesn't have movement component",
moving_entity.entity_id
);
continue;
};
movement
.pending_movement_vec
.extend(moving_entity.move_infos);
}
}

View file

@ -0,0 +1,5 @@
mod transform;
mod vector;
pub use transform::Transform;
pub use vector::Vector3f;

View file

@ -0,0 +1,49 @@
use shorekeeper_protocol::{Rotator, TransformData};
use super::Vector3f;
#[derive(Default, Clone, Debug)]
pub struct Transform {
pub position: Vector3f,
pub rotation: Vector3f,
}
impl Transform {
pub fn get_position_protobuf(&self) -> shorekeeper_protocol::Vector {
self.position.to_protobuf()
}
pub fn get_rotation_protobuf(&self) -> Rotator {
Rotator {
pitch: self.rotation.x,
yaw: self.rotation.y,
roll: self.rotation.z,
}
}
pub fn set_position_from_protobuf(&mut self, pos: &shorekeeper_protocol::Vector) {
self.position.x = pos.x;
self.position.y = pos.y;
self.position.z = pos.z;
}
pub fn set_rotation_from_protobuf(&mut self, rotator: &Rotator) {
self.rotation.x = rotator.pitch;
self.rotation.y = rotator.yaw;
self.rotation.z = rotator.roll;
}
pub fn load_from_save(data: TransformData) -> Self {
Self {
position: Vector3f::from_save(data.position.unwrap_or_default()),
rotation: Vector3f::from_save(data.rotation.unwrap_or_default()),
}
}
pub fn build_save_data(&self) -> TransformData {
TransformData {
position: Some(self.position.save_data()),
rotation: Some(self.rotation.save_data()),
}
}
}

View file

@ -0,0 +1,42 @@
use shorekeeper_protocol::{Vector, VectorData};
#[derive(Default, Clone, PartialEq, Debug)]
pub struct Vector3f {
pub x: f32,
pub y: f32,
pub z: f32,
}
impl Vector3f {
pub fn to_protobuf(&self) -> Vector {
Vector {
x: self.x,
y: self.y,
z: self.z,
}
}
pub fn from_save(data: VectorData) -> Self {
Self {
x: data.x,
y: data.y,
z: data.z,
}
}
pub fn save_data(&self) -> VectorData {
VectorData {
x: self.x,
y: self.y,
z: self.z,
}
}
pub fn from_data(data: &shorekeeper_data::VectorData) -> Self {
Self {
x: data.get_x(),
y: data.get_y(),
z: data.get_z(),
}
}
}

View file

@ -0,0 +1,9 @@
pub mod components;
pub mod ecs;
pub mod handler;
pub mod math;
pub mod player;
pub mod role;
pub mod systems;
pub mod thread_mgr;
pub mod utils;

View file

@ -0,0 +1,72 @@
use shorekeeper_protocol::{
player_attr, BasicInfoNotify, PlayerAttr, PlayerAttrKey, PlayerAttrType, PlayerBasicData,
};
pub struct PlayerBasicInfo {
pub id: i32,
pub name: String,
pub sex: i32,
pub level: i32,
pub exp: i32,
pub head_photo: i32,
pub head_frame: i32,
}
impl PlayerBasicInfo {
pub fn build_notify(&self) -> BasicInfoNotify {
BasicInfoNotify {
id: self.id,
attributes: vec![
build_str_attr(PlayerAttrKey::Name, self.name.as_str()),
build_int_attr(PlayerAttrKey::Level, self.level),
build_int_attr(PlayerAttrKey::Exp, self.exp),
build_int_attr(PlayerAttrKey::Sex, self.sex),
build_int_attr(PlayerAttrKey::HeadPhoto, self.head_photo),
build_int_attr(PlayerAttrKey::HeadFrame, self.head_frame),
],
..Default::default()
}
}
pub fn load_from_save(data: PlayerBasicData) -> Self {
Self {
id: data.id,
name: data.name,
sex: data.sex,
level: data.level,
exp: data.exp,
head_photo: data.head_photo,
head_frame: data.head_frame,
}
}
pub fn build_save_data(&self) -> PlayerBasicData {
PlayerBasicData {
id: self.id,
name: self.name.clone(),
sex: self.sex,
level: self.level,
exp: self.exp,
head_photo: self.head_photo,
head_frame: self.head_frame,
}
}
}
#[inline]
fn build_int_attr(key: PlayerAttrKey, value: i32) -> PlayerAttr {
PlayerAttr {
key: key.into(),
value_type: PlayerAttrType::Int32.into(),
value: Some(player_attr::Value::Int32Value(value)),
}
}
#[inline]
fn build_str_attr(key: PlayerAttrKey, value: &str) -> PlayerAttr {
PlayerAttr {
key: key.into(),
value_type: PlayerAttrType::String.into(),
value: Some(player_attr::Value::StringValue(value.to_string())),
}
}

View file

@ -0,0 +1,9 @@
// In-world player metadata
pub struct InWorldPlayer {
pub player_id: i32,
pub player_name: String,
pub player_icon: i32,
pub level: i32,
pub group_type: i32,
}

View file

@ -0,0 +1,43 @@
use shorekeeper_data::instance_dungeon_data;
use shorekeeper_protocol::PlayerLocationData;
use crate::logic::math::{Transform, Vector3f};
pub struct PlayerLocation {
pub instance_id: i32,
pub position: Transform,
}
impl PlayerLocation {
const DEFAULT_INSTANCE_ID: i32 = 8;
pub fn load_from_save(data: PlayerLocationData) -> Self {
Self {
instance_id: data.instance_id,
position: Transform::load_from_save(data.position.unwrap_or_default()),
}
}
pub fn build_save_data(&self) -> PlayerLocationData {
PlayerLocationData {
instance_id: self.instance_id,
position: Some(self.position.build_save_data()),
}
}
}
impl Default for PlayerLocation {
fn default() -> Self {
let inst_data = instance_dungeon_data::iter()
.find(|d| d.id == Self::DEFAULT_INSTANCE_ID)
.unwrap();
Self {
instance_id: inst_data.id,
position: Transform {
position: Vector3f::from_data(&inst_data.born_position),
rotation: Vector3f::from_data(&inst_data.born_rotation),
},
}
}
}

View file

@ -0,0 +1,243 @@
use std::{cell::RefCell, collections::HashSet, rc::Rc, sync::Arc};
use basic_info::PlayerBasicInfo;
use common::time_util;
use location::PlayerLocation;
use player_func::PlayerFunc;
use shorekeeper_protocol::{
message::Message, PbGetRoleListNotify, PlayerBasicData, PlayerRoleData, PlayerSaveData,
ProtocolUnit,
};
use crate::session::Session;
use super::{
ecs::world::World,
role::{Role, RoleFormation},
};
mod basic_info;
mod in_world_player;
mod location;
mod player_func;
pub use in_world_player::InWorldPlayer;
pub struct Player {
session: Option<Arc<Session>>,
pub basic_info: PlayerBasicInfo,
pub role_list: Vec<Role>,
pub formation_list: Vec<RoleFormation>,
pub location: PlayerLocation,
pub func: PlayerFunc,
pub world: Rc<RefCell<World>>,
pub last_save_time: u64,
}
impl Player {
pub fn init(&mut self) {
if self.role_list.is_empty() {
self.on_first_enter();
}
// we need shorekeeper
// TODO: remove this part after implementing team switch
if !self.role_list.iter().any(|r| r.role_id == 1505) {
self.role_list.push(Role::new(1505));
}
self.formation_list.clear();
self.formation_list.push(RoleFormation {
id: 1,
cur_role: 1505,
role_id_set: HashSet::from([1505]),
is_current: true,
});
// End shorekeeper hardcode part
self.ensure_current_formation();
}
pub fn notify_general_data(&self) {
self.notify(self.basic_info.build_notify());
self.notify(self.func.build_func_open_notify());
self.notify(self.build_role_list_notify());
}
fn on_first_enter(&mut self) {
self.role_list.push(Self::create_main_character_role(
self.basic_info.name.clone(),
self.basic_info.sex,
));
let role = &self.role_list[0];
self.formation_list.push(RoleFormation {
id: 1,
cur_role: role.role_id,
role_id_set: HashSet::from([role.role_id]),
is_current: true,
});
self.location = PlayerLocation::default();
}
fn ensure_current_formation(&mut self) {
if self.formation_list.is_empty() {
let role = &self.role_list[0];
self.formation_list.push(RoleFormation {
id: 1,
cur_role: role.role_id,
role_id_set: HashSet::from([role.role_id]),
is_current: true,
});
}
if !self.formation_list.iter().any(|rf| rf.is_current) {
self.formation_list[0].is_current = true;
}
if let Some(rf) = self.formation_list.iter_mut().find(|rf| rf.is_current) {
if rf.role_id_set.is_empty() {
rf.role_id_set.insert(self.role_list[0].role_id);
}
if !rf.role_id_set.contains(&rf.cur_role) {
rf.cur_role = *rf.role_id_set.iter().nth(0).unwrap();
}
}
}
pub fn build_in_world_player(&self) -> InWorldPlayer {
InWorldPlayer {
player_id: self.basic_info.id,
player_name: self.basic_info.name.clone(),
player_icon: 0,
level: self.basic_info.level,
group_type: 1,
}
}
pub fn get_current_formation_role_list(&self) -> Vec<&Role> {
self.formation_list
.iter()
.find(|rf| rf.is_current)
.unwrap()
.role_id_set
.iter()
.map(|id| self.role_list.iter().find(|r| r.role_id == *id))
.flatten()
.collect()
}
pub fn get_cur_role_id(&self) -> i32 {
self.formation_list
.iter()
.find(|rf| rf.is_current)
.unwrap()
.cur_role
}
pub fn load_from_save(save_data: PlayerSaveData) -> Self {
let role_data = save_data.role_data.unwrap_or_default();
Self {
session: None,
basic_info: PlayerBasicInfo::load_from_save(save_data.basic_data.unwrap_or_default()),
role_list: role_data
.role_list
.into_iter()
.map(Role::load_from_save)
.collect(),
formation_list: role_data
.role_formation_list
.into_iter()
.map(RoleFormation::load_from_save)
.collect(),
location: save_data
.location_data
.map(PlayerLocation::load_from_save)
.unwrap_or_default(),
func: save_data
.func_data
.map(PlayerFunc::load_from_save)
.unwrap_or_default(),
world: Rc::new(RefCell::new(World::new())),
last_save_time: time_util::unix_timestamp(),
}
}
pub fn build_save_data(&self) -> PlayerSaveData {
PlayerSaveData {
basic_data: Some(self.basic_info.build_save_data()),
role_data: Some(PlayerRoleData {
role_list: self.role_list.iter().map(|r| r.build_save_data()).collect(),
role_formation_list: self
.formation_list
.iter()
.map(|rf| rf.build_save_data())
.collect(),
}),
location_data: Some(self.location.build_save_data()),
func_data: Some(self.func.build_save_data()),
}
}
pub fn set_session(&mut self, session: Arc<Session>) {
self.session = Some(session);
}
pub fn build_role_list_notify(&self) -> PbGetRoleListNotify {
PbGetRoleListNotify {
role_list: self.role_list.iter().map(|r| r.to_protobuf()).collect(),
}
}
pub fn notify(&self, content: impl ProtocolUnit) {
if let Some(session) = self.session.as_ref() {
session.forward_to_gateway(Message::Push {
sequence_number: 0,
message_id: content.get_message_id(),
payload: Some(content.encode_to_vec().into_boxed_slice()),
});
}
}
pub fn respond(&self, content: impl ProtocolUnit, rpc_id: u16) {
if let Some(session) = self.session.as_ref() {
session.forward_to_gateway(Message::Response {
sequence_number: 0,
message_id: content.get_message_id(),
rpc_id,
payload: Some(content.encode_to_vec().into_boxed_slice()),
});
}
}
fn create_main_character_role(name: String, sex: i32) -> Role {
let mut role = match sex {
0 => Role::new(Role::MAIN_CHARACTER_FEMALE_ID),
1 => Role::new(Role::MAIN_CHARACTER_MALE_ID),
_ => unreachable!(),
};
role.name = name;
role
}
pub fn create_default_save_data(id: i32, name: String, sex: i32) -> PlayerSaveData {
PlayerSaveData {
basic_data: Some(PlayerBasicData {
id,
name,
sex,
level: 1,
head_photo: 1505,
head_frame: 80060009,
..Default::default()
}),
..Default::default()
}
}
}

View file

@ -0,0 +1,46 @@
use std::collections::HashMap;
use shorekeeper_data::function_condition_data;
use shorekeeper_protocol::{FuncOpenNotify, Function, PlayerFuncData};
pub struct PlayerFunc {
pub func_map: HashMap<i32, i32>,
}
impl PlayerFunc {
pub fn load_from_save(data: PlayerFuncData) -> Self {
PlayerFunc {
func_map: data.func_map,
}
}
pub fn build_save_data(&self) -> PlayerFuncData {
PlayerFuncData {
func_map: self.func_map.clone(),
}
}
pub fn build_func_open_notify(&self) -> FuncOpenNotify {
FuncOpenNotify {
func: self
.func_map
.iter()
.map(|(id, flag)| Function {
id: *id,
flag: *flag,
})
.collect(),
}
}
}
impl Default for PlayerFunc {
fn default() -> Self {
Self {
func_map: function_condition_data::iter()
.filter(|fc| fc.open_condition_id == 0 && fc.is_on)
.map(|fc| (fc.function_id, 2))
.collect(),
}
}
}

View file

@ -0,0 +1,30 @@
use std::collections::HashSet;
use shorekeeper_protocol::RoleFormationData;
pub struct RoleFormation {
pub id: i32,
pub cur_role: i32,
pub role_id_set: HashSet<i32>,
pub is_current: bool,
}
impl RoleFormation {
pub fn load_from_save(data: RoleFormationData) -> Self {
Self {
id: data.formation_id,
cur_role: data.cur_role,
role_id_set: data.role_id_list.into_iter().collect(),
is_current: data.is_current,
}
}
pub fn build_save_data(&self) -> RoleFormationData {
RoleFormationData {
formation_id: self.id,
cur_role: self.cur_role,
role_id_list: self.role_id_set.iter().cloned().collect(),
is_current: self.is_current,
}
}
}

View file

@ -0,0 +1,89 @@
use std::collections::HashMap;
use common::time_util;
use shorekeeper_data::role_info_data;
use shorekeeper_protocol::{ArrayIntInt, RoleData, RoleInfo};
mod formation;
pub use formation::RoleFormation;
pub struct Role {
pub role_id: i32,
pub name: String,
pub level: i32,
pub exp: i32,
pub breakthrough: i32,
pub skill_map: HashMap<i32, i32>,
pub star: i32,
pub favor: i32,
pub create_time: u32,
}
impl Role {
pub const MAIN_CHARACTER_MALE_ID: i32 = 1501;
pub const MAIN_CHARACTER_FEMALE_ID: i32 = 1502;
pub fn new(role_id: i32) -> Self {
let data = role_info_data::iter().find(|d| d.id == role_id).unwrap();
Self {
role_id,
name: String::with_capacity(0),
level: data.max_level,
exp: 0,
breakthrough: 0,
skill_map: HashMap::new(), // TODO!
star: 0,
favor: 0,
create_time: time_util::unix_timestamp() as u32,
}
}
pub fn to_protobuf(&self) -> RoleInfo {
RoleInfo {
role_id: self.role_id,
name: self.name.clone(),
level: self.level,
exp: self.exp,
breakthrough: self.breakthrough,
create_time: self.create_time,
skills: self
.skill_map
.iter()
.map(|(k, v)| ArrayIntInt { key: *k, value: *v })
.collect(),
star: self.star,
favor: self.favor,
..Default::default()
}
}
pub fn load_from_save(data: RoleData) -> Self {
Self {
role_id: data.role_id,
name: data.name,
level: data.level,
exp: data.exp,
breakthrough: data.breakthrough,
skill_map: data.skill_map,
star: data.star,
favor: data.favor,
create_time: data.create_time,
}
}
pub fn build_save_data(&self) -> RoleData {
RoleData {
role_id: self.role_id,
name: self.name.clone(),
level: self.level,
exp: self.exp,
breakthrough: self.breakthrough,
skill_map: self.skill_map.clone(),
star: self.star,
favor: self.favor,
create_time: self.create_time,
..Default::default()
}
}
}

View file

@ -0,0 +1,28 @@
use std::{cell::RefMut, sync::LazyLock};
use super::{ecs::world::World, player::Player};
mod movement;
use movement::MovementSystem;
macro_rules! enabled_systems {
($($sys:ident;)*) => {
static SYSTEMS: LazyLock<Box<[Box<dyn System>]>> = LazyLock::new(|| {
vec![
$(Box::new($sys::new()) as Box<dyn System>,)*
].into_boxed_slice()
});
pub fn tick_systems(world: &mut World, players: &mut [::std::cell::RefMut<Player>]) {
SYSTEMS.iter().for_each(|system| system.tick(world, players));
}
};
}
pub trait System: Send + Sync + 'static {
fn tick(&self, world: &mut World, players: &mut [RefMut<Player>]);
}
enabled_systems! {
MovementSystem;
}

View file

@ -0,0 +1,70 @@
use shorekeeper_protocol::{MovePackageNotify, MovingEntityData};
use crate::{logic::ecs::component::ComponentContainer, query_components};
use std::cell::RefMut;
use crate::{
logic::{ecs::world::World, player::Player},
query_with,
};
use super::System;
pub(super) struct MovementSystem;
impl System for MovementSystem {
fn tick(&self, world: &mut World, players: &mut [RefMut<Player>]) {
let mut notify = MovePackageNotify::default();
for (entity, mut movement, mut position) in query_with!(world, Movement, Position) {
if movement.pending_movement_vec.is_empty() {
continue;
}
let mut moving_entity_data = MovingEntityData {
entity_id: entity.into(),
..Default::default()
};
while let Some(info) = movement.pending_movement_vec.pop_front() {
if let Some(location) = info.location.as_ref() {
position.0.set_position_from_protobuf(location);
}
if let Some(rotation) = info.rotation.as_ref() {
position.0.set_rotation_from_protobuf(rotation);
}
moving_entity_data.move_infos.push(info);
}
tracing::debug!(
"MovementSystem: entity with id {} moved to {:?}",
i64::from(entity),
&position.0.position
);
notify.moving_entities.push(moving_entity_data);
if let (Some(_), Some(owner)) =
query_components!(world, i64::from(entity), PlayerEntityMarker, OwnerPlayer)
{
if let Some(player) = players.iter_mut().find(|pl| pl.basic_info.id == owner.0) {
player.location.position = position.0.clone();
}
}
}
if !notify.moving_entities.is_empty() {
players
.iter()
.for_each(|player| player.notify(notify.clone()))
}
}
}
impl MovementSystem {
pub fn new() -> Self {
Self
}
}

View file

@ -0,0 +1,217 @@
use std::{
cell::RefCell,
collections::{HashMap, VecDeque},
rc::Rc,
sync::{
atomic::{AtomicUsize, Ordering},
mpsc, Arc, OnceLock,
},
thread,
time::Duration,
};
use common::time_util;
use shorekeeper_protocol::{message::Message, JoinSceneNotify, TransitionOptionPb};
use shorekeeper_protocol::{AfterJoinSceneNotify, EnterGameResponse, PlayerSaveData};
use crate::{
player_save_task::{self, PlayerSaveReason},
session::Session,
};
use super::{ecs::world::World, player::Player, utils::world_util};
pub enum LogicInput {
AddPlayer {
player_id: i32,
enter_rpc_id: u16,
session: Arc<Session>,
player_save_data: PlayerSaveData,
},
RemovePlayer {
player_id: i32,
},
ProcessMessage {
player_id: i32,
message: Message,
},
}
#[derive(Clone)]
pub struct LogicThreadHandle {
sender: mpsc::Sender<LogicInput>,
load: Arc<AtomicUsize>,
}
static THREAD_HANDLES: OnceLock<Box<[LogicThreadHandle]>> = OnceLock::new();
pub fn start_logic_threads(num_threads: usize) {
if THREAD_HANDLES.get().is_some() {
tracing::error!("start_logic_threads: logic threads are already running!");
return;
}
let _ = THREAD_HANDLES.set(
(0..num_threads)
.map(|_| {
let (tx, rx) = mpsc::channel();
let load = Arc::new(AtomicUsize::new(0));
let handle = LogicThreadHandle {
sender: tx,
load: load.clone(),
};
thread::spawn(move || logic_thread_func(rx, load));
handle
})
.collect(),
);
}
// Thread-local logic state
struct LogicState {
thread_load: Arc<AtomicUsize>, // shared parameter for load-balancing
worlds: HashMap<i32, Rc<RefCell<World>>>, // owner_id - world
players: HashMap<i32, RefCell<Player>>, // id - player
}
fn logic_thread_func(receiver: mpsc::Receiver<LogicInput>, load: Arc<AtomicUsize>) {
const RECV_TIMEOUT: Duration = Duration::from_millis(50);
const PLAYER_SAVE_PERIOD: u64 = 30;
let mut state = LogicState {
thread_load: load,
worlds: HashMap::new(),
players: HashMap::new(),
};
let mut input_queue = VecDeque::with_capacity(32);
loop {
if let Ok(input) = receiver.recv_timeout(RECV_TIMEOUT) {
input_queue.push_back(input);
while let Ok(input) = receiver.try_recv() {
input_queue.push_back(input);
}
}
while let Some(input) = input_queue.pop_front() {
handle_logic_input(&mut state, input);
}
state.worlds.values().for_each(|world| {
let mut world = world.borrow_mut();
let mut players = world
.player_ids()
.map(|id| state.players.get(id).map(|pl| pl.borrow_mut()))
.flatten()
.collect::<Box<_>>();
super::systems::tick_systems(&mut world, &mut players);
});
state.players.values().for_each(|player| {
let mut player = player.borrow_mut();
if time_util::unix_timestamp() - player.last_save_time > PLAYER_SAVE_PERIOD {
player_save_task::push(
player.basic_info.id,
player.build_save_data(),
PlayerSaveReason::PeriodicalSave,
);
player.last_save_time = time_util::unix_timestamp();
}
})
}
}
fn handle_logic_input(state: &mut LogicState, input: LogicInput) {
match input {
LogicInput::AddPlayer {
player_id,
enter_rpc_id,
session,
player_save_data,
} => {
let player = state
.players
.entry(player_id)
.or_insert(RefCell::new(Player::load_from_save(player_save_data)));
let mut player = player.borrow_mut();
state.worlds.insert(player_id, player.world.clone());
player.init();
player.set_session(session);
player.notify_general_data();
player
.world
.borrow_mut()
.set_in_world_player_data(player.build_in_world_player());
world_util::add_player_entities(&mut player.world.borrow_mut(), &player);
let scene_info = world_util::build_scene_information(
&player.world.borrow(),
player.location.instance_id,
player.basic_info.id,
);
player.notify(JoinSceneNotify {
max_entity_id: i64::MAX,
scene_info: Some(scene_info),
transition_option: Some(TransitionOptionPb::default()),
});
player.respond(EnterGameResponse::default(), enter_rpc_id);
player.notify(AfterJoinSceneNotify::default());
drop(player);
state
.thread_load
.store(state.players.len(), Ordering::Relaxed);
}
LogicInput::ProcessMessage { player_id, message } => {
let Some(player) = state.players.get_mut(&player_id) else {
tracing::warn!("logic_thread: process message requested, but player with id {player_id} doesn't exist");
return;
};
super::handler::handle_logic_message(&mut player.borrow_mut(), message);
}
LogicInput::RemovePlayer { player_id } => {
let Some(player) = state.players.remove(&player_id) else {
tracing::warn!(
"logic_thread: player remove requested, but it doesn't exist (id: {player_id})"
);
return;
};
let _ = state.worlds.remove(&player_id);
// TODO: kick co-op players from removed world
player_save_task::push(
player_id,
player.borrow().build_save_data(),
PlayerSaveReason::PlayerLogicStopped,
);
}
}
}
impl LogicThreadHandle {
pub fn input(&self, input: LogicInput) {
let _ = self.sender.send(input);
}
}
pub fn get_least_loaded_thread() -> LogicThreadHandle {
let handles = THREAD_HANDLES.get().unwrap();
handles
.iter()
.min_by_key(|h| h.load.load(Ordering::Relaxed))
.unwrap()
.clone()
}

View file

@ -0,0 +1,26 @@
use crate::logic::ecs::component::ComponentContainer;
use shorekeeper_protocol::{EntityPb, PlayerSceneAoiData};
use crate::{logic::ecs::world::World, query_with};
pub fn build_scene_add_on_init_data(world: &World) -> PlayerSceneAoiData {
let entities = query_with!(world, PlayerEntityMarker)
.into_iter()
.map(|(e, _)| e)
.collect::<Vec<_>>();
let mut aoi_data = PlayerSceneAoiData::default();
for entity in entities {
let mut pb = EntityPb::default();
pb.id = entity.into();
world
.get_entity_components(entity)
.into_iter()
.for_each(|comp| comp.set_pb_data(&mut pb));
aoi_data.entities.push(pb);
}
aoi_data
}

View file

@ -0,0 +1,2 @@
pub mod entity_serializer;
pub mod world_util;

View file

@ -0,0 +1,127 @@
use shorekeeper_data::base_property_data;
use shorekeeper_protocol::{
EntityConfigType, FightRoleInfo, FightRoleInfos, LivingStatus, SceneInformation, SceneMode,
ScenePlayerInformation, SceneTimeInfo,
};
use crate::{
logic::{
components::{
Attribute, EntityConfig, Movement, OwnerPlayer, PlayerEntityMarker, Position,
Visibility,
},
ecs::{component::ComponentContainer, world::World},
player::Player,
},
query_with,
};
use super::entity_serializer;
pub fn add_player_entities(world: &mut World, player: &Player) {
let cur_role_id = player.get_cur_role_id();
for role in player.get_current_formation_role_list() {
let id = world
.create_entity()
.with(ComponentContainer::PlayerEntityMarker(PlayerEntityMarker))
.with(ComponentContainer::EntityConfig(EntityConfig {
config_id: role.role_id,
config_type: EntityConfigType::Character,
}))
.with(ComponentContainer::OwnerPlayer(OwnerPlayer(
player.basic_info.id,
)))
.with(ComponentContainer::Position(Position(
player.location.position.clone(),
)))
.with(ComponentContainer::Visibility(Visibility(
role.role_id == cur_role_id,
)))
.with(ComponentContainer::Attribute(Attribute::from_data(
base_property_data::iter()
.find(|d| d.id == role.role_id)
.unwrap(),
)))
.with(ComponentContainer::Movement(Movement::default()))
.build();
tracing::debug!(
"created player entity, id: {}, role_id: {}",
i64::from(id),
role.role_id
);
}
}
pub fn build_scene_information(world: &World, instance_id: i32, owner_id: i32) -> SceneInformation {
SceneInformation {
scene_id: String::new(),
instance_id,
owner_id,
dynamic_entity_list: Vec::new(),
blackboard_params: Vec::new(),
end_time: 0,
aoi_data: Some(entity_serializer::build_scene_add_on_init_data(world)),
player_infos: build_player_info_list(world),
mode: SceneMode::Single.into(),
time_info: Some(SceneTimeInfo {
owner_time_clock_time_span: 0,
hour: 8,
minute: 0,
}),
cur_context_id: owner_id as i64,
..Default::default()
}
}
fn build_player_info_list(world: &World) -> Vec<ScenePlayerInformation> {
world
.players()
.map(|sp| {
let (cur_role_id, transform) = query_with!(
world,
PlayerEntityMarker,
OwnerPlayer,
Visibility,
EntityConfig,
Position
)
.into_iter()
.find_map(|(_, _, owner, visibility, conf, pos)| {
(sp.player_id == owner.0 && visibility.0).then_some((conf.config_id, pos.0.clone()))
})
.unwrap_or_default();
let active_characters =
query_with!(world, PlayerEntityMarker, OwnerPlayer, EntityConfig)
.into_iter()
.filter(|(_, _, owner, _)| owner.0 == sp.player_id);
ScenePlayerInformation {
cur_role: cur_role_id,
group_type: sp.group_type,
player_id: sp.player_id,
player_icon: sp.player_icon,
player_name: sp.player_name.clone(),
level: sp.level,
location: Some(transform.get_position_protobuf()),
rotation: Some(transform.get_rotation_protobuf()),
fight_role_infos: Vec::from([FightRoleInfos {
group_type: sp.group_type,
living_status: LivingStatus::Alive.into(),
cur_role: cur_role_id,
is_retain: true,
fight_role_infos: active_characters
.map(|(id, _, _, conf)| FightRoleInfo {
entity_id: id.into(),
role_id: conf.config_id,
})
.collect(),
..Default::default()
}]),
..Default::default()
}
})
.collect()
}

35
game-server/src/main.rs Normal file
View file

@ -0,0 +1,35 @@
use std::sync::{Arc, LazyLock};
use anyhow::Result;
use common::config_util;
use config::ServiceConfig;
use session::SessionManager;
mod config;
mod gateway_connection;
mod logic;
mod player_save_task;
mod service_message_handler;
mod session;
#[tokio::main]
async fn main() -> Result<()> {
static CONFIG: LazyLock<ServiceConfig> =
LazyLock::new(|| config_util::load_or_create("gameserver.toml"));
static SESSION_MGR: LazyLock<SessionManager> = LazyLock::new(SessionManager::default);
::common::splash::print_splash();
::common::logging::init(::tracing::Level::DEBUG);
shorekeeper_data::load_json_data("assets/logic/json")?;
let database = Arc::new(shorekeeper_database::connect_to(&CONFIG.database).await?);
shorekeeper_database::run_migrations(database.as_ref()).await?;
logic::thread_mgr::start_logic_threads(1);
player_save_task::start(database.clone());
gateway_connection::init(CONFIG.service_id, &CONFIG.gateway_end_point);
service_message_handler::run(&CONFIG.service_end_point, &SESSION_MGR, database).await?;
Ok(())
}

View file

@ -0,0 +1,61 @@
use std::sync::{Arc, OnceLock};
use tokio::sync::mpsc;
use shorekeeper_database::{query, PgPool};
use shorekeeper_protocol::{PlayerSaveData, Protobuf};
static SENDER: OnceLock<mpsc::Sender<PlayerSaveQuery>> = OnceLock::new();
#[derive(Debug)]
pub enum PlayerSaveReason {
PeriodicalSave,
PlayerLogicStopped,
}
pub fn start(db: Arc<PgPool>) {
let _ = SENDER.get_or_init(|| {
let (tx, rx) = mpsc::channel(32);
tokio::spawn(async move { task_loop(rx, db).await });
tx
});
}
pub fn push(player_id: i32, save_data: PlayerSaveData, reason: PlayerSaveReason) {
tracing::debug!(
"player_save_task: requesting save for player with id {player_id}, reason: {reason:?}"
);
let _ = SENDER.get().unwrap().blocking_send(PlayerSaveQuery {
player_id,
save_data,
});
}
struct PlayerSaveQuery {
pub player_id: i32,
pub save_data: PlayerSaveData,
}
async fn task_loop(mut receiver: mpsc::Receiver<PlayerSaveQuery>, db: Arc<PgPool>) {
loop {
let Some(save_query) = receiver.recv().await else {
tracing::warn!("player_save_task: channel was closed, exitting");
return;
};
let bin_data = save_query.save_data.encode_to_vec();
let _ = query("UPDATE t_player_data SET bin_data = ($1) WHERE player_id = ($2)")
.bind(bin_data)
.bind(save_query.player_id)
.execute(db.as_ref())
.await
.inspect_err(|err| {
tracing::error!(
"player_save_task: failed to save data for player_id: {}, err: {err}",
save_query.player_id
)
});
}
}

View file

@ -0,0 +1,318 @@
use std::sync::{Arc, OnceLock};
use common::time_util;
use shorekeeper_database::{models, query, query_as, PgPool};
use shorekeeper_network::{config::ServiceEndPoint, ServiceListener, ServiceMessage};
use shorekeeper_protocol::{
message::Message, CreatePlayerDataRequest, CreatePlayerDataResponse, ErrorCode,
ForwardClientMessagePush, MessageID, PlayerSaveData, Protobuf, ProtocolUnit,
StartPlayerSessionRequest, StartPlayerSessionResponse, StopPlayerSessionPush,
};
use crate::{
gateway_connection,
logic::{self, player::Player, thread_mgr::LogicInput},
session::{Session, SessionManager},
};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Failed to bind ServiceListener")]
BindFailed,
}
pub async fn run(
listen_end_point: &ServiceEndPoint,
session_mgr: &'static SessionManager,
db: Arc<PgPool>,
) -> Result<(), Error> {
static IS_STARTED: OnceLock<()> = OnceLock::new();
if IS_STARTED.set(()).is_err() {
tracing::error!("service_message_handler: task already started");
return Ok(());
}
let listener = ServiceListener::bind(listen_end_point)
.await
.map_err(|_| Error::BindFailed)?;
handler_loop(listener, session_mgr, db).await;
Ok(())
}
async fn handler_loop(
mut listener: ServiceListener,
session_mgr: &'static SessionManager,
db: Arc<PgPool>,
) {
loop {
let Some(message) = listener.receive().await else {
tracing::warn!("service_message_handler: channel was closed, exitting handler task");
return;
};
tracing::debug!(
"received message from service: {}, rpc_id: {} message_id: {}",
message.src_service_id,
message.rpc_id,
message.message_id
);
match message.message_id {
CreatePlayerDataRequest::MESSAGE_ID => {
on_create_player_data_request(message, db.as_ref()).await
}
StartPlayerSessionRequest::MESSAGE_ID => {
on_start_player_session_request(message, session_mgr, db.as_ref()).await
}
StopPlayerSessionPush::MESSAGE_ID => {
on_stop_player_session_push(message, session_mgr).await
}
ForwardClientMessagePush::MESSAGE_ID => {
on_forward_client_message_push(message, session_mgr).await
}
unhandled => tracing::warn!(
"unhandled service message id: {unhandled}, from service_id: {}",
message.src_service_id
),
}
}
}
async fn on_start_player_session_request(
message: ServiceMessage,
session_mgr: &'static SessionManager,
db: &PgPool,
) {
let Ok(request) = StartPlayerSessionRequest::decode(message.data.as_ref()) else {
tracing::warn!(
"failed to decode StartPlayerSessionRequest, data: {}",
hex::encode(message.data.as_ref())
);
return;
};
tracing::debug!(
"StartPlayerSession: gateway_id: {}, session_id: {}, player_id: {}",
message.src_service_id,
request.gateway_session_id,
request.player_id
);
let mut response = StartPlayerSessionResponse {
code: ErrorCode::Success.into(),
gateway_session_id: request.gateway_session_id,
};
let Ok(player_data): Result<models::PlayerDataRow, _> =
query_as("SELECT * FROM t_player_data WHERE player_id = ($1)")
.bind(request.player_id)
.fetch_one(db)
.await
.inspect_err(|err| {
tracing::error!(
"failed to fetch player data, player_id: {}, err: {err}",
request.player_id
)
})
else {
response.code = ErrorCode::QueryPlayerDataFailed.into();
send_to_gateway(response, message.rpc_id).await;
return;
};
let Ok(player_save_data) = PlayerSaveData::decode(player_data.bin_data.as_slice()) else {
tracing::error!(
"StartPlayerSession: player data is corrupted, player id: {}",
request.player_id
);
return;
};
let logic_thread = logic::thread_mgr::get_least_loaded_thread();
let session = Arc::new(Session {
gateway_id: message.src_service_id,
session_id: request.gateway_session_id,
player_id: player_data.player_id,
logic_thread,
});
session.logic_thread.input(LogicInput::AddPlayer {
player_id: player_data.player_id,
enter_rpc_id: message.rpc_id,
session: session.clone(),
player_save_data,
});
session_mgr.add(session.clone());
send_to_gateway(response, message.rpc_id).await;
}
async fn on_stop_player_session_push(
message: ServiceMessage,
session_mgr: &'static SessionManager,
) {
let Ok(push) = StopPlayerSessionPush::decode(message.data.as_ref()) else {
tracing::warn!(
"failed to decode StopPlayerSessionPush, data: {}",
hex::encode(message.data.as_ref())
);
return;
};
let Some(session) = session_mgr.remove(message.src_service_id, push.gateway_session_id) else {
tracing::debug!(
"StopPlayerSessionPush: session with id {} ({}-{}) not found",
Session::global_id(message.src_service_id, push.gateway_session_id),
message.src_service_id,
push.gateway_session_id
);
return;
};
session.logic_thread.input(LogicInput::RemovePlayer {
player_id: session.player_id,
});
tracing::debug!(
"StopPlayerSession: player_id: {}, session stopped successfully",
session.player_id
);
}
async fn on_forward_client_message_push(
message: ServiceMessage,
session_mgr: &'static SessionManager,
) {
let Ok(push) = ForwardClientMessagePush::decode(message.data.as_ref()) else {
tracing::warn!(
"failed to decode ForwardClientMessagePush, data: {}",
hex::encode(message.data.as_ref())
);
return;
};
let Some(session) = session_mgr.get(message.src_service_id, push.gateway_session_id) else {
tracing::debug!(
"ForwardClientMessagePush: session with id {} ({}-{}) not found",
Session::global_id(message.src_service_id, push.gateway_session_id),
message.src_service_id,
push.gateway_session_id
);
return;
};
let Ok(message) = Message::decode(&push.data).inspect_err(|err| {
tracing::warn!("ForwardClientMessagePush: failed to decode underlying message, err: {err}")
}) else {
return;
};
session.logic_thread.input(LogicInput::ProcessMessage {
player_id: session.player_id,
message,
});
}
async fn on_create_player_data_request(message: ServiceMessage, db: &PgPool) {
let Ok(request) = CreatePlayerDataRequest::decode(message.data.as_ref()) else {
tracing::warn!(
"failed to decode CreatePlayerDataRequest, data: {}",
hex::encode(message.data.as_ref())
);
return;
};
let mut response = CreatePlayerDataResponse {
code: ErrorCode::Success.into(),
session_id: request.session_id,
name: request.name.clone(),
sex: request.sex,
player_id: 0,
};
if !matches!(request.name.len(), (1..=12)) {
tracing::debug!(
"character name is too long, name: {}, len: {}",
&request.name,
request.name.len()
);
response.code = ErrorCode::InvalidCharacterName.into();
send_to_gateway(response, message.rpc_id).await;
return;
}
if !matches!(request.sex, (0..=1)) {
response.code = ErrorCode::CreateCharacterFailed.into();
send_to_gateway(response, message.rpc_id).await;
return;
}
if query("SELECT * from t_user_uid WHERE user_id = ($1)")
.bind(request.user_id.as_str())
.fetch_optional(db)
.await
.inspect_err(|err| tracing::error!("failed to fetch data from t_user_uid: {err}"))
.ok()
.flatten()
.is_some()
{
response.code = ErrorCode::CharacterAlreadyCreated.into();
send_to_gateway(response, message.rpc_id).await;
return;
}
let user_uid_row: models::UserUidRow = match query_as(
"INSERT INTO t_user_uid (user_id, sex, create_time_stamp) VALUES ($1, $2, $3) RETURNING *",
)
.bind(request.user_id.as_str())
.bind(request.sex)
.bind(time_util::unix_timestamp() as i64)
.fetch_one(db)
.await
{
Ok(row) => row,
Err(err) => {
tracing::error!("failed to create t_user_uid entry, error: {err}");
response.code = ErrorCode::InternalError.into();
send_to_gateway(response, message.rpc_id).await;
return;
}
};
if let Some(err) =
query("INSERT INTO t_player_data (player_id, name, bin_data) VALUES ($1, $2, $3)")
.bind(user_uid_row.player_id)
.bind(request.name.as_str())
.bind(
Player::create_default_save_data(
user_uid_row.player_id,
request.name.clone(),
request.sex,
)
.encode_to_vec(),
)
.execute(db)
.await
.err()
{
tracing::error!("failed to create default player data entry, error: {err}");
response.code = ErrorCode::InternalError.into();
send_to_gateway(response, message.rpc_id).await;
return;
}
response.player_id = user_uid_row.player_id;
send_to_gateway(response, message.rpc_id).await;
}
async fn send_to_gateway(content: impl ProtocolUnit, rpc_id: u16) {
gateway_connection::push_message(ServiceMessage {
src_service_id: 0,
rpc_id,
message_id: content.get_message_id(),
data: content.encode_to_vec().into_boxed_slice(),
})
.await;
}

View file

@ -0,0 +1,28 @@
use std::sync::Arc;
use dashmap::{mapref::one::Ref, DashMap};
use super::Session;
#[derive(Default)]
pub struct SessionManager {
session_map: DashMap<u64, Arc<Session>>,
}
impl SessionManager {
pub fn add(&self, session: Arc<Session>) {
self.session_map
.insert(session.get_global_session_id(), session);
}
pub fn get(&self, gateway_id: u32, session_id: u32) -> Option<Ref<'_, u64, Arc<Session>>> {
self.session_map
.get(&Session::global_id(gateway_id, session_id))
}
pub fn remove(&self, gateway_id: u32, session_id: u32) -> Option<Arc<Session>> {
self.session_map
.remove(&Session::global_id(gateway_id, session_id))
.map(|kv| kv.1)
}
}

View file

@ -0,0 +1,41 @@
mod manager;
pub use manager::SessionManager;
use shorekeeper_network::ServiceMessage;
use shorekeeper_protocol::{message::Message, ForwardClientMessagePush, MessageID, Protobuf};
use crate::{gateway_connection, logic::thread_mgr::LogicThreadHandle};
pub struct Session {
pub gateway_id: u32,
pub session_id: u32,
pub player_id: i32,
pub logic_thread: LogicThreadHandle,
}
impl Session {
pub fn forward_to_gateway(&self, message: Message) {
let mut data = vec![0u8; message.get_encoding_length()];
message.encode(&mut data).unwrap();
let push = ForwardClientMessagePush {
gateway_session_id: self.session_id,
data,
};
gateway_connection::push_message_sync(ServiceMessage {
src_service_id: 0,
rpc_id: 0,
message_id: ForwardClientMessagePush::MESSAGE_ID,
data: push.encode_to_vec().into_boxed_slice(),
})
}
pub fn get_global_session_id(&self) -> u64 {
Self::global_id(self.gateway_id, self.session_id)
}
#[inline]
pub fn global_id(gateway_id: u32, session_id: u32) -> u64 {
((gateway_id as u64) << 32) | session_id as u64
}
}

31
gateway-server/Cargo.toml Normal file
View file

@ -0,0 +1,31 @@
[package]
name = "gateway-server"
edition = "2021"
version.workspace = true
[dependencies]
# Framework
tokio.workspace = true
# Networking
kcp = { workspace = true, features = ["tokio"] }
# Serialization
serde.workspace = true
# Tracing
tracing.workspace = true
# Util
anyhow.workspace = true
thiserror.workspace = true
paste.workspace = true
dashmap.workspace = true
hex.workspace = true
# Internal
common.workspace = true
shorekeeper-database.workspace = true
shorekeeper-network.workspace = true
shorekeeper-protocol.workspace = true
shorekeeper-protokey.workspace = true

View file

@ -0,0 +1,20 @@
service_id = 1
[network]
kcp_port = 7777
[protokey]
builtin_encryption_msg_id = [111, 112]
use_client_key = true
[service_end_point]
addr = "tcp://127.0.0.1:10003"
[game_server_end_point]
addr = "tcp://127.0.0.1:10004"
[database]
host = "localhost:5432"
user_name = "postgres"
password = ""
db_name = "shorekeeper"

View file

@ -0,0 +1,19 @@
[package]
name = "kcp"
edition = "2021"
version.workspace = true
[features]
fastack-conserve = []
tokio = ["dep:tokio"]
[dependencies]
bytes = "1.6.0"
log = "0.4.21"
thiserror = "1.0.58"
tokio = { version = "1.37.0", optional = true, features = ["io-util"] }
[dev-dependencies]
time = "0.3.34"
rand = "0.8.5"

View file

@ -0,0 +1,54 @@
use std::{
error::Error as StdError,
io::{self, ErrorKind},
};
/// KCP protocol errors
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("conv inconsistent, expected {0}, found {1}")]
ConvInconsistent(u32, u32),
#[error("invalid mtu {0}")]
InvalidMtu(usize),
#[error("invalid segment size {0}")]
InvalidSegmentSize(usize),
#[error("invalid segment data size, expected {0}, found {1}")]
InvalidSegmentDataSize(usize, usize),
#[error("{0}")]
IoError(
#[from]
#[source]
io::Error,
),
#[error("need to call update() once")]
NeedUpdate,
#[error("recv queue is empty")]
RecvQueueEmpty,
#[error("expecting fragment")]
ExpectingFragment,
#[error("command {0} is not supported")]
UnsupportedCmd(u8),
#[error("user's send buffer is too big")]
UserBufTooBig,
#[error("user's recv buffer is too small")]
UserBufTooSmall,
}
fn make_io_error<T>(kind: ErrorKind, msg: T) -> io::Error
where
T: Into<Box<dyn StdError + Send + Sync>>,
{
io::Error::new(kind, msg)
}
impl From<Error> for io::Error {
fn from(err: Error) -> Self {
let kind = match err {
Error::IoError(err) => return err,
Error::RecvQueueEmpty | Error::ExpectingFragment => ErrorKind::WouldBlock,
_ => ErrorKind::Other,
};
make_io_error(kind, err)
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,17 @@
extern crate bytes;
#[macro_use]
extern crate log;
mod error;
mod kcp;
/// The `KCP` prelude
pub mod prelude {
pub use super::{get_conv, Kcp, KCP_OVERHEAD};
}
pub use error::Error;
pub use kcp::{get_conv, get_sn, set_conv, Kcp, KCP_OVERHEAD};
/// KCP result
pub type KcpResult<T> = Result<T, Error>;

View file

@ -0,0 +1,8 @@
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAwAcVNpSs+nDFnAY9W2AccjI2H9/heX8KJVj9/ABF00U72dqSQqKO
nrxCvp6P3yAAbhELFUbEkSPiPs67CC4gkg51k2gelQcyTADqV9hr3oYw4SnCLQCj
+np34uqjd3gfwPWLiYFI1KDzAFO8dlVEKVnQjOJcRYQDGx9XmGHy2OtaOoQOQlgx
iATkOyRXc4jtlpXtWvB6cAE0KSqgkmnfDxSN/tYP3ltprtdAQN8ZiYQrAEmKhnCj
KPi1TC/EWyWxRiZK5KmtLXWMNaODlL1XiqKQbHwo9bKz7I/z3bdb+/YtaqpiiwWO
IAJsAahEp/Os8Pc+BVruOTF7ZEUEMa2poQIDAQAB
-----END RSA PUBLIC KEY-----

View file

@ -0,0 +1,24 @@
use common::config_util::TomlConfig;
use serde::Deserialize;
use shorekeeper_database::DatabaseSettings;
use shorekeeper_network::config::ServiceEndPoint;
use shorekeeper_protokey::ProtoKeySettings;
#[derive(Deserialize)]
pub struct ServerConfig {
pub service_id: u32,
pub network: NetworkSettings,
pub database: DatabaseSettings,
pub protokey: ProtoKeySettings,
pub service_end_point: ServiceEndPoint,
pub game_server_end_point: ServiceEndPoint,
}
#[derive(Deserialize)]
pub struct NetworkSettings {
pub kcp_port: u16,
}
impl TomlConfig for ServerConfig {
const DEFAULT_TOML: &str = include_str!("../gateway.default.toml");
}

View file

@ -0,0 +1,108 @@
use std::sync::OnceLock;
use shorekeeper_network::ServiceMessage;
use shorekeeper_protocol::{
message::Message, proto_config, ForwardClientMessagePush, MessageID, MessageRoute, Protobuf,
};
use tokio::sync::mpsc;
use crate::session::{Session, SessionManager};
use super::game_server_connection;
static MSG_TX: OnceLock<mpsc::Sender<(u32, Message)>> = OnceLock::new();
pub fn start_task(session_mgr: &'static SessionManager) {
let (tx, rx) = mpsc::channel(32);
if MSG_TX.set(tx).is_err() {
tracing::error!("client_message_handler task is already running");
return;
}
tokio::spawn(async move { handler_task_fn(rx, session_mgr).await });
}
pub async fn push_message(session_id: u32, msg: Message) {
let _ = MSG_TX.get().unwrap().send((session_id, msg)).await;
}
async fn handler_task_fn(
mut rx: mpsc::Receiver<(u32, Message)>,
session_mgr: &'static SessionManager,
) {
loop {
let Some((session_id, message)) = rx.recv().await else {
continue;
};
let Some(mut session) = session_mgr.get_mut(session_id) else {
continue;
};
if let Err(err) = handle_message(session.value_mut(), message).await {
tracing::error!(
"handle_message failed, session_id: {}, err: {err}",
session_id
);
}
}
}
async fn handle_message(
session: &mut Session,
message: Message,
) -> Result<(), crate::session::SessionError> {
match proto_config::get_message_flags(message.get_message_id()).into() {
MessageRoute::Gateway => handle_message_impl(session, message).await,
MessageRoute::GameServer => forward_to_game_server(session, message).await,
route => {
tracing::warn!(
"received message with wrong route, id: {}, route: {route:?}",
message.get_message_id()
);
Ok(())
}
}
}
async fn forward_to_game_server(
session: &Session,
message: Message,
) -> Result<(), crate::session::SessionError> {
let mut data = vec![0u8; message.get_encoding_length()];
message.encode(&mut data)?;
game_server_connection::push_message(ServiceMessage {
src_service_id: 0,
rpc_id: message.get_rpc_id(),
message_id: ForwardClientMessagePush::MESSAGE_ID,
data: ForwardClientMessagePush {
gateway_session_id: session.get_conv_id(),
data,
}
.encode_to_vec()
.into_boxed_slice(),
})
.await;
Ok(())
}
async fn handle_message_impl(
session: &mut Session,
message: Message,
) -> Result<(), crate::session::SessionError> {
if message.is_request() {
super::client_request_handler::handle_request(session, message).await
} else if message.is_push() {
super::client_push_handler::handle_push(session, message).await
} else {
tracing::warn!(
"handle_message: wrong message type: {}, message_id: {}, session_id: {}",
message.get_message_type(),
message.get_message_id(),
session.get_conv_id()
);
Ok(())
}
}

View file

@ -0,0 +1,48 @@
use shorekeeper_network::ServiceMessage;
use shorekeeper_protocol::{
message::Message, AceAntiDataPush, ExitGamePush, MessageID, Protobuf, StopPlayerSessionPush,
};
use crate::session::Session;
use super::game_server_connection;
pub async fn handle_push(
session: &mut Session,
msg: Message,
) -> Result<(), crate::session::SessionError> {
match msg.get_message_id() {
AceAntiDataPush::MESSAGE_ID => on_ace_anti_data_push(session, msg),
ExitGamePush::MESSAGE_ID => on_exit_game_push(session, msg).await,
unhandled => tracing::warn!("can't find handler for push with message_id={unhandled}"),
}
Ok(())
}
async fn on_exit_game_push(session: &Session, _: Message) {
game_server_connection::push_message(ServiceMessage {
src_service_id: 0,
rpc_id: 0,
message_id: StopPlayerSessionPush::MESSAGE_ID,
data: StopPlayerSessionPush {
gateway_session_id: session.get_conv_id(),
}
.encode_to_vec()
.into_boxed_slice(),
})
.await;
}
fn on_ace_anti_data_push(session: &Session, mut msg: Message) {
let Ok(push) = AceAntiDataPush::decode(msg.remove_payload().as_ref()) else {
tracing::warn!("failed to decode AceAntiDataPush");
return;
};
tracing::debug!(
"received AceAntiDataPush from session_id={}, data={}",
session.get_conv_id(),
hex::encode(&*push.anti_data)
);
}

View file

@ -0,0 +1,215 @@
use common::time_util;
use shorekeeper_database::{models, query_as};
use shorekeeper_network::ServiceMessage;
use shorekeeper_protocol::{
message::Message, CreateCharacterRequest, CreatePlayerDataRequest, EnterGameRequest, ErrorCode,
HeartbeatRequest, HeartbeatResponse, LoginRequest, LoginResponse, MessageID, ProtoKeyRequest,
ProtoKeyResponse, Protobuf, StartPlayerSessionRequest,
};
use crate::session::Session;
use super::game_server_connection;
macro_rules! requests {
($($name:ident;)*) => {
async fn handle_request_impl(session: &mut crate::session::Session, mut msg: Message) -> Result<(), crate::session::SessionError> {
use ::shorekeeper_protocol::{MessageID, Protobuf};
::paste::paste! {
match msg.get_message_id() {
$(
::shorekeeper_protocol::[<$name Request>]::MESSAGE_ID => {
let request = ::shorekeeper_protocol::[<$name Request>]::decode(&*msg.remove_payload())?;
let mut response = ::shorekeeper_protocol::[<$name Response>]::default();
[<on_ $name:snake _request>](session, request, &mut response).await;
session.send_response(response, msg.get_rpc_id()).await?;
},
)*
unhandled => ::tracing::warn!("can't find handler for request with message_id={unhandled}")
}
}
Ok(())
}
};
}
requests! {
ProtoKey;
Login;
Heartbeat;
}
pub async fn handle_request(
session: &mut Session,
msg: Message,
) -> Result<(), crate::session::SessionError> {
match msg.get_message_id() {
CreateCharacterRequest::MESSAGE_ID => on_create_character_request(session, msg).await,
EnterGameRequest::MESSAGE_ID => on_enter_game_request(session, msg).await,
_ => handle_request_impl(session, msg).await,
}
}
async fn on_proto_key_request(
session: &mut Session,
request: ProtoKeyRequest,
response: &mut ProtoKeyResponse,
) {
tracing::debug!("on_proto_key_request: {request:?}");
let Ok(key) = session.generate_session_key() else {
response.error_code = ErrorCode::InternalError.into();
return;
};
response.r#type = 2;
response.key = key;
}
async fn on_login_request(
session: &mut Session,
request: LoginRequest,
response: &mut LoginResponse,
) {
tracing::debug!("on_login: {request:?}");
let Some(account): Option<models::AccountRow> =
query_as("SELECT * FROM t_user_account WHERE user_id = ($1)")
.bind(&request.account)
.fetch_optional(session.database.as_ref())
.await
.inspect_err(|err| tracing::error!("failed to fetch account: {err}"))
.ok()
.flatten()
else {
tracing::debug!("login: account '{}' not found", &request.account);
response.code = ErrorCode::InvalidUserId.into();
return;
};
// TODO: token check. We don't have proper sdk/login system yet.
let last_login_trace_id = account.last_login_trace_id.unwrap_or_default();
if last_login_trace_id != request.login_trace_id {
tracing::debug!(
"login: trace_id mismatch! Server: {}, client: {}, user_id: {}",
&last_login_trace_id,
&request.login_trace_id,
&account.user_id
);
response.code = ErrorCode::LoginRetry.into();
return;
}
if let Some(ban_time_stamp) = account.ban_time_stamp {
let cur_time_stamp = time_util::unix_timestamp() as i64;
if ban_time_stamp > cur_time_stamp {
tracing::debug!(
"login: account with id {} is banned until {} ({} seconds remaining)",
&account.user_id,
ban_time_stamp,
ban_time_stamp - cur_time_stamp
);
response.code = ErrorCode::AccountIsBlocked.into();
return;
}
}
session.user_id = Some(request.account.clone());
let player_id = query_as("SELECT * from t_user_uid WHERE user_id = ($1)")
.bind(&request.account)
.fetch_optional(session.database.as_ref())
.await
.inspect_err(|err| tracing::error!("failed to fetch player_id: {err}"))
.ok()
.flatten()
.map(|u: models::UserUidRow| u.player_id);
let Some(player_id) = player_id else {
tracing::debug!(
"login: first login on account {}, awaiting create character request",
&account.user_id
);
response.code = ErrorCode::HaveNoCharacter.into();
return;
};
session.player_id = Some(player_id);
response.code = ErrorCode::Success.into();
response.timestamp = time_util::unix_timestamp() as i64;
tracing::info!(
"login success, user_id: {}, player_id: {}",
&request.account,
player_id
);
}
async fn on_heartbeat_request(
session: &mut Session,
_: HeartbeatRequest,
_: &mut HeartbeatResponse,
) {
session.update_last_heartbeat_time();
}
async fn on_enter_game_request(
session: &mut Session,
message: Message,
) -> Result<(), crate::session::SessionError> {
let Some(player_id) = session.player_id else {
tracing::debug!(
"EnterGameRequest: player_id is None, session_id: {}",
session.get_conv_id()
);
return Ok(());
};
game_server_connection::push_message(ServiceMessage {
src_service_id: 0,
rpc_id: message.get_rpc_id(),
message_id: StartPlayerSessionRequest::MESSAGE_ID,
data: StartPlayerSessionRequest {
gateway_session_id: session.get_conv_id(),
player_id,
}
.encode_to_vec()
.into_boxed_slice(),
})
.await;
Ok(())
}
async fn on_create_character_request(
session: &mut Session,
mut message: Message,
) -> Result<(), crate::session::SessionError> {
let client_request = CreateCharacterRequest::decode(message.remove_payload().as_ref())?;
let Some(user_id) = session.user_id.clone() else {
tracing::error!("create_character: session.user_id is None");
return Ok(());
};
let create_player_request = CreatePlayerDataRequest {
session_id: session.get_conv_id(),
user_id,
sex: client_request.sex,
name: client_request.name,
};
game_server_connection::push_message(ServiceMessage {
src_service_id: 0,
rpc_id: message.get_rpc_id(),
message_id: CreatePlayerDataRequest::MESSAGE_ID,
data: create_player_request.encode_to_vec().into_boxed_slice(),
})
.await;
Ok(())
}

View file

@ -0,0 +1,18 @@
use std::sync::OnceLock;
use shorekeeper_network::{config::ServiceEndPoint, ServiceClient, ServiceMessage};
static CLIENT: OnceLock<ServiceClient> = OnceLock::new();
pub fn init(own_service_id: u32, game_server_end_point: &'static ServiceEndPoint) {
if CLIENT.get().is_some() {
tracing::error!("game_server_connection: already initialized");
return;
}
let _ = CLIENT.set(ServiceClient::new(own_service_id, game_server_end_point));
}
pub async fn push_message(message: ServiceMessage) {
CLIENT.get().unwrap().push(message).await
}

View file

@ -0,0 +1,6 @@
pub mod client_message_handler;
pub mod game_server_connection;
pub mod service_message_handler;
mod client_push_handler;
mod client_request_handler;

View file

@ -0,0 +1,210 @@
use std::sync::OnceLock;
use common::time_util;
use shorekeeper_network::{config::ServiceEndPoint, ServiceListener, ServiceMessage};
use shorekeeper_protocol::{
message::Message, CreateCharacterResponse, CreatePlayerDataResponse, EnterGameResponse,
ErrorCode, ForwardClientMessagePush, MessageID, Protobuf, StartPlayerSessionResponse,
};
use crate::session::SessionManager;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Failed to bind ServiceListener")]
BindFailed,
}
pub async fn start_task(
listen_end_point: &ServiceEndPoint,
session_mgr: &'static SessionManager,
) -> Result<(), Error> {
static IS_STARTED: OnceLock<()> = OnceLock::new();
if IS_STARTED.set(()).is_err() {
tracing::error!("service_message_handler: task already started");
return Ok(());
}
let listener = ServiceListener::bind(listen_end_point)
.await
.map_err(|_| Error::BindFailed)?;
tokio::spawn(async move { handler_task_fn(listener, session_mgr).await });
Ok(())
}
async fn handler_task_fn(mut listener: ServiceListener, session_mgr: &'static SessionManager) {
loop {
let Some(message) = listener.receive().await else {
continue;
};
tracing::debug!(
"received message from service: {}, rpc_id: {} message_id: {}",
message.src_service_id,
message.rpc_id,
message.message_id
);
match message.message_id {
CreatePlayerDataResponse::MESSAGE_ID => {
on_create_player_data_response(message, session_mgr).await
}
StartPlayerSessionResponse::MESSAGE_ID => {
on_start_player_session_response(message, session_mgr).await
}
ForwardClientMessagePush::MESSAGE_ID => {
on_forward_client_message_push(message, session_mgr).await
}
unhandled => tracing::warn!(
"unhandled service message id: {unhandled}, from service_id: {}",
message.src_service_id
),
}
}
}
async fn on_forward_client_message_push(
message: ServiceMessage,
session_mgr: &'static SessionManager,
) {
let Ok(push) = ForwardClientMessagePush::decode(message.data.as_ref()) else {
tracing::error!(
"failed to decode ForwardClientMessagePush, data: {}",
hex::encode(&message.data)
);
return;
};
let Ok(message) = Message::decode(&push.data) else {
tracing::error!(
"ForwardClientMessage: failed to decode underlying message data, session_id: {}",
push.gateway_session_id
);
return;
};
let Some(mut session) = session_mgr.get_mut(push.gateway_session_id) else {
tracing::error!(
"ForwardClientMessage: session not found, id: {}",
push.gateway_session_id
);
return;
};
tracing::debug!(
"forward message from game server to client, message_id: {}, session_id: {}",
message.get_message_id(),
push.gateway_session_id
);
let _ = session.send_message(message).await.inspect_err(|err| {
tracing::error!("ForwardClientMessage: failed to send message, error: {err}")
});
}
async fn on_start_player_session_response(
message: ServiceMessage,
session_mgr: &'static SessionManager,
) {
let Ok(response) = StartPlayerSessionResponse::decode(message.data.as_ref()) else {
tracing::error!(
"failed to decode StartPlayerSessionResponse, data: {}",
hex::encode(&message.data)
);
return;
};
let Some(mut session) = session_mgr.get_mut(response.gateway_session_id) else {
tracing::error!(
"StartPlayerSession: session not found, id: {}",
response.gateway_session_id
);
return;
};
if response.code != 0 {
tracing::debug!(
"StartPlayerSession failed, session_id: {}, player_id: {:?}, user_id: {:?}, error code: {}",
response.gateway_session_id,
&session.player_id,
&session.user_id,
response.code
);
let _ = session
.send_response(
EnterGameResponse {
error_code: response.code,
..Default::default()
},
message.rpc_id,
)
.await;
} else {
tracing::debug!(
"StartPlayerSession success, game server should forward subsequent messages now. player_id: {}",
session.player_id.unwrap_or_default()
);
}
}
async fn on_create_player_data_response(
message: ServiceMessage,
session_mgr: &'static SessionManager,
) {
let Ok(response) = CreatePlayerDataResponse::decode(message.data.as_ref()) else {
tracing::error!(
"failed to decode CreatePlayerDataResponse, data: {}",
hex::encode(&message.data)
);
return;
};
let Some(mut session) = session_mgr.get_mut(response.session_id) else {
tracing::debug!("session with id {} not found", response.session_id);
return;
};
if response.code != 0 {
tracing::warn!("CreatePlayerData failed, code: {}", response.code);
let _ = session
.send_response(
CreateCharacterResponse {
error_code: response.code,
..Default::default()
},
message.rpc_id,
)
.await;
return;
}
let Some(user_id) = session.user_id.as_ref() else {
tracing::debug!(
"session.user_id is None, session_id: {}",
response.session_id
);
return;
};
tracing::info!(
"CreateCharacter success, player_id: {}, user_id: {}",
response.player_id,
user_id
);
session.player_id = Some(response.player_id);
let _ = session
.send_response(
CreateCharacterResponse {
error_code: ErrorCode::Success.into(),
player_id: response.player_id,
name: response.name,
create_time: time_util::unix_timestamp() as i32,
},
message.rpc_id,
)
.await;
}

View file

@ -0,0 +1,48 @@
use anyhow::Result;
use handler::{client_message_handler, game_server_connection, service_message_handler};
use session::SessionManager;
use std::sync::{Arc, LazyLock, OnceLock};
use udp_server::UdpServer;
use common::config_util;
use config::ServerConfig;
use shorekeeper_protokey::ServerProtoKeyHelper;
mod config;
mod handler;
mod session;
mod udp_server;
const CLIENT_PUBLIC_KEY: &str = include_str!("../security/client_public_key.pem");
#[tokio::main]
async fn main() -> Result<()> {
static PROTOKEY_HELPER: OnceLock<ServerProtoKeyHelper> = OnceLock::new();
static CONFIG: LazyLock<ServerConfig> =
LazyLock::new(|| config_util::load_or_create("gateway.toml"));
static SESSION_MGR: LazyLock<SessionManager> = LazyLock::new(SessionManager::default);
::common::splash::print_splash();
::common::logging::init(::tracing::Level::DEBUG);
let protokey_helper =
ServerProtoKeyHelper::with_public_key(&CONFIG.protokey, CLIENT_PUBLIC_KEY)?;
let database = shorekeeper_database::connect_to(&CONFIG.database).await?;
shorekeeper_database::run_migrations(&database).await?;
game_server_connection::init(CONFIG.service_id, &CONFIG.game_server_end_point);
client_message_handler::start_task(&SESSION_MGR);
service_message_handler::start_task(&CONFIG.service_end_point, &SESSION_MGR).await?;
let server = UdpServer::new(
&CONFIG.network,
PROTOKEY_HELPER.get_or_init(|| protokey_helper),
&SESSION_MGR,
Arc::new(database),
)
.await?;
server.serve().await;
Ok(())
}

View file

@ -0,0 +1,11 @@
#[derive(thiserror::Error, Debug)]
pub enum SessionError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("KCP transport error: {0}")]
KcpError(#[from] kcp::Error),
#[error("Crypto error: {0}")]
CryptoError(#[from] shorekeeper_protokey::Error),
#[error("Protobuf decode error: {0}")]
DecodeFailed(#[from] shorekeeper_protocol::ProtobufDecodeError),
}

View file

@ -0,0 +1,18 @@
use dashmap::{mapref::one::RefMut, DashMap};
use crate::session::Session;
#[derive(Default)]
pub struct SessionManager {
session_map: DashMap<u32, Session>,
}
impl SessionManager {
pub fn add(&self, id: u32, session: Session) {
self.session_map.insert(id, session);
}
pub fn get_mut(&self, id: u32) -> Option<RefMut<'_, u32, Session>> {
self.session_map.get_mut(&id)
}
}

View file

@ -0,0 +1,198 @@
use std::{fmt, net::SocketAddr, sync::Arc, task};
use common::time_util;
use kcp::Kcp;
use shorekeeper_database::PgPool;
use shorekeeper_protocol::{message::Message, ProtocolUnit};
use shorekeeper_protokey::ServerProtoKeyHelper;
use tokio::{io::AsyncWrite, net::UdpSocket};
use util::LengthFieldBasedDecoder;
use crate::handler::client_message_handler;
mod error;
mod manager;
mod util;
pub use error::SessionError;
pub use manager::SessionManager;
pub struct Session {
kcp: Kcp<SessionOutput>,
conv_id: u32,
addr: SocketAddr,
decoder: LengthFieldBasedDecoder,
start_time_ms: u64,
protokey_helper: &'static ServerProtoKeyHelper,
session_key: Option<[u8; 32]>,
last_heartbeat_time_ms: u64,
pub database: Arc<PgPool>,
pub user_id: Option<String>,
pub player_id: Option<i32>,
}
struct SessionOutput {
output_addr: SocketAddr,
udp_socket: Arc<UdpSocket>,
}
impl Session {
pub fn new(
conv_id: u32,
addr: SocketAddr,
socket: Arc<UdpSocket>,
helper: &'static ServerProtoKeyHelper,
database: Arc<PgPool>,
) -> Self {
let output = SessionOutput {
output_addr: addr,
udp_socket: socket,
};
let cur_time_ms = time_util::unix_timestamp_ms();
Self {
protokey_helper: helper,
kcp: Kcp::new(conv_id, true, output),
decoder: LengthFieldBasedDecoder::new(),
start_time_ms: cur_time_ms,
last_heartbeat_time_ms: cur_time_ms,
session_key: None,
user_id: None,
player_id: None,
conv_id,
addr,
database,
}
}
pub async fn on_receive(&mut self, buf: &[u8]) -> Result<(), SessionError> {
self.kcp.input(buf)?;
self.kcp.async_update(self.time()).await?;
self.kcp.async_flush().await?;
if self.kcp.peeksize().is_ok() {
let mut buf = [0u8; 1500];
while let Ok(size) = self.kcp.recv(&mut buf) {
self.decoder.input(&buf[..size]);
}
}
while let Some(mut message) = self.next_message() {
if let Some(session_key) = self.session_key.as_ref() {
let payload = message.remove_payload();
message.set_payload(self.protokey_helper.decrypt(
message.get_message_id(),
message.get_sequence_number(),
session_key,
payload,
)?);
}
client_message_handler::push_message(self.conv_id, message).await;
}
Ok(())
}
pub async fn send_response(
&mut self,
response: impl ProtocolUnit,
rpc_id: u16,
) -> Result<(), SessionError> {
let message = Message::Response {
sequence_number: 0,
rpc_id,
message_id: response.get_message_id(),
payload: Some(response.encode_to_vec().into_boxed_slice()),
};
self.send_message(message).await?;
Ok(())
}
pub async fn send_message(&mut self, mut message: Message) -> Result<(), SessionError> {
if let Some(session_key) = self.session_key.as_ref() {
let payload = message.remove_payload();
message.set_payload(self.protokey_helper.encrypt(
message.get_message_id(),
message.get_sequence_number(),
session_key,
payload,
)?);
}
let encoding_length = message.get_encoding_length();
let mut data = vec![0u8; encoding_length + 3];
data[0] = (encoding_length & 0xFF) as u8;
data[1] = ((encoding_length >> 8) & 0xFF) as u8;
data[2] = ((encoding_length >> 16) & 0xFF) as u8;
message.encode(&mut data[3..])?;
self.kcp.send(&data)?;
self.kcp.async_flush().await?;
Ok(())
}
pub fn update_last_heartbeat_time(&mut self) {
self.last_heartbeat_time_ms = time_util::unix_timestamp_ms();
}
pub fn generate_session_key(&mut self) -> Result<Vec<u8>, SessionError> {
let (session_key, encrypted_key) = self.protokey_helper.generate_session_key()?;
self.session_key = Some(session_key);
Ok(encrypted_key.unwrap_or_default())
}
pub fn get_conv_id(&self) -> u32 {
self.conv_id
}
fn time(&self) -> u32 {
(time_util::unix_timestamp_ms() - self.start_time_ms) as u32
}
fn next_message(&mut self) -> Option<Message> {
self.decoder.pop_with(|buf| {
Message::decode(&buf)
.inspect_err(|err| {
tracing::error!(
"failed to decode a message, err: {err}, buf: {}",
hex::encode(&buf)
)
})
.ok()
})
}
}
impl fmt::Display for Session {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "session(conv_id={}, addr={})", self.conv_id, self.addr)
}
}
impl AsyncWrite for SessionOutput {
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut task::Context<'_>,
buf: &[u8],
) -> task::Poll<Result<usize, std::io::Error>> {
self.udp_socket.poll_send_to(cx, buf, self.output_addr)
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
_cx: &mut task::Context<'_>,
) -> task::Poll<Result<(), std::io::Error>> {
task::Poll::Ready(Ok(()))
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
_cx: &mut task::Context<'_>,
) -> task::Poll<Result<(), std::io::Error>> {
task::Poll::Ready(Ok(()))
}
}

View file

@ -0,0 +1,53 @@
pub struct LengthFieldBasedDecoder {
buffer: Box<[u8]>,
cur_index: usize,
}
impl LengthFieldBasedDecoder {
const INITIAL_CAPACITY: usize = 8192;
pub fn new() -> Self {
Self {
buffer: vec![0u8; Self::INITIAL_CAPACITY].into_boxed_slice(),
cur_index: 0,
}
}
pub fn input(&mut self, data: &[u8]) {
self.ensure_capacity(data.len());
(&mut self.buffer[self.cur_index..self.cur_index + data.len()]).copy_from_slice(data);
self.cur_index += data.len();
}
pub fn pop_with<T>(&mut self, decode: impl FnOnce(&[u8]) -> Option<T>) -> Option<T> {
if self.cur_index >= 3 {
let frame_length = read_length_field(&self.buffer);
let segment_size = 3 + frame_length;
if self.cur_index >= segment_size {
let retval = decode(&self.buffer[3..segment_size]);
self.buffer.copy_within(segment_size..self.cur_index, 0);
self.cur_index -= segment_size;
return retval;
}
}
None
}
#[inline]
fn ensure_capacity(&mut self, add_len: usize) {
if self.buffer.len() < self.cur_index + add_len {
let mut new_buf = vec![0u8; self.cur_index + add_len];
new_buf[..self.cur_index].copy_from_slice(&self.buffer[..self.cur_index]);
self.buffer = new_buf.into_boxed_slice();
}
}
}
#[inline]
fn read_length_field(buf: &[u8]) -> usize {
buf[0] as usize | (buf[1] as usize) << 8 | (buf[2] as usize) << 16
}

View file

@ -0,0 +1,93 @@
use std::{net::SocketAddr, sync::Arc};
use shorekeeper_database::PgPool;
use shorekeeper_protokey::ServerProtoKeyHelper;
use tokio::net::UdpSocket;
use crate::{config::NetworkSettings, session::Session, session::SessionManager};
pub struct UdpServer {
socket: Arc<UdpSocket>,
protokey_helper: &'static ServerProtoKeyHelper,
session_mgr: &'static SessionManager,
db: Arc<PgPool>,
}
impl UdpServer {
const MTU: usize = 1000;
const CMD_SYN: u8 = 0xED;
const CMD_ACK: u8 = 0xEE;
pub async fn new(
network_settings: &'static NetworkSettings,
protokey_helper: &'static ServerProtoKeyHelper,
session_mgr: &'static SessionManager,
db: Arc<PgPool>,
) -> Result<Self, tokio::io::Error> {
let socket = UdpSocket::bind(&format!("0.0.0.0:{}", network_settings.kcp_port)).await?;
Ok(Self {
socket: Arc::new(socket),
protokey_helper,
session_mgr,
db,
})
}
pub async fn serve(&self) {
let mut kcp_conv_cnt = 0;
let mut recv_buf = [0u8; Self::MTU];
loop {
let Ok((len, addr)) = self
.socket
.recv_from(&mut recv_buf)
.await
.inspect_err(|err| tracing::debug!("recv_from failed: {err}"))
else {
continue;
};
match len {
1 if recv_buf[0] == Self::CMD_SYN => {
kcp_conv_cnt += 1;
self.create_session(kcp_conv_cnt, addr).await
}
20.. => self.handle_packet(&recv_buf[..len]).await,
_ => (),
}
}
}
pub async fn create_session(&self, conv_id: u32, addr: SocketAddr) {
let session = Session::new(
conv_id,
addr,
self.socket.clone(),
&self.protokey_helper,
self.db.clone(),
);
self.session_mgr.add(conv_id, session);
let mut ack = Vec::with_capacity(5);
ack.push(Self::CMD_ACK);
ack.extend(conv_id.to_le_bytes());
let _ = self.socket.send_to(&ack, addr).await;
tracing::debug!("new connection from {addr}, conv_id: {conv_id}");
}
pub async fn handle_packet(&self, buf: &[u8]) {
let conv_id = kcp::get_conv(buf);
let Some(mut session) = self.session_mgr.get_mut(conv_id) else {
tracing::debug!("received kcp packet: session with conv_id={conv_id} doesn't exist");
return;
};
let _ = session
.value_mut()
.on_receive(buf)
.await
.inspect_err(|err| tracing::error!("Session::on_receive failed, error: {err}"));
}
}

View file

@ -0,0 +1,22 @@
[package]
name = "hotpatch-server"
edition = "2021"
version.workspace = true
[dependencies]
# Framework
tokio.workspace = true
shorekeeper-http.workspace = true
# Serialization
serde.workspace = true
toml.workspace = true
# Util
anyhow.workspace = true
# Tracing
tracing.workspace = true
# Internal
common.workspace = true

View file

@ -0,0 +1,6 @@
[network]
http_addr = "0.0.0.0:10002"
[encryption]
key = "t+AEu5SGdpz06tomonajLMau9AJgmyTvVhz9VtGf1+0="
iv = "fprc5lBWADQB7tim0R2JxQ=="

View file

@ -0,0 +1,49 @@
use std::fs;
use std::sync::LazyLock;
use anyhow::Result;
use common::config_util::{self, TomlConfig};
use serde::Deserialize;
use shorekeeper_http::{
config::{AesSettings, NetworkSettings},
Application, Path,
};
#[derive(Deserialize)]
pub struct ServerConfig {
pub network: NetworkSettings,
pub encryption: AesSettings,
}
impl TomlConfig for ServerConfig {
const DEFAULT_TOML: &str = include_str!("../hotpatch.default.toml");
}
#[tokio::main]
async fn main() -> Result<()> {
static CONFIG: LazyLock<ServerConfig> =
LazyLock::new(|| config_util::load_or_create("hotpatch.toml"));
::common::splash::print_splash();
::common::logging::init(::tracing::Level::DEBUG);
Application::new()
.get("/:env/client/:hash/:platform/config.json", get_config)
.with_encryption(&CONFIG.encryption)
.serve(&CONFIG.network)
.await?;
Ok(())
}
#[tracing::instrument]
async fn get_config(Path((env, _hash, platform)): Path<(String, String, String)>) -> String {
tracing::debug!("hotpatch config requested");
let local_path = format!("assets/hotpatch/{env}/{platform}/config.json");
fs::read_to_string(&local_path).unwrap_or_else(|_| {
tracing::warn!("requested config was not found");
String::from("{}")
})
}

22
login-server/Cargo.toml Normal file
View file

@ -0,0 +1,22 @@
[package]
name = "login-server"
edition = "2021"
version.workspace = true
[dependencies]
# Framework
tokio.workspace = true
shorekeeper-http.workspace = true
shorekeeper-database.workspace = true
# Serialization
serde.workspace = true
# Util
anyhow.workspace = true
# Tracing
tracing.workspace = true
# Internal
common.workspace = true

View file

@ -0,0 +1,12 @@
[network]
http_addr = "0.0.0.0:5500"
[gateway]
host = "127.0.0.1"
port = 7777
[database]
host = "localhost:5432"
user_name = "postgres"
password = ""
db_name = "shorekeeper"

View file

@ -0,0 +1,21 @@
use common::config_util::TomlConfig;
use serde::Deserialize;
use shorekeeper_database::DatabaseSettings;
use shorekeeper_http::config::NetworkSettings;
#[derive(Deserialize)]
pub struct ServerConfig {
pub network: NetworkSettings,
pub database: DatabaseSettings,
pub gateway: GatewayConfig,
}
#[derive(Deserialize)]
pub struct GatewayConfig {
pub host: String,
pub port: u16,
}
impl TomlConfig for ServerConfig {
const DEFAULT_TOML: &str = include_str!("../loginserver.default.toml");
}

View file

@ -0,0 +1,52 @@
use crate::{schema, ServiceState};
use common::time_util;
use shorekeeper_database::{models::AccountRow, query_as};
use shorekeeper_http::{Json, Query, State};
#[tracing::instrument(skip(state))]
pub async fn handle_login_api_call(
State(state): State<ServiceState>,
Query(parameters): Query<schema::LoginParameters>,
) -> Json<schema::LoginResult> {
tracing::debug!("login requested");
let user_data = parameters.user_data;
let result = match login(&state, parameters).await {
Ok(result) => result,
Err(err) => {
tracing::warn!("login: internal error occurred {err:?}");
schema::LoginResult::error(-1, String::from("Internal server error"))
}
};
Json(result.with_user_data(user_data))
}
async fn login(state: &ServiceState, params: schema::LoginParameters) -> Result<schema::LoginResult, shorekeeper_database::Error> {
if params.login_type != 0 {
return Ok(schema::LoginResult::error(-1, String::from("SDK login is not allowed on this server")));
}
let result: Option<AccountRow> = query_as("SELECT * FROM t_user_account WHERE user_id = ($1)").bind(params.user_id.as_str()).fetch_optional(&state.pool).await?;
let account: AccountRow = match result {
Some(account) => {
if let Some(ban_time_stamp) = account.ban_time_stamp {
if time_util::unix_timestamp() < ban_time_stamp as u64 {
return Ok(schema::LoginResult::banned(String::from("You're banned MF"), ban_time_stamp as i64));
}
}
query_as("UPDATE t_user_account SET last_login_trace_id = ($1) where user_id = ($2) RETURNING *").bind(params.login_trace_id).bind(params.user_id).fetch_one(&state.pool).await?
},
None => query_as("INSERT INTO t_user_account (user_name, user_id, token, create_time_stamp, create_device_id, last_login_trace_id) values ($1, $2, $3, $4, $5, $6) RETURNING *")
.bind(params.user_name.as_str())
.bind(params.user_id.as_str())
.bind(params.token.as_str())
.bind(time_util::unix_timestamp() as i64)
.bind(params.device_id.0.as_str())
.bind(params.login_trace_id.as_str())
.fetch_one(&state.pool).await?
};
Ok(schema::LoginResult::success(&account.token, &state.gateway.host, state.gateway.port, account.sex))
}

46
login-server/src/main.rs Normal file
View file

@ -0,0 +1,46 @@
use std::{process, sync::LazyLock};
use anyhow::Result;
use config::{GatewayConfig, ServerConfig};
use shorekeeper_database::PgPool;
use shorekeeper_http::{Application, StatusCode};
mod config;
mod handler;
mod schema;
#[derive(Clone)]
pub struct ServiceState {
pub pool: PgPool,
pub gateway: &'static GatewayConfig,
}
#[tokio::main]
async fn main() -> Result<()> {
static CONFIG: LazyLock<ServerConfig> =
LazyLock::new(|| ::common::config_util::load_or_create("loginserver.toml"));
::common::splash::print_splash();
::common::logging::init(::tracing::Level::DEBUG);
let Ok(pool) = shorekeeper_database::connect_to(&CONFIG.database).await else {
tracing::error!(
"Failed to connect to database with connection string: {}",
&CONFIG.database
);
process::exit(1);
};
shorekeeper_database::run_migrations(&pool).await?;
Application::new_with_state(ServiceState {
pool,
gateway: &CONFIG.gateway,
})
.get("/health", || async { StatusCode::OK })
.get("/api/login", handler::handle_login_api_call)
.serve(&CONFIG.network)
.await?;
Ok(())
}

View file

@ -0,0 +1,75 @@
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Debug)]
pub struct DeviceId(pub String);
impl Default for DeviceId {
fn default() -> Self {
Self(String::from("ffffffff-ffff-ffff-ffff-ffffffffffff"))
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct LoginParameters {
pub login_type: u32,
pub user_id: String,
pub user_name: String,
pub token: String,
pub user_data: i32,
#[serde(default)]
pub device_id: DeviceId,
pub login_trace_id: String,
}
#[derive(Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct LoginResult {
code: i32,
has_rpc: bool,
err_message: Option<String>,
token: Option<String>,
host: Option<String>,
port: Option<u16>,
user_data: Option<i32>,
ban_time_stamp: Option<i64>,
sex: Option<i32>,
}
impl LoginResult {
pub fn success(token: &str, host: &str, port: u16, sex: i32) -> Self {
Self {
code: 0,
token: Some(token.to_string()),
host: Some(host.to_string()),
port: Some(port),
sex: Some(sex),
..Default::default()
}
}
pub fn error(code: i32, err_message: String) -> Self {
Self {
code,
err_message: Some(err_message),
..Default::default()
}
}
pub fn banned(err_message: String, ban_time_stamp: i64) -> Self {
Self {
code: -1,
err_message: Some(err_message),
ban_time_stamp: Some(ban_time_stamp),
..Default::default()
}
}
pub fn with_user_data(self, user_data: i32) -> Self {
Self {
user_data: Some(user_data),
has_rpc: true,
..self
}
}
}

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6 MiB

View file

@ -0,0 +1,10 @@
[package]
name = "shorekeeper-data"
edition = "2021"
version.workspace = true
[dependencies]
serde.workspace = true
serde_json.workspace = true
paste.workspace = true
thiserror.workspace = true

View file

@ -0,0 +1,143 @@
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct BasePropertyData {
pub id: i32,
pub lv: i32,
pub life_max: i32,
pub life: i32,
pub sheild: i32,
pub sheild_damage_change: i32,
pub sheild_damage_reduce: i32,
pub atk: i32,
pub crit: i32,
pub crit_damage: i32,
pub def: i32,
pub energy_efficiency: i32,
pub cd_reduse: i32,
pub reaction_efficiency: i32,
pub damage_change_normal_skill: i32,
pub damage_change: i32,
pub damage_reduce: i32,
pub damage_change_auto: i32,
pub damage_change_cast: i32,
pub damage_change_ultra: i32,
pub damage_change_qte: i32,
pub damage_change_phys: i32,
pub damage_change_element1: i32,
pub damage_change_element2: i32,
pub damage_change_element3: i32,
pub damage_change_element4: i32,
pub damage_change_element5: i32,
pub damage_change_element6: i32,
pub damage_resistance_phys: i32,
pub damage_resistance_element1: i32,
pub damage_resistance_element2: i32,
pub damage_resistance_element3: i32,
pub damage_resistance_element4: i32,
pub damage_resistance_element5: i32,
pub damage_resistance_element6: i32,
pub heal_change: i32,
pub healed_change: i32,
pub damage_reduce_phys: i32,
pub damage_reduce_element1: i32,
pub damage_reduce_element2: i32,
pub damage_reduce_element3: i32,
pub damage_reduce_element4: i32,
pub damage_reduce_element5: i32,
pub damage_reduce_element6: i32,
pub reaction_change1: i32,
pub reaction_change2: i32,
pub reaction_change3: i32,
pub reaction_change4: i32,
pub reaction_change5: i32,
pub reaction_change6: i32,
pub reaction_change7: i32,
pub reaction_change8: i32,
pub reaction_change9: i32,
pub reaction_change10: i32,
pub reaction_change11: i32,
pub reaction_change12: i32,
pub reaction_change13: i32,
pub reaction_change14: i32,
pub reaction_change15: i32,
pub energy_max: i32,
pub energy: i32,
pub special_energy_1_max: i32,
pub special_energy_1: i32,
pub special_energy_2_max: i32,
pub special_energy_2: i32,
pub special_energy_3_max: i32,
pub special_energy_3: i32,
pub special_energy_4_max: i32,
pub special_energy_4: i32,
pub strength_max: i32,
pub strength: i32,
pub strength_recover: i32,
pub strength_punish_time: i32,
pub strength_run: i32,
pub strength_swim: i32,
pub strength_fast_swim: i32,
pub hardness_max: i32,
pub hardness: i32,
pub hardness_recover: i32,
pub hardness_punish_time: i32,
pub hardness_change: i32,
pub hardness_reduce: i32,
pub rage_max: i32,
pub rage: i32,
pub rage_recover: i32,
pub rage_punish_time: i32,
pub rage_change: i32,
pub rage_reduce: i32,
pub tough_max: i32,
pub tough: i32,
pub tough_recover: i32,
pub tough_change: i32,
pub tough_reduce: i32,
pub tough_recover_delay_time: i32,
pub element_power1: i32,
pub element_power2: i32,
pub element_power3: i32,
pub element_power4: i32,
pub element_power5: i32,
pub element_power6: i32,
pub special_damage_change: i32,
pub strength_fast_climb_cost: i32,
pub element_property_type: i32,
pub weak_time: i32,
pub ignore_def_rate: i32,
pub ignore_damage_resistance_phys: i32,
pub ignore_damage_resistance_element1: i32,
pub ignore_damage_resistance_element2: i32,
pub ignore_damage_resistance_element3: i32,
pub ignore_damage_resistance_element4: i32,
pub ignore_damage_resistance_element5: i32,
pub ignore_damage_resistance_element6: i32,
pub skill_tough_ratio: i32,
pub strength_climb_jump: i32,
pub strength_gliding: i32,
pub mass: i32,
pub braking_friction_factor: i32,
pub gravity_scale: i32,
pub speed_ratio: i32,
pub damage_change_phantom: i32,
pub auto_attack_speed: i32,
pub cast_attack_speed: i32,
pub status_build_up_1_max: i32,
pub status_build_up_1: i32,
pub status_build_up_2_max: i32,
pub status_build_up_2: i32,
pub status_build_up_3_max: i32,
pub status_build_up_3: i32,
pub status_build_up_4_max: i32,
pub status_build_up_4: i32,
pub status_build_up_5_max: i32,
pub status_build_up_5: i32,
pub paralysis_time_max: i32,
pub paralysis_time: i32,
pub paralysis_time_recover: i32,
pub element_energy_max: i32,
pub element_energy: i32,
}

View file

@ -0,0 +1,10 @@
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct FunctionConditionData {
pub function_id: i32,
pub name: String,
pub is_on: bool,
pub open_condition_id: i32,
}

View file

@ -0,0 +1,47 @@
use std::collections::HashMap;
use serde::Deserialize;
use crate::{EntranceEntityData, VectorData};
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct InstanceDungeonData {
pub id: i32,
pub map_config_id: i32,
pub map_name: String,
pub inst_type: i32,
pub inst_sub_type: i32,
pub custom_types: Vec<i32>,
pub mini_map_id: i32,
pub sub_levels: Vec<String>,
pub fight_formation_id: i32,
pub trial_role_info: Vec<i32>,
pub revive_id: i32,
pub born_position: VectorData,
pub born_rotation: VectorData,
pub recover_world_location: Vec<i32>,
pub entrance_entities: Vec<EntranceEntityData>,
pub exit_entities: Vec<i32>,
pub first_reward_id: i32,
pub reward_id: i32,
pub repeat_reward_id: i32,
pub enter_control_id: i32,
pub enter_condition: Vec<i32>,
pub entity_level: i32,
pub recommend_level: HashMap<i32, i32>,
pub recommend_role: Vec<i32>,
pub recommend_element: Vec<i32>,
pub share_attri: i32,
pub can_use_item: i32,
pub guide_type: i32,
pub guide_value: i32,
pub settle_button_type: i32,
pub auto_leave_time: i32,
pub limit_time: i32,
pub leave_wait_time: i32,
pub verify_creature_gen: bool,
pub enter_count: i32,
pub enter_condition_group: i32,
pub drop_vision_limit: i32,
}

View file

@ -0,0 +1,50 @@
use paste::paste;
mod misc_data;
pub use misc_data::*;
#[derive(thiserror::Error, Debug)]
pub enum LoadDataError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Failed to parse json: {0}")]
Json(#[from] serde_json::Error),
}
macro_rules! json_data {
($($table_type:ident;)*) => {
$(paste! {
mod [<$table_type:snake>];
pub use [<$table_type:snake>]::[<$table_type Data>];
})*
$(paste! {
pub mod [<$table_type:snake _data>] {
use std::sync::OnceLock;
type Data = super::[<$table_type Data>];
pub(crate) static TABLE: OnceLock<Vec<Data>> = OnceLock::new();
pub fn iter() -> std::slice::Iter<'static, Data> {
TABLE.get().unwrap().iter()
}
}
})*
pub fn load_json_data(base_path: &str) -> Result<(), LoadDataError> {
$(paste! {
let json_content = std::fs::read_to_string(&format!("{}/{}.json", base_path, stringify!($table_type)))?;
let _ = [<$table_type:snake _data>]::TABLE.set(serde_json::from_str(&json_content)?);
})*
Ok(())
}
};
}
json_data! {
RoleInfo;
WeaponConf;
BaseProperty;
InstanceDungeon;
FunctionCondition;
}

View file

@ -0,0 +1,34 @@
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct PropValueData {
pub id: i32,
pub value: f32,
pub is_ratio: bool,
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct VectorData([f32; 3]);
impl VectorData {
pub fn get_x(&self) -> f32 {
self.0[0]
}
pub fn get_y(&self) -> f32 {
self.0[1]
}
pub fn get_z(&self) -> f32 {
self.0[2]
}
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct EntranceEntityData {
pub dungeon_id: i32,
pub entrance_entity_id: i32,
}

View file

@ -0,0 +1,41 @@
use std::collections::HashMap;
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct RoleInfoData {
pub id: i32,
pub quality_id: i32,
pub role_type: i32,
pub is_trial: bool,
pub name: String,
pub nick_name: String,
pub introduction: String,
pub tag: Vec<i32>,
pub parent_id: i32,
pub priority: i32,
pub show_property: Vec<i32>,
pub element_id: i32,
pub spillover_item: HashMap<i32, i32>,
pub breach_model: i32,
pub special_energy_bar_id: i32,
pub entity_property: i32,
pub max_level: i32,
pub level_consume_id: i32,
pub breach_id: i32,
pub skill_id: i32,
pub skill_tree_group_id: i32,
pub resonance_id: i32,
pub resonant_chain_group_id: i32,
pub is_show: bool,
pub exchange_consume: HashMap<i32, i32>,
pub init_weapon_item_id: i32,
pub weapon_type: i32,
pub party_id: i32,
pub item_quality_id: i32,
pub num_limit: i32,
pub trial_role: i32,
pub is_aim: bool,
pub role_guide: i32,
}

View file

@ -0,0 +1,25 @@
use serde::Deserialize;
use crate::PropValueData;
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct WeaponConfData {
pub item_id: i32,
pub weapon_name: String,
pub quality_id: i32,
pub model_id: i32,
pub transform_id: i32,
pub models: Vec<i32>,
pub reson_level_limit: i32,
pub first_prop_id: PropValueData,
pub first_curve: i32,
pub second_prop_id: PropValueData,
pub second_curve: i32,
pub reson_id: i32,
pub level_id: i32,
pub breach_id: i32,
#[serde(rename = "MaxCapcity")] // kuro!
pub max_capacity: i32,
pub destructible: bool,
}

View file

@ -0,0 +1,8 @@
[package]
name = "shorekeeper-database"
edition = "2021"
version.workspace = true
[dependencies]
sqlx.workspace = true
serde.workspace = true

View file

@ -0,0 +1,23 @@
CREATE TABLE t_user_account (
user_id varchar(64) primary key,
user_name varchar(64) NOT NULL,
token varchar(64) NOT NULL,
sex int DEFAULT -1,
create_time_stamp bigint NOT NULL,
create_device_id varchar(64) NOT NULL,
ban_time_stamp bigint DEFAULT NULL,
last_login_trace_id varchar(64) DEFAULT NULL
);
CREATE TABLE t_user_uid (
player_id int primary key generated always as identity,
user_id varchar(64) NOT NULL,
sex int NOT NULL,
create_time_stamp bigint NOT NULL
);
CREATE TABLE t_player_data (
player_id int primary key,
name varchar(16) NOT NULL,
bin_data bytea NOT NULL
);

Some files were not shown because too many files have changed in this diff Show more