This commit is contained in:
xeon 2024-11-25 02:17:19 +03:00
commit b63c7c0f0d
177 changed files with 99444 additions and 0 deletions

9
.gitignore vendored Normal file
View file

@ -0,0 +1,9 @@
target/
/autopatch.toml
/dispatch.toml
/gateserver.toml
/gameserver.toml
/sdk_server.toml
*.proto

5090
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

77
Cargo.toml Normal file
View file

@ -0,0 +1,77 @@
[workspace]
members = ["crates/*", "crates/gate-server/kcp", "crates/yanagi-proto/yanagi-proto-derive", "crates/qwer/qwer-derive", "crates/protocol/protocol-macros", "crates/qwer-rpc/qwer-server-example", "crates/qwer-rpc/qwer-client-example", "crates/yanagi-data/blockfile"]
resolver = "2"
[workspace.package]
version = "0.0.1"
[workspace.dependencies]
# Framework
tokio = { version = "1.40.0", features = ["full"] }
futures = "0.3.31"
axum = { version = "0.7.6" }
axum-server = "0.7.1"
# Http
ureq = "2.10.1"
tower-http = { version = "0.6.1", features = ["fs"] }
# Serialization
serde = { version = "1.0.210", features = ["derive"] }
serde_json = "1.0.128"
toml = "0.8.19"
regex = "1.10.6"
rbase64 = "2.0.3"
hex = "0.4.3"
# Flatbuffers
flatbuffers = "24.3.25"
flatc-rust = "0.2.0"
# Protobuf
prost = "0.13.3"
prost-types = "0.13.3"
prost-build = "0.13.3"
# Cryptography
rsa = { version = "0.9.6", features = ["sha2"] }
xxhash-rust = { version = "0.8.12", features = ["const_xxh64"] }
# Database
surrealdb = "2.0.4"
# Error processing
anyhow = "1.0.93"
thiserror = "2.0.0"
# Util
byteorder = "1.5.0"
dashmap = "6.1.0"
rand = "0.8.5"
rand_mt = "4.2.2"
password-hash = { version = "0.5.0", features = ["alloc", "rand_core"] }
pbkdf2 = { version = "0.12.2", features = ["simple"] }
paste = "1.0.15"
const_format = "0.2.33"
# Tracing
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
# Internal
common = { path = "crates/common" }
qwer = { path = "crates/qwer" }
qwer-rpc = { path = "crates/qwer-rpc" }
qwer-derive = { path = "crates/qwer/qwer-derive" }
protocol = { path = "crates/protocol" }
yanagi-data = { path = "crates/yanagi-data" }
yanagi-eventgraph = { path = "crates/yanagi-eventgraph" }
yanagi-proto = { path = "crates/yanagi-proto" }
yanagi-encryption = { path = "crates/yanagi-encryption" }
yanagi-http-client = { path = "crates/yanagi-http-client" }
[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.

52
README.md Normal file
View file

@ -0,0 +1,52 @@
# YanagiZS
![screenshot](screenshot.png)
## About
**YanagiZS** is an open source server emulator for the game **Zenless Zone Zero**.
## Features
- NPC spawn and interaction logic
- training battles
- player data persistence
- version-agnostic protocol library, allowing to modify protocol capabilities in most painless way
## Getting started
### Requirements
- [Rust](https://www.rust-lang.org/tools/install)
- [SurrealDB](https://surrealdb.com/docs/surrealdb/installation)
##### NOTE: to start SurrealDB, use this command: `surreal start -u root -p root surrealkv://yanagi`
### Setup
#### a) building from sources (preferred)
```sh
git clone https://git.xeondev.com/HollowSpecialOperationsS6/YanagiZS.git
cd YanagiZS
cargo run --bin yanagi-autopatch-server
cargo run --bin yanagi-sdk-server
cargo run --bin yanagi-dispatch-server
cargo run --bin yanagi-gate-server
cargo run --bin yanagi-game-server
```
#### b) using pre-built binaries
Navigate to the [Releases](https://git.xeondev.com/HollowSpecialOperationsS6/YanagiZS/releases) page and download the latest release for your platform.<br>
Launch all services: `yanagi-autopatch-server`, `yanagi-sdk-server`, `yanagi-dispatch-server`, `yanagi-gate-server`, `yanagi-game-server`
### Configuration
You should configure each service using their own config files. They're being created in current working directory upon first startup.
### Connecting
You have to get a compatible game client. Currently supported one is `CNBetaWin1.4.2`, you can [get it here](https://git.xeondev.com/xeon/3/raw/branch/3/ZZZ_1.4_beta_reversedrooms.torrent). Next, you have to apply [this patch](https://git.xeondev.com/HollowSpecialOperationsS6/Yanagi-Patch/releases), it allows you to connect to local server and replaces RSA encryption keys with custom ones.
##### NOTE: you have to create in-game account, by default, you can do so at https://127.0.0.1:10001/account/register
##### NOTE2: to register an account, you should have `yanagi-sdk-server` up and running!
## Community
[Our Discord Server](https://discord.gg/reversedrooms) is open for everyone who's interested in our projects!
## Support
Your support for this project is greatly appreciated! If you'd like to contribute, feel free to send a tip [via Boosty](https://boosty.to/xeondev/donate)!
## Friendly reminder
The server is in a very early state. Right now, it's NOT recommended to run this on a production environment. Please don't open issues about missing features, I'm well aware of this.
## Sanity
If you want to lose your sanity, consider checking [this](crates/qwer-rpc/src/)

View file

@ -0,0 +1,22 @@
{
"VersionInfoGroups": {
"OSPRODWin1.3.0": {
"Seed": "",
"Platform": "StandaloneWindows64",
"Environment": "oversea",
"ServerListUrl": "http://127.0.0.1:10000/design_data/NAP_Publish_AppStore_1.4/serverlist.json",
"EncryptionConfigUrl": "http://127.0.0.1:10000/design_data/NAP_Publish_AppStore_1.4/encryption.json",
"DesignDataUrl": "https://autopatchos.zenlesszonezero.com/design_data/1.3_live/output_4626751_a32b289717/client",
"CdnCheckUrl": "https://autopatchcn.juequling.com/nap_test.txt"
},
"CNBetaWin1.4.2": {
"Seed": "",
"Platform": "StandaloneWindows64",
"Environment": "oversea",
"ServerListUrl": "http://127.0.0.1:10000/design_data/NAP_Publish_AppStore_1.4/serverlist.json",
"EncryptionConfigUrl": "http://127.0.0.1:10000/design_data/NAP_Publish_AppStore_1.4/encryption.json",
"DesignDataUrl": "http://127.0.0.1:10000/design_data/beta_live/output_5016531_79764a0a26/client",
"CdnCheckUrl": "https://autopatchcn.juequling.com/nap_test.txt"
}
}
}

View file

@ -0,0 +1 @@
{"3": {"serverPrivateKey": "30820152020100300d06092a864886f70d01010105000482013c30820138020100024077ccf288987058ad46a1ca2ed62ca700a6c7f5fdbbb80b6a15679cf916ebe86f49173c80b2a1f61bb88aa8f57ad2522d2e027f8ef715cb33de13d790d61e858f0203010001024007031bff1acd18a3abcab486e14a63cc09f71252f3b5e615238399108b62eb8be374051c522fbc93beec25806cdf300f9708b0c8713ef85732134952733cd181022100bad8cb93f1d04df5b0903823ecf3edd2799f51c2f759e373f18887711516d831022100a423b581c15958e6881e732ed3912ec7adb1f0974683795b643c9ac0af3189bf022023171c0488e16a02be4a178107bb37ead3548c726529c881b9d6be390a90e3c1022034546623ae1eec26e332c20a25cd5d9aaf97d15a895295383be6ce77abe321450220582b45c388a0254daeac667c4629c4d5d224c126353ffc2c3757fc5c6f97b1d1", "clientPublicKey": "30819f300d06092a864886f70d010101050003818d0030818902818100ae44280ad192e584abcf19bcf56ab719247fbb0e40243c06aaefad917562670385f07eb180beca3e2d8e5404c70a868d14b4c00f99e52d28e0d291c402a1da7ef5edce3e0687cb6f17603affcc81e5c793dceb33a3d511e5dee1b08e79b316f7ed052940b1d8a312acefd168849ef86795762e0498fb45ef6c6c4c1f18711d370203010001"}}

View file

@ -0,0 +1 @@
[{"sid":1,"serverName":"YanagiZS","ip": "127.0.0.1","port": 20501,"noticeRegion":"nap_dev_01","protocol":"kcp","rsaVer": 3,"clientSecretKey":"45633262100000000c577fdb4e6d2b22c852bf79b1e4303400080000a82f1ac8f61a5e41647fa266804999eb8cb6f45733e369a630988e09d0a5a7ca05c21af4a2d1e39b4a0eb912e37ecd5ae2e63581330df64b5b33aed7d21fb2ad3957fa4f6dc47220f50752b33829c909d98a2a073106579e643c65af6dcb66b8b083e145a8214c461c794cacaf74d7191ac952022f5d6d9ab685851f74356513efbef34599880aecac6498030398b08c7de138518462500e60cea2f21010d3debabc2bf70cfa75d5755eacafb6cea308b44dd617cee76d23103d25c88004086593f309342c728506deae679e7ac10797a2bb3794f054e6c50af3a794659de0a0883cbc61e1fd2176edae680de7a8e4cb35286fa610a661713066f3c159bb2da43632c7136441819d88bac5e5d5371b8b5aa032dd206e1de7b3d2ff019759e03743600247a28fbc8f87b073ce7d1a5c1c8ec984d72b9f494ca4764b7e918d0d15e9d0ad0123147d61d355ec0003e70a788c405ef0a137c3c951807cc71b2d6c277434e0f535810a35d73bf1d189544213c6d7d8f49899c43b2c31c241a99bf813dbb3f3e39943b4aa6febde6702730583cf2b0c471a49806c1f97c56f5320def6d1e52484469ae326eb6bcf5ebb9c788775ee547242989ec534e542b9e5781d4bcb6938b49d3f8b219dc33d5abd18344ff614f999c327d96cf4cdac50bf2eb38fe00ecab55cf8aa32dd9250e8d772ab10333f65c3140aa9ee2dd8942478f1ac9033ef39bf90162ae34a0e2800a1335a2e9ff918c21fbf5ca8cb9c059d69b01835e3b16b6264c3e607b8791e3c2b28db2d7cd381a6a05123dcd56bd17998cbdf87078b4892e9a0f8b60d5c1f0e94ab059220f7b16cea61c2e256fd8bb8e0323eec9485e18d78d1b7dbe3164962690afb48d19cccb85dcf86a2c1adb47b6245951239f7ae0c3ce61b48ca9e578ec754a6f6c767a7a37406c29c193a007ecf7049c438acbce8adc42812b14bbe28f03f2c8a3671511e323ce22b49de966318d8c8ab93cdb168bf4eb6d41973db5e3f1e96a06861447a8230d9a5f71d91426b1c5fffb2ac1238b5ef2844f9ff542eefa6305e512a939a1157f714d040a86e9c202dfea2fad56cf1f8c76d3794648bf4df64fc47aef8fbfb0ada6eb08d98191b34ff342c17cae23e9fac70691a899b5cfa6dba83f8c571c763b0dd751ddfce273ac29700ed4608bb6b103120f3654244aa09376b446f1caeadaa91957c1ecb1fc1eaad6255d9d2707f73f80aa356631b4bd04341d4acd74310097d56476c849ad5809687a8eee7ed0706f41f79636632466a4e0c32f2c55745aa0206594c7764959b08ed2ddf75c22f660f127a83202f9def78b0fc7df001d7310086d985542672f6a6c9b27369abc62c511371ad1c97f04c70097bda032229c863dbb68e999f68f02cc04fc2b5c5aa9e0ed14d54518a827b2d74a3deba998b881adae734c4850f8af0f092b31e7e6bbe22cef9d1fe07bc75c54a69915471def592b58cdbfea8e6a5221f325725532b016429e26b2bf9459e5eb4e7a1e604de1d0b9856eba8dacd4e7a08a6f78fa0418638a117244255e19a246aa4353029ed60609fc2b300f5f3d6fe101e039d05f01418e0e54acedef3aa658b5588f25787e412a187457a5e36f679f5cb9e89419df64bd0740b0f9649f9138cc682d966deb829f37e4db8019dbb089a05c4f18334aea0ce90ebab680a019399e7fcafd23589f5904ea3480821d0eb8dd807ec6ae26d12dacdb97cc62ca44d58cc4a7b92d387cee3902a621849ef7917fa0f508e4bc1351bbb782df846ece3f63babee7750fc1f99d83d5ab43b768335aa8bba39539af9016e49390b8ed0df269fabbaf6d314f33ba414233847a6f16b00a87eba259ec37455d5cae8779f10df4930f7abd92a78d0a13b444afa5411b8d7b1195355168514fe5e515304f825d9a6921b3b048fc37c1591795fbf3653877b3fd4dd87fa1e621c7262a7d6ef95f50c85056b8e95a62d269a725e6ba4b1c4bb6a32f9d32c049a401dba4fae4e4caacc1a6bc4c54c783b6c05d0fc2cc051259b57763438463deeb5fc0da4fa5dbfa78d52c37384fead8ece84115776f94f8023a56196917e4267de4d8322e6e0e254de05aef6c044f708449368d254e15c3b877d46b6f9002dd715496fc1d7bdc79a091b838ccad6cafae2aca06d6b0c06e344841903b0ac4199ff61b5a75e406e3fcfe2626398d647a6b78238b85e00ca40424510fabef1fb60f663e7f82fbc6defe653ff2ec4dd8feea30e990b190c4bcacab988b30c9cb42c22b7044f0e5131ef880b905e87e72a0a9e09311444a7accaf84fd9ac685447d59619ae9f46cb59bd15203d0c7cce519f6dec009470653c6a0e17da2accd4cc1f4efe90ce67f9b94a15fa63a5b64678c97c127ccc245a9f50da47713fab54dd63c7e64e135922fb6dd205ee702d9c540730688a8228ee00fc7eb9eacc366278af2c506e6777122dd93bcf9d90cf7855b4e7134b7f37a7eb4387fd2003430380c1758be7999136187fd7de80f9f726b3bb68be7987a8eaa04ddc73d9ff234b8db175ece2bb0cacd4acee8cebd88a36877e314ea86c18037564c5c0f25af0cce04f6cd3a8e389c7020cb97e0e5684b5cdfd6ce8e2b1166daeddca60a2db213631951a87e378fbe45dbce52771e16e15e1676b2f57605ab3160e73e6f9c83a357f85f99291fca5646aafe76ecd296e2d8979663427e2898d8f8f331b8999a2703064061ad07cab0cc9567fc0d64a45fdc8b71b7282a7114e8d5a76f59326d264b152caaa44fbb12a3171c56f5e5e6667ddb2b115185e26735fcea74300afe3a80c54222d11210273a410a2e137a9e1c21646544dc473b45a2ddeddbbdd649fa3b9c05e8549fb2d600cf17bdf3e4d6084a836a2d9317de5a60b"}]

View file

@ -0,0 +1 @@
{"remoteParentDir":"","files":[{"remoteName":"Blocks/3328852140.blk","md5":"9122950185770172588","fileSize":578833,"isPatch":false,"tags":[1]},{"remoteName":"Blocks/3790922690.blk","md5":"17935600715388216258","fileSize":4194814,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/2284334622.blk","md5":"1921214220396408350","fileSize":9994135,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/2668462595.blk","md5":"15799288027094548995","fileSize":7501775,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/3780966874.blk","md5":"3623952593348000218","fileSize":8255948,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/3464772020.blk","md5":"6404785737841260980","fileSize":8212979,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/1466521055.blk","md5":"14504298224428864991","fileSize":7868859,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/3563588734.blk","md5":"7183384049378397310","fileSize":10485827,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/902016349.blk","md5":"12423097650797325661","fileSize":7955779,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/1503146322.blk","md5":"17774132456989405522","fileSize":9217404,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/3616722805.blk","md5":"15234963238410178421","fileSize":7413055,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/3284963563.blk","md5":"10056350654794600683","fileSize":7901056,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/98028081.blk","md5":"3393097878461729329","fileSize":8422035,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/2381137273.blk","md5":"9315744742684575097","fileSize":7646161,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/3405069113.blk","md5":"1010670692224809785","fileSize":7775021,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/612355727.blk","md5":"3760073409598312079","fileSize":8102962,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/2388337555.blk","md5":"18037935375034033043","fileSize":183748,"isPatch":false,"tags":[3]},{"remoteName":"Blocks/1584573291.blk","md5":"3023418238579465498","fileSize":55527,"isPatch":false}]}

View file

@ -0,0 +1 @@
{"remoteParentDir":"","files":[{"remoteName":"Blocks/1688854445.blk","md5":"14618248753171219425","fileSize":26,"isPatch":false}]}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
{"remoteParentDir":"","files":[{"remoteName":"Blocks/3328852140.blk","md5":"9122950185770172588","fileSize":578833,"isPatch":false,"tags":[1]},{"remoteName":"Blocks/3790922690.blk","md5":"17935600715388216258","fileSize":4194814,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/2284334622.blk","md5":"1921214220396408350","fileSize":9994135,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/2668462595.blk","md5":"15799288027094548995","fileSize":7501775,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/3780966874.blk","md5":"3623952593348000218","fileSize":8255948,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/3464772020.blk","md5":"6404785737841260980","fileSize":8212979,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/1466521055.blk","md5":"14504298224428864991","fileSize":7868859,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/3563588734.blk","md5":"7183384049378397310","fileSize":10485827,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/902016349.blk","md5":"12423097650797325661","fileSize":7955779,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/1503146322.blk","md5":"17774132456989405522","fileSize":9217404,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/3616722805.blk","md5":"15234963238410178421","fileSize":7413055,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/3284963563.blk","md5":"10056350654794600683","fileSize":7901056,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/98028081.blk","md5":"3393097878461729329","fileSize":8422035,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/2381137273.blk","md5":"9315744742684575097","fileSize":7646161,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/3405069113.blk","md5":"1010670692224809785","fileSize":7775021,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/612355727.blk","md5":"3760073409598312079","fileSize":8102962,"isPatch":false,"tags":[2]},{"remoteName":"Blocks/2388337555.blk","md5":"18037935375034033043","fileSize":183748,"isPatch":false,"tags":[3]},{"remoteName":"Blocks/1584573291.blk","md5":"3023418238579465498","fileSize":55527,"isPatch":false}]}

File diff suppressed because one or more lines are too long

1
autopatch/nap_test.txt Normal file
View file

@ -0,0 +1 @@
nap_test

View file

@ -0,0 +1,23 @@
[package]
name = "yanagi-autopatch-server"
edition = "2021"
version.workspace = true
[dependencies]
# Framework
tokio.workspace = true
axum.workspace = true
axum-server.workspace = true
tower-http.workspace = true
# Error processing
anyhow.workspace = true
# Serialization
serde.workspace = true
# Tracing
tracing.workspace = true
# Internal
common.workspace = true

View file

@ -0,0 +1,2 @@
http_addr = "0.0.0.0:10000"
serve_dir = "./autopatch"

View file

@ -0,0 +1,38 @@
use anyhow::Result;
use axum::{handler::HandlerWithoutStateExt, http::StatusCode, Router};
use common::config::TomlConfig;
use serde::Deserialize;
use tower_http::services::ServeDir;
#[derive(Deserialize)]
struct AutopatchConfig {
pub http_addr: String,
pub serve_dir: String,
}
impl TomlConfig for AutopatchConfig {
const DEFAULT_TOML: &str = include_str!("../autopatch.toml");
}
#[tokio::main]
async fn main() -> Result<()> {
let config = AutopatchConfig::load_or_create("autopatch.toml");
common::print_splash();
common::logging::init(tracing::Level::DEBUG);
axum_server::bind(config.http_addr.parse()?)
.serve(
Router::new()
.nest_service(
"/",
ServeDir::new(&config.serve_dir).not_found_service(not_found.into_service()),
)
.into_make_service(),
)
.await?;
Ok(())
}
async fn not_found() -> (StatusCode, &'static str) {
(StatusCode::NOT_FOUND, "404 page not found")
}

14
crates/common/Cargo.toml Normal file
View file

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

View file

@ -0,0 +1,21 @@
use std::collections::HashMap;
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct AppConfig {
pub version_info_groups: HashMap<String, ConfigurationInfo>,
}
#[derive(Deserialize)]
#[serde(rename_all = "PascalCase")]
pub struct ConfigurationInfo {
pub seed: String,
pub server_list_url: String,
pub platform: String,
pub environment: String,
pub encryption_config_url: String,
pub design_data_url: String,
pub cdn_check_url: String,
}

View file

@ -0,0 +1,15 @@
use super::from_hex;
use std::collections::HashMap;
use serde::Deserialize;
pub type EncryptionConfMap = HashMap<u32, EncryptionConfig>;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EncryptionConfig {
#[serde(deserialize_with = "from_hex")]
pub server_private_key: Box<[u8]>,
#[serde(deserialize_with = "from_hex")]
pub client_public_key: Box<[u8]>,
}

View file

@ -0,0 +1,22 @@
use serde::{de::DeserializeOwned, Deserialize};
pub trait TomlConfig: DeserializeOwned {
const DEFAULT_TOML: &str;
fn load_or_create(path: &str) -> Self {
std::fs::read_to_string(path).map_or_else(
|_| {
std::fs::write(path, Self::DEFAULT_TOML).unwrap();
toml::from_str(Self::DEFAULT_TOML).unwrap()
},
|data| toml::from_str(&data).unwrap(),
)
}
}
#[derive(Deserialize)]
pub struct DatabaseSettings {
pub url: String,
pub username: String,
pub password: String,
}

View file

@ -0,0 +1,23 @@
mod local_toml;
pub use local_toml::{DatabaseSettings, TomlConfig};
mod app;
mod encryption;
mod server_list;
pub use app::*;
pub use encryption::*;
use serde::{Deserialize, Deserializer};
pub use server_list::*;
pub fn from_hex<'de, D>(deserializer: D) -> Result<Box<[u8]>, D::Error>
where
D: Deserializer<'de>,
{
use serde::de::Error;
String::deserialize(deserializer).and_then(|string| {
hex::decode(&string)
.map(|vec| vec.into_boxed_slice())
.map_err(|err| Error::custom(err.to_string()))
})
}

View file

@ -0,0 +1,26 @@
use super::from_hex;
use serde::Deserialize;
pub type ServerList = Vec<ServerListInfo>;
#[derive(Deserialize)]
pub enum ServerProtocolType {
#[serde(rename = "tcp")]
Tcp,
#[serde(rename = "kcp")]
Kcp,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerListInfo {
pub sid: u32,
pub server_name: String,
pub ip: String,
pub port: u16,
pub notice_region: String,
pub protocol: ServerProtocolType,
pub rsa_ver: u32,
#[serde(deserialize_with = "from_hex")]
pub client_secret_key: Box<[u8]>,
}

View file

@ -0,0 +1,7 @@
pub const UID_INCREMENT_QUERY: &str = "RETURN fn::increment($counter_name)";
pub const UID_COUNTER_NAME_BIND: &str = "counter_name";
pub const DEFINE_INCREMENT_FUNC_QUERY: &str = r#"DEFINE FUNCTION fn::increment($name: string) {
RETURN (UPSERT ONLY type::thing('counter', $name)
SET value += 1).value;
};"#;

13
crates/common/src/lib.rs Normal file
View file

@ -0,0 +1,13 @@
pub mod config;
pub mod db_const;
pub mod logging;
pub mod time_util;
pub enum ServiceStatus {
StopServer(u64),
Running,
}
pub fn print_splash() {
println!(" __ __ _ ______ _____ \n \\ \\ / / (_)___ // ____|\n \\ \\_/ /_ _ _ __ __ _ __ _ _ / /| (___ \n \\ / _` | '_ \\ / _` |/ _` | | / / \\___ \\ \n | | (_| | | | | (_| | (_| | |/ /__ ____) |\n |_|\\__,_|_| |_|\\__,_|\\__, |_/_____|_____/ \n __/ | \n |___/ ");
}

View file

@ -0,0 +1,17 @@
use tracing::level_filters::LevelFilter;
use tracing_subscriber::filter::EnvFilter;
pub fn init(level: tracing::Level) {
tracing_subscriber::fmt()
.with_max_level(level)
.with_env_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.into())
.from_env()
.unwrap()
.add_directive("ureq=error".parse().unwrap()),
)
.without_time()
.with_target(false)
.init();
}

View file

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

View file

@ -0,0 +1,27 @@
[package]
name = "yanagi-dispatch-server"
edition = "2021"
version.workspace = true
[dependencies]
# Framework
tokio.workspace = true
axum.workspace = true
axum-server.workspace = true
# Error processing
anyhow.workspace = true
thiserror.workspace = true
# Serialization
serde.workspace = true
serde_json.workspace = true
rbase64.workspace = true
# Tracing
tracing.workspace = true
# Internal
common.workspace = true
yanagi-encryption.workspace = true
yanagi-http-client.workspace = true

View file

@ -0,0 +1,3 @@
http_addr = "0.0.0.0:10002"
outer_http_url = "http://127.0.0.1:10002"
design_data_url = "http://127.0.0.1:10000/design_data/NAP_Publish_AppStore_1.4"

View file

@ -0,0 +1,155 @@
use serde::Serialize;
#[derive(Serialize, Default)]
pub struct ServerDispatchData {
pub retcode: i32,
#[serde(skip_serializing_if = "String::is_empty")]
pub msg: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub title: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub region_name: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub client_secret_key: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub cdn_check_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub gateway: Option<ServerGateway>,
#[serde(skip_serializing_if = "String::is_empty")]
pub oaserver_url: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub force_update_url: String,
#[serde(skip_serializing_if = "String::is_empty")]
pub stop_jump_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub cdn_conf_ext: Option<CdnConfExt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub region_ext: Option<RegionExtension>,
}
#[derive(Serialize)]
pub struct ServerGateway {
pub ip: String,
pub port: u16,
}
#[derive(Serialize)]
pub struct RegionExtension {
pub func_switch: RegionSwitchFunc,
pub feedback_url: String,
pub exchange_url: String,
pub pgc_webview_method: i32,
#[serde(rename = "mtrNap")]
pub mtr_nap: String,
#[serde(rename = "mtrSdk")]
pub mtr_sdk: String,
#[serde(rename = "urlCheckNap")]
pub url_check_nap: String,
#[serde(rename = "urlCheckSdk")]
pub url_check_sdk: String,
}
#[derive(Serialize, Default)]
pub struct RegionSwitchFunc {
#[serde(rename = "Close_Medium_Package_Download")]
pub close_medium_package_download: i64,
#[serde(rename = "Disable_Audio_Download")]
pub disable_audio_download: i64,
#[serde(rename = "Hide_Download_complete_resources")]
pub hide_download_complete_resources: i64,
#[serde(rename = "Hide_Download_resources_popups")]
pub hide_download_resources_popups: i64,
#[serde(rename = "Hide_download_progress")]
pub hide_download_progress: i64,
#[serde(rename = "Medium_Package_Play")]
pub medium_package_play: i64,
#[serde(rename = "Play_The_Music")]
pub play_the_music: i64,
pub disable_anim_allocator_opt: i64,
#[serde(rename = "disableAsyncSRPSubmit")]
pub disable_async_srpsubmit: i64,
pub disable_execute_async: i64,
#[serde(rename = "disableMetalPSOCreateAsync")]
pub disable_metal_psocreate_async: i64,
pub disable_object_instance_cache: i64,
#[serde(rename = "disableSRPHelper")]
pub disable_srp_helper: i64,
#[serde(rename = "disableSRPInstancing")]
pub disable_srp_instancing: i64,
pub disable_skin_mesh_strip: i64,
pub disable_step_preload_monster: i64,
pub disable_tex_streaming_visbility_opt: i64,
#[serde(rename = "disableiOSGPUBufferOpt")]
pub disable_ios_gpubuffer_opt: i64,
#[serde(rename = "disableiOSShaderHibernation")]
pub disable_ios_shader_hibernation: i64,
#[serde(rename = "enableiOSShaderWarmupOnStartup")]
pub enable_ios_shader_warmup_on_startup: i64,
#[serde(rename = "isKcp")]
pub is_kcp: i32,
#[serde(rename = "mtrConfig")]
pub mtr_config: Option<String>,
#[serde(rename = "perfSwitch1")]
pub perf_switch_1: i32,
#[serde(rename = "perfSwitch2")]
pub perf_switch_2: i32,
#[serde(rename = "enableNoticeMobileConsole")]
pub enable_notice_mobile_console: i32,
#[serde(rename = "enableGachaMobileConsole")]
pub enable_gacha_mobile_console: i32,
#[serde(rename = "Disable_Popup_Notification")]
pub disable_popup_notification: i32,
#[serde(rename = "open_hotfix_popups")]
pub open_hotfix_popups: i32,
pub enable_operation_log: i32,
#[serde(rename = "Turnoff_Push_notifications")]
pub turnoff_push_notifications: i32,
#[serde(rename = "Disable_Frequent_attempts")]
pub disable_frequent_attempts: i32,
pub enable_performance_log: i32,
#[serde(rename = "Turnoff_unsafepreload_cloudgame")]
pub turnoff_unsafepreload_cloudgame: i32,
#[serde(rename = "Hide_Code_Login")]
pub hide_code_login: i32,
}
#[derive(Serialize)]
pub struct CdnConfExt {
pub game_res: CdnGameRes,
pub design_data: CdnDesignData,
pub silence_data: CdnSilenceData,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_download: Option<CdnGameRes>,
}
#[derive(Serialize)]
pub struct CdnGameRes {
pub base_url: String,
pub res_revision: String,
pub audio_revision: String,
pub branch: String,
pub md5_files: String, // Vec<VersionFileInfo> packed as string
}
#[derive(Serialize)]
pub struct CdnDesignData {
pub base_url: String,
pub data_revision: String,
pub md5_files: String, // Vec<VersionFileInfo> packed as string
}
#[derive(Serialize)]
pub struct CdnSilenceData {
pub base_url: String,
pub silence_revision: String,
pub md5_files: String, // Vec<VersionFileInfo> packed as string
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct VersionFileInfo {
pub file_name: String,
pub file_size: i64,
#[serde(rename = "fileMD5")]
pub file_md5: String,
}

View file

@ -0,0 +1,23 @@
use serde::Serialize;
#[derive(Serialize, Default)]
pub struct ServerListInfo {
pub area: u8,
pub biz: String,
pub dispatch_url: String,
pub env: u8,
pub is_recommend: bool,
pub name: String,
pub ping_url: String,
pub retcode: i32,
pub title: String,
}
#[derive(Serialize, Default)]
pub struct QueryDispatchRsp {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub region_list: Vec<ServerListInfo>,
pub retcode: i32,
#[serde(skip_serializing_if = "String::is_empty")]
pub msg: String,
}

View file

@ -0,0 +1,5 @@
mod gateway;
mod global;
pub use gateway::*;
pub use global::*;

View file

@ -0,0 +1,205 @@
use axum::extract::{Path, Query, State};
use axum::routing::get;
use axum::{Json, Router};
use serde::{Deserialize, Serialize};
use crate::data::{
CdnConfExt, CdnDesignData, CdnGameRes, CdnSilenceData, RegionExtension, RegionSwitchFunc,
ServerDispatchData, ServerGateway,
};
use crate::{data, AppState};
use common::config::{EncryptionConfig, ServerProtocolType};
pub fn routes() -> Router<&'static AppState> {
Router::new()
.route("/query_dispatch", get(query_dispatch))
.route("/query_gateway/:server_name", get(query_gateway))
}
#[derive(Deserialize)]
struct QueryDispatchParam {
pub version: String,
}
async fn query_dispatch(
State(state): State<&'static AppState>,
Query(param): Query<QueryDispatchParam>,
) -> Json<data::QueryDispatchRsp> {
let Some(server_list_map) = state.server_list.get() else {
return Json(data::QueryDispatchRsp {
retcode: 71, // maintenance (stop_server)
..Default::default()
});
};
let Some(server_list) = server_list_map.get(&param.version) else {
return Json(data::QueryDispatchRsp {
retcode: 70,
..Default::default()
});
};
Json(data::QueryDispatchRsp {
retcode: 0,
msg: String::new(),
region_list: server_list
.iter()
.map(|info| data::ServerListInfo {
retcode: 0,
name: info.notice_region.clone(),
title: info.server_name.clone(),
dispatch_url: format!(
"{}/query_gateway/{}",
&state.dispatch_config.outer_http_url, &info.notice_region
),
biz: String::from("nap_global"),
env: 2,
..Default::default()
})
.collect(),
})
}
#[derive(Serialize)]
#[serde(untagged)]
enum QueryGatewayRsp {
Plaintext(ServerDispatchData),
Encrypted { content: String, sign: String },
}
impl QueryGatewayRsp {
pub fn error(retcode: i32, msg: &str) -> Self {
Self::Plaintext(ServerDispatchData {
retcode,
msg: msg.to_string(),
..Default::default()
})
}
pub fn encrypt(self, config: &EncryptionConfig) -> Self {
match self {
Self::Encrypted { .. } => self,
Self::Plaintext(data) => {
let data = serde_json::to_string(&data).unwrap();
let content = yanagi_encryption::rsa::encrypt(config, data.as_bytes());
let sign = yanagi_encryption::rsa::sign(config, data.as_bytes());
Self::Encrypted {
content: rbase64::encode(&content),
sign: rbase64::encode(&sign),
}
}
}
}
}
#[derive(Deserialize)]
struct QueryGatewayParam {
pub version: String,
pub rsa_ver: u32,
pub seed: String,
}
async fn query_gateway(
State(state): State<&'static AppState>,
Path(server_name): Path<String>,
Query(param): Query<QueryGatewayParam>,
) -> Json<QueryGatewayRsp> {
let (Some(app_config), Some(server_list_map), Some(encryption_conf_map)) = (
state.app_config.get(),
state.server_list.get(),
state.encryption_config.get(),
) else {
tracing::debug!("query_gateway requested, but server is in stop mode");
return Json(QueryGatewayRsp::error(71, ""));
};
let Some(encryption_conf) = encryption_conf_map
.get(&param.version)
.map(|k| k.get(&param.rsa_ver))
.flatten()
else {
tracing::debug!("EncryptionConfig for version {} not found", param.version);
return Json(QueryGatewayRsp::error(74, ""));
};
let Some(version_info) = app_config.version_info_groups.get(&param.version) else {
tracing::debug!("VersionInfoGroup for {} not found", param.version);
return Json(QueryGatewayRsp::error(70, "").encrypt(encryption_conf));
};
if !version_info.seed.is_empty() && version_info.seed != param.seed {
tracing::debug!(
"dispatch seed for version {} doesn't match. Config seed: {}, client seed: {}",
param.version,
version_info.seed,
param.seed
);
return Json(QueryGatewayRsp::error(75, "").encrypt(encryption_conf));
}
let Some(server_list) = server_list_map.get(&param.version) else {
return Json(QueryGatewayRsp::error(70, "").encrypt(encryption_conf));
};
let Some(server_info) = server_list.iter().find(|s| s.notice_region == server_name) else {
return Json(QueryGatewayRsp::error(70, "").encrypt(encryption_conf));
};
Json(QueryGatewayRsp::Plaintext(ServerDispatchData {
retcode: 0,
msg: String::new(),
region_name: server_info.notice_region.clone(),
title: server_info.server_name.clone(),
client_secret_key: rbase64::encode(&server_info.client_secret_key),
cdn_check_url: version_info.cdn_check_url.clone(),
gateway: Some(ServerGateway {
ip: server_info.ip.clone(),
port: server_info.port,
}),
oaserver_url: String::new(),
force_update_url: String::new(),
stop_jump_url: String::new(),
cdn_conf_ext: Some(CdnConfExt {
// TODO: move this stuff to VersionInfo in config.json
design_data: CdnDesignData {
base_url: String::from("http://127.0.0.1:10000/design_data/beta_live/output_5016531_79764a0a26/client/"),
data_revision: String::from("5010994"),
md5_files: String::from(r#"[{"fileName":"data_version","fileSize":2056,"fileMD5":"847307868890712853"}]"#),
},
game_res: CdnGameRes {
audio_revision: String::from("5010994"),
base_url: String::from("http://127.0.0.1:10000/game_res/beta_live/output_5016531_79764a0a26/client/"),
branch: String::from("beta_live"),
md5_files: String::from(r#"[{"fileName":"res_version","fileSize":1225259,"fileMD5":"13780047615044516895"},{"fileName":"audio_version","fileSize":14386,"fileMD5":"1213735845266261736"},{"fileName":"base_revision","fileSize":4,"fileMD5":"4524394692449115962"}]"#),
res_revision: String::from("5016531"),
},
silence_data: CdnSilenceData {
base_url: String::from("http://127.0.0.1:10000/design_data/beta_live/output_5016531_79764a0a26/client_silence/"),
md5_files: String::from(r#"[{"fileName":"silence_version","fileSize":130,"fileMD5":"2077712550601860122"}]"#),
silence_revision: String::from("5010994"),
},
pre_download: None,
}),
region_ext: Some(RegionExtension {
exchange_url: String::new(),
feedback_url: String::new(),
func_switch: RegionSwitchFunc {
disable_frequent_attempts: 1,
enable_gacha_mobile_console: 1,
enable_notice_mobile_console: 1,
enable_performance_log: 1,
is_kcp: match server_info.protocol {
ServerProtocolType::Tcp => 0,
ServerProtocolType::Kcp => 1,
},
..Default::default()
},
mtr_nap: String::new(),
mtr_sdk: String::new(),
pgc_webview_method: 1,
url_check_nap: String::new(),
url_check_sdk: String::new(),
}),
}).encrypt(encryption_conf))
}

View file

@ -0,0 +1,83 @@
use std::{
collections::HashMap,
sync::{LazyLock, OnceLock},
time::Duration,
};
use anyhow::Result;
use common::config::TomlConfig;
use common::config::{AppConfig, EncryptionConfMap, ServerList};
use serde::Deserialize;
use yanagi_http_client::AutopatchClient;
mod data;
mod http_handlers;
#[derive(Deserialize)]
struct DispatchConfig {
pub http_addr: String,
pub outer_http_url: String,
pub design_data_url: String,
}
impl TomlConfig for DispatchConfig {
const DEFAULT_TOML: &str = include_str!("../dispatch.toml");
}
struct AppState {
pub dispatch_config: &'static DispatchConfig,
pub app_config: OnceLock<AppConfig>,
pub server_list: OnceLock<HashMap<String, ServerList>>,
pub encryption_config: OnceLock<HashMap<String, EncryptionConfMap>>,
}
#[tokio::main]
async fn main() -> Result<()> {
static CONFIG: LazyLock<DispatchConfig> =
LazyLock::new(|| DispatchConfig::load_or_create("dispatch.toml"));
static STATE: OnceLock<AppState> = OnceLock::new();
common::print_splash();
common::logging::init(tracing::Level::DEBUG);
let state = STATE.get_or_init(|| AppState {
dispatch_config: &CONFIG,
app_config: OnceLock::new(),
server_list: OnceLock::new(),
encryption_config: OnceLock::new(),
});
std::thread::spawn(move || fetch_configuration(state));
let router = http_handlers::routes().with_state(state);
axum_server::bind(CONFIG.http_addr.parse()?)
.serve(router.into_make_service())
.await?;
Ok(())
}
fn fetch_configuration(state: &AppState) {
const RETRY_TIME: Duration = Duration::from_secs(5);
let design_data_url = &state.dispatch_config.design_data_url;
let client = AutopatchClient::new(design_data_url).retry_after(RETRY_TIME);
let app_config: AppConfig = client.fetch_until_success("/config.json");
let mut server_list_map = HashMap::with_capacity(app_config.version_info_groups.len());
let mut encryption_conf_map = HashMap::with_capacity(app_config.version_info_groups.len());
for (version, info) in app_config.version_info_groups.iter() {
let server_list = client.fetch_until_success(&info.server_list_url);
let encryption_conf = client.fetch_until_success(&info.encryption_config_url);
server_list_map.insert(version.clone(), server_list);
encryption_conf_map.insert(version.clone(), encryption_conf);
}
tracing::info!("successfully fetched all remote configuration from autopatch!");
let _ = state.app_config.set(app_config);
let _ = state.server_list.set(server_list_map);
let _ = state.encryption_config.set(encryption_conf_map);
}

View file

@ -0,0 +1,38 @@
[package]
name = "yanagi-game-server"
edition = "2021"
version.workspace = true
[dependencies]
# Framework
tokio.workspace = true
# Service
qwer.workspace = true
qwer-rpc.workspace = true
protocol.workspace = true
# Database
surrealdb.workspace = true
# Tracing
tracing.workspace = true
# Error processing
anyhow.workspace = true
thiserror.workspace = true
# Serialization
serde.workspace = true
serde_json.workspace = true
rbase64.workspace = true
# Util
paste.workspace = true
dashmap.workspace = true
# Internal
common.workspace = true
yanagi-data.workspace = true
yanagi-eventgraph.workspace = true
yanagi-http-client.workspace = true

View file

@ -0,0 +1,8 @@
server_name = "nap_dev_01"
bind_client_version = "CNBetaWin1.4.2"
design_data_url = "http://127.0.0.1:10000/design_data/NAP_Publish_AppStore_1.4"
[database]
url = "localhost:8000"
username = "root"
password = "root"

View file

@ -0,0 +1,111 @@
use std::io::Cursor;
use common::config::DatabaseSettings;
use protocol::player_info::PlayerInfo;
use serde::{Deserialize, Serialize};
use surrealdb::{
engine::remote::ws::{Client, Ws},
Surreal,
};
use crate::player_util::{self, UidCounter};
const DB_NAMESPACE: &str = "yanagi";
const GAME_DB_NAME: &str = "nap";
const PLAYER_DATA_TABLE: &str = "player_data";
#[derive(Clone)]
pub struct DbContext(Surreal<Client>);
type Result<T> = std::result::Result<T, surrealdb::Error>;
#[derive(Deserialize, Serialize)]
struct PlayerData {
pub player_uid: u64,
pub game_uid_counter: u32,
pub player_info_blob: String,
}
impl DbContext {
pub async fn connect(settings: &DatabaseSettings) -> Result<Self> {
use surrealdb::opt::auth::Root;
let database = Surreal::new::<Ws>(&settings.url).await?;
database
.signin(Root {
username: &settings.username,
password: &settings.password,
})
.await?;
database.use_ns(DB_NAMESPACE).use_db(GAME_DB_NAME).await?;
Ok(Self(database))
}
pub async fn get_or_create_player_data(
&self,
player_uid: u64,
) -> Result<(UidCounter, PlayerInfo)> {
let player_uid_str = player_uid.to_string();
let data: Option<PlayerData> = self.0.select((PLAYER_DATA_TABLE, &player_uid_str)).await?;
if let Some(data) = data {
let uid_counter =
UidCounter::new((player_uid & 0xFFFFFFFF) as u32, data.game_uid_counter);
let player_info = deserialize_player_info(&data.player_info_blob);
return Ok((uid_counter, player_info));
}
let (uid_counter, player_info) =
player_util::create_starting_player_info(player_uid, "ReversedRooms");
let player_info_blob = serialize_player_info(&player_info);
let _: PlayerData = self
.0
.create((PLAYER_DATA_TABLE, player_uid_str))
.content(PlayerData {
player_uid,
game_uid_counter: uid_counter.last_uid(),
player_info_blob,
})
.await?
.unwrap();
Ok((uid_counter, player_info))
}
pub async fn save_player_data(&self, last_uid: u32, player_info: &PlayerInfo) -> Result<()> {
let player_uid = player_info.uid.unwrap();
let _: PlayerData = self
.0
.update((PLAYER_DATA_TABLE, player_uid.to_string()))
.content(PlayerData {
player_uid,
game_uid_counter: last_uid,
player_info_blob: serialize_player_info(player_info),
})
.await?
.unwrap();
Ok(())
}
}
pub fn serialize_player_info(player_info: &PlayerInfo) -> String {
use qwer::OctData;
let mut buf = Vec::new();
player_info
.marshal_to(&mut Cursor::new(&mut buf), 2)
.unwrap();
rbase64::encode(&buf)
}
pub fn deserialize_player_info(blob_b64: &str) -> PlayerInfo {
use qwer::OctData;
let buf = rbase64::decode(blob_b64).unwrap();
PlayerInfo::unmarshal_from(&mut Cursor::new(&buf[..]), 2).unwrap()
}

View file

@ -0,0 +1,186 @@
use protocol::player_info::{NpcInfo, NpcSceneData, Transform};
use qwer::{phashset, PropertyHashSet};
use tracing::debug;
use yanagi_eventgraph::{action_pb, ConfigEvent, ConfigEventAction, SectionEventGraphConfig};
use crate::{level::BoundInteractInfo, PlayerSession};
#[derive(Debug)]
pub enum EventGraphGroup {
OnAdd,
OnEnter,
}
pub fn trigger_group(
session: &mut PlayerSession,
config: &SectionEventGraphConfig,
group: EventGraphGroup,
scene_uid: u64,
section_id: i32,
) {
debug!(
"[EventGraph] player {} triggered event group {group:?}",
session.player_uid
);
match group {
EventGraphGroup::OnAdd => config.on_add.iter().for_each(|event_name| {
trigger_event(
session,
event_name,
config.events.get(event_name).unwrap(),
scene_uid,
section_id,
)
}),
EventGraphGroup::OnEnter => config.on_enter.iter().for_each(|event_name| {
trigger_event(
session,
event_name,
config.events.get(event_name).unwrap(),
scene_uid,
section_id,
)
}),
}
}
pub fn trigger_event(
session: &mut PlayerSession,
event_name: &str,
config: &ConfigEvent,
scene_uid: u64,
section_id: i32,
) {
debug!(
"[EventGraph] player {} triggered event {event_name} (id: {})",
session.player_uid, config.id,
);
config
.actions
.iter()
.for_each(|action| execute_action(session, action, scene_uid, section_id));
}
pub fn execute_action(
session: &mut PlayerSession,
action: &ConfigEventAction,
scene_uid: u64,
section_id: i32,
) {
use ConfigEventAction::*;
match action {
ActionCreateNpcCfg { id, tag_id } => {
let uid = session.uid_counter.next();
let sdg = session.player_info.single_dungeon_group.as_mut().unwrap();
sdg.npcs.as_mut().unwrap().insert(
scene_uid,
uid,
NpcInfo {
uid,
id: *id,
tag_value: *tag_id,
scene_uid,
parent_uid: 0,
owner_uid: 0,
scene_data: NpcSceneData {
section_id,
transform: Transform::default(),
},
references: phashset![],
},
);
}
ActionChangeInteractCfg {
interact_id,
tag_ids,
participators,
interact_scale,
section_listen_events,
..
} => {
let sdg = session.player_info.single_dungeon_group.as_mut().unwrap();
sdg.npcs
.as_ref()
.unwrap()
.iter()
.filter(|(s_uid, _, _)| **s_uid == scene_uid)
.for_each(|(_, &uid, npc)| {
if tag_ids.contains(&npc.tag_value) {
session.level_event_graph_mgr.bound_interact_map.insert(
uid,
(
*interact_id,
BoundInteractInfo {
participators: participators.clone(),
scale_x: interact_scale.x,
scale_y: interact_scale.y,
scale_z: interact_scale.z,
scale_w: 0.0,
scale_r: 0.0,
name: String::from("A"),
interact_id: *interact_id,
},
),
);
session
.level_event_graph_mgr
.listen_events
.insert(*interact_id, section_listen_events.clone());
}
});
}
ActionSetMainCityObjectState { object_state, .. } => {
let main_city_objects_state = session
.player_info
.main_city_objects_state
.as_mut()
.unwrap();
object_state
.iter()
.for_each(|(&obj, &state)| main_city_objects_state.insert(obj, state));
}
ActionOpenUI {
ui,
args,
store_template_id,
} => {
use yanagi_eventgraph::Message;
session
.level_event_graph_mgr
.push_protocol_action(protocol::ActionInfo {
action_type: 5,
body: action_pb::ActionOpenUi {
ui: ui.clone(),
args: *args,
npc_id: 0,
store_template_id: *store_template_id,
}
.encode_to_vec(),
});
}
ActionSwitchSection {
section_id,
transform,
camera_x,
camera_y,
} => {
use yanagi_eventgraph::Message;
session
.level_event_graph_mgr
.push_protocol_action(protocol::ActionInfo {
action_type: 6,
body: action_pb::ActionSwitchSection {
section: *section_id,
transform_id: transform.clone(),
camera_x: *camera_x,
camera_y: *camera_y,
}
.encode_to_vec(),
});
}
}
}

View file

@ -0,0 +1,139 @@
use std::{
collections::{HashMap, VecDeque},
sync::OnceLock,
};
use event_graph_runner::EventGraphGroup;
use protocol::PtcSyncEventInfoArg;
use qwer_rpc::RpcPtcContext;
use tracing::instrument;
use yanagi_eventgraph::MainCityConfig;
use crate::PlayerSession;
mod event_graph_runner;
static MAINCITY_CONFIG: OnceLock<MainCityConfig> = OnceLock::new();
pub fn load_script_config(main_city_config_data: &str) {
let _ = MAINCITY_CONFIG.set(
serde_json::from_str(main_city_config_data).expect("failed to load main city config data"),
);
}
pub struct BoundInteractInfo {
pub interact_id: i32,
pub participators: HashMap<u32, String>,
pub name: String,
pub scale_x: f64,
pub scale_y: f64,
pub scale_z: f64,
pub scale_w: f64,
pub scale_r: f64,
}
#[derive(Default)]
pub struct LevelEventGraphManager {
pub bound_interact_map: HashMap<u64, (i32, BoundInteractInfo)>,
pub listen_events: HashMap<i32, HashMap<String, String>>,
pub scene_uid: u64,
pub section_id: i32,
pending_events_info_sync: VecDeque<PtcSyncEventInfoArg>,
cur_interaction: i32,
cur_interact_unit_tag: i32,
}
impl LevelEventGraphManager {
pub fn begin_interact(&mut self, interaction: i32, unit_tag: i32) {
self.cur_interaction = interaction;
self.cur_interact_unit_tag = unit_tag;
}
pub fn push_protocol_action(&mut self, action_info: protocol::ActionInfo) {
self.pending_events_info_sync
.push_back(PtcSyncEventInfoArg {
owner_id: self.cur_interaction as u32,
npc_interaction: String::from("OnInteract"),
tag: self.cur_interact_unit_tag as u32,
owner_type: 3, // SceneUnit = 3,
action_list: vec![action_info],
});
}
pub async fn sync_event_info(&mut self, ctx: &RpcPtcContext) {
while let Some(ptc) = self.pending_events_info_sync.pop_front() {
ctx.send_ptc(ptc).await;
}
}
}
#[instrument(skip(session))]
pub fn on_section_added(session: &mut PlayerSession, scene_uid: u64, section_id: i32) {
let section_config = MAINCITY_CONFIG
.get()
.unwrap()
.sections
.get(&section_id)
.unwrap();
event_graph_runner::trigger_group(
session,
&section_config.section_progress,
EventGraphGroup::OnAdd,
scene_uid,
section_id,
);
}
#[instrument(skip(session))]
pub fn on_section_enter(session: &mut PlayerSession, scene_uid: u64, section_id: i32) {
let section_config = MAINCITY_CONFIG
.get()
.unwrap()
.sections
.get(&section_id)
.unwrap();
session.level_event_graph_mgr.scene_uid = scene_uid;
session.level_event_graph_mgr.section_id = section_id;
session.level_event_graph_mgr.bound_interact_map.clear();
event_graph_runner::trigger_group(
session,
&section_config.section_progress,
EventGraphGroup::OnEnter,
scene_uid,
section_id,
);
}
#[instrument(skip(session))]
pub fn fire_event(session: &mut PlayerSession, interact_id: i32, event_name: &str) {
if let Some(event_graph_name) = session
.level_event_graph_mgr
.listen_events
.get(&interact_id)
.map(|e| e.get(event_name))
.flatten()
.cloned()
{
let section_config = MAINCITY_CONFIG
.get()
.unwrap()
.sections
.get(&session.level_event_graph_mgr.section_id)
.unwrap();
event_graph_runner::trigger_event(
session,
event_name,
section_config
.section_progress
.events
.get(&event_graph_name)
.unwrap(),
session.level_event_graph_mgr.scene_uid,
session.level_event_graph_mgr.section_id,
);
}
}

View file

@ -0,0 +1,149 @@
use std::{
sync::{LazyLock, OnceLock},
time::Duration,
};
use anyhow::Result;
use common::{
config::{DatabaseSettings, TomlConfig},
time_util,
};
use dashmap::DashMap;
use database::DbContext;
use level::LevelEventGraphManager;
use player_info::PlayerInfo;
use player_util::UidCounter;
use qwer::ProtocolID;
use qwer_rpc::{
middleware::MiddlewareModel, ProtocolServiceFrontend, RpcPtcContext, RpcPtcServiceFrontend,
};
use protocol::*;
use serde::Deserialize;
use tracing::{error, info, warn};
use yanagi_data::{ArchiveFile, NapFileCfg};
mod database;
mod level;
mod player_util;
mod remote_config;
mod rpc_ptc;
mod scene_section_util;
#[derive(Deserialize)]
pub struct GameServerConfig {
pub server_name: String,
pub bind_client_version: String,
pub design_data_url: String,
pub database: DatabaseSettings,
}
impl TomlConfig for GameServerConfig {
const DEFAULT_TOML: &str = include_str!("../gameserver.toml");
}
struct PlayerSession {
pub player_uid: u64,
pub uid_counter: UidCounter,
pub player_info: PlayerInfo,
pub last_save_time: u64,
pub level_event_graph_mgr: LevelEventGraphManager,
}
static FILECFG: OnceLock<NapFileCfg> = OnceLock::new();
static PLAYER_MAP: LazyLock<DashMap<u64, PlayerSession>> = LazyLock::new(|| DashMap::new());
static DB_CONTEXT: OnceLock<DbContext> = OnceLock::new();
#[tokio::main]
async fn main() -> Result<()> {
static CONFIG: LazyLock<GameServerConfig> =
LazyLock::new(|| GameServerConfig::load_or_create("gameserver.toml"));
static DESIGN_DATA: OnceLock<ArchiveFile> = OnceLock::new();
common::print_splash();
common::logging::init(tracing::Level::DEBUG);
let remote_cfg = remote_config::download(&CONFIG);
let design_data_blk = remote_config::download_design_data_blk(&remote_cfg.version_info);
let main_city_script =
remote_config::download_main_city_script_config(&remote_cfg.version_info);
let _ = DESIGN_DATA.set(yanagi_data::read_archive_file(std::io::Cursor::new(
&design_data_blk,
))?);
level::load_script_config(&main_city_script);
let nap_cfg = NapFileCfg::new(&DESIGN_DATA.get().unwrap());
FILECFG.get_or_init(|| nap_cfg);
let db_context = DbContext::connect(&CONFIG.database).await?;
DB_CONTEXT.get_or_init(|| db_context);
let service = RpcPtcServiceFrontend::new(ProtocolServiceFrontend::new());
let listen_point = service.create_point(Some("0.0.0.0:10101".parse()?)).await?;
listen_point.register_rpc_recv(RpcPlayerLoginArg::PROTOCOL_ID, on_rpc_player_login_arg);
rpc_ptc::register_handlers(&listen_point);
// sleep, service stuff is running in separate task
tokio::time::sleep(Duration::from_secs(u64::MAX)).await;
Ok(())
}
pub async fn on_rpc_player_login_arg(ctx: RpcPtcContext) {
let _arg: RpcPlayerLoginArg = ctx.get_arg().unwrap();
let Some(MiddlewareModel::Account(account_mw)) = ctx
.middleware_list
.iter()
.find(|&mw| matches!(mw, MiddlewareModel::Account(_)))
else {
warn!("login failed: account middleware is missing");
return;
};
let Ok((uid_counter, player_info)) = DB_CONTEXT
.get()
.unwrap()
.get_or_create_player_data(account_mw.player_uid)
.await
.inspect_err(|err| error!("login failed: get_or_create_player_data failed: {err}"))
else {
ctx.send_ret(RpcPlayerLoginRet { retcode: 1 }).await;
return;
};
PLAYER_MAP.insert(
account_mw.player_uid,
PlayerSession {
player_uid: account_mw.player_uid,
uid_counter,
player_info,
level_event_graph_mgr: LevelEventGraphManager::default(),
last_save_time: time_util::unix_timestamp(),
},
);
info!("player with uid {} is logging in!", account_mw.player_uid);
ctx.send_ret(RpcPlayerLoginRet { retcode: 0 }).await;
}
async fn post_rpc_handle(session: &mut PlayerSession) {
let timestamp = time_util::unix_timestamp();
if (timestamp - session.last_save_time) >= 30 {
session.last_save_time = timestamp;
DB_CONTEXT
.get()
.unwrap()
.save_player_data(session.uid_counter.last_uid(), &session.player_info)
.await
.expect("failed to save player data");
info!(
"successfully saved player data (uid: {})",
session.player_uid
);
}
}

View file

@ -0,0 +1,346 @@
use common::time_util;
use protocol::{
item_info::ItemInfo,
player_info::{
ArchiveInfo, AreasInfo, BGMInfo, BattleEventInfo, BeginnerProcedureInfo, CollectMap,
DungeonCollection, Embattles, EquipGachaInfo, FairyInfo, GMData, HollowInfo,
LoadingPageTipsInfo, MUIPData, MainCityQuestData, NewbieInfo, OperationMailReceiveInfo,
PayInfo, PlayerInfo, PlayerMailExtInfos, PlayerPosInMainCity, PopupWindowInfo, QuestData,
RamenData, ShopsInfo, SingleDungeonGroup, TipsInfo, Transform, UnlockInfo, VHSStoreData,
Vector3f, VideotapeInfo, YorozuyaInfo,
},
AutoRecoveryInfo,
};
use qwer::{
pdkhashmap, phashmap, phashset, PropertyDoubleKeyHashMap, PropertyHashMap, PropertyHashSet,
};
use crate::FILECFG;
pub struct UidCounter {
player_uid: u32,
counter: u32,
}
impl UidCounter {
pub fn new(player_uid: u32, last_uid: u32) -> Self {
Self {
player_uid,
counter: last_uid,
}
}
pub fn next(&mut self) -> u64 {
self.counter += 1;
((self.player_uid as u64) << 32) | self.counter as u64
}
pub fn last_uid(&self) -> u32 {
self.counter
}
}
pub fn create_starting_player_info(uid: u64, nick_name: &str) -> (UidCounter, PlayerInfo) {
let mut counter = UidCounter::new((uid & 0xFFFFFFFF) as u32, 0);
let mut player_info = PlayerInfo {
uid: Some(uid),
account_name: Some(uid.to_string()),
last_enter_world_timestamp: Some(0),
items: Some(phashmap!()),
dungeon_collection: Some(DungeonCollection {
dungeons: Some(qwer::phashmap![]),
scenes: Some(qwer::phashmap![]),
default_scene_uid: Some(0),
transform: Some(Transform::default()),
used_story_mode: Some(true),
used_manual_qte_mode: Some(true),
}),
properties: Some(pdkhashmap![]),
scene_properties: Some(pdkhashmap![]),
quest_data: Some(QuestData {
quests: Some(pdkhashmap![]),
is_afk: Some(false),
unlock_condition_progress: Some(pdkhashmap![]),
world_quest_collection_uid: Some(0),
world_quest_for_cur_dungeon: Some(0),
world_quest_for_cur_dungeon_afk: Some(0),
}),
joined_chat_rooms: Some(Vec::with_capacity(0)),
scene_uid: Some(0),
archive_info: Some(ArchiveInfo {
videotaps_info: Some(phashmap![(
1010001,
VideotapeInfo {
finished: true,
star_count: phashmap![],
awarded_star: phashmap![],
}
)]),
hollow_archive_id: Some(phashset![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
}),
auto_recovery_info: Some(phashmap![(
501,
AutoRecoveryInfo {
buy_times: 0,
last_recovery_timestamp: 0,
}
)]),
unlock_info: Some(UnlockInfo {
condition_progress: Some(pdkhashmap![]),
unlocked_list: Some(phashset![]),
}),
yorozuya_info: Some(YorozuyaInfo {
yorozuya_level: Some(1),
yorozuya_rank: Some(1),
gm_enabled: Some(true),
gm_quests: Some(phashmap![]),
finished_hollow_quest_count: Some(0),
finished_hollow_quest_count_of_type: Some(phashmap![]),
hollow_quests: Some(pdkhashmap![]),
urgent_quests_queue: Some(phashmap![]),
unlock_hollow_id: Some(vec![1001]),
unlock_hollow_id_progress: Some(pdkhashmap![]),
last_refresh_timestamp_common: Some(0),
last_refresh_timestamp_urgent: Some(0),
next_refresh_timestamp_urgent: Some(0),
}),
equip_gacha_info: Some(EquipGachaInfo {
avatar_level_advance_times: Some(0),
equip_star_up_times: Some(0),
security_num_by_lv: Some(phashmap![]),
smithy_level: Some(0),
total_gacha_times: Some(0),
}),
beginner_procedure_info: Some(BeginnerProcedureInfo {
procedure_id: Some(0),
}),
pos_in_main_city: Some(PlayerPosInMainCity {
position: Some(Vector3f {
x: 17.35,
y: 0.37,
z: 6.01,
}),
rotation: Some(Vector3f {
x: 0.0,
y: 216.0,
z: 0.0,
}),
initial_pos_id: Some(String::from("Workshop_PlayerPos_Default")),
}),
fairy_info: Some(FairyInfo {
condition_progress: Some(pdkhashmap![]),
fairy_groups: Some(phashmap![]),
}),
popup_window_info: Some(PopupWindowInfo {
condition_progress: Some(pdkhashmap![]),
popup_window_list: Some(Vec::new()),
}),
tips_info: Some(TipsInfo {
tips_list: Some(Vec::new()),
tips_group: Some(Vec::new()),
tips_condition_progress: Some(pdkhashmap![]),
tips_group_condition_progress: Some(pdkhashmap![]),
}),
main_city_quest_data: Some(MainCityQuestData {
in_progress_quests: Some(Vec::new()),
exicing_finish_script_group: Some(vec![10020001]),
}),
embattles: Some(Embattles {
last_embattles: Some(phashmap![]),
}),
day_change_info: Some(protocol::player_info::DayChangeInfo {
last_daily_refresh_timing: Some(time_util::unix_timestamp()),
}),
npcs_info: Some(protocol::player_info::PlayerNPCsInfo {
npcs_info: Some(phashmap![]),
destroy_npc_when_leave_section: Some(phashset![]),
}),
scripts_to_execute: Some(pdkhashmap![]),
scripts_to_remove: Some(phashmap![]),
last_leave_world_timestamp: Some(0),
muip_data: Some(MUIPData {
ban_begin_time: Some(String::with_capacity(0)),
ban_end_time: Some(String::with_capacity(0)),
tag_value: Some(0),
dungeon_enter_times: Some(phashmap![]),
scene_enter_times: Some(phashmap![]),
dungeon_pass_times: Some(phashmap![]),
scene_pass_times: Some(phashmap![]),
alread_cmd_uids: Some(phashset![]),
game_total_time: Some(0),
language_type: Some(0),
}),
nick_name: Some(nick_name.to_string()),
ramen_data: Some(RamenData {
unlock_ramen: Some(phashset![20301, 20401, 20501, 20601, 20201]),
cur_ramen: Some(0),
used_times: Some(0),
unlock_initiative_item: Some(phashset![]),
unlock_ramen_condition_progress: Some(pdkhashmap![]),
unlock_item_condition_progress: Some(pdkhashmap![]),
has_mystical_spice: Some(true),
unlock_has_mystical_spice_condition_progress: Some(phashmap![]),
cur_mystical_spice: Some(0),
unlock_mystical_spice: Some(phashset![
30101, 30601, 30201, 30501, 30301, 30801, 31201, 30401, 31401, 31001
]),
unlock_mystical_spice_condition_progress: Some(pdkhashmap![]),
unlock_initiative_item_group: Some(phashset![]),
hollow_item_history: Some(phashmap![]),
initial_item_ability: Some(0),
new_unlock_ramen: Some(Vec::new()),
eat_ramen_times: Some(0),
make_hollow_item_times: Some(0),
new_unlock_initiative_item: Some(phashset![]),
}),
shop: Some(ShopsInfo {
shops: Some(phashmap![]),
shop_buy_times: Some(0),
vip_level: Some(0),
}),
vhs_store_data: Some(VHSStoreData {
store_level: Some(0),
unreceived_reward: Some(0),
hollow_enter_times: Some(0),
last_receive_time: Some(0),
vhs_collection_slot: Some(Vec::new()),
unlock_vhs_collection: Some(phashset![]),
already_trending: Some(phashset![]),
unlock_trending_condition_progress: Some(pdkhashmap![]),
is_need_refresh: Some(true),
scripts_id: Some(phashset![]),
store_exp: Some(0),
is_level_chg_tips: Some(true),
vhs_hollow: Some(Vec::new()),
is_receive_trending_reward: Some(false),
is_need_first_trending: Some(false),
last_basic_script: Some(0),
is_complete_first_trending: Some(false),
last_basic_npc: Some(0),
can_random_trending: Some(phashset![]),
vhs_trending_info: Some(Vec::new()),
unlock_vhs_trending_info: Some(phashmap![]),
vhs_flow: Some(0),
received_reward: Some(0),
last_reward: Some(0),
last_exp: Some(0),
last_flow: Some(0),
last_vhs_trending_info: Some(Vec::new()),
new_know_trend: Some(Vec::new()),
quest_finish_script: Some(pdkhashmap![]),
quest_finish_scripts_id: Some(phashset![]),
total_received_reward: Some(phashmap![]),
last_vhs_npc_info: Some(Vec::new()),
vhs_npc_info: Some(Vec::new()),
npc_info: Some(phashset![]),
total_received_reward_times: Some(0),
}),
operation_mail_receive_info: Some(OperationMailReceiveInfo {
receive_list: Some(phashset![]),
condition_progress: Some(pdkhashmap![]),
}),
second_last_enter_world_timestamp: Some(0),
login_times: Some(0),
create_timestamp: Some(time_util::unix_timestamp()),
gender: Some(0),
avatar_id: Some(2021),
prev_scene_uid: Some(0),
register_cps: Some(String::with_capacity(0)),
register_platform: Some(3),
pay_info: Some(PayInfo {
month_total_pay: Some(0),
}),
private_npcs: Some(phashmap![]),
battle_event_info: Some(BattleEventInfo {
unlock_battle: Some(phashset![]),
unlock_battle_condition_progress: Some(pdkhashmap![]),
alread_rand_battle: Some(pdkhashmap![]),
alread_battle_stage: Some(Vec::new()),
rand_battle_type: Some(phashmap![]),
}),
gm_data: Some(GMData {
register_conditions: Some(phashset![]),
condition_proress: Some(pdkhashmap![]),
completed_conditions: Some(phashset![]),
}),
player_mail_ext_infos: Some(PlayerMailExtInfos {
player_mail_ext_info: Some(phashmap![]),
}),
single_dungeon_group: Some(SingleDungeonGroup {
dungeons: Some(phashmap![]),
scenes: Some(pdkhashmap![]),
npcs: Some(pdkhashmap![]),
section: Some(pdkhashmap![]),
}),
newbie_info: Some(NewbieInfo {
unlocked_id: Some(phashset![3]),
condition_progress: Some(pdkhashmap!()),
}),
loading_page_tips_info: Some(LoadingPageTipsInfo {
unlocked_id: Some(phashset![1, 2, 3]),
condition_progress: Some(pdkhashmap![]),
}),
switch_of_story_mode: Some(true),
switch_of_qte: Some(true),
collect_map: Some(CollectMap {
card_map: Some(phashset![]),
curse_map: Some(phashset![]),
unlock_cards: Some(phashset![]),
unlock_curses: Some(phashset![]),
event_icon_map: Some(phashset![]),
unlock_events: Some(phashset![]),
new_card_map: Some(phashset![]),
new_curse_map: Some(phashset![]),
new_event_icon_map: Some(phashset![]),
unlock_event_icon_condition_progress: Some(pdkhashmap![]),
unlock_card_condition_progress: Some(pdkhashmap![]),
unlock_curse_condition_progress: Some(pdkhashmap![]),
unlock_event_condition_progress: Some(pdkhashmap![]),
unlock_event_icons: Some(phashset![]),
}),
areas_info: Some(AreasInfo {
area_owners_info: Some(pdkhashmap!()),
sequence: Some(0),
}),
bgm_info: Some(BGMInfo { bgm_id: Some(1005) }),
main_city_objects_state: Some(phashmap!()),
hollow_info: Some(HollowInfo {
banned_hollow_event: Some(phashset!()),
}),
main_city_avatar_id: Some(1221),
};
// Give all avatars
FILECFG
.get()
.unwrap()
.avatar_base_template_tb
.data()
.unwrap_or_default()
.iter()
.filter(|tmpl| tmpl.camp() != 0)
.for_each(|tmpl| {
let uid = counter.next();
player_info.items.as_mut().unwrap().insert(
uid,
ItemInfo::AvatarInfo {
uid,
id: tmpl.id(),
count: 1,
package: 0,
first_get_time: time_util::unix_timestamp(),
star: 6,
exp: 0,
level: 60,
rank: 6,
unlocked_talent_num: 6,
talent_switch: (0..6).map(|i| i >= 3).collect(),
skills: PropertyHashMap::Base((0..=6).map(|st| (st, 1)).collect()),
is_custom_by_dungeon: false,
robot_id: 0,
},
);
});
(counter, player_info)
}

View file

@ -0,0 +1,81 @@
use std::time::Duration;
use common::config::*;
use serde::Deserialize;
use yanagi_http_client::AutopatchClient;
use crate::GameServerConfig;
pub struct RemoteConfiguration {
pub version_info: ConfigurationInfo,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct FileEntry {
pub remote_name: String,
#[expect(unused)]
pub md5: String,
#[expect(unused)]
pub file_size: u64,
#[expect(unused)]
pub is_patch: bool,
#[serde(default)]
pub tags: Vec<u8>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct DataVersion {
#[expect(unused)]
pub remote_parent_dir: String,
pub files: Vec<FileEntry>,
}
pub fn download_design_data_blk(version_info: &ConfigurationInfo) -> Box<[u8]> {
const RETRY_TIME: Duration = Duration::from_secs(5);
let url = format!(
"{}/{}/{}/",
version_info.design_data_url, version_info.platform, version_info.environment
);
let client = AutopatchClient::new(&url).retry_after(RETRY_TIME);
let data_version: DataVersion = client.fetch_until_success("data_version");
let file = data_version
.files
.iter()
.filter(|e| &e.tags == &[2])
.rev()
.next()
.unwrap();
client.fetch_bytes_until_success(&file.remote_name)
}
pub fn download_main_city_script_config(version_info: &ConfigurationInfo) -> String {
const RETRY_TIME: Duration = Duration::from_secs(5);
let url = format!(
"{}/{}/{}/",
version_info.design_data_url, version_info.platform, version_info.environment
);
let client = AutopatchClient::new(&url).retry_after(RETRY_TIME);
String::from_utf8_lossy(&client.fetch_bytes_until_success("ServerOnlyData/MainCity_1.json"))
.to_string()
}
pub fn download(config: &'static GameServerConfig) -> RemoteConfiguration {
const RETRY_TIME: Duration = Duration::from_secs(5);
let client = AutopatchClient::new(&config.design_data_url).retry_after(RETRY_TIME);
let mut app_config: AppConfig = client.fetch_until_success("/config.json");
let version_info = app_config
.version_info_groups
.remove(&config.bind_client_version)
.expect(
"Fatal: remote config doesn't contain configuration for specified bind_client_version",
);
RemoteConfiguration { version_info }
}

View file

@ -0,0 +1,31 @@
use super::*;
pub async fn on_rpc_get_abyss_info_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetAbyssInfoArg,
) -> Result<RpcGetAbyssInfoRet, i32> {
Ok(RpcGetAbyssInfoRet {
retcode: 0,
abyss_info: AbyssInfo::default(),
})
}
pub async fn on_rpc_get_abyss_arpeggio_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetAbyssArpeggioDataArg,
) -> Result<RpcGetAbyssArpeggioDataRet, i32> {
Ok(RpcGetAbyssArpeggioDataRet::default())
}
pub async fn on_rpc_get_abyss_reward_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetAbyssRewardDataArg,
) -> Result<RpcGetAbyssRewardDataRet, i32> {
Ok(RpcGetAbyssRewardDataRet {
retcode: 0,
abyss_reward_data: AbyssRewardData::default(),
})
}

View file

@ -0,0 +1,20 @@
use super::*;
pub async fn on_rpc_get_activity_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetActivityDataArg,
) -> Result<RpcGetActivityDataRet, i32> {
Ok(RpcGetActivityDataRet {
retcode: 0,
..Default::default()
})
}
pub async fn on_rpc_get_web_activity_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetWebActivityDataArg,
) -> Result<RpcGetWebActivityDataRet, i32> {
Ok(RpcGetWebActivityDataRet::default())
}

View file

@ -0,0 +1,9 @@
use super::*;
pub async fn on_rpc_get_arcade_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetArcadeDataArg,
) -> Result<RpcGetArcadeDataRet, i32> {
Ok(RpcGetArcadeDataRet::default())
}

View file

@ -0,0 +1,9 @@
use super::*;
pub async fn on_rpc_get_babel_tower_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetBabelTowerDataArg,
) -> Result<RpcGetBabelTowerDataRet, i32> {
Ok(RpcGetBabelTowerDataRet::default())
}

View file

@ -0,0 +1,9 @@
use super::*;
pub async fn on_rpc_get_battle_pass_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetBattlePassDataArg,
) -> Result<RpcGetBattlePassDataRet, i32> {
Ok(RpcGetBattlePassDataRet::default())
}

View file

@ -0,0 +1,12 @@
use super::*;
pub async fn on_rpc_get_camp_idle_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetCampIdleDataArg,
) -> Result<RpcGetCampIdleDataRet, i32> {
Ok(RpcGetCampIdleDataRet {
retcode: 0,
camp_idle_data: CampIdleData::default(),
})
}

View file

@ -0,0 +1,14 @@
use super::*;
pub async fn on_rpc_get_daily_challenge_info_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetDailyChallengeInfoArg,
) -> Result<RpcGetDailyChallengeInfoRet, i32> {
Ok(RpcGetDailyChallengeInfoRet {
retcode: 0,
info: DailyChallengeInfo {
..Default::default()
},
})
}

View file

@ -0,0 +1,20 @@
use super::*;
pub async fn on_rpc_get_embattles_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetEmbattlesDataArg,
) -> Result<RpcGetEmbattlesDataRet, i32> {
Ok(RpcGetEmbattlesDataRet {
retcode: 0,
embattles_data: EmbattlesData::default(),
})
}
pub async fn on_rpc_report_embattle_info_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcReportEmbattleInfoArg,
) -> Result<RpcReportEmbattleInfoRet, i32> {
Ok(RpcReportEmbattleInfoRet::default())
}

View file

@ -0,0 +1,12 @@
use super::*;
pub async fn on_rpc_get_gacha_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
arg: RpcGetGachaDataArg,
) -> Result<RpcGetGachaDataRet, i32> {
Ok(RpcGetGachaDataRet {
gacha_type: arg.gacha_type,
..Default::default()
})
}

View file

@ -0,0 +1,9 @@
use super::*;
pub async fn on_rpc_get_hadal_zone_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetHadalZoneDataArg,
) -> Result<RpcGetHadalZoneDataRet, i32> {
Ok(RpcGetHadalZoneDataRet::default())
}

View file

@ -0,0 +1,51 @@
use tracing::{debug, instrument};
use crate::level;
use super::*;
#[instrument(skip_all)]
pub async fn on_rpc_interact_with_client_entity_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
arg: RpcInteractWithClientEntityArg,
) -> Result<RpcInteractWithClientEntityRet, i32> {
debug!("{arg:?}");
Ok(RpcInteractWithClientEntityRet::default())
}
#[instrument(skip_all)]
pub async fn on_rpc_interact_with_unit_arg(
ctx: &RpcPtcContext,
session: &mut PlayerSession,
arg: RpcInteractWithUnitArg,
) -> Result<RpcInteractWithUnitRet, i32> {
debug!("{arg:?}");
session
.level_event_graph_mgr
.begin_interact(arg.interaction, arg.unit_tag);
level::fire_event(session, arg.interaction, "OnInteract");
session.level_event_graph_mgr.sync_event_info(ctx).await;
Ok(RpcInteractWithUnitRet::default())
}
pub async fn on_rpc_run_event_graph_arg(
ctx: &RpcPtcContext,
_: &mut PlayerSession,
arg: RpcRunEventGraphArg,
) -> Result<RpcRunEventGraphRet, i32> {
ctx.send_ptc(PtcUpdateEventGraphArg {
owner_type: arg.owner_type,
tag: arg.tag,
event_graph_uid: arg.event_graph_uid,
npc_interaction: String::from("OnInteract"),
is_event_success: true,
event_graph_owner_uid: arg.owner_id,
})
.await;
Ok(RpcRunEventGraphRet::default())
}

View file

@ -0,0 +1,72 @@
use super::*;
pub async fn on_rpc_get_weapon_data_arg(
_: &RpcPtcContext,
session: &mut PlayerSession,
_: RpcGetWeaponDataArg,
) -> Result<RpcGetWeaponDataRet, i32> {
Ok(RpcGetWeaponDataRet {
retcode: 0,
weapon_list: protocol::util::build_sync_weapon_info_list(&session.player_info),
})
}
pub async fn on_rpc_get_equip_data_arg(
_: &RpcPtcContext,
session: &mut PlayerSession,
_: RpcGetEquipDataArg,
) -> Result<RpcGetEquipDataRet, i32> {
Ok(RpcGetEquipDataRet {
retcode: 0,
equip_list: protocol::util::build_sync_equip_info_list(&session.player_info),
})
}
pub async fn on_rpc_get_resource_data_arg(
_: &RpcPtcContext,
session: &mut PlayerSession,
_: RpcGetResourceDataArg,
) -> Result<RpcGetResourceDataRet, i32> {
Ok(RpcGetResourceDataRet {
retcode: 0,
resource_list: protocol::util::build_sync_resource_info_list(&session.player_info),
auto_recovery_info: session
.player_info
.auto_recovery_info
.as_ref()
.unwrap()
.iter()
.map(|(id, info)| (*id as u32, info.clone()))
.collect(),
})
}
pub async fn on_rpc_get_avatar_data_arg(
_: &RpcPtcContext,
session: &mut PlayerSession,
_: RpcGetAvatarDataArg,
) -> Result<RpcGetAvatarDataRet, i32> {
Ok(RpcGetAvatarDataRet {
retcode: 0,
avatar_list: protocol::util::build_sync_avatar_info_list(&session.player_info),
})
}
pub async fn on_rpc_get_wishlist_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetWishlistDataArg,
) -> Result<RpcGetWishlistDataRet, i32> {
Ok(RpcGetWishlistDataRet {
retcode: 0,
wishlist_plan_list: Vec::with_capacity(0),
})
}
pub async fn on_rpc_get_buddy_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetBuddyDataArg,
) -> Result<RpcGetBuddyDataRet, i32> {
Ok(RpcGetBuddyDataRet::default())
}

View file

@ -0,0 +1,173 @@
use tracing::{debug, warn};
use crate::scene_section_util;
use super::*;
pub async fn on_rpc_get_ramen_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetRamenDataArg,
) -> Result<RpcGetRamenDataRet, i32> {
Ok(RpcGetRamenDataRet {
retcode: 0,
ramen_data: RamenData::default(),
})
}
pub async fn on_rpc_get_cafe_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetCafeDataArg,
) -> Result<RpcGetCafeDataRet, i32> {
Ok(RpcGetCafeDataRet {
retcode: 0,
cafe_data: CafeData::default(),
})
}
pub async fn on_rpc_get_reward_buff_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetRewardBuffDataArg,
) -> Result<RpcGetRewardBuffDataRet, i32> {
Ok(RpcGetRewardBuffDataRet {
retcode: 0,
info: RewardBuffData::default(),
})
}
pub async fn on_rpc_get_news_stand_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetNewsStandDataArg,
) -> Result<RpcGetNewsStandDataRet, i32> {
Ok(RpcGetNewsStandDataRet {
retcode: 0,
news_stand_data: NewsStandData::default(),
})
}
pub async fn on_rpc_get_trashbin_hermit_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetTrashbinHermitDataArg,
) -> Result<RpcGetTrashbinHermitDataRet, i32> {
Ok(RpcGetTrashbinHermitDataRet {
retcode: 0,
trashbin_hermit_data: TrashbinHermitData::default(),
})
}
pub async fn on_rpc_get_main_city_revival_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetMainCityRevivalDataArg,
) -> Result<RpcGetMainCityRevivalDataRet, i32> {
Ok(RpcGetMainCityRevivalDataRet {
retcode: 0,
main_city_revival: MainCityRevivalData::default(),
})
}
pub async fn on_rpc_get_character_quest_list_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetCharacterQuestListArg,
) -> Result<RpcGetCharacterQuestListRet, i32> {
Ok(RpcGetCharacterQuestListRet::default())
}
pub async fn on_rpc_get_exploration_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetExplorationDataArg,
) -> Result<RpcGetExplorationDataRet, i32> {
Ok(RpcGetExplorationDataRet::default())
}
pub async fn on_rpc_get_miniscape_entrust_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetMiniscapeEntrustDataArg,
) -> Result<RpcGetMiniscapeEntrustDataRet, i32> {
Ok(RpcGetMiniscapeEntrustDataRet::default())
}
pub async fn on_rpc_get_journey_info_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetJourneyInfoArg,
) -> Result<RpcGetJourneyInfoRet, i32> {
Ok(RpcGetJourneyInfoRet::default())
}
pub async fn on_rpc_get_photo_wall_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetPhotoWallDataArg,
) -> Result<RpcGetPhotoWallDataRet, i32> {
Ok(RpcGetPhotoWallDataRet::default())
}
pub async fn on_rpc_mod_time_arg(
ctx: &RpcPtcContext,
session: &mut PlayerSession,
arg: RpcModTimeArg,
) -> Result<RpcModTimeRet, i32> {
debug!("{arg:?}");
let player_info = &mut session.player_info;
let scene_uid = player_info.scene_uid.unwrap();
let dungeon_collection = player_info.dungeon_collection.as_mut().unwrap();
if let Some(protocol::scene_info::SceneInfo::Hall {
main_city_time_info,
..
}) = dungeon_collection
.scenes
.as_mut()
.unwrap()
.get_mut(&scene_uid)
{
let prev_time = main_city_time_info.initial_time;
main_city_time_info.initial_time = match arg.time_period {
1 => 6 * 60,
2 => 12 * 60,
3 => 18 * 60,
4 => 0 * 60,
_ => 0,
};
if prev_time > main_city_time_info.initial_time {
main_city_time_info.day_of_week = (main_city_time_info.day_of_week + 1) % 7;
}
let mut ptc = protocol::util::build_hall_refresh_arg(player_info, scene_uid, true).unwrap();
scene_section_util::add_scene_units_to_hall_refresh_arg(session, scene_uid, &mut ptc);
ctx.send_ptc(ptc).await;
} else {
warn!("RpcModTime: currently not in Hall");
}
Ok(RpcModTimeRet { retcode: 0 })
}
pub async fn on_rpc_mod_main_city_avatar_arg(
ctx: &RpcPtcContext,
session: &mut PlayerSession,
arg: RpcModMainCityAvatarArg,
) -> Result<RpcModMainCityAvatarRet, i32> {
debug!("{arg:?}");
let player_info = &mut session.player_info;
player_info.main_city_avatar_id = Some(arg.main_city_avatar_id);
ctx.send_ptc(PtcPlayerSyncArg {
basic_info: Some(protocol::util::build_player_basic_info(player_info)),
..Default::default()
})
.await;
Ok(RpcModMainCityAvatarRet::default())
}

View file

@ -0,0 +1,184 @@
use qwer_rpc::{middleware::MiddlewareModel, RpcPtcContext, RpcPtcPoint};
use crate::PlayerSession;
use paste::paste;
use protocol::*;
use qwer::*;
mod abyss;
mod activity;
mod arcade;
mod babel_tower;
mod battle_pass;
mod camp_idle;
mod daily_challenge;
mod embattles;
mod gacha;
mod hadal_zone;
mod interact;
mod item;
mod main_city;
mod player;
mod quest;
mod shop;
mod social;
mod unlock;
mod vhs_store;
mod world;
use abyss::*;
use activity::*;
use arcade::*;
use babel_tower::*;
use battle_pass::*;
use camp_idle::*;
use daily_challenge::*;
use embattles::*;
use gacha::*;
use hadal_zone::*;
use interact::*;
use item::*;
use main_city::*;
use player::*;
use quest::*;
use shop::*;
use social::*;
use unlock::*;
use vhs_store::*;
use world::*;
macro_rules! rpc_handlers {
(($rpc_ptc_point:ident) $($name:ident;)*) => {
paste! {
$(
async fn [<_on_ $name:snake _arg>](ctx: ::qwer_rpc::RpcPtcContext) {
let Ok(arg) = ctx.get_arg::<::protocol::[<$name Arg>]>() else {
::tracing::warn!("failed to unmarshal arg {}", stringify!($name));
return;
};
let Some(MiddlewareModel::Account(account_mw)) = ctx
.middleware_list
.iter()
.find(|&mw| matches!(mw, MiddlewareModel::Account(_)))
else {
::tracing::warn!("failed to handle {}: account middleware is missing", stringify!($name));
return;
};
let Some(mut session) = crate::PLAYER_MAP.get_mut(&account_mw.player_uid) else {
::tracing::warn!("failed to handle {}: player session with uid {} is not active", stringify!($name), account_mw.player_uid);
return;
};
match [<on_ $name:snake _arg>](&ctx, &mut session, arg).await {
Ok(ret) => {
ctx.send_ret(ret).await;
::tracing::info!("successfully handled {}Arg", stringify!($name));
},
Err(retcode) => {
::tracing::warn!("on_{}_arg returned error code: {}", stringify!([<$name:snake>]), retcode);
ctx.send_ret([<$name Ret>] {
retcode,
..Default::default()
}).await;
}
}
crate::post_rpc_handle(&mut session).await;
}
)*
}
$(
paste!($rpc_ptc_point.register_rpc_recv(::protocol::[<$name Arg>]::PROTOCOL_ID, [<_on_ $name:snake _arg>]));
)*
};
}
pub fn register_handlers(listen_point: &RpcPtcPoint) {
rpc_handlers! {
(listen_point)
RpcGetPlayerBasicInfo;
RpcGetWeaponData;
RpcGetEquipData;
RpcGetResourceData;
RpcGetAvatarData;
RpcGetWishlistData; // new 1.4
RpcGetQuestData;
RpcGetArchiveInfo;
RpcGetYorozuyaInfo;
RpcGetAbyssInfo;
RpcGetBuddyData;
RpcGetAbyssArpeggioData;
RpcGetServerTimestamp;
RpcGetVideoUsmKeyData;
RpcGetAuthkey;
RpcGetGachaData;
RpcGetCampIdleData;
RpcSavePlayerSystemSetting;
RpcGetRamenData;
RpcGetCafeData;
RpcGetRewardBuffData;
RpcGetPlayerMails;
RpcGetFairyInfo;
RpcGetTipsInfo;
RpcGetClientSystemsInfo;
RpcGetPrivateMessageData;
RpcGetCollectMap;
RpcGetWorkbenchInfo;
RpcGetAbyssRewardData;
RpcGetVhsStoreInfo;
RpcGetActivityData;
RpcGetWebActivityData;
RpcGetEmbattlesData;
RpcGetNewsStandData;
RpcGetTrashbinHermitData;
RpcGetMainCityRevivalData;
RpcGetArcadeData;
RpcGetBattlePassData;
RpcGetHadalZoneData;
RpcGetBabelTowerData;
RpcGetDailyChallengeInfo;
RpcGetRoleCardData;
RpcGetChatEmojiList;
RpcGetFriendList;
RpcGetCharacterQuestList;
RpcGetExplorationData;
RpcGetFashionStoreInfo;
RpcGetShoppingMallInfo;
// new 1.4
RpcGetMiniscapeEntrustData;
RpcGetJourneyInfo;
RpcGetOnlineFriendsList;
RpcEnterWorld;
RpcSceneTransition;
RpcEnterSectionComplete;
RpcReportEmbattleInfo;
RpcGetMonthCardRewardList;
RpcGetDisplayCaseData;
RpcGetPhotoWallData;
RpcSavePosInMainCity;
RpcReportUiLayoutPlatform;
RpcPlayerOperation;
RpcPlayerTransaction;
RpcGetRechargeItemList;
RpcModTime;
RpcModMainCityAvatar;
RpcInteractWithClientEntity;
RpcInteractWithUnit;
RpcRunEventGraph;
RpcEnterSection;
RpcRefreshSection;
RpcCheckYorozuyaInfoRefresh;
RpcBeginTrainingCourseBattle;
RpcBattleReport;
RpcEndBattle;
RpcLeaveCurDungeon;
};
}

View file

@ -0,0 +1,109 @@
use common::time_util;
use super::*;
pub async fn on_rpc_get_player_basic_info_arg(
_ctx: &RpcPtcContext,
session: &mut PlayerSession,
_arg: RpcGetPlayerBasicInfoArg,
) -> Result<RpcGetPlayerBasicInfoRet, i32> {
Ok(RpcGetPlayerBasicInfoRet {
retcode: 0,
basic_info: protocol::util::build_player_basic_info(&session.player_info),
})
}
pub async fn on_rpc_get_server_timestamp_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetServerTimestampArg,
) -> Result<RpcGetServerTimestampRet, i32> {
Ok(RpcGetServerTimestampRet {
retcode: 0,
utc_offset: 3,
timestamp: time_util::unix_timestamp(),
})
}
pub async fn on_rpc_get_video_usm_key_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetVideoUsmKeyDataArg,
) -> Result<RpcGetVideoUsmKeyDataRet, i32> {
Ok(RpcGetVideoUsmKeyDataRet { retcode: 0 })
}
pub async fn on_rpc_get_authkey_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetAuthkeyArg,
) -> Result<RpcGetAuthkeyRet, i32> {
Ok(RpcGetAuthkeyRet::default())
}
pub async fn on_rpc_save_player_system_setting_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
arg: RpcSavePlayerSystemSettingArg,
) -> Result<RpcSavePlayerSystemSettingRet, i32> {
tracing::info!("save_player_system_setting: {arg:?}");
Ok(RpcSavePlayerSystemSettingRet { retcode: 0 })
}
pub async fn on_rpc_get_player_mails_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetPlayerMailsArg,
) -> Result<RpcGetPlayerMailsRet, i32> {
Ok(RpcGetPlayerMailsRet::default())
}
pub async fn on_rpc_get_role_card_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetRoleCardDataArg,
) -> Result<RpcGetRoleCardDataRet, i32> {
Ok(RpcGetRoleCardDataRet {
retcode: 0,
role_card_data: RoleCardData::default(),
})
}
pub async fn on_rpc_get_month_card_reward_list_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetMonthCardRewardListArg,
) -> Result<RpcGetMonthCardRewardListRet, i32> {
Ok(RpcGetMonthCardRewardListRet::default())
}
pub async fn on_rpc_get_display_case_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetDisplayCaseDataArg,
) -> Result<RpcGetDisplayCaseDataRet, i32> {
Ok(RpcGetDisplayCaseDataRet::default())
}
pub async fn on_rpc_player_operation_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcPlayerOperationArg,
) -> Result<RpcPlayerOperationRet, i32> {
Ok(RpcPlayerOperationRet::default())
}
pub async fn on_rpc_player_transaction_arg(
_: &RpcPtcContext,
session: &mut PlayerSession,
_: RpcPlayerTransactionArg,
) -> Result<RpcPlayerTransactionRet, i32> {
let player_uid = session.player_info.uid.unwrap_or_default();
let scene_uid = session.player_info.scene_uid.unwrap_or_default();
Ok(RpcPlayerTransactionRet {
retcode: 0,
transaction: format!("{player_uid}-{scene_uid}"),
})
}

View file

@ -0,0 +1,80 @@
use super::*;
pub async fn on_rpc_get_quest_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
arg: RpcGetQuestDataArg,
) -> Result<RpcGetQuestDataRet, i32> {
Ok(RpcGetQuestDataRet {
retcode: 0,
quest_type: arg.quest_type,
quest_data: QuestData::default(),
})
}
pub async fn on_rpc_get_archive_info_arg(
_: &RpcPtcContext,
session: &mut PlayerSession,
_: RpcGetArchiveInfoArg,
) -> Result<RpcGetArchiveInfoRet, i32> {
let archive_info = session.player_info.archive_info.as_ref().unwrap();
Ok(RpcGetArchiveInfoRet {
retcode: 0,
archive_info: ArchiveInfo {
hollow_archive_id_list: archive_info
.hollow_archive_id
.as_ref()
.map(|set| set.iter().map(|id| *id as u32).collect())
.unwrap_or_default(),
videotaps_info: archive_info
.videotaps_info
.as_ref()
.unwrap()
.iter()
.map(|(id, videotape)| VideotapeInfo {
archive_file_id: *id as u32,
finished: videotape.finished,
})
.collect(),
},
})
}
pub async fn on_rpc_get_yorozuya_info_arg(
_: &RpcPtcContext,
session: &mut PlayerSession,
_: RpcGetYorozuyaInfoArg,
) -> Result<RpcGetYorozuyaInfoRet, i32> {
let yorozuya_info = session.player_info.yorozuya_info.as_ref().unwrap();
Ok(RpcGetYorozuyaInfoRet {
retcode: 0,
yorozuya_info: YorozuyaInfo {
unlock_hollow_id_list: yorozuya_info
.unlock_hollow_id
.as_ref()
.map(|list| list.iter().map(|id| *id as u32).collect())
.unwrap_or_default(),
},
})
}
pub async fn on_rpc_get_fairy_info_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetFairyInfoArg,
) -> Result<RpcGetFairyInfoRet, i32> {
Ok(RpcGetFairyInfoRet {
retcode: 0,
info: FairyInfo::default(),
})
}
pub async fn on_rpc_check_yorozuya_info_refresh_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcCheckYorozuyaInfoRefreshArg,
) -> Result<RpcCheckYorozuyaInfoRefreshRet, i32> {
Ok(RpcCheckYorozuyaInfoRefreshRet::default())
}

View file

@ -0,0 +1,31 @@
use super::*;
pub async fn on_rpc_get_fashion_store_info_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetFashionStoreInfoArg,
) -> Result<RpcGetFashionStoreInfoRet, i32> {
Ok(RpcGetFashionStoreInfoRet {
retcode: 0,
info: FashionStoreInfo::default(),
})
}
pub async fn on_rpc_get_shopping_mall_info_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetShoppingMallInfoArg,
) -> Result<RpcGetShoppingMallInfoRet, i32> {
Ok(RpcGetShoppingMallInfoRet {
retcode: 0,
shopping_mall_info: ShoppingMallInfo::default(),
})
}
pub async fn on_rpc_get_recharge_item_list_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetRechargeItemListArg,
) -> Result<RpcGetRechargeItemListRet, i32> {
Ok(RpcGetRechargeItemListRet::default())
}

View file

@ -0,0 +1,25 @@
use super::*;
pub async fn on_rpc_get_friend_list_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetFriendListArg,
) -> Result<RpcGetFriendListRet, i32> {
Ok(RpcGetFriendListRet::default())
}
pub async fn on_rpc_get_chat_emoji_list_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetChatEmojiListArg,
) -> Result<RpcGetChatEmojiListRet, i32> {
Ok(RpcGetChatEmojiListRet::default())
}
pub async fn on_rpc_get_online_friends_list_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetOnlineFriendsListArg,
) -> Result<RpcGetOnlineFriendsListRet, i32> {
Ok(RpcGetOnlineFriendsListRet::default())
}

View file

@ -0,0 +1,65 @@
use super::*;
pub async fn on_rpc_get_tips_info_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetTipsInfoArg,
) -> Result<RpcGetTipsInfoRet, i32> {
Ok(RpcGetTipsInfoRet {
retcode: 0,
tips_info: TipsInfo::default(),
..Default::default()
})
}
pub async fn on_rpc_get_client_systems_info_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetClientSystemsInfoArg,
) -> Result<RpcGetClientSystemsInfoRet, i32> {
Ok(RpcGetClientSystemsInfoRet {
retcode: 0,
info: ClientSystemsInfo::default(),
})
}
pub async fn on_rpc_get_private_message_data_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetPrivateMessageDataArg,
) -> Result<RpcGetPrivateMessageDataRet, i32> {
Ok(RpcGetPrivateMessageDataRet {
retcode: 0,
private_message_data: PrivateMessageData::default(),
})
}
pub async fn on_rpc_get_collect_map_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetCollectMapArg,
) -> Result<RpcGetCollectMapRet, i32> {
Ok(RpcGetCollectMapRet {
retcode: 0,
collect_map: CollectMap::default(),
})
}
pub async fn on_rpc_get_workbench_info_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetWorkbenchInfoArg,
) -> Result<RpcGetWorkbenchInfoRet, i32> {
Ok(RpcGetWorkbenchInfoRet {
retcode: 0,
workbench_info: WorkbenchInfo::default(),
})
}
pub async fn on_rpc_report_ui_layout_platform_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcReportUiLayoutPlatformArg,
) -> Result<RpcReportUiLayoutPlatformRet, i32> {
Ok(RpcReportUiLayoutPlatformRet::default())
}

View file

@ -0,0 +1,12 @@
use super::*;
pub async fn on_rpc_get_vhs_store_info_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcGetVhsStoreInfoArg,
) -> Result<RpcGetVhsStoreInfoRet, i32> {
Ok(RpcGetVhsStoreInfoRet {
retcode: 0,
info: VhsStoreInfo::default(),
})
}

View file

@ -0,0 +1,407 @@
use common::time_util;
use dungeon_info::BuddyUnitInfo;
use item_info::ItemInfo;
use tracing::{debug, error};
use util::{build_client_dungeon_info, build_client_scene_info};
use crate::{level, scene_section_util};
use super::*;
pub async fn on_rpc_enter_world_arg(
ctx: &RpcPtcContext,
session: &mut PlayerSession,
_: RpcEnterWorldArg,
) -> Result<RpcEnterWorldRet, i32> {
let player_info = &mut session.player_info;
if player_info
.dungeon_collection
.as_ref()
.unwrap()
.default_scene_uid
.unwrap()
== 0
{
let dungeon_uid = session.uid_counter.next();
let scene_uid = session.uid_counter.next();
let dungeon_info = protocol::dungeon_info::DungeonInfo {
uid: dungeon_uid,
id: 1,
default_scene_uid: scene_uid,
start_timestamp: time_util::unix_timestamp_ms(),
to_be_destroyed: false,
back_scene_uid: 0,
quest_collection_uid: 0,
avatars: phashmap![],
buddy: BuddyUnitInfo {
uid: 0,
properties: 0,
},
world_quest_id: 0,
scene_properties_uid: 0,
drop_poll_chg_infos: phashmap![],
is_in_dungeon: false,
initiative_item: 0,
initiative_item_used_times: 0,
avatar_map: phashmap![],
battle_report: Vec::new(),
dungeon_group_uid: session.player_uid,
entered_times: 0,
is_preset_avatar: false,
hollow_event_version: 0,
};
let scene_info = protocol::scene_info::SceneInfo::Hall {
uid: scene_uid,
id: 1,
dungeon_uid,
end_timestamp: 0,
back_scene_uid: 0,
entered_times: 1,
section_id: 2,
open_ui: UIType::Default,
to_be_destroyed: false,
camera_x: 0xFFFFFFFF,
camera_y: 0xFFFFFFFF,
main_city_time_info: scene_info::MainCityTimeInfo {
initial_time: 60 * 8,
day_of_week: 5,
passed_milliseconds: 0,
executing_event_groups: phashset![],
unlocked_time_events: phashset![],
time_event_groups_info: phashmap![],
condition_progress_of_unlock: pdkhashmap![],
condition_progress_of_end: pdkhashmap![],
ended_time_events: phashset![],
leave_time: 0,
},
};
let dungeon_collection = player_info.dungeon_collection.as_mut().unwrap();
dungeon_collection
.dungeons
.as_mut()
.unwrap()
.insert(dungeon_uid, dungeon_info);
dungeon_collection
.scenes
.as_mut()
.unwrap()
.insert(scene_uid, scene_info);
dungeon_collection.default_scene_uid = Some(scene_uid);
}
let scene_uid = session
.player_info
.dungeon_collection
.as_ref()
.unwrap()
.default_scene_uid
.unwrap();
session.player_info.scene_uid = Some(scene_uid);
if let Some(section_id) = session
.player_info
.dungeon_collection
.as_ref()
.unwrap()
.scenes
.as_ref()
.unwrap()
.get(&scene_uid)
.map(|sc| *sc.get_section_id())
{
scene_section_util::init_hall_scene_section(session, scene_uid, section_id);
level::on_section_enter(session, scene_uid, section_id);
}
let player_info = &mut session.player_info;
player_info.second_last_enter_world_timestamp = player_info.last_enter_world_timestamp;
player_info.last_enter_world_timestamp = Some(time_util::unix_timestamp_ms());
let mut scene_info = build_client_scene_info(player_info, scene_uid).unwrap();
scene_section_util::add_scene_units_to_scene_info(session, scene_uid, &mut scene_info);
ctx.send_ptc(PtcEnterSceneArg {
scene_info,
dungeon_info: build_client_dungeon_info(&session.player_info, scene_uid),
})
.await;
Ok(RpcEnterWorldRet::default())
}
pub async fn on_rpc_scene_transition_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcSceneTransitionArg,
) -> Result<RpcSceneTransitionRet, i32> {
Ok(RpcSceneTransitionRet::default())
}
pub async fn on_rpc_enter_section_complete_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcEnterSectionCompleteArg,
) -> Result<RpcEnterSectionCompleteRet, i32> {
Ok(RpcEnterSectionCompleteRet::default())
}
pub async fn on_rpc_save_pos_in_main_city_arg(
_: &RpcPtcContext,
session: &mut PlayerSession,
arg: RpcSavePosInMainCityArg,
) -> Result<RpcSavePosInMainCityRet, i32> {
let pos_in_main_city = session.player_info.pos_in_main_city.as_mut().unwrap();
let scene_uid = session.player_info.scene_uid.unwrap();
let dungeon_collection = session.player_info.dungeon_collection.as_ref().unwrap();
let Some(protocol::scene_info::SceneInfo::Hall { section_id, .. }) =
dungeon_collection.scenes.as_ref().unwrap().get(&scene_uid)
else {
return Err(-1);
};
if *section_id == arg.section_id as i32 {
if let (Ok(position), Ok(rotation)) = (
arg.position.position.clone().try_into(),
arg.position.rotation.clone().try_into(),
) {
pos_in_main_city.position = Some(position);
pos_in_main_city.rotation = Some(rotation);
pos_in_main_city.initial_pos_id = Some(String::with_capacity(0));
debug!(
"player_uid: {}, pos in main city updated: {arg:?}",
session.player_uid
);
} else {
error!(
"player_uid: {}, failed to save player pos: {arg:?}",
session.player_uid
);
}
}
Ok(RpcSavePosInMainCityRet::default())
}
pub async fn on_rpc_enter_section_arg(
ctx: &RpcPtcContext,
session: &mut PlayerSession,
arg: RpcEnterSectionArg,
) -> Result<RpcEnterSectionRet, i32> {
let player_info = &mut session.player_info;
let cur_scene_uid = player_info.scene_uid.unwrap();
let dungeon_collection = player_info.dungeon_collection.as_mut().unwrap();
let Some(scene_info::SceneInfo::Hall { section_id, .. }) = dungeon_collection
.scenes
.as_mut()
.unwrap()
.get_mut(&cur_scene_uid)
else {
error!("RpcEnterSection: current scene is not Hall!");
return Err(-1);
};
*section_id = arg.section_id as i32;
let player_pos_in_main_city = player_info.pos_in_main_city.as_mut().unwrap();
player_pos_in_main_city.initial_pos_id = Some(arg.transform_id);
scene_section_util::init_hall_scene_section(session, cur_scene_uid, arg.section_id as i32);
level::on_section_enter(session, cur_scene_uid, arg.section_id as i32);
let mut scene_info = build_client_scene_info(&session.player_info, cur_scene_uid).unwrap();
scene_section_util::add_scene_units_to_scene_info(session, cur_scene_uid, &mut scene_info);
ctx.send_ptc(PtcEnterSceneArg {
scene_info,
dungeon_info: build_client_dungeon_info(&session.player_info, cur_scene_uid),
})
.await;
Ok(RpcEnterSectionRet::default())
}
pub async fn on_rpc_refresh_section_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcRefreshSectionArg,
) -> Result<RpcRefreshSectionRet, i32> {
Ok(RpcRefreshSectionRet {
retcode: 0,
refresh_status: HallRefreshStatus::Auto as u32,
})
}
pub async fn on_rpc_begin_training_course_battle_arg(
ctx: &RpcPtcContext,
session: &mut PlayerSession,
arg: RpcBeginTrainingCourseBattleArg,
) -> Result<RpcBeginTrainingCourseBattleRet, i32> {
let player_info = &mut session.player_info;
let dungeon_uid = session.uid_counter.next();
let scene_uid = session.uid_counter.next();
let cur_scene_uid = player_info.scene_uid.unwrap();
let dungeon_info = protocol::dungeon_info::DungeonInfo {
uid: dungeon_uid,
id: 12254000,
default_scene_uid: scene_uid,
start_timestamp: time_util::unix_timestamp_ms(),
to_be_destroyed: true,
back_scene_uid: cur_scene_uid,
quest_collection_uid: 0,
avatars: PropertyHashMap::Base(
arg.avatars
.iter()
.map(|avatar_id| {
let (avatar_uid, _) = player_info
.items
.as_ref()
.unwrap()
.iter()
.find(|(_, item)| {
if let ItemInfo::AvatarInfo { id, .. } = item {
(*id as u32) == *avatar_id
} else {
false
}
})
.unwrap();
(
*avatar_uid,
dungeon_info::AvatarUnitInfo {
uid: *avatar_uid,
properties_uid: 0,
hp_add_hollow: 0,
hp_lost_hollow: 0,
modified_property: pdkhashmap![],
layer_property_change: phashmap![],
is_banned: false,
},
)
})
.collect(),
),
buddy: BuddyUnitInfo {
uid: 0,
properties: 0,
},
world_quest_id: 12254000,
scene_properties_uid: 0,
drop_poll_chg_infos: phashmap![],
is_in_dungeon: false,
initiative_item: 0,
initiative_item_used_times: 0,
avatar_map: phashmap![],
battle_report: Vec::new(),
dungeon_group_uid: session.player_uid,
entered_times: 0,
is_preset_avatar: false,
hollow_event_version: 0,
};
let scene_info = protocol::scene_info::SceneInfo::Fight {
uid: scene_uid,
id: 19800014,
dungeon_uid,
end_timestamp: 0,
back_scene_uid: cur_scene_uid,
entered_times: 1,
section_id: 0,
open_ui: UIType::Default,
to_be_destroyed: true,
camera_x: 0xFFFFFFFF,
camera_y: 0xFFFFFFFF,
end_hollow: true,
local_play_type: LocalPlayType::TrainingRoom as u32,
time: TimePeriodType::Morning,
weather: WeatherType::Rain,
};
let dungeon_collection = player_info.dungeon_collection.as_mut().unwrap();
dungeon_collection
.dungeons
.as_mut()
.unwrap()
.insert(dungeon_uid, dungeon_info);
dungeon_collection
.scenes
.as_mut()
.unwrap()
.insert(scene_uid, scene_info);
let mut scene_info = build_client_scene_info(&session.player_info, scene_uid).unwrap();
scene_section_util::add_scene_units_to_scene_info(session, scene_uid, &mut scene_info);
ctx.send_ptc(PtcEnterSceneArg {
scene_info,
dungeon_info: build_client_dungeon_info(&session.player_info, scene_uid),
})
.await;
Ok(RpcBeginTrainingCourseBattleRet::default())
}
pub async fn on_rpc_battle_report_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcBattleReportArg,
) -> Result<RpcBattleReportRet, i32> {
Ok(RpcBattleReportRet::default())
}
pub async fn on_rpc_end_battle_arg(
_: &RpcPtcContext,
_: &mut PlayerSession,
_: RpcEndBattleArg,
) -> Result<RpcEndBattleRet, i32> {
Ok(RpcEndBattleRet::default())
}
pub async fn on_rpc_leave_cur_dungeon_arg(
ctx: &RpcPtcContext,
session: &mut PlayerSession,
_: RpcLeaveCurDungeonArg,
) -> Result<RpcLeaveCurDungeonRet, i32> {
let scene_uid = session
.player_info
.dungeon_collection
.as_ref()
.unwrap()
.default_scene_uid
.unwrap();
session.player_info.scene_uid = Some(scene_uid);
if let Some(section_id) = session
.player_info
.dungeon_collection
.as_ref()
.unwrap()
.scenes
.as_ref()
.unwrap()
.get(&scene_uid)
.map(|sc| *sc.get_section_id())
{
scene_section_util::init_hall_scene_section(session, scene_uid, section_id);
level::on_section_enter(session, scene_uid, section_id);
}
let mut scene_info = build_client_scene_info(&session.player_info, scene_uid).unwrap();
scene_section_util::add_scene_units_to_scene_info(session, scene_uid, &mut scene_info);
ctx.send_ptc(PtcEnterSceneArg {
scene_info,
dungeon_info: build_client_dungeon_info(&session.player_info, scene_uid),
})
.await;
Ok(RpcLeaveCurDungeonRet::default())
}

View file

@ -0,0 +1,104 @@
use std::collections::HashMap;
use protocol::{
player_info::{EventGraphsInfo, SectionInfo},
scene_ext::SectionInfoExt,
PtcHallRefreshArg, SceneUnitProtocolInfo,
};
use qwer::{phashmap, phashset, PropertyHashMap, PropertyHashSet};
use crate::{level, PlayerSession};
pub fn init_hall_scene_section(session: &mut PlayerSession, scene_uid: u64, section_id: i32) {
let single_dungeon_group = session.player_info.single_dungeon_group.as_mut().unwrap();
if single_dungeon_group
.section
.as_ref()
.unwrap()
.contains(&scene_uid, &section_id)
{
return;
}
let section_map = single_dungeon_group.section.as_mut().unwrap();
section_map.insert(
scene_uid,
section_id,
SectionInfo {
scene_uid,
id: section_id,
event_graphs_info: EventGraphsInfo {
event_graphs_info: phashmap![],
default_event_graph_id: -1,
},
section_info_ext: SectionInfoExt::Hall {
destroy_npc_when_no_player: phashset![],
},
},
);
level::on_section_added(session, scene_uid, section_id);
}
pub fn add_scene_units_to_scene_info(
session: &mut PlayerSession,
scene_uid: u64,
scene_info: &mut protocol::SceneInfo,
) {
let Some(hall_scene_info) = scene_info.hall_scene_info.as_mut() else {
return;
};
hall_scene_info.scene_unit_list =
build_scene_unit_protocol_info(session, scene_uid, hall_scene_info.section_id);
}
pub fn add_scene_units_to_hall_refresh_arg(
session: &mut PlayerSession,
scene_uid: u64,
refresh_arg: &mut PtcHallRefreshArg,
) {
refresh_arg.scene_unit_list =
build_scene_unit_protocol_info(session, scene_uid, refresh_arg.section_id);
}
fn build_scene_unit_protocol_info(
session: &mut PlayerSession,
scene_uid: u64,
section_id: u32,
) -> Vec<SceneUnitProtocolInfo> {
let sdg = session.player_info.single_dungeon_group.as_ref().unwrap();
sdg.npcs
.as_ref()
.unwrap()
.iter()
.filter(|(s_uid, _, npc)| {
**s_uid == scene_uid && npc.scene_data.section_id == section_id as i32
})
.map(|(_, uid, npc)| SceneUnitProtocolInfo {
npc_id: npc.tag_value as u32,
is_interactable: true,
interacts_info: session
.level_event_graph_mgr
.bound_interact_map
.get(uid)
.map(|(_, interact)| {
HashMap::from([(
interact.interact_id as u32,
protocol::InteractInfo {
name: interact.name.clone(),
participators: interact.participators.clone(),
scale_x: interact.scale_x,
scale_y: interact.scale_y,
scale_z: interact.scale_z,
scale_w: interact.scale_w,
scale_r: interact.scale_r,
interact_id: npc.tag_value,
interact_target_list: vec![2],
},
)])
})
.unwrap_or_default(),
})
.collect()
}

View file

@ -0,0 +1,39 @@
[package]
name = "yanagi-gate-server"
edition = "2021"
version.workspace = true
[dependencies]
# Framework
tokio.workspace = true
# Database
surrealdb.workspace = true
# Error processing
anyhow.workspace = true
thiserror.workspace = true
# Serialization
hex.workspace = true
serde.workspace = true
serde_json.workspace = true
rbase64.workspace = true
# Tracing
tracing.workspace = true
# Util
byteorder.workspace = true
rand.workspace = true
paste.workspace = true
# Internal
kcp = { path = "./kcp" }
common.workspace = true
qwer.workspace = true
qwer-rpc.workspace = true
protocol.workspace = true
yanagi-proto.workspace = true
yanagi-encryption.workspace = true
yanagi-http-client.workspace = true

View file

@ -0,0 +1,8 @@
server_name = "nap_dev_01"
bind_client_version = "OSPRODWin1.3.0"
design_data_url = "http://127.0.0.1:10000/design_data/NAP_Publish_AppStore_1.4"
[database]
url = "localhost:8000"
username = "root"
password = "root"

View file

@ -0,0 +1,14 @@
[package]
name = "kcp"
edition = "2021"
version.workspace = true
[dependencies]
bytes = "1.7.2"
log = "0.4.22"
thiserror.workspace = true
[dev-dependencies]
time = "0.3.36"
rand = "0.8.5"
env_logger = "0.11.5"

View file

@ -0,0 +1,56 @@
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,
#[error("token mismatch, expected {0}, found {1}")]
TokenMismatch(u32, u32),
}
fn make_io_error<T>(kind: ErrorKind, msg: T) -> io::Error
where
T: Into<Box<dyn StdError + Send + Sync>>,
{
io::Error::new(kind, msg)
}
impl From<Error> for io::Error {
fn from(err: Error) -> Self {
let kind = match err {
Error::IoError(err) => return err,
Error::RecvQueueEmpty | Error::ExpectingFragment => ErrorKind::WouldBlock,
_ => ErrorKind::Other,
};
make_io_error(kind, err)
}
}

File diff suppressed because it is too large Load diff

View file

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

View file

@ -0,0 +1,85 @@
use common::{config::DatabaseSettings, db_const};
use serde::{Deserialize, Serialize};
use surrealdb::{
engine::remote::ws::{Client, Ws},
Surreal,
};
const DB_NAMESPACE: &str = "yanagi";
const GAME_DB_NAME: &str = "nap";
const USER_UID_COUNTER: &str = "user_uid_cnt";
const USER_UID_TABLE: &str = "user_uid";
#[derive(Clone)]
pub struct DbContext(Surreal<Client>);
type Result<T> = std::result::Result<T, surrealdb::Error>;
#[derive(Serialize, Deserialize)]
pub struct UserUid {
account_uid: String,
account_token: String,
player_uid: u32,
}
impl DbContext {
pub async fn connect(settings: &DatabaseSettings) -> Result<Self> {
use surrealdb::opt::auth::Root;
let database = Surreal::new::<Ws>(&settings.url).await?;
database
.signin(Root {
username: &settings.username,
password: &settings.password,
})
.await?;
database.use_ns(DB_NAMESPACE).use_db(GAME_DB_NAME).await?;
// For uid auto-increment
database
.query(db_const::DEFINE_INCREMENT_FUNC_QUERY)
.await?;
Ok(Self(database))
}
pub async fn get_or_create_uid(
&self,
account_uid: &str,
account_token: &str,
) -> Result<Option<u32>> {
if let Some(user_uid) = self.get_user_uid_by_account(account_uid).await? {
return Ok((user_uid.account_token == account_token).then_some(user_uid.player_uid));
}
let uid = self.get_next_uid(USER_UID_COUNTER).await?;
let _: UserUid = self
.0
.create((USER_UID_TABLE, account_uid.to_string()))
.content(UserUid {
account_uid: account_uid.to_string(),
account_token: account_token.to_string(),
player_uid: uid,
})
.await?
.unwrap();
Ok(Some(uid))
}
async fn get_user_uid_by_account(&self, account_uid: &str) -> Result<Option<UserUid>> {
self.0.select((USER_UID_TABLE, account_uid)).await
}
async fn get_next_uid(&self, counter_name: &'static str) -> Result<u32> {
Ok(self
.0
.query(db_const::UID_INCREMENT_QUERY)
.bind((db_const::UID_COUNTER_NAME_BIND, counter_name))
.await?
.check()?
.take::<Option<u32>>(0)?
.unwrap())
}
}

View file

@ -0,0 +1,56 @@
use std::sync::{LazyLock, OnceLock};
use anyhow::Result;
use common::config::{DatabaseSettings, TomlConfig};
use database::DbContext;
use remote_config::RemoteConfiguration;
use serde::Deserialize;
use tracing::Level;
use udp_server::UdpServer;
mod database;
mod net;
mod packet;
mod remote_config;
mod session;
mod udp_server;
#[derive(Deserialize)]
pub struct GateServerConfig {
pub server_name: String,
pub bind_client_version: String,
pub design_data_url: String,
pub database: DatabaseSettings,
}
struct AppState {
remote_config: RemoteConfiguration,
db_context: DbContext,
}
impl TomlConfig for GateServerConfig {
const DEFAULT_TOML: &str = include_str!("../gateserver.toml");
}
#[tokio::main]
async fn main() -> Result<()> {
static CONFIG: LazyLock<GateServerConfig> =
LazyLock::new(|| GateServerConfig::load_or_create("gateserver.toml"));
static STATE: OnceLock<AppState> = OnceLock::new();
common::print_splash();
common::logging::init(Level::DEBUG);
let config = remote_config::download(&CONFIG);
let gateway_port = config.port;
let db_context = DbContext::connect(&CONFIG.database).await?;
let state = STATE.get_or_init(|| AppState {
remote_config: config,
db_context,
});
let server = UdpServer::new(&format!("0.0.0.0:{gateway_port}"), state)?;
tokio::task::spawn_blocking(|| server.serve()).await?;
Ok(())
}

View file

@ -0,0 +1,111 @@
use common::time_util;
use kcp::{Kcp, KcpResult};
use std::{
collections::HashMap,
io::Write,
net::{SocketAddr, UdpSocket},
sync::{mpsc, Arc},
thread,
};
use tracing::error;
use super::packet_processor;
struct UdpOutput {
peer_addr: SocketAddr,
socket: Arc<UdpSocket>,
}
pub enum KcpEvent {
Establish(u32, SocketAddr),
Recv(Box<[u8]>),
Send(Box<[u8]>),
Drop,
}
pub fn start(
rx: mpsc::Receiver<(u32, KcpEvent)>,
tx: tokio::sync::mpsc::Sender<packet_processor::Input>,
udp_socket: Arc<UdpSocket>,
) {
thread::spawn(move || kcp_loop(rx, tx, udp_socket));
}
fn kcp_loop(
event_rx: mpsc::Receiver<(u32, KcpEvent)>,
tx: tokio::sync::mpsc::Sender<packet_processor::Input>,
udp_socket: Arc<UdpSocket>,
) {
let mut conv_map = HashMap::new();
loop {
match event_rx.recv() {
Ok((conv, KcpEvent::Establish(token, addr))) => {
conv_map.insert(
conv,
Kcp::new(
conv,
token,
time_util::unix_timestamp_ms(),
false,
UdpOutput {
peer_addr: addr,
socket: udp_socket.clone(),
},
),
);
}
Ok((conv, KcpEvent::Recv(pk))) => {
if let Some(kcp) = conv_map.get_mut(&conv) {
if let Err(err) = perform_recv(kcp, &pk, &tx) {
error!("kcp recv fail: {err}");
}
}
}
Ok((conv, KcpEvent::Send(pk))) => {
if let Some(kcp) = conv_map.get_mut(&conv) {
if let Err(err) = perform_send(kcp, &pk) {
error!("kcp send fail: {err}");
}
}
}
Ok((conv, KcpEvent::Drop)) => {
conv_map.remove(&conv);
}
Err(_) => return, // channel closed
};
}
}
fn perform_recv(
kcp: &mut Kcp<UdpOutput>,
pk: &[u8],
tx: &tokio::sync::mpsc::Sender<packet_processor::Input>,
) -> KcpResult<()> {
kcp.input(pk)?;
kcp.update((time_util::unix_timestamp_ms() - kcp.estab_ts()) as u32)?;
let mut buf = [0u8; 16384];
while let Ok(len) = kcp.recv(&mut buf) {
let _ = tx.blocking_send(packet_processor::Input::Packet(
kcp.conv(),
buf[..len].into(),
));
}
kcp.flush()
}
fn perform_send(kcp: &mut Kcp<UdpOutput>, pk: &[u8]) -> KcpResult<()> {
kcp.send(pk)?;
kcp.flush()
}
impl Write for UdpOutput {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.socket.send_to(buf, self.peer_addr)
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}

View file

@ -0,0 +1,3 @@
pub mod kcp_conn_mgr;
pub mod packet_handler;
pub mod packet_processor;

View file

@ -0,0 +1,122 @@
use crate::{
packet::{read_common_values, DecodeError, NetPacket},
session::Session,
AppState,
};
use qwer_rpc::{
middleware::{AccountMiddlewareModel, MiddlewareModel},
RpcCallError,
};
use rand::RngCore;
use std::{net::SocketAddr, time::Duration};
use tracing::{debug, error};
use yanagi_encryption::rsa;
use yanagi_proto::*;
const GAME_SERVER_END_POINT: &str = "127.0.0.1:10101";
#[derive(thiserror::Error, Debug)]
pub enum PacketHandlingError {
#[error("decode error: {0}")]
Decode(#[from] DecodeError),
#[error("rpc call error: {0}")]
RpcCallError(#[from] RpcCallError),
}
pub async fn decode_and_handle(
session: &crate::session::Session,
state: &'static AppState,
buf: &[u8],
) -> Result<(), PacketHandlingError> {
let (cmd_id, _, _) = read_common_values(buf)?;
let end_point = GAME_SERVER_END_POINT.parse::<SocketAddr>().unwrap();
tracing::debug!("received cmd_id: {cmd_id}");
match cmd_id {
PlayerGetTokenCsReq::CMD_ID => {
let packet = NetPacket::<PlayerGetTokenCsReq>::decode(buf)?;
on_player_get_token_cs_req(session, state, packet.head, packet.body).await;
}
cmd_id if session.is_logged_in() => {
let middleware_list = vec![MiddlewareModel::Account(AccountMiddlewareModel {
player_uid: session.get_player_uid() as u64,
client_protocol_uid: 1,
is_resend: false,
})];
decode_and_forward_proto!(
cmd_id,
buf,
session,
session.rpc_ptc_point.lock().await,
end_point,
middleware_list,
Duration::from_secs(30)
)
}
cmd_id => debug!("received cmd_id: {cmd_id}, session is not logged in, expected PlayerGetTokenCsReq (cmd_id: {})", PlayerGetTokenCsReq::CMD_ID),
}
Ok(())
}
async fn on_player_get_token_cs_req(
session: &Session,
state: &'static AppState,
head: PacketHead,
req: PlayerGetTokenCsReq,
) {
if session.is_logged_in() {
debug!(
"received PlayerGetTokenCsReq but session is already logged in! account_uid: {}",
req.account_uid
);
session.send_rsp(
head.packet_id,
PlayerGetTokenScRsp {
retcode: 1008,
..Default::default()
},
);
return;
}
let conf = &state.remote_config.encryption_conf;
let client_rand_key = u64::from_le_bytes(
rsa::decrypt(conf, &rbase64::decode(&req.client_rand_key).unwrap())
.try_into()
.unwrap(),
);
let server_rand_key = rand::thread_rng().next_u64();
session.set_secret_key(server_rand_key ^ client_rand_key);
let server_rand_key = server_rand_key.to_le_bytes();
let (retcode, uid) = match state
.db_context
.get_or_create_uid(&req.account_uid, &req.token)
.await
{
Ok(Some(uid)) => {
session.set_player_uid(uid);
(0, uid)
}
Ok(None) => (1007, 0), // token mismatch
Err(err) => {
error!("get_or_create_uid failed: {err}");
(1, 0)
}
};
session.send_rsp(
head.packet_id,
PlayerGetTokenScRsp {
retcode,
uid,
server_rand_key: rbase64::encode(&rsa::encrypt(conf, &server_rand_key)),
sign: rbase64::encode(&rsa::sign(conf, &server_rand_key)),
..Default::default()
},
);
}

View file

@ -0,0 +1,93 @@
use qwer::ProtocolID;
use qwer_rpc::{ProtocolServiceFrontend, RpcPtcContext, RpcPtcServiceFrontend};
use std::{
collections::HashMap,
net::SocketAddr,
sync::{Arc, OnceLock},
};
use tokio::sync::{mpsc, Mutex};
use tracing::{info_span, Instrument};
use yanagi_proto::{forward_as_notify, register_ptc_handlers};
use crate::{session::Session, AppState};
use super::kcp_conn_mgr::KcpEvent;
pub enum Input {
CreateSession(u32, SocketAddr),
RemoveSession(u32),
Packet(u32, Box<[u8]>),
Notify(u32, RpcPtcContext),
}
static TX: OnceLock<mpsc::Sender<Input>> = OnceLock::new();
pub fn start(
kcp_evt_tx: std::sync::mpsc::Sender<(u32, KcpEvent)>,
state: &'static AppState,
) -> mpsc::Sender<Input> {
let (tx, rx) = mpsc::channel(32);
tokio::spawn(async move {
processing_loop(rx, kcp_evt_tx, state).await;
});
TX.get_or_init(|| tx.clone());
tx
}
async fn processing_loop(
mut rx: mpsc::Receiver<Input>,
tx: std::sync::mpsc::Sender<(u32, KcpEvent)>,
state: &'static AppState,
) {
let rpc_ptc_service = RpcPtcServiceFrontend::new(ProtocolServiceFrontend::new());
let mut session_map: HashMap<u32, Arc<Session>> = HashMap::new();
loop {
match rx.recv().await {
Some(Input::CreateSession(conv, addr)) => {
let rpc_ptc_point = rpc_ptc_service.create_point(None).await.unwrap();
let conv_id = conv;
register_ptc_handlers!(rpc_ptc_point, conv_id, TX);
session_map.insert(
conv,
Arc::new(Session::new(
conv,
addr,
&state.remote_config.xorpad,
tx.clone(),
Mutex::new(rpc_ptc_point),
)),
);
}
Some(Input::RemoveSession(conv)) => {
session_map.remove(&conv);
}
Some(Input::Packet(conv, mut pk)) => {
if let Some(session) = session_map.get_mut(&conv).cloned() {
let addr = session.addr;
tokio::spawn(
async move {
let _ = session
.process(pk.as_mut(), state)
.await
.inspect_err(|err| {
tracing::warn!("Session::process failed: {err}")
});
}
.instrument(info_span!("session", addr = %addr)),
);
}
}
Some(Input::Notify(conv, ctx)) => {
if let Some(session) = session_map.get(&conv) {
forward_as_notify!(session, ctx);
}
}
None => break,
}
}
}

View file

@ -0,0 +1,94 @@
pub const CONTROL_PACKET_SIZE: usize = 20;
pub struct ControlPacket([u8; CONTROL_PACKET_SIZE]);
impl ControlPacket {
pub fn build(ty: ControlPacketType, conv: u32, token: u32, data: u32) -> Self {
use byteorder::{ByteOrder, BE};
let (head, tail) = ty.to_magic();
let mut buf = [0u8; CONTROL_PACKET_SIZE];
BE::write_u32(&mut buf[0..4], head);
BE::write_u32(&mut buf[4..8], conv);
BE::write_u32(&mut buf[8..12], token);
BE::write_u32(&mut buf[12..16], data);
BE::write_u32(&mut buf[16..20], tail);
Self(buf)
}
pub fn get_type(&self) -> ControlPacketType {
ControlPacketType::from_magic((
u32::from_be_bytes(self.0[0..4].try_into().unwrap()),
u32::from_be_bytes(self.0[16..20].try_into().unwrap()),
))
.unwrap()
}
pub fn get_conv(&self) -> u32 {
u32::from_be_bytes(self.0[4..8].try_into().unwrap())
}
pub fn get_token(&self) -> u32 {
u32::from_be_bytes(self.0[8..12].try_into().unwrap())
}
pub fn get_data(&self) -> u32 {
u32::from_be_bytes(self.0[12..16].try_into().unwrap())
}
pub fn as_slice(&self) -> &[u8] {
&self.0
}
}
#[derive(Debug, PartialEq)]
pub enum ControlPacketType {
Connect,
Establish,
Disconnect,
}
impl ControlPacketType {
const CONNECT_MAGIC: (u32, u32) = (0xFF, 0xFFFFFFFF);
const ESTABLISH_MAGIC: (u32, u32) = (0x145, 0x14514545);
const DISCONNECT_MAGIC: (u32, u32) = (0x194, 0x19419494);
pub fn to_magic(self) -> (u32, u32) {
match self {
Self::Connect => Self::CONNECT_MAGIC,
Self::Establish => Self::ESTABLISH_MAGIC,
Self::Disconnect => Self::DISCONNECT_MAGIC,
}
}
pub fn from_magic(magic: (u32, u32)) -> Option<ControlPacketType> {
Some(match magic {
Self::CONNECT_MAGIC => Self::Connect,
Self::ESTABLISH_MAGIC => Self::Establish,
Self::DISCONNECT_MAGIC => Self::Disconnect,
_ => return None,
})
}
}
#[derive(thiserror::Error, Debug)]
pub enum TryFromError {
#[error("unknown magic received: (0x{0:X}, 0x{1:X})")]
UnknownMagic(u32, u32),
}
impl TryFrom<[u8; CONTROL_PACKET_SIZE]> for ControlPacket {
type Error = TryFromError;
fn try_from(value: [u8; CONTROL_PACKET_SIZE]) -> Result<Self, Self::Error> {
let magic = (
u32::from_be_bytes(value[0..4].try_into().unwrap()),
u32::from_be_bytes(value[16..20].try_into().unwrap()),
);
ControlPacketType::from_magic(magic)
.map(|_| Self(value))
.ok_or(TryFromError::UnknownMagic(magic.0, magic.1))
}
}

View file

@ -0,0 +1,5 @@
mod control_packet;
mod net_packet;
pub use control_packet::{ControlPacket, ControlPacketType, CONTROL_PACKET_SIZE};
pub use net_packet::{read_common_values, DecodeError, NetPacket};

View file

@ -0,0 +1,92 @@
use byteorder::{ByteOrder, BE};
use yanagi_proto::{PacketHead, Protobuf, ProtobufDecodeError};
pub struct NetPacket<Proto> {
pub head: PacketHead,
pub body: Proto,
}
#[derive(thiserror::Error, Debug)]
pub enum DecodeError {
#[error("head magic mismatch")]
HeadMagicMismatch,
#[error("tail magic mismatch")]
TailMagicMismatch,
#[error("input buffer is less than overhead, len: {0}, overhead: {1}")]
InputLessThanOverhead(usize, usize),
#[error("out of bounds ({0}/{1})")]
OutOfBounds(usize, usize),
#[error("failed to decode PacketHead: {0}")]
HeadDecode(ProtobufDecodeError),
#[error("failed to decode body: {0}")]
BodyDecode(ProtobufDecodeError),
}
const OVERHEAD: usize = 16;
const HEAD_MAGIC: [u8; 4] = 0x01234567_u32.to_be_bytes();
const TAIL_MAGIC: [u8; 4] = 0x89ABCDEF_u32.to_be_bytes();
impl<Proto> NetPacket<Proto>
where
Proto: yanagi_proto::NapMessage,
{
pub fn decode(buf: &[u8]) -> Result<Self, DecodeError> {
let (_, head_len, body_len) = read_common_values(buf)?;
if &buf[(12 + head_len + body_len)..(16 + head_len + body_len)] != TAIL_MAGIC {
return Err(DecodeError::TailMagicMismatch);
}
let head = PacketHead::decode(&buf[12..12 + head_len]).map_err(DecodeError::HeadDecode)?;
let mut body = Proto::decode(&buf[12 + head_len..12 + head_len + body_len])
.map_err(DecodeError::BodyDecode)?;
body.xor_fields();
Ok(NetPacket { head, body })
}
pub fn encode(&self) -> Box<[u8]> {
let head_len = self.head.encoded_len();
let body_len = self.body.encoded_len();
let encoded_len = OVERHEAD + head_len + body_len;
let mut buf = vec![0u8; encoded_len];
(&mut buf[0..4]).copy_from_slice(&HEAD_MAGIC);
BE::write_u16(&mut buf[4..6], self.body.get_cmd_id());
BE::write_u16(&mut buf[6..8], head_len as u16);
BE::write_u32(&mut buf[8..12], body_len as u32);
self.head
.encode(&mut buf[12..12 + head_len].as_mut())
.unwrap();
self.body
.encode(&mut buf[12 + head_len..12 + head_len + body_len].as_mut())
.unwrap();
(&mut buf[12 + head_len + body_len..16 + head_len + body_len]).copy_from_slice(&TAIL_MAGIC);
buf.into_boxed_slice()
}
}
pub fn read_common_values(buf: &[u8]) -> Result<(u16, usize, usize), DecodeError> {
if buf.len() < OVERHEAD {
return Err(DecodeError::InputLessThanOverhead(buf.len(), OVERHEAD));
}
if &buf[0..4] != HEAD_MAGIC {
return Err(DecodeError::HeadMagicMismatch);
}
let cmd_id = BE::read_u16(&buf[4..6]);
let head_len = BE::read_u16(&buf[6..8]) as usize;
let body_len = BE::read_u32(&buf[8..12]) as usize;
let required_len = 4 + head_len + body_len;
if required_len > buf.len() {
Err(DecodeError::OutOfBounds(required_len, buf.len()))
} else {
Ok((cmd_id, head_len, body_len))
}
}

View file

@ -0,0 +1,51 @@
use std::time::Duration;
use common::config::*;
use yanagi_encryption::xor::MhyXorpad;
use yanagi_http_client::AutopatchClient;
use crate::GateServerConfig;
pub struct RemoteConfiguration {
pub xorpad: MhyXorpad,
pub encryption_conf: EncryptionConfig,
pub port: u16,
}
pub fn download(config: &'static GateServerConfig) -> RemoteConfiguration {
const RETRY_TIME: Duration = Duration::from_secs(5);
let client = AutopatchClient::new(&config.design_data_url).retry_after(RETRY_TIME);
let app_config: AppConfig = client.fetch_until_success("/config.json");
let version_info = app_config
.version_info_groups
.get(&config.bind_client_version)
.expect(
"Fatal: remote config doesn't contain configuration for specified bind_client_version",
);
let server_list: ServerList = client.fetch_until_success(&version_info.server_list_url);
let server_info = server_list
.into_iter()
.find(|info| info.notice_region == config.server_name)
.expect("Fatal: remote config doesn't contain configuration with specified server_name");
let mut encryption_conf: EncryptionConfMap =
client.fetch_until_success(&version_info.encryption_config_url);
let Some(encryption_conf) = encryption_conf.remove(&server_info.rsa_ver) else {
panic!(
"Fatal: remote config doesn't contain encryption config with rsa_ver: {}",
server_info.rsa_ver,
)
};
let xorpad = MhyXorpad::from_ec2b(&server_info.client_secret_key).unwrap();
RemoteConfiguration {
xorpad,
encryption_conf,
port: server_info.port,
}
}

View file

@ -0,0 +1,158 @@
use std::{
net::SocketAddr,
sync::{
atomic::{AtomicU32, Ordering},
mpsc, Arc, OnceLock,
},
};
use qwer_rpc::RpcPtcPoint;
use tokio::sync::Mutex;
use tracing::{debug, instrument};
use yanagi_encryption::xor::MhyXorpad;
use yanagi_proto::{NapMessage, NullMessage, PlayerGetTokenScRsp};
use crate::{
net::{
kcp_conn_mgr::KcpEvent,
packet_handler::{self, PacketHandlingError},
},
packet::{read_common_values, DecodeError, NetPacket},
AppState,
};
pub struct SessionID {
pub conv: u32,
pub token: u32,
}
pub struct Session {
pub conv: u32,
pub addr: SocketAddr,
pub rpc_ptc_point: Mutex<Arc<RpcPtcPoint>>,
player_uid: OnceLock<u32>,
initial_xorpad: &'static MhyXorpad,
secret_key: OnceLock<MhyXorpad>,
kcp_evt_tx: mpsc::Sender<(u32, KcpEvent)>,
seq_id: AtomicU32,
}
impl Session {
pub fn new(
conv: u32,
addr: SocketAddr,
initial_xorpad: &'static MhyXorpad,
kcp_evt_tx: mpsc::Sender<(u32, KcpEvent)>,
rpc_ptc_point: Mutex<Arc<RpcPtcPoint>>,
) -> Self {
Self {
conv,
addr,
kcp_evt_tx,
rpc_ptc_point,
initial_xorpad,
seq_id: AtomicU32::new(0),
secret_key: OnceLock::new(),
player_uid: OnceLock::new(),
}
}
pub async fn process(
&self,
buf: &mut [u8],
state: &'static AppState,
) -> Result<(), PacketHandlingError> {
self.xor_packet_body(buf)?;
packet_handler::decode_and_handle(self, state, buf).await
}
pub fn send_rsp(&self, rpc_id: u32, mut msg: impl NapMessage) {
use yanagi_proto::CmdID;
let cmd_id = msg.get_cmd_id();
debug!("send_rsp: {cmd_id}");
msg.xor_fields();
let seq_id = (cmd_id != yanagi_proto::PlayerGetTokenScRsp::CMD_ID)
.then_some(
self.seq_id
.fetch_add(1, std::sync::atomic::Ordering::SeqCst),
)
.unwrap_or(0);
self.send(NetPacket {
head: yanagi_proto::PacketHead {
packet_id: seq_id,
request_id: rpc_id,
..Default::default()
},
body: msg,
});
}
#[instrument(skip_all, name = "session", fields(addr = %self.addr))]
pub fn notify(&self, mut msg: impl NapMessage) {
debug!("notify: {}", msg.get_cmd_id());
msg.xor_fields();
self.send(NetPacket {
head: yanagi_proto::PacketHead {
..Default::default()
},
body: msg,
})
}
pub fn send_null_rsp(&self, rpc_id: u32) {
let seq_id = self.seq_id.fetch_add(1, Ordering::SeqCst);
self.send(NetPacket {
head: yanagi_proto::PacketHead {
packet_id: seq_id,
request_id: rpc_id,
..Default::default()
},
body: NullMessage::default(),
});
}
fn send<Proto: NapMessage>(&self, packet: NetPacket<Proto>) {
let mut buf = packet.encode();
self.xor_packet_body(&mut buf).unwrap();
let _ = self.kcp_evt_tx.send((self.conv, KcpEvent::Send(buf)));
}
pub fn set_secret_key(&self, seed: u64) {
let _ = self.secret_key.set(MhyXorpad::new::<byteorder::BE>(seed));
}
pub fn is_logged_in(&self) -> bool {
self.player_uid.get().is_some()
}
pub fn get_player_uid(&self) -> u32 {
self.player_uid.get().copied().unwrap_or(0)
}
pub fn set_player_uid(&self, uid: u32) {
let _ = self.player_uid.set(uid);
}
#[inline]
pub fn xor_packet_body(&self, packet: &mut [u8]) -> Result<(), DecodeError> {
use yanagi_proto::CmdID;
let (cmd_id, head_len, body_len) = read_common_values(packet)?;
let body = &mut packet[12 + head_len..12 + head_len + body_len];
match self.secret_key.get() {
_ if cmd_id == PlayerGetTokenScRsp::CMD_ID => self.initial_xorpad.xor(body),
Some(key) => key.xor(body),
None => self.initial_xorpad.xor(body),
}
Ok(())
}
}

View file

@ -0,0 +1,158 @@
use std::{
collections::HashMap,
net::{SocketAddr, UdpSocket},
sync::{mpsc, Arc},
};
use rand::RngCore;
use crate::{
net::{
kcp_conn_mgr::{self, KcpEvent},
packet_processor,
},
packet::{self, ControlPacket, ControlPacketType},
session::SessionID,
AppState,
};
pub struct UdpServer {
socket: Arc<UdpSocket>,
sessions: HashMap<u32, SessionID>,
kcp_conn_mgr_tx: mpsc::Sender<(u32, KcpEvent)>,
packet_processor_tx: tokio::sync::mpsc::Sender<packet_processor::Input>,
}
impl UdpServer {
pub fn new(udp_addr: &str, state: &'static AppState) -> std::io::Result<Self> {
let socket = Arc::new(UdpSocket::bind(udp_addr)?);
let (kcp_tx, kcp_rx) = mpsc::channel();
let proc_tx = packet_processor::start(kcp_tx.clone(), state);
kcp_conn_mgr::start(kcp_rx, proc_tx.clone(), socket.clone());
Ok(Self {
kcp_conn_mgr_tx: kcp_tx,
packet_processor_tx: proc_tx,
socket,
sessions: HashMap::new(),
})
}
pub fn serve(mut self) {
tracing::info!("UDP server is up at {}", self.socket.local_addr().unwrap());
let mut session_counter = 0;
let mut buf = [0u8; 1400];
loop {
let Ok((len, addr)) = self
.socket
.recv_from(&mut buf)
.inspect_err(|err| tracing::debug!("recv_from failed: {err}"))
else {
continue;
};
match len {
packet::CONTROL_PACKET_SIZE => self.handle_control_packet(
buf[..packet::CONTROL_PACKET_SIZE].try_into().unwrap(),
addr,
&mut session_counter,
),
kcp::KCP_OVERHEAD.. => {
let buf = &buf[..len];
if let Some(id) = self.sessions.get(&kcp::get_conv(buf)) {
let token = kcp::get_token(buf);
if token != id.token {
tracing::debug!(
"session token mismatch! Expected: {}, received: {}, conv: {} client_addr: {}",
id.token,
token,
id.conv,
addr,
);
}
self.kcp_conn_mgr_tx
.send((id.conv, KcpEvent::Recv(buf.into())))
.unwrap();
}
}
_ => (),
}
}
}
fn handle_control_packet(
&mut self,
buf: [u8; packet::CONTROL_PACKET_SIZE],
addr: SocketAddr,
s_counter: &mut u32,
) {
let Ok(packet) = ControlPacket::try_from(buf).inspect_err(|err| {
tracing::debug!("ControlPacket::try_from failed: {err}, client_addr: {addr}")
}) else {
return;
};
match packet.get_type() {
ControlPacketType::Connect => {
tracing::info!("new connection from {addr}, data: {}", packet.get_data());
*s_counter += 1;
self.on_connect(*s_counter, rand::thread_rng().next_u32(), addr);
}
ControlPacketType::Disconnect => {
if let Some(id) = self.sessions.get(&packet.get_conv()) {
if id.token != packet.get_token() {
tracing::debug!(
"disconnect: session token mismatch! Expected: {}, received: {}, conv: {} client_addr: {}",
id.token,
packet.get_token(),
id.conv,
addr,
);
return;
}
self.on_disconnect(packet, addr);
}
}
unsupported => tracing::debug!("received {unsupported:?} from client_addr: {addr}"),
}
}
fn on_connect(&mut self, conv: u32, token: u32, addr: SocketAddr) {
self.kcp_conn_mgr_tx
.send((conv, KcpEvent::Establish(token, addr)))
.unwrap();
self.packet_processor_tx
.blocking_send(packet_processor::Input::CreateSession(conv, addr))
.unwrap();
self.sessions.insert(conv, SessionID { conv, token });
self.send_control_packet(
ControlPacket::build(ControlPacketType::Establish, conv, token, 0),
addr,
);
}
fn on_disconnect(&mut self, pk: ControlPacket, addr: SocketAddr) {
let conv = pk.get_conv();
self.send_control_packet(pk, addr);
tracing::info!("client from {addr} disconnected, (conv: {conv})");
self.sessions.remove(&conv);
self.kcp_conn_mgr_tx.send((conv, KcpEvent::Drop)).unwrap();
self.packet_processor_tx
.blocking_send(packet_processor::Input::RemoveSession(conv))
.unwrap();
}
fn send_control_packet(&self, packet: ControlPacket, addr: SocketAddr) {
let socket = self.socket.clone();
let _ = socket.send_to(packet.as_slice(), addr);
}
}

View file

@ -0,0 +1,9 @@
[package]
name = "protocol"
edition = "2021"
version.workspace = true
[dependencies]
thiserror.workspace = true
qwer.workspace = true
protocol-macros = { path = "protocol-macros" }

View file

@ -0,0 +1,12 @@
[package]
name = "protocol-macros"
version = "0.1.0"
edition = "2021"
[dependencies]
syn = "2.0.53"
quote = "1.0.35"
proc-macro2 = "1.0.79"
[lib]
proc-macro = true

View file

@ -0,0 +1,150 @@
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{
braced, bracketed,
parse::{Parse, ParseStream},
parse_macro_input,
punctuated::Punctuated,
token, Ident, LitInt, Token, Type,
};
struct PolymorphicInput {
name: Ident,
_brace_token: token::Bracket,
common_fields: Punctuated<FieldEntry, Token![,]>,
structs: Punctuated<StructEntry, Token![,]>,
}
struct FieldEntry {
name: Ident,
_colon_token: Token![:],
ty: Type,
}
struct StructEntry {
name: Ident,
_brace_token: token::Brace,
fields: Punctuated<FieldEntry, Token![,]>,
_eq_token: Token![=],
discriminant: LitInt,
}
impl Parse for PolymorphicInput {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
Ok(Self {
name: input.parse()?,
_brace_token: bracketed!(content in input),
common_fields: content.parse_terminated(FieldEntry::parse, Token![,])?,
structs: input.parse_terminated(StructEntry::parse, Token![,])?,
})
}
}
impl Parse for FieldEntry {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
name: input.parse()?,
_colon_token: input.parse()?,
ty: input.parse()?,
})
}
}
impl Parse for StructEntry {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
Ok(Self {
name: input.parse()?,
_brace_token: braced!(content in input),
fields: content.parse_terminated(FieldEntry::parse, Token![,])?,
_eq_token: input.parse()?,
discriminant: input.parse()?,
})
}
}
#[proc_macro]
pub fn polymorphic(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as PolymorphicInput);
let enum_name = &input.name;
let mut variants = quote!();
for stru in input.structs.iter() {
let variant_name = &stru.name;
let variant_discr = &stru.discriminant;
let mut fields = quote!();
for field in input.common_fields.iter().chain(stru.fields.iter()) {
let field_name = &field.name;
let field_type = &field.ty;
fields.extend(quote! {
#field_name: #field_type,
});
}
variants.extend(quote! {
#variant_name {
#fields
} = #variant_discr,
});
}
let mut getters_and_setters = quote!();
for field in input.common_fields.iter() {
let field_type = &field.ty;
let field_name = &field.name;
let getter_name = Ident::new(&format!("get_{}", field_name), Span::call_site());
let setter_name = Ident::new(&format!("set_{}", field_name), Span::call_site());
let mut getter_match_arms = quote!();
let mut setter_match_arms = quote!();
for stru in input.structs.iter() {
let stru_name = &stru.name;
getter_match_arms.extend(quote! {
Self::#stru_name { #field_name, .. } => #field_name,
});
setter_match_arms.extend(quote! {
Self::#stru_name { #field_name, .. } => { *#field_name = value; },
});
}
getters_and_setters.extend(quote! {
pub fn #getter_name(&self) -> &#field_type {
match self {
#getter_match_arms
}
}
pub fn #setter_name(&mut self, value: #field_type) {
match self {
#setter_match_arms
}
}
});
}
// can't use base_fields_count directly in 'quote!'
// 'quote!' puts suffixed literal, but it's not allowed in attributes
let base_fields_count = input.common_fields.len();
let base_attr = format!("#[base = {base_fields_count}]")
.parse::<proc_macro2::TokenStream>()
.unwrap();
quote! {
#[derive(Debug, Clone, ::qwer::OctData)]
#[repr(u32)]
#base_attr
pub enum #enum_name {
#variants
}
impl #enum_name {
#getters_and_setters
}
}
.into()
}

View file

@ -0,0 +1,74 @@
use protocol_macros::polymorphic;
use qwer::{OctData, PropertyHashMap};
use crate::{player_info::ChoiceInfo, HollowShopCurrency, HollowShopType};
#[derive(OctData, Clone, Debug)]
pub struct ConfigItem {
pub uid: i32,
pub item_id: i32,
pub count: i32,
pub value: i32,
pub base_value: i32,
pub discount: i32,
}
#[derive(OctData, Clone, Debug)]
pub struct ConfigShopInfo {
pub goods: Vec<ConfigItem>,
pub currency: HollowShopCurrency,
}
polymorphic!(
ActionInfo []
ServerChoices {
choices: Vec<ChoiceInfo>,
finished: bool,
} = 52,
DropHollowItem {
drop_item: i32,
} = 162,
FinishBlackout {
finished: bool,
show_tips: bool,
} = 133,
Loop {
loop_times: u16,
} = 141,
Perform {
step: u8,
r#return: PropertyHashMap<String, i32>,
} = 23,
/*PrepareNextHollow {
section_id: i32,
finished: bool,
show_other: bool,
main_map: HollowGridMapProtocolInfo,
} = 130,*/ // TODO!
ActionRandomChallenge {
choices: Vec<i32>,
choice_result: i32,
finished: bool,
} = 109,
RemoveCurse {
curse_can_remove: Vec<u64>,
to_remove_num: u8,
choosed: bool,
} = 105,
SetHollowSystemState {
finished: bool,
} = 134,
Shop {
shop_info: PropertyHashMap<HollowShopType, ConfigShopInfo>,
finished: bool,
} = 62,
SlotMachine {
indexes: Vec<i32>,
index: i32,
finished: bool,
} = 131,
TriggerBattle {
next_action_id: i32,
finished: bool,
} = 56,
);

View file

@ -0,0 +1,63 @@
use qwer::{OctData, PropertyDoubleKeyHashMap, PropertyHashMap};
use crate::{DungeonContentDropPoolType, PropertyType, ReportType};
#[derive(OctData, Clone, Debug)]
pub struct AvatarPropertyChgInHollow {
pub hp_lost: i32,
pub hp_add: i32,
}
#[derive(OctData, Clone, Debug)]
pub struct AvatarUnitInfo {
pub uid: u64,
pub properties_uid: u64,
pub is_banned: bool,
pub modified_property: PropertyDoubleKeyHashMap<u64, PropertyType, i32>,
pub hp_lost_hollow: i32,
pub hp_add_hollow: i32,
pub layer_property_change: PropertyHashMap<i32, AvatarPropertyChgInHollow>,
}
#[derive(OctData, Clone, Debug)]
pub struct BuddyUnitInfo {
pub uid: u64,
pub properties: u64,
}
#[derive(OctData, Clone, Debug)]
pub struct DungeonDropPollInfo {
pub action_card_mask: PropertyHashMap<i32, i32>,
}
#[derive(OctData, Clone, Debug)]
pub struct BattleReport {
pub index: i32,
pub report_type: ReportType,
pub id: i32,
}
#[derive(OctData, Clone, Debug)]
pub struct DungeonInfo {
pub uid: u64,
pub id: i32,
pub default_scene_uid: u64,
pub start_timestamp: u64,
pub to_be_destroyed: bool,
pub back_scene_uid: u64,
pub quest_collection_uid: u64,
pub avatars: PropertyHashMap<u64, AvatarUnitInfo>,
pub buddy: BuddyUnitInfo,
pub world_quest_id: i32,
pub scene_properties_uid: u64,
pub drop_poll_chg_infos: PropertyHashMap<DungeonContentDropPoolType, DungeonDropPollInfo>,
pub is_in_dungeon: bool,
pub initiative_item: i32,
pub initiative_item_used_times: i32,
pub avatar_map: PropertyHashMap<i8, AvatarUnitInfo>,
pub battle_report: Vec<BattleReport>,
pub dungeon_group_uid: u64,
pub entered_times: u16,
pub is_preset_avatar: bool,
pub hollow_event_version: i32,
}

View file

@ -0,0 +1,426 @@
use qwer::OctData;
#[derive(OctData, Clone, Debug, PartialEq, Eq)]
#[repr(i16)]
pub enum ActionState {
Init = 0,
Running = 1,
Finished = 2,
Error = 3,
}
#[derive(OctData, Clone, Debug, PartialEq, Eq)]
#[repr(i16)]
pub enum EventState {
Initing = 0,
Running = 1,
Pause = 2,
WaitingMsg = 3,
WaitingClient = 4,
Finished = 5,
Error = 6,
}
#[derive(OctData, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(i16)]
pub enum HollowQuestType {
Common = 0,
MainQuest = 1,
SideQuest = 2,
Urgent = 3,
UrgentSupplement = 4,
Challenge = 5,
ChallengeChaos = 6,
AvatarSide = 7,
}
#[derive(OctData, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(i16)]
pub enum HollowShopType {
None = 0,
Item = 1,
Card = 2,
Curse = 3,
HollowItem = 4,
Discount = 5,
Gachashop = 6,
UpgradeCard = 7,
}
#[derive(OctData, Clone, Debug)]
#[repr(i16)]
pub enum HollowShopCurrency {
Coin = 1,
Curse = 2,
Random = 3,
}
#[derive(OctData, Clone, Debug, PartialEq, Eq)]
#[repr(i16)]
pub enum HollowBattleEventType {
Default = 0,
Normal = 1,
Elite = 2,
Boss = 3,
LevelEnd = 4,
LevelFin = 5,
}
#[derive(OctData, Clone, Debug, PartialEq, Eq)]
#[repr(i16)]
pub enum LocalPlayType {
ArchiveLongFight = 212,
TrainingRootTactics = 292,
OperationBetaDemo = 216,
LevelZero = 205,
BossLittleBattleLongfight = 215,
BuddyTowerdefenseBattle = 227,
TrainingRoom = 290,
ChessBoardLongfihgtBattle = 204,
BossBattle = 210,
DualElite = 208,
HadalZoneBosschallenge = 224,
BigBossBattle = 211,
BigBossBattleLongfight = 217,
SideScrollingThegunBattle = 221,
MpBigBossBattle = 214,
RallyLongFight = 207,
PureHollowBattle = 280,
S2RogueBattle = 226,
AvatarDemoTrial = 213,
GuideSpecial = 203,
BossRushBattle = 218,
HadalZone = 209,
OperationTeamCoop = 219,
PureHollowBattleLonghfight = 281,
MapChallengeBattle = 291,
BossNestHardBattle = 220,
PureHollowBattleHardmode = 282,
BabelTower = 223,
MiniScapeBattle = 228,
DailyChallenge = 206,
HadalZoneAlivecount = 222,
Unknown = 0,
ArchiveBattle = 201,
ChessBoardBattle = 202,
}
#[derive(OctData, Clone, Debug, PartialEq, Eq)]
#[repr(i32)]
pub enum WeatherType {
None = -1,
SunShine = 0,
Cloudy = 2,
Rain = 3,
Thunder = 4,
ThickFog = 5,
ThickCloudy = 6,
}
#[derive(OctData, Clone, Debug, PartialEq, Eq)]
#[repr(i16)]
pub enum TimePeriodType {
Morning = 0,
Evening = 1,
Night = 2,
}
impl WeatherType {
pub fn to_protocol_string(&self) -> String {
format!("{self:?}")
}
}
impl TimePeriodType {
pub fn to_protocol_string(&self) -> String {
format!("{self:?}")
}
}
#[derive(OctData, Clone, Debug, PartialEq, Eq)]
#[repr(i16)]
pub enum MailState {
New = 0,
Old = 1,
Read = 2,
Awarded = 3,
Removed = 4,
}
#[derive(OctData, Clone, Debug, PartialEq, Eq)]
#[repr(i16)]
pub enum ReportType {
Fairy = 0,
Dialog = 1,
Task = 2,
DialogInFairy = 3,
}
#[derive(OctData, Copy, Clone, Debug, PartialEq, Eq)]
#[repr(u16)]
pub enum UIType {
Default = 0,
None = 1,
HollowQuest = 2,
Archive = 3,
}
#[derive(OctData, Clone, Debug)]
#[repr(i16)]
pub enum QuestState {
Unlocked = 0,
Ready = 10,
InProgress = 1,
ToFinish = 2,
Finished = 3,
}
#[derive(OctData, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum QuestStatisticsType {
ArrivedLevel = 1,
EventCount = 2,
CostTime = 3,
KilledEnemyCount = 4,
ArcanaCount = 5,
TarotCardCount = 6,
StaminaOverLevelTimes = 7,
RebornTimes = 8,
FinishedEventTypeCount = 9,
FinishedEventIDCount = 10,
}
#[derive(OctData, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u8)]
pub enum DungeonContentDropPoolType {
Card = 0,
BaneCard = 1,
Arcana = 2,
Blessing = 3,
Curse = 4,
Reward = 5,
HollowItem = 6,
}
#[derive(OctData, Clone, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum FairyState {
Unlock = 0,
Close = 1,
}
#[derive(OctData, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(i16)]
pub enum QuestType {
ArchiveFile = 1,
DungeonInner = 2,
Hollow = 3,
Manual = 4,
MainCity = 5,
HollowChallenge = 6,
ArchiveBattle = 7,
Knowledge = 8,
}
#[derive(OctData, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u32)]
pub enum HallRefreshStatus {
Auto = 0,
True = 1,
False = 2,
}
#[derive(OctData, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(u32)]
pub enum PropertyType {
SpMaxDelta = 11503,
AtkDelta = 12103,
ElementAbnormalPowerBase = 31401,
BreakStunDeltaRl = 12205,
BreakStun = 122,
HpMaxBase = 11101,
AddedDamageRatioFire3 = 31603,
BreakStunDelta = 12203,
HpHealRatio1 = 30601,
CritBase = 20101,
CritRes = 202,
CurBuddyBattery = 320,
DefDelta = 13103,
AddedElementAccumulationRatio3 = 31303,
AddedDamageRatioPhysics = 315,
AddedDamageRatioPhysicsBattle = 1315,
ShieldMax = 113,
AddedDamageRatioPhysicsRl = 31505,
EnduranceBattle = 1301,
PenValue = 232,
DamageTakeRatio3 = 30803,
BreakStunRatioRl = 12204,
ShieldMaxRatioRl = 11304,
PenDeltaRl = 23205,
MaxIndividualFever = 118,
PenRatioBattle = 1231,
PenRatioRl = 23105,
AllDamageResistBattle = 1309,
HpMax = 111,
StunMax = 114,
AtkBase = 12101,
CritDmg = 211,
AddedElementAccumulationRatio = 313,
PenValueBase = 23201,
StunMaxBattle = 1114,
DefDeltaRl = 13105,
AddedDamageRatioIce1 = 31701,
HpMaxRatio = 11102,
AddedDamageRatio = 307,
FeverGetRatioRl = 31105,
ElementAbnormalPowerBattle = 1314,
DefBase = 13101,
BreakStunRatio = 12202,
SpRecover = 305,
Sp = 5,
AllDamageResist1 = 30901,
ArmorMaxRatioRl = 11204,
ElementMysteryBase = 31201,
AddedDamageRatio1 = 30701,
AddedDamageRatioEther1 = 31901,
SpGetRatio1 = 31001,
ElementMysteryDelta = 31203,
CritResDelta = 20203,
Crit = 201,
HpHealRatio3 = 30603,
AtkBattle = 1121,
MapHpreserveCurhp = 10330,
ArmorMaxDelta = 11203,
UspMaxBase = 11601,
ElementAbnormalPowerDelta = 31403,
DamageTakeRatio1 = 30801,
AddedDamageRatio3 = 30703,
DamageTakeRatioBattle = 1308,
CritDmgRes = 212,
ArmorMaxBase = 11201,
CritDmgResRl = 21205,
PenDeltaBattle = 1232,
AddedDamageRatioIceRl = 31705,
AddedDamageRatioElec = 318,
EnduranceBase = 30101,
SpGetRatio = 310,
AddedDamageRatioPhysics1 = 31501,
Atk = 121,
DefRatioRl = 13104,
Armor = 2,
AtkDeltaRl = 12105,
ArmorMaxRatio = 11202,
SpBattle = 1115,
CritDmgRl = 21105,
PenDelta = 23103,
UspMax = 116,
MapHpreserveMaxhp = 10320,
HpMaxBattle = 1111,
SpRecoverRatioRl = 30504,
FeverGetRatio3 = 31103,
HpHealRatioRl = 30605,
EnumCount = 100,
AddedDamageRatioFireRl = 31605,
AddedDamageRatioElec3 = 31803,
SpMaxDeltaRl = 11505,
CritDmgResBase = 21201,
SpGetRatioRl = 31005,
AddedDamageRatioElecRl = 31805,
ShieldMaxBattle = 1113,
AllDamageResist = 309,
EnduranceRatio = 30102,
EnduranceDeltaRl = 30105,
ElementAbnormalPower = 314,
EnduranceDelta = 30103,
HpHealRatio = 306,
ArmorMax = 112,
CritDmgResDelta = 21203,
StunMaxRatio = 11402,
AddedDamageRatioElecBattle = 1318,
ShieldMaxRatio = 11302,
AddedElementAccumulationRatioBattle = 1313,
SpRecoverRatio = 30502,
DamageTakeRatio = 308,
ShieldMaxDelta = 11303,
EnduranceRatioRl = 30104,
AddedDamageRatioIce3 = 31703,
CritResBattle = 1202,
AddedElementAccumulationRatioRl = 31305,
Stun = 4,
AddedDamageRatioEther3 = 31903,
AtkRatio = 12102,
FeverGetRatioBattle = 1311,
SpGetRatioBattle = 1310,
ElementMysteryBattle = 1312,
DamageTakeRatioRl = 30805,
StunMaxBase = 11401,
BreakStunBase = 12201,
FeverGetRatio = 311,
SpMax = 115,
HpMaxDeltaRl = 11105,
ActorMaxCurHP = 10350,
ShieldMaxBase = 11301,
MaxBuddyBattery = 321,
Hp = 1,
AtkRatioRl = 12104,
AddedDamageRatioFire1 = 31601,
ArmorMaxDeltaRl = 11205,
ArmorMaxBattle = 1112,
AddedDamageRatioPhysics3 = 31503,
Shield = 3,
CritBattle = 1201,
Usp = 6,
HpHealRatioBattle = 1306,
CritDelta = 20103,
AddedDamageRatioFire = 316,
SpRecoverDeltaRl = 30505,
AddedDamageRatioIce = 317,
HpMaxRatioRl = 11104,
DefBattle = 1131,
AddedDamageRatioRl = 30705,
DefRatio = 13102,
AddedDamageRatioEtherRl = 31905,
UspBattle = 1116,
Endurance = 301,
AddedDamageRatioEtherBattle = 1319,
AllDamageResistRl = 30905,
SpRecoverBase = 30501,
AtkTrans = 12109,
AddedDamageRatioFireBattle = 1316,
AllDamageResist3 = 30903,
CritResBase = 20201,
StunMaxRatioRl = 11404,
UspMaxDelta = 11603,
Pen = 231,
StunMaxDeltaRl = 11405,
UspMaxDeltaRl = 11605,
SpMaxBase = 11501,
ShieldMaxDeltaRl = 11305,
AddedDamageRatioBattle = 1307,
Def = 131,
AddedDamageRatioEther = 319,
MapHpreserveAbsolute = 10340,
PenValueDelta = 23203,
SpRecoverDelta = 30503,
IndividualFever = 8,
AtkTransBase = 12108,
FeverGetRatio1 = 31101,
AddedDamageRatioElec1 = 31801,
CritRl = 20105,
SpGetRatio3 = 31003,
CritResRl = 20205,
BreakStunBattle = 1122,
ElementAbnormalPowerRatio = 31402,
SpRecoverBattle = 1305,
CritDmgResBattle = 1212,
Dead = 99,
CritDmgBattle = 1211,
AddedElementAccumulationRatio1 = 31301,
StunMaxDelta = 11403,
HpMaxDelta = 11103,
AddedDamageRatioIceBattle = 1317,
CritDmgBase = 21101,
PenBase = 23101,
CritDmgDelta = 21103,
ElementMystery = 312,
}

View file

@ -0,0 +1,33 @@
use protocol_macros::polymorphic;
use qwer::{OctData, PropertyHashMap};
use crate::{player_info::EventInfo, InteractInfo};
#[derive(OctData, Clone, Debug)]
pub struct EventListenerInfo {
pub event_graph_id: i32,
pub events_to_trigger: Vec<String>,
}
polymorphic!(
EventGraphInfo [
events_info: PropertyHashMap<i32, EventInfo>,
specials: PropertyHashMap<String, u64>,
is_new: bool,
finished: bool,
list_specials: PropertyHashMap<String, Vec<u64>>,
]
Hollow {
fired_count: u8,
hollow_event_template_id: i32,
uid: u64,
is_created_by_gm: bool,
} = 3,
NPC {
sequence_of_group: u16,
section_list_events: PropertyHashMap<String, EventListenerInfo>,
interact_info: InteractInfo,
hide: bool,
} = 2,
Section { } = 1,
);

View file

@ -0,0 +1,78 @@
use protocol_macros::polymorphic;
use qwer::{OctData, PropertyHashMap};
use crate::PropertyType;
#[derive(OctData, Clone, Debug, PartialEq, Eq)]
pub struct PropertyKeyValue {
pub key: PropertyType,
pub value: i32,
}
polymorphic!(
ItemInfo [
uid: u64,
id: i32,
count: i32,
package: u16,
first_get_time: u64
]
Arcana {
affix_list: Vec<i32>,
dress_index: u8,
} = 33,
AvatarInfo {
star: u8,
exp: u32,
level: u8,
rank: u8,
unlocked_talent_num: u8,
talent_switch: Vec<bool>,
skills: PropertyHashMap<u8, u8>,
is_custom_by_dungeon: bool,
robot_id: i32
} = 3,
AvatarLevelUpMaterial {} = 12,
AvatarPiece {} = 4,
Bless {
remain_time: i32,
get_time: u64,
ban_character: Vec<i32>,
specials: PropertyHashMap<String, i32>,
slot: u8,
is_super_curse: bool,
} = 32,
Buddy {} = 8,
Consumable {} = 10,
Currency {} = 1,
Equip {
avatar_uid: u64,
avatar_dressed_index: u8,
rand_properties: Vec<PropertyKeyValue>,
star: u8,
exp: u32,
level: u8,
lock: u8,
base_rand_properties: Vec<PropertyKeyValue>,
rand_properties_lv: Vec<i32>,
} = 7,
EquipLevelUpMaterial { } = 14,
Gift { } = 51,
HollowItem { } = 15,
OptionalGift { } = 52,
Resource { } = 2,
TarotCard {
is_mute: bool,
specials: PropertyHashMap<String, i32>,
} = 31,
Useable { } = 11,
Weapon {
avatar_uid: u64,
star: u8,
exp: u32,
level: u8,
lock: u8,
refine_level: u8,
} = 5,
WeaponLevelUpMaterial { } = 13,
);

1119
crates/protocol/src/lib.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,887 @@
use std::collections::{HashMap, HashSet};
use qwer::{OctData, PropertyDoubleKeyHashMap, PropertyHashMap, PropertyHashSet};
use crate::action_info::ActionInfo;
use crate::dungeon_info::DungeonInfo;
use crate::event_graph_info::EventGraphInfo;
use crate::item_info::ItemInfo;
use crate::quest_info::QuestInfo;
use crate::scene_ext::{DungeonTableExt, SceneTableExt, SectionInfoExt};
use crate::scene_info::SceneInfo;
use crate::{
ActionState, AutoRecoveryInfo, EventState, FairyState, HollowBattleEventType, HollowQuestType,
InteractInfo, MailState, QuestType,
};
#[derive(OctData, Copy, Clone, Debug, Default)]
pub struct Vector3f {
pub x: f64,
pub y: f64,
pub z: f64,
}
#[derive(thiserror::Error, Debug)]
#[error("vector length mismatch, expected 3, got: {0}")]
pub struct VectorLengthError(usize);
impl TryFrom<Vec<f64>> for Vector3f {
type Error = VectorLengthError;
fn try_from(value: Vec<f64>) -> Result<Self, Self::Error> {
(value.len() == 3)
.then_some(Self {
x: value[0],
y: value[1],
z: value[2],
})
.ok_or(VectorLengthError(value.len()))
}
}
impl From<Vector3f> for Vec<f64> {
fn from(value: Vector3f) -> Self {
vec![value.x, value.y, value.z]
}
}
#[derive(OctData, Clone, Debug, Default)]
pub struct Transform {
pub position: Vector3f,
pub rotation: Vector3f,
}
#[derive(OctData, Clone, Debug)]
pub struct EventStackFrame {
pub action_info: ActionInfo,
pub action_id: i32,
}
#[derive(OctData, Clone, Debug)]
pub struct EventInfo {
pub id: i32,
pub cur_action_id: i32,
pub action_move_path: Vec<i32>,
pub state: EventState,
pub prev_state: EventState,
pub cur_action_info: ActionInfo,
pub cur_action_state: ActionState,
pub predicated_failed_actions: PropertyHashSet<i32>,
pub stack_frames: Vec<EventStackFrame>,
}
#[derive(OctData, Clone, Debug)]
pub struct ChoiceInfo {
pub id: i32,
pub hide_info: bool,
pub forbidden: bool,
}
#[derive(OctData, Clone, Debug)]
pub struct EventGraphsInfo {
pub event_graphs_info: PropertyHashMap<i32, EventGraphInfo>,
pub default_event_graph_id: i32,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object(u16, 0x01)]
#[root]
pub struct PlayerInfo {
#[tag = 1]
pub uid: Option<u64>,
#[tag = 2]
pub account_name: Option<String>,
#[tag = 3]
pub last_enter_world_timestamp: Option<u64>,
#[tag = 4]
pub items: Option<PropertyHashMap<u64, ItemInfo>>,
#[tag = 5]
pub dungeon_collection: Option<DungeonCollection>,
#[tag = 6]
#[server_only]
pub properties: Option<PropertyDoubleKeyHashMap<u64, u16, i32>>,
#[tag = 7]
pub scene_properties: Option<PropertyDoubleKeyHashMap<u64, u16, i32>>,
#[tag = 8]
pub quest_data: Option<QuestData>,
#[tag = 9]
pub joined_chat_rooms: Option<Vec<u64>>,
#[tag = 10]
pub scene_uid: Option<u64>,
#[tag = 11]
pub archive_info: Option<ArchiveInfo>,
#[tag = 12]
pub auto_recovery_info: Option<PropertyHashMap<i32, AutoRecoveryInfo>>,
#[tag = 13]
pub unlock_info: Option<UnlockInfo>,
#[tag = 14]
pub yorozuya_info: Option<YorozuyaInfo>,
#[tag = 15]
pub equip_gacha_info: Option<EquipGachaInfo>,
#[tag = 16]
pub beginner_procedure_info: Option<BeginnerProcedureInfo>,
#[tag = 17]
pub pos_in_main_city: Option<PlayerPosInMainCity>,
#[tag = 18]
pub fairy_info: Option<FairyInfo>,
#[tag = 19]
pub popup_window_info: Option<PopupWindowInfo>,
#[tag = 20]
pub tips_info: Option<TipsInfo>,
#[tag = 21]
pub main_city_quest_data: Option<MainCityQuestData>,
#[tag = 22]
pub embattles: Option<Embattles>,
#[tag = 23]
#[server_only]
pub day_change_info: Option<DayChangeInfo>,
#[tag = 24]
#[server_only]
pub npcs_info: Option<PlayerNPCsInfo>,
#[tag = 25]
#[server_only]
pub scripts_to_execute: Option<PropertyDoubleKeyHashMap<i32, i32, ToExecuteScriptInfo>>,
#[tag = 26]
#[server_only]
pub scripts_to_remove: Option<PropertyHashMap<i32, PropertyHashSet<i32>>>,
#[tag = 27]
pub last_leave_world_timestamp: Option<u64>,
#[tag = 28]
#[server_only]
pub muip_data: Option<MUIPData>,
#[tag = 29]
pub nick_name: Option<String>,
#[tag = 30]
pub ramen_data: Option<RamenData>,
#[tag = 31]
pub shop: Option<ShopsInfo>,
#[tag = 32]
pub vhs_store_data: Option<VHSStoreData>,
#[tag = 33]
#[server_only]
pub operation_mail_receive_info: Option<OperationMailReceiveInfo>,
#[tag = 34]
pub second_last_enter_world_timestamp: Option<u64>,
#[tag = 35]
pub login_times: Option<u32>,
#[tag = 36]
pub create_timestamp: Option<u64>,
#[tag = 37]
pub gender: Option<u8>,
#[tag = 38]
pub avatar_id: Option<u32>,
#[tag = 39]
pub prev_scene_uid: Option<u64>,
#[tag = 40]
pub register_cps: Option<String>,
#[tag = 41]
pub register_platform: Option<u32>,
#[tag = 42]
pub pay_info: Option<PayInfo>,
#[tag = 43]
#[server_only]
pub private_npcs: Option<PropertyHashMap<u64, NpcInfo>>,
#[tag = 44]
pub battle_event_info: Option<BattleEventInfo>,
#[tag = 45]
pub gm_data: Option<GMData>,
#[tag = 46]
#[server_only]
pub player_mail_ext_infos: Option<PlayerMailExtInfos>,
#[tag = 47]
#[server_only]
pub single_dungeon_group: Option<SingleDungeonGroup>,
#[tag = 48]
pub newbie_info: Option<NewbieInfo>,
#[tag = 49]
pub loading_page_tips_info: Option<LoadingPageTipsInfo>,
#[tag = 50]
pub switch_of_story_mode: Option<bool>,
#[tag = 51]
pub switch_of_qte: Option<bool>,
#[tag = 52]
pub collect_map: Option<CollectMap>,
#[tag = 53]
pub areas_info: Option<AreasInfo>,
#[tag = 54]
pub bgm_info: Option<BGMInfo>,
#[tag = 55]
pub main_city_objects_state: Option<PropertyHashMap<i32, i32>>,
#[tag = 56]
pub hollow_info: Option<HollowInfo>,
#[tag = 57]
pub main_city_avatar_id: Option<u32>,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct DungeonCollection {
#[tag = 1]
pub dungeons: Option<PropertyHashMap<u64, DungeonInfo>>,
#[tag = 2]
pub scenes: Option<PropertyHashMap<u64, SceneInfo>>,
#[tag = 3]
pub default_scene_uid: Option<u64>,
#[tag = 4]
pub transform: Option<Transform>,
#[tag = 5]
pub used_story_mode: Option<bool>,
#[tag = 6]
pub used_manual_qte_mode: Option<bool>,
}
#[derive(OctData, Clone, Debug)]
pub struct BoundNPCAndInteractInfo {
pub is_bound_npc: bool,
pub interacts: PropertyHashSet<i32>,
pub npc_reference_uid: u64,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct QuestData {
#[tag = 1]
pub quests: Option<PropertyDoubleKeyHashMap<u64, i32, QuestInfo>>,
#[tag = 2]
pub world_quest_for_cur_dungeon: Option<i32>,
#[tag = 3]
pub world_quest_collection_uid: Option<u64>,
#[tag = 4]
#[server_only]
pub unlock_condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
#[tag = 5]
pub is_afk: Option<bool>,
#[tag = 6]
pub world_quest_for_cur_dungeon_afk: Option<i32>,
}
#[derive(OctData, Clone, Debug)]
pub struct VideotapeInfo {
pub star_count: PropertyHashMap<u8, u16>,
pub finished: bool,
pub awarded_star: PropertyHashMap<u8, HashSet<u16>>,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct ArchiveInfo {
#[tag = 1]
pub videotaps_info: Option<PropertyHashMap<i32, VideotapeInfo>>,
#[tag = 2]
pub hollow_archive_id: Option<PropertyHashSet<i32>>,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct UnlockInfo {
#[tag = 1]
pub unlocked_list: Option<PropertyHashSet<i32>>,
#[tag = 2]
#[server_only]
pub condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct YorozuyaInfo {
#[tag = 1]
pub last_refresh_timestamp_common: Option<u64>,
#[tag = 2]
pub yorozuya_level: Option<u32>,
#[tag = 3]
pub yorozuya_rank: Option<u32>,
#[tag = 4]
pub gm_quests: Option<PropertyHashMap<HollowQuestType, Vec<i32>>>,
#[tag = 5]
pub gm_enabled: Option<bool>,
#[tag = 6]
pub hollow_quests: Option<PropertyDoubleKeyHashMap<i32, HollowQuestType, PropertyHashSet<i32>>>,
#[tag = 7]
pub urgent_quests_queue: Option<PropertyHashMap<i32, Vec<i32>>>,
#[tag = 8]
pub last_refresh_timestamp_urgent: Option<u64>,
#[tag = 9]
pub next_refresh_timestamp_urgent: Option<u64>,
#[tag = 10]
pub finished_hollow_quest_count: Option<u32>,
#[tag = 11]
pub finished_hollow_quest_count_of_type: Option<PropertyHashMap<i16, u32>>,
#[tag = 12]
pub unlock_hollow_id: Option<Vec<i32>>,
#[tag = 13]
#[server_only]
pub unlock_hollow_id_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct EquipGachaInfo {
#[tag = 1]
pub smithy_level: Option<i32>,
#[tag = 2]
pub security_num_by_lv: Option<PropertyHashMap<i32, i32>>,
#[tag = 3]
#[server_only]
pub total_gacha_times: Option<i32>,
#[tag = 4]
#[server_only]
pub equip_star_up_times: Option<i32>,
#[tag = 5]
#[server_only]
pub avatar_level_advance_times: Option<i32>,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct BeginnerProcedureInfo {
#[tag = 1]
pub procedure_id: Option<i32>,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct PlayerPosInMainCity {
#[tag = 1]
pub position: Option<Vector3f>,
#[tag = 2]
pub rotation: Option<Vector3f>,
#[tag = 3]
pub initial_pos_id: Option<String>,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct FairyInfo {
#[tag = 1]
pub fairy_groups: Option<PropertyHashMap<i32, FairyState>>,
#[tag = 2]
#[server_only]
pub condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct PopupWindowInfo {
#[tag = 1]
pub popup_window_list: Option<Vec<i32>>,
#[tag = 2]
#[server_only]
pub condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct TipsInfo {
#[tag = 1]
pub tips_list: Option<Vec<i32>>,
#[tag = 2]
#[server_only]
pub tips_condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
#[tag = 3]
pub tips_group: Option<Vec<i32>>,
#[tag = 4]
#[server_only]
pub tips_group_condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct MainCityQuestData {
#[tag = 1]
pub exicing_finish_script_group: Option<Vec<i32>>,
#[tag = 2]
pub in_progress_quests: Option<Vec<i32>>,
}
#[derive(OctData, Clone, Debug)]
pub struct EmbattleInfo {
pub avatars: Vec<i32>,
pub buddy: i32,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct Embattles {
#[tag = 1]
pub last_embattles: Option<PropertyHashMap<QuestType, EmbattleInfo>>,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct DayChangeInfo {
#[tag = 1]
pub last_daily_refresh_timing: Option<u64>,
}
#[derive(OctData, Clone, Debug)]
pub struct PlayerNPCInfo {
pub interact_info: InteractInfo,
pub npc_uid: u64,
pub event_graphs_info: EventGraphsInfo,
pub npc_tag_id: i32,
pub vhs_trending_id: i32,
pub visible: bool,
pub invisible_by_quest: PropertyHashSet<i32>,
pub look_ik: bool,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct PlayerNPCsInfo {
#[tag = 1]
pub npcs_info: Option<PropertyHashMap<u64, PlayerNPCInfo>>,
#[tag = 2]
pub destroy_npc_when_leave_section: Option<PropertyHashSet<u64>>,
}
#[derive(OctData, Clone, Debug)]
pub struct ToExecuteScriptInfo {
pub remove_after_finish: bool,
pub specials: PropertyHashMap<String, i64>,
pub event_graphs: PropertyHashSet<i32>,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct MUIPData {
#[tag = 1]
pub ban_begin_time: Option<String>,
#[tag = 2]
pub ban_end_time: Option<String>,
#[tag = 3]
pub tag_value: Option<u64>,
#[tag = 4]
pub dungeon_enter_times: Option<PropertyHashMap<i32, i32>>,
#[tag = 5]
pub scene_enter_times: Option<PropertyHashMap<i32, i32>>,
#[tag = 6]
pub dungeon_pass_times: Option<PropertyHashMap<i32, i32>>,
#[tag = 7]
pub scene_pass_times: Option<PropertyHashMap<i32, i32>>,
#[tag = 8]
pub alread_cmd_uids: Option<PropertyHashSet<u64>>,
#[tag = 9]
pub game_total_time: Option<u64>,
#[tag = 10]
pub language_type: Option<u16>,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct RamenData {
#[tag = 1]
pub unlock_ramen: Option<PropertyHashSet<i32>>,
#[tag = 2]
pub cur_ramen: Option<i32>,
#[tag = 3]
pub used_times: Option<i32>,
#[tag = 4]
pub unlock_initiative_item: Option<PropertyHashSet<i32>>,
#[tag = 5]
#[server_only]
pub unlock_ramen_condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
#[tag = 6]
#[server_only]
pub unlock_item_condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
#[tag = 7]
pub has_mystical_spice: Option<bool>,
#[tag = 8]
#[server_only]
pub unlock_has_mystical_spice_condition_progress: Option<PropertyHashMap<i32, i32>>,
#[tag = 9]
pub cur_mystical_spice: Option<i32>,
#[tag = 10]
pub unlock_mystical_spice: Option<PropertyHashSet<i32>>,
#[tag = 11]
#[server_only]
pub unlock_mystical_spice_condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
#[tag = 12]
pub unlock_initiative_item_group: Option<PropertyHashSet<i32>>,
#[tag = 13]
pub hollow_item_history: Option<PropertyHashMap<i32, i32>>,
#[tag = 14]
pub initial_item_ability: Option<u64>,
#[tag = 15]
#[property_object(u8, 0x01)]
pub new_unlock_ramen: Option<Vec<i32>>,
#[tag = 16]
#[server_only]
pub eat_ramen_times: Option<i32>,
#[tag = 17]
#[server_only]
pub make_hollow_item_times: Option<i32>,
#[tag = 18]
pub new_unlock_initiative_item: Option<PropertyHashSet<i32>>,
}
#[derive(OctData, Clone, Debug)]
pub struct GoodsInfo {
pub id: i32,
pub purchased_num: u32,
pub last_refresh_time: u64,
pub discount: u16,
}
#[derive(OctData, Clone, Debug)]
pub struct ShelfInfo {
pub id: i32,
pub custom_goods_in_shelf: PropertyHashSet<i32>,
pub goods_info: PropertyHashMap<i32, GoodsInfo>,
}
#[derive(OctData, Clone, Debug)]
pub struct ShopInfo {
pub id: i32,
pub shelf_info: PropertyHashMap<i32, ShelfInfo>,
pub refreshed_count: i32,
pub last_refresh_time: u64,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct ShopsInfo {
#[tag = 1]
pub vip_level: Option<u8>,
#[tag = 2]
#[server_only]
pub shops: Option<PropertyHashMap<i32, ShopInfo>>,
#[tag = 3]
#[server_only]
pub shop_buy_times: Option<i32>,
}
#[derive(OctData, Clone, Debug)]
pub struct VHSTrendingInfo {
pub trend_id: i32,
pub state: u16,
pub match_level: u16,
pub is_accept: bool,
}
#[derive(OctData, Clone, Debug)]
pub struct VHSTrendingCfgInfo {
pub trend_id: i32,
pub complete_level: i16,
pub know_state: i16,
}
#[derive(OctData, Clone, Debug)]
pub struct VHSNpcInfo {
pub npc_id: i32,
pub state: i16,
pub new_know: bool,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct VHSStoreData {
#[tag = 1]
pub store_level: Option<i32>,
#[tag = 2]
pub unreceived_reward: Option<i32>,
#[tag = 3]
#[server_only]
pub hollow_enter_times: Option<i32>,
#[tag = 4]
pub last_receive_time: Option<i32>,
#[tag = 5]
#[property_object(u8, 0x01)]
pub vhs_collection_slot: Option<Vec<i32>>,
#[tag = 6]
pub unlock_vhs_collection: Option<PropertyHashSet<i32>>,
#[tag = 7]
#[server_only]
pub already_trending: Option<PropertyHashSet<i32>>,
#[tag = 8]
#[server_only]
pub unlock_trending_condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
#[tag = 9]
pub is_need_refresh: Option<bool>,
#[tag = 10]
#[server_only]
pub scripts_id: Option<PropertyHashSet<i32>>,
#[tag = 11]
pub store_exp: Option<i32>,
#[tag = 12]
pub is_level_chg_tips: Option<bool>,
#[tag = 13]
#[server_only]
pub vhs_hollow: Option<Vec<i32>>,
#[tag = 14]
#[server_only]
pub is_receive_trending_reward: Option<bool>,
#[tag = 15]
#[server_only]
pub is_need_first_trending: Option<bool>,
#[tag = 16]
#[server_only]
pub last_basic_script: Option<i32>,
#[tag = 17]
#[server_only]
pub is_complete_first_trending: Option<bool>,
#[tag = 18]
#[server_only]
pub last_basic_npc: Option<u64>,
#[tag = 19]
#[server_only]
pub can_random_trending: Option<PropertyHashSet<i32>>,
#[tag = 20]
#[property_object(u8, 0x01)]
pub vhs_trending_info: Option<Vec<VHSTrendingInfo>>,
#[tag = 21]
pub unlock_vhs_trending_info: Option<PropertyHashMap<i32, VHSTrendingCfgInfo>>,
#[tag = 22]
pub vhs_flow: Option<i32>,
#[tag = 23]
pub received_reward: Option<i32>,
#[tag = 24]
pub last_reward: Option<i32>,
#[tag = 25]
pub last_exp: Option<i32>,
#[tag = 26]
pub last_flow: Option<i32>,
#[tag = 27]
#[property_object(u8, 0x01)]
pub last_vhs_trending_info: Option<Vec<VHSTrendingInfo>>,
#[tag = 28]
#[property_object(u8, 0x01)]
pub new_know_trend: Option<Vec<i32>>,
#[tag = 29]
#[server_only]
pub quest_finish_script: Option<PropertyDoubleKeyHashMap<i32, i32, HashMap<String, u64>>>,
#[tag = 30]
#[server_only]
pub quest_finish_scripts_id: Option<PropertyHashSet<i32>>,
#[tag = 31]
#[server_only]
pub total_received_reward: Option<PropertyHashMap<i32, i32>>,
#[tag = 32]
#[property_object(u8, 0x01)]
pub last_vhs_npc_info: Option<Vec<VHSNpcInfo>>,
#[tag = 33]
#[server_only]
pub vhs_npc_info: Option<Vec<VHSNpcInfo>>,
#[tag = 34]
#[server_only]
pub npc_info: Option<PropertyHashSet<i32>>,
#[tag = 35]
#[server_only]
pub total_received_reward_times: Option<i32>,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct OperationMailReceiveInfo {
#[tag = 1]
pub receive_list: Option<PropertyHashSet<i32>>,
#[tag = 2]
pub condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct PayInfo {
#[tag = 1]
pub month_total_pay: Option<i32>,
}
#[derive(OctData, Clone, Debug)]
pub struct NpcSceneData {
pub section_id: i32,
pub transform: Transform,
}
#[derive(OctData, Clone, Debug)]
pub struct NpcInfo {
pub uid: u64,
pub id: i32,
pub tag_value: i32,
pub scene_uid: u64,
pub parent_uid: u64,
pub owner_uid: u64,
pub scene_data: NpcSceneData,
pub references: PropertyHashSet<u64>,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct BattleEventInfo {
#[tag = 1]
#[server_only]
pub unlock_battle: Option<PropertyHashSet<i32>>,
#[tag = 2]
#[server_only]
pub unlock_battle_condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
#[tag = 3]
#[server_only]
pub alread_rand_battle: Option<PropertyDoubleKeyHashMap<i32, i32, HashSet<i32>>>,
#[tag = 4]
pub rand_battle_type: Option<PropertyHashMap<i32, HollowBattleEventType>>,
#[tag = 5]
#[property_object(u8, 0x01)]
pub alread_battle_stage: Option<Vec<String>>,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct GMData {
#[tag = 1]
#[server_only]
pub condition_proress: Option<PropertyDoubleKeyHashMap<String, i32, i32>>,
#[tag = 2]
#[server_only]
pub completed_conditions: Option<PropertyHashSet<String>>,
#[tag = 3]
#[server_only]
pub register_conditions: Option<PropertyHashSet<String>>,
}
#[derive(OctData, Clone, Debug)]
pub struct PlayerMailExtInfo {
pub timestamp: u64,
pub mail_state: MailState,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct PlayerMailExtInfos {
#[tag = 1]
pub player_mail_ext_info: Option<PropertyHashMap<String, PlayerMailExtInfo>>,
}
#[derive(OctData, Clone, Debug)]
pub struct DungeonTable {
pub uid: u64,
pub id: i32,
pub begin_timestamp: u64,
pub dungeon_ext: DungeonTableExt,
pub to_be_destroyed: bool,
}
#[derive(OctData, Clone, Debug)]
pub struct SceneTable {
pub uid: u64,
pub id: i32,
pub begin_timestamp: u64,
pub scene_ext: SceneTableExt,
pub to_be_destroyed: bool,
}
#[derive(OctData, Clone, Debug)]
pub struct SectionInfo {
pub id: i32,
pub scene_uid: u64,
pub event_graphs_info: EventGraphsInfo,
pub section_info_ext: SectionInfoExt,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct SingleDungeonGroup {
#[tag = 1]
pub dungeons: Option<PropertyHashMap<u64, DungeonTable>>,
#[tag = 2]
pub scenes: Option<PropertyDoubleKeyHashMap<u64, u64, SceneTable>>,
#[tag = 3]
pub section: Option<PropertyDoubleKeyHashMap<u64, i32, SectionInfo>>,
#[tag = 4]
pub npcs: Option<PropertyDoubleKeyHashMap<u64, u64, NpcInfo>>,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct NewbieInfo {
#[tag = 1]
pub unlocked_id: Option<PropertyHashSet<i32>>,
#[tag = 2]
#[server_only]
pub condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct LoadingPageTipsInfo {
#[tag = 1]
pub unlocked_id: Option<PropertyHashSet<i32>>,
#[tag = 2]
#[server_only]
pub condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
}
#[derive(OctData, Clone, Debug, Default)]
#[property_object]
pub struct CollectMap {
#[tag = 1]
pub card_map: Option<PropertyHashSet<i32>>,
#[tag = 2]
pub curse_map: Option<PropertyHashSet<i32>>,
#[tag = 3]
pub event_icon_map: Option<PropertyHashSet<i32>>,
#[tag = 4]
#[server_only]
pub unlock_cards: Option<PropertyHashSet<i32>>,
#[tag = 5]
#[server_only]
pub unlock_card_condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
#[tag = 6]
#[server_only]
pub unlock_curses: Option<PropertyHashSet<i32>>,
#[tag = 7]
#[server_only]
pub unlock_curse_condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
#[tag = 8]
#[server_only]
pub unlock_events: Option<PropertyHashSet<i32>>,
#[tag = 9]
#[server_only]
pub unlock_event_condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
#[tag = 10]
#[server_only]
pub unlock_event_icons: Option<PropertyHashSet<i32>>,
#[tag = 11]
#[server_only]
pub unlock_event_icon_condition_progress: Option<PropertyDoubleKeyHashMap<i32, i32, i32>>,
#[tag = 12]
pub new_card_map: Option<PropertyHashSet<i32>>,
#[tag = 13]
pub new_curse_map: Option<PropertyHashSet<i32>>,
#[tag = 14]
pub new_event_icon_map: Option<PropertyHashSet<i32>>,
}
#[derive(OctData, Clone, Debug)]
pub struct AreaNPCInfo {
pub tag_id: i32,
pub interacts: PropertyHashSet<i32>,
}
#[derive(OctData, Clone, Debug)]
pub struct AreaOwnerInfo {
pub owner_type: u16,
pub owner_id: i32,
pub npcs: PropertyHashMap<u64, AreaNPCInfo>,
pub sequence: u32,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct AreasInfo {
#[tag = 1]
pub area_owners_info: Option<PropertyDoubleKeyHashMap<u16, i32, AreaOwnerInfo>>,
#[tag = 2]
pub sequence: Option<u32>,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct BGMInfo {
#[tag = 1]
pub bgm_id: Option<u32>,
}
#[derive(OctData, Clone, Debug)]
#[property_object]
pub struct HollowInfo {
#[tag = 1]
pub banned_hollow_event: Option<PropertyHashSet<i32>>,
}

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