Hi
This commit is contained in:
parent
cfb00eaf29
commit
099ac9f871
127 changed files with 207074 additions and 1 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal 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
2687
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
61
Cargo.toml
Normal file
61
Cargo.toml
Normal 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.
|
58
README.md
58
README.md
|
@ -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
52
assets/config/index.json
Normal 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
|
||||
}
|
||||
}
|
60
assets/hotpatch/prod/Windows/config.json
Normal file
60
assets/hotpatch/prod/Windows/config.json
Normal 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
|
||||
}
|
122739
assets/logic/json/BaseProperty.json
Normal file
122739
assets/logic/json/BaseProperty.json
Normal file
File diff suppressed because it is too large
Load diff
1034
assets/logic/json/FunctionCondition.json
Normal file
1034
assets/logic/json/FunctionCondition.json
Normal file
File diff suppressed because it is too large
Load diff
44971
assets/logic/json/InstanceDungeon.json
Normal file
44971
assets/logic/json/InstanceDungeon.json
Normal file
File diff suppressed because it is too large
Load diff
3874
assets/logic/json/RoleInfo.json
Normal file
3874
assets/logic/json/RoleInfo.json
Normal file
File diff suppressed because it is too large
Load diff
5680
assets/logic/json/WeaponConf.json
Normal file
5680
assets/logic/json/WeaponConf.json
Normal file
File diff suppressed because it is too large
Load diff
11
common/Cargo.toml
Normal file
11
common/Cargo.toml
Normal 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
18
common/src/config_util.rs
Normal 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
4
common/src/lib.rs
Normal 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
8
common/src/logging.rs
Normal 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
3
common/src/splash.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub fn print_splash() {
|
||||
println!(" _____ __ __ \n / ___// /_ ____ ________ / /_____ ___ ____ ___ _____\n \\__ \\/ __ \\/ __ \\/ ___/ _ \\/ //_/ _ \\/ _ \\/ __ \\/ _ \\/ ___/\n ___/ / / / / /_/ / / / __/ ,< / __/ __/ /_/ / __/ / \n/____/_/ /_/\\____/_/ \\___/_/|_|\\___/\\___/ .___/\\___/_/ \n /_/ ");
|
||||
}
|
15
common/src/time_util.rs
Normal file
15
common/src/time_util.rs
Normal 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
22
config-server/Cargo.toml
Normal 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
|
6
config-server/configserver.default.toml
Normal file
6
config-server/configserver.default.toml
Normal 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
45
config-server/src/main.rs
Normal 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
28
game-server/Cargo.toml
Normal 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
|
13
game-server/gameserver.default.toml
Normal file
13
game-server/gameserver.default.toml
Normal 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
16
game-server/src/config.rs
Normal 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");
|
||||
}
|
22
game-server/src/gateway_connection.rs
Normal file
22
game-server/src/gateway_connection.rs
Normal 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)
|
||||
}
|
201
game-server/src/logic/components/attribute.rs
Normal file
201
game-server/src/logic/components/attribute.rs
Normal 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
|
||||
);
|
||||
}
|
15
game-server/src/logic/components/entity_config.rs
Normal file
15
game-server/src/logic/components/entity_config.rs
Normal 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();
|
||||
}
|
||||
}
|
15
game-server/src/logic/components/mod.rs
Normal file
15
game-server/src/logic/components/mod.rs
Normal 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;
|
14
game-server/src/logic/components/movement.rs
Normal file
14
game-server/src/logic/components/movement.rs
Normal 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) {}
|
||||
}
|
9
game-server/src/logic/components/owner_player.rs
Normal file
9
game-server/src/logic/components/owner_player.rs
Normal 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;
|
||||
}
|
||||
}
|
11
game-server/src/logic/components/player_entity_marker.rs
Normal file
11
game-server/src/logic/components/player_entity_marker.rs
Normal 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();
|
||||
}
|
||||
}
|
11
game-server/src/logic/components/position.rs
Normal file
11
game-server/src/logic/components/position.rs
Normal 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());
|
||||
}
|
||||
}
|
9
game-server/src/logic/components/visibility.rs
Normal file
9
game-server/src/logic/components/visibility.rs
Normal 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;
|
||||
}
|
||||
}
|
37
game-server/src/logic/ecs/component.rs
Normal file
37
game-server/src/logic/ecs/component.rs
Normal 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);
|
||||
}
|
57
game-server/src/logic/ecs/entity.rs
Normal file
57
game-server/src/logic/ecs/entity.rs
Normal 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
|
||||
}
|
||||
}
|
64
game-server/src/logic/ecs/mod.rs
Normal file
64
game-server/src/logic/ecs/mod.rs
Normal 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), )*))
|
||||
};
|
||||
}
|
58
game-server/src/logic/ecs/world.rs
Normal file
58
game-server/src/logic/ecs/world.rs
Normal 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);
|
||||
}
|
||||
}
|
80
game-server/src/logic/handler/mod.rs
Normal file
80
game-server/src/logic/handler/mod.rs
Normal 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,
|
||||
),
|
||||
}
|
||||
}
|
77
game-server/src/logic/handler/scene.rs
Normal file
77
game-server/src/logic/handler/scene.rs
Normal 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);
|
||||
}
|
||||
}
|
5
game-server/src/logic/math/mod.rs
Normal file
5
game-server/src/logic/math/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod transform;
|
||||
mod vector;
|
||||
|
||||
pub use transform::Transform;
|
||||
pub use vector::Vector3f;
|
49
game-server/src/logic/math/transform.rs
Normal file
49
game-server/src/logic/math/transform.rs
Normal 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()),
|
||||
}
|
||||
}
|
||||
}
|
42
game-server/src/logic/math/vector.rs
Normal file
42
game-server/src/logic/math/vector.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
9
game-server/src/logic/mod.rs
Normal file
9
game-server/src/logic/mod.rs
Normal 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;
|
72
game-server/src/logic/player/basic_info.rs
Normal file
72
game-server/src/logic/player/basic_info.rs
Normal 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())),
|
||||
}
|
||||
}
|
9
game-server/src/logic/player/in_world_player.rs
Normal file
9
game-server/src/logic/player/in_world_player.rs
Normal 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,
|
||||
}
|
43
game-server/src/logic/player/location.rs
Normal file
43
game-server/src/logic/player/location.rs
Normal 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),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
243
game-server/src/logic/player/mod.rs
Normal file
243
game-server/src/logic/player/mod.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
46
game-server/src/logic/player/player_func.rs
Normal file
46
game-server/src/logic/player/player_func.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
30
game-server/src/logic/role/formation.rs
Normal file
30
game-server/src/logic/role/formation.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
89
game-server/src/logic/role/mod.rs
Normal file
89
game-server/src/logic/role/mod.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
28
game-server/src/logic/systems/mod.rs
Normal file
28
game-server/src/logic/systems/mod.rs
Normal 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;
|
||||
}
|
70
game-server/src/logic/systems/movement.rs
Normal file
70
game-server/src/logic/systems/movement.rs
Normal 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
|
||||
}
|
||||
}
|
217
game-server/src/logic/thread_mgr.rs
Normal file
217
game-server/src/logic/thread_mgr.rs
Normal 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()
|
||||
}
|
26
game-server/src/logic/utils/entity_serializer.rs
Normal file
26
game-server/src/logic/utils/entity_serializer.rs
Normal 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
|
||||
}
|
2
game-server/src/logic/utils/mod.rs
Normal file
2
game-server/src/logic/utils/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod entity_serializer;
|
||||
pub mod world_util;
|
127
game-server/src/logic/utils/world_util.rs
Normal file
127
game-server/src/logic/utils/world_util.rs
Normal 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
35
game-server/src/main.rs
Normal 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(())
|
||||
}
|
61
game-server/src/player_save_task.rs
Normal file
61
game-server/src/player_save_task.rs
Normal 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
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
318
game-server/src/service_message_handler.rs
Normal file
318
game-server/src/service_message_handler.rs
Normal 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;
|
||||
}
|
28
game-server/src/session/manager.rs
Normal file
28
game-server/src/session/manager.rs
Normal 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)
|
||||
}
|
||||
}
|
41
game-server/src/session/mod.rs
Normal file
41
game-server/src/session/mod.rs
Normal 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
31
gateway-server/Cargo.toml
Normal 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
|
20
gateway-server/gateway.default.toml
Normal file
20
gateway-server/gateway.default.toml
Normal 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"
|
19
gateway-server/kcp/Cargo.toml
Normal file
19
gateway-server/kcp/Cargo.toml
Normal 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"
|
||||
|
54
gateway-server/kcp/src/error.rs
Normal file
54
gateway-server/kcp/src/error.rs
Normal 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)
|
||||
}
|
||||
}
|
1504
gateway-server/kcp/src/kcp.rs
Normal file
1504
gateway-server/kcp/src/kcp.rs
Normal file
File diff suppressed because it is too large
Load diff
17
gateway-server/kcp/src/lib.rs
Normal file
17
gateway-server/kcp/src/lib.rs
Normal 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>;
|
8
gateway-server/security/client_public_key.pem
Normal file
8
gateway-server/security/client_public_key.pem
Normal 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-----
|
24
gateway-server/src/config.rs
Normal file
24
gateway-server/src/config.rs
Normal 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");
|
||||
}
|
108
gateway-server/src/handler/client_message_handler.rs
Normal file
108
gateway-server/src/handler/client_message_handler.rs
Normal 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(())
|
||||
}
|
||||
}
|
48
gateway-server/src/handler/client_push_handler.rs
Normal file
48
gateway-server/src/handler/client_push_handler.rs
Normal 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)
|
||||
);
|
||||
}
|
215
gateway-server/src/handler/client_request_handler.rs
Normal file
215
gateway-server/src/handler/client_request_handler.rs
Normal 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(())
|
||||
}
|
18
gateway-server/src/handler/game_server_connection.rs
Normal file
18
gateway-server/src/handler/game_server_connection.rs
Normal 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
|
||||
}
|
6
gateway-server/src/handler/mod.rs
Normal file
6
gateway-server/src/handler/mod.rs
Normal 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;
|
210
gateway-server/src/handler/service_message_handler.rs
Normal file
210
gateway-server/src/handler/service_message_handler.rs
Normal 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;
|
||||
}
|
48
gateway-server/src/main.rs
Normal file
48
gateway-server/src/main.rs
Normal 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(())
|
||||
}
|
11
gateway-server/src/session/error.rs
Normal file
11
gateway-server/src/session/error.rs
Normal 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),
|
||||
}
|
18
gateway-server/src/session/manager.rs
Normal file
18
gateway-server/src/session/manager.rs
Normal 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)
|
||||
}
|
||||
}
|
198
gateway-server/src/session/mod.rs
Normal file
198
gateway-server/src/session/mod.rs
Normal 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(()))
|
||||
}
|
||||
}
|
53
gateway-server/src/session/util.rs
Normal file
53
gateway-server/src/session/util.rs
Normal 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
|
||||
}
|
93
gateway-server/src/udp_server.rs
Normal file
93
gateway-server/src/udp_server.rs
Normal 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}"));
|
||||
}
|
||||
}
|
22
hotpatch-server/Cargo.toml
Normal file
22
hotpatch-server/Cargo.toml
Normal 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
|
6
hotpatch-server/hotpatch.default.toml
Normal file
6
hotpatch-server/hotpatch.default.toml
Normal file
|
@ -0,0 +1,6 @@
|
|||
[network]
|
||||
http_addr = "0.0.0.0:10002"
|
||||
|
||||
[encryption]
|
||||
key = "t+AEu5SGdpz06tomonajLMau9AJgmyTvVhz9VtGf1+0="
|
||||
iv = "fprc5lBWADQB7tim0R2JxQ=="
|
49
hotpatch-server/src/main.rs
Normal file
49
hotpatch-server/src/main.rs
Normal 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
22
login-server/Cargo.toml
Normal 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
|
12
login-server/loginserver.default.toml
Normal file
12
login-server/loginserver.default.toml
Normal 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"
|
21
login-server/src/config.rs
Normal file
21
login-server/src/config.rs
Normal 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");
|
||||
}
|
52
login-server/src/handler.rs
Normal file
52
login-server/src/handler.rs
Normal 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
46
login-server/src/main.rs
Normal 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(())
|
||||
}
|
75
login-server/src/schema.rs
Normal file
75
login-server/src/schema.rs
Normal 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
BIN
screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6 MiB |
10
shorekeeper-data/Cargo.toml
Normal file
10
shorekeeper-data/Cargo.toml
Normal 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
|
143
shorekeeper-data/src/base_property.rs
Normal file
143
shorekeeper-data/src/base_property.rs
Normal 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,
|
||||
}
|
10
shorekeeper-data/src/function_condition.rs
Normal file
10
shorekeeper-data/src/function_condition.rs
Normal 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,
|
||||
}
|
47
shorekeeper-data/src/instance_dungeon.rs
Normal file
47
shorekeeper-data/src/instance_dungeon.rs
Normal 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,
|
||||
}
|
50
shorekeeper-data/src/lib.rs
Normal file
50
shorekeeper-data/src/lib.rs
Normal 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;
|
||||
}
|
34
shorekeeper-data/src/misc_data.rs
Normal file
34
shorekeeper-data/src/misc_data.rs
Normal 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,
|
||||
}
|
41
shorekeeper-data/src/role_info.rs
Normal file
41
shorekeeper-data/src/role_info.rs
Normal 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,
|
||||
}
|
25
shorekeeper-data/src/weapon_conf.rs
Normal file
25
shorekeeper-data/src/weapon_conf.rs
Normal 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,
|
||||
}
|
8
shorekeeper-database/Cargo.toml
Normal file
8
shorekeeper-database/Cargo.toml
Normal file
|
@ -0,0 +1,8 @@
|
|||
[package]
|
||||
name = "shorekeeper-database"
|
||||
edition = "2021"
|
||||
version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
sqlx.workspace = true
|
||||
serde.workspace = true
|
23
shorekeeper-database/migrations/0001_tables.sql
Normal file
23
shorekeeper-database/migrations/0001_tables.sql
Normal 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
Loading…
Reference in a new issue