Hi!
This commit is contained in:
commit
b63c7c0f0d
177 changed files with 99444 additions and 0 deletions
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
target/
|
||||
|
||||
/autopatch.toml
|
||||
/dispatch.toml
|
||||
/gateserver.toml
|
||||
/gameserver.toml
|
||||
/sdk_server.toml
|
||||
|
||||
*.proto
|
5090
Cargo.lock
generated
Normal file
5090
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
77
Cargo.toml
Normal file
77
Cargo.toml
Normal 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
52
README.md
Normal 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/)
|
BIN
autopatch/design_data/NAP_Publish_AppStore_1.4/1698061277.blk
Normal file
BIN
autopatch/design_data/NAP_Publish_AppStore_1.4/1698061277.blk
Normal file
Binary file not shown.
22
autopatch/design_data/NAP_Publish_AppStore_1.4/config.json
Normal file
22
autopatch/design_data/NAP_Publish_AppStore_1.4/config.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
{"3": {"serverPrivateKey": "30820152020100300d06092a864886f70d01010105000482013c30820138020100024077ccf288987058ad46a1ca2ed62ca700a6c7f5fdbbb80b6a15679cf916ebe86f49173c80b2a1f61bb88aa8f57ad2522d2e027f8ef715cb33de13d790d61e858f0203010001024007031bff1acd18a3abcab486e14a63cc09f71252f3b5e615238399108b62eb8be374051c522fbc93beec25806cdf300f9708b0c8713ef85732134952733cd181022100bad8cb93f1d04df5b0903823ecf3edd2799f51c2f759e373f18887711516d831022100a423b581c15958e6881e732ed3912ec7adb1f0974683795b643c9ac0af3189bf022023171c0488e16a02be4a178107bb37ead3548c726529c881b9d6be390a90e3c1022034546623ae1eec26e332c20a25cd5d9aaf97d15a895295383be6ce77abe321450220582b45c388a0254daeac667c4629c4d5d224c126353ffc2c3757fc5c6f97b1d1", "clientPublicKey": "30819f300d06092a864886f70d010101050003818d0030818902818100ae44280ad192e584abcf19bcf56ab719247fbb0e40243c06aaefad917562670385f07eb180beca3e2d8e5404c70a868d14b4c00f99e52d28e0d291c402a1da7ef5edce3e0687cb6f17603affcc81e5c793dceb33a3d511e5dee1b08e79b316f7ed052940b1d8a312acefd168849ef86795762e0498fb45ef6c6c4c1f18711d370203010001"}}
|
|
@ -0,0 +1 @@
|
|||
[{"sid":1,"serverName":"YanagiZS","ip": "127.0.0.1","port": 20501,"noticeRegion":"nap_dev_01","protocol":"kcp","rsaVer": 3,"clientSecretKey":"45633262100000000c577fdb4e6d2b22c852bf79b1e4303400080000a82f1ac8f61a5e41647fa266804999eb8cb6f45733e369a630988e09d0a5a7ca05c21af4a2d1e39b4a0eb912e37ecd5ae2e63581330df64b5b33aed7d21fb2ad3957fa4f6dc47220f50752b33829c909d98a2a073106579e643c65af6dcb66b8b083e145a8214c461c794cacaf74d7191ac952022f5d6d9ab685851f74356513efbef34599880aecac6498030398b08c7de138518462500e60cea2f21010d3debabc2bf70cfa75d5755eacafb6cea308b44dd617cee76d23103d25c88004086593f309342c728506deae679e7ac10797a2bb3794f054e6c50af3a794659de0a0883cbc61e1fd2176edae680de7a8e4cb35286fa610a661713066f3c159bb2da43632c7136441819d88bac5e5d5371b8b5aa032dd206e1de7b3d2ff019759e03743600247a28fbc8f87b073ce7d1a5c1c8ec984d72b9f494ca4764b7e918d0d15e9d0ad0123147d61d355ec0003e70a788c405ef0a137c3c951807cc71b2d6c277434e0f535810a35d73bf1d189544213c6d7d8f49899c43b2c31c241a99bf813dbb3f3e39943b4aa6febde6702730583cf2b0c471a49806c1f97c56f5320def6d1e52484469ae326eb6bcf5ebb9c788775ee547242989ec534e542b9e5781d4bcb6938b49d3f8b219dc33d5abd18344ff614f999c327d96cf4cdac50bf2eb38fe00ecab55cf8aa32dd9250e8d772ab10333f65c3140aa9ee2dd8942478f1ac9033ef39bf90162ae34a0e2800a1335a2e9ff918c21fbf5ca8cb9c059d69b01835e3b16b6264c3e607b8791e3c2b28db2d7cd381a6a05123dcd56bd17998cbdf87078b4892e9a0f8b60d5c1f0e94ab059220f7b16cea61c2e256fd8bb8e0323eec9485e18d78d1b7dbe3164962690afb48d19cccb85dcf86a2c1adb47b6245951239f7ae0c3ce61b48ca9e578ec754a6f6c767a7a37406c29c193a007ecf7049c438acbce8adc42812b14bbe28f03f2c8a3671511e323ce22b49de966318d8c8ab93cdb168bf4eb6d41973db5e3f1e96a06861447a8230d9a5f71d91426b1c5fffb2ac1238b5ef2844f9ff542eefa6305e512a939a1157f714d040a86e9c202dfea2fad56cf1f8c76d3794648bf4df64fc47aef8fbfb0ada6eb08d98191b34ff342c17cae23e9fac70691a899b5cfa6dba83f8c571c763b0dd751ddfce273ac29700ed4608bb6b103120f3654244aa09376b446f1caeadaa91957c1ecb1fc1eaad6255d9d2707f73f80aa356631b4bd04341d4acd74310097d56476c849ad5809687a8eee7ed0706f41f79636632466a4e0c32f2c55745aa0206594c7764959b08ed2ddf75c22f660f127a83202f9def78b0fc7df001d7310086d985542672f6a6c9b27369abc62c511371ad1c97f04c70097bda032229c863dbb68e999f68f02cc04fc2b5c5aa9e0ed14d54518a827b2d74a3deba998b881adae734c4850f8af0f092b31e7e6bbe22cef9d1fe07bc75c54a69915471def592b58cdbfea8e6a5221f325725532b016429e26b2bf9459e5eb4e7a1e604de1d0b9856eba8dacd4e7a08a6f78fa0418638a117244255e19a246aa4353029ed60609fc2b300f5f3d6fe101e039d05f01418e0e54acedef3aa658b5588f25787e412a187457a5e36f679f5cb9e89419df64bd0740b0f9649f9138cc682d966deb829f37e4db8019dbb089a05c4f18334aea0ce90ebab680a019399e7fcafd23589f5904ea3480821d0eb8dd807ec6ae26d12dacdb97cc62ca44d58cc4a7b92d387cee3902a621849ef7917fa0f508e4bc1351bbb782df846ece3f63babee7750fc1f99d83d5ab43b768335aa8bba39539af9016e49390b8ed0df269fabbaf6d314f33ba414233847a6f16b00a87eba259ec37455d5cae8779f10df4930f7abd92a78d0a13b444afa5411b8d7b1195355168514fe5e515304f825d9a6921b3b048fc37c1591795fbf3653877b3fd4dd87fa1e621c7262a7d6ef95f50c85056b8e95a62d269a725e6ba4b1c4bb6a32f9d32c049a401dba4fae4e4caacc1a6bc4c54c783b6c05d0fc2cc051259b57763438463deeb5fc0da4fa5dbfa78d52c37384fead8ece84115776f94f8023a56196917e4267de4d8322e6e0e254de05aef6c044f708449368d254e15c3b877d46b6f9002dd715496fc1d7bdc79a091b838ccad6cafae2aca06d6b0c06e344841903b0ac4199ff61b5a75e406e3fcfe2626398d647a6b78238b85e00ca40424510fabef1fb60f663e7f82fbc6defe653ff2ec4dd8feea30e990b190c4bcacab988b30c9cb42c22b7044f0e5131ef880b905e87e72a0a9e09311444a7accaf84fd9ac685447d59619ae9f46cb59bd15203d0c7cce519f6dec009470653c6a0e17da2accd4cc1f4efe90ce67f9b94a15fa63a5b64678c97c127ccc245a9f50da47713fab54dd63c7e64e135922fb6dd205ee702d9c540730688a8228ee00fc7eb9eacc366278af2c506e6777122dd93bcf9d90cf7855b4e7134b7f37a7eb4387fd2003430380c1758be7999136187fd7de80f9f726b3bb68be7987a8eaa04ddc73d9ff234b8db175ece2bb0cacd4acee8cebd88a36877e314ea86c18037564c5c0f25af0cce04f6cd3a8e389c7020cb97e0e5684b5cdfd6ce8e2b1166daeddca60a2db213631951a87e378fbe45dbce52771e16e15e1676b2f57605ab3160e73e6f9c83a357f85f99291fca5646aafe76ecd296e2d8979663427e2898d8f8f331b8999a2703064061ad07cab0cc9567fc0d64a45fdc8b71b7282a7114e8d5a76f59326d264b152caaa44fbb12a3171c56f5e5e6667ddb2b115185e26735fcea74300afe3a80c54222d11210273a410a2e137a9e1c21646544dc473b45a2ddeddbbdd649fa3b9c05e8549fb2d600cf17bdf3e4d6084a836a2d9317de5a60b"}]
|
Binary file not shown.
File diff suppressed because it is too large
Load diff
|
@ -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}]}
|
|
@ -0,0 +1 @@
|
|||
5010994
|
|
@ -0,0 +1 @@
|
|||
{"remoteParentDir":"","files":[{"remoteName":"Blocks/1688854445.blk","md5":"14618248753171219425","fileSize":26,"isPatch":false}]}
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1 @@
|
|||
5010994
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1 @@
|
|||
null
|
|
@ -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
1
autopatch/nap_test.txt
Normal file
|
@ -0,0 +1 @@
|
|||
nap_test
|
23
crates/autopatch-server/Cargo.toml
Normal file
23
crates/autopatch-server/Cargo.toml
Normal 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
|
2
crates/autopatch-server/autopatch.toml
Normal file
2
crates/autopatch-server/autopatch.toml
Normal file
|
@ -0,0 +1,2 @@
|
|||
http_addr = "0.0.0.0:10000"
|
||||
serve_dir = "./autopatch"
|
38
crates/autopatch-server/src/main.rs
Normal file
38
crates/autopatch-server/src/main.rs
Normal 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
14
crates/common/Cargo.toml
Normal 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
|
21
crates/common/src/config/app.rs
Normal file
21
crates/common/src/config/app.rs
Normal 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,
|
||||
}
|
15
crates/common/src/config/encryption.rs
Normal file
15
crates/common/src/config/encryption.rs
Normal 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]>,
|
||||
}
|
22
crates/common/src/config/local_toml.rs
Normal file
22
crates/common/src/config/local_toml.rs
Normal 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,
|
||||
}
|
23
crates/common/src/config/mod.rs
Normal file
23
crates/common/src/config/mod.rs
Normal 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()))
|
||||
})
|
||||
}
|
26
crates/common/src/config/server_list.rs
Normal file
26
crates/common/src/config/server_list.rs
Normal 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]>,
|
||||
}
|
7
crates/common/src/db_const.rs
Normal file
7
crates/common/src/db_const.rs
Normal 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
13
crates/common/src/lib.rs
Normal 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 |___/ ");
|
||||
}
|
17
crates/common/src/logging.rs
Normal file
17
crates/common/src/logging.rs
Normal 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();
|
||||
}
|
15
crates/common/src/time_util.rs
Normal file
15
crates/common/src/time_util.rs
Normal file
|
@ -0,0 +1,15 @@
|
|||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub fn unix_timestamp() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs() as u64
|
||||
}
|
||||
|
||||
pub fn unix_timestamp_ms() -> u64 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_millis() as u64
|
||||
}
|
27
crates/dispatch-server/Cargo.toml
Normal file
27
crates/dispatch-server/Cargo.toml
Normal 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
|
3
crates/dispatch-server/dispatch.toml
Normal file
3
crates/dispatch-server/dispatch.toml
Normal 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"
|
155
crates/dispatch-server/src/data/gateway.rs
Normal file
155
crates/dispatch-server/src/data/gateway.rs
Normal 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,
|
||||
}
|
23
crates/dispatch-server/src/data/global.rs
Normal file
23
crates/dispatch-server/src/data/global.rs
Normal 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,
|
||||
}
|
5
crates/dispatch-server/src/data/mod.rs
Normal file
5
crates/dispatch-server/src/data/mod.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
mod gateway;
|
||||
mod global;
|
||||
|
||||
pub use gateway::*;
|
||||
pub use global::*;
|
205
crates/dispatch-server/src/http_handlers.rs
Normal file
205
crates/dispatch-server/src/http_handlers.rs
Normal 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(¶m.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(¶m.version)
|
||||
.map(|k| k.get(¶m.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(¶m.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(¶m.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))
|
||||
}
|
83
crates/dispatch-server/src/main.rs
Normal file
83
crates/dispatch-server/src/main.rs
Normal 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);
|
||||
}
|
38
crates/game-server/Cargo.toml
Normal file
38
crates/game-server/Cargo.toml
Normal 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
|
8
crates/game-server/gameserver.toml
Normal file
8
crates/game-server/gameserver.toml
Normal 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"
|
111
crates/game-server/src/database.rs
Normal file
111
crates/game-server/src/database.rs
Normal 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()
|
||||
}
|
186
crates/game-server/src/level/event_graph_runner.rs
Normal file
186
crates/game-server/src/level/event_graph_runner.rs
Normal 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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
139
crates/game-server/src/level/mod.rs
Normal file
139
crates/game-server/src/level/mod.rs
Normal 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(§ion_id)
|
||||
.unwrap();
|
||||
|
||||
event_graph_runner::trigger_group(
|
||||
session,
|
||||
§ion_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(§ion_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,
|
||||
§ion_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,
|
||||
);
|
||||
}
|
||||
}
|
149
crates/game-server/src/main.rs
Normal file
149
crates/game-server/src/main.rs
Normal 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
|
||||
);
|
||||
}
|
||||
}
|
346
crates/game-server/src/player_util.rs
Normal file
346
crates/game-server/src/player_util.rs
Normal 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)
|
||||
}
|
81
crates/game-server/src/remote_config.rs
Normal file
81
crates/game-server/src/remote_config.rs
Normal 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 }
|
||||
}
|
31
crates/game-server/src/rpc_ptc/abyss.rs
Normal file
31
crates/game-server/src/rpc_ptc/abyss.rs
Normal 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(),
|
||||
})
|
||||
}
|
20
crates/game-server/src/rpc_ptc/activity.rs
Normal file
20
crates/game-server/src/rpc_ptc/activity.rs
Normal 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())
|
||||
}
|
9
crates/game-server/src/rpc_ptc/arcade.rs
Normal file
9
crates/game-server/src/rpc_ptc/arcade.rs
Normal 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())
|
||||
}
|
9
crates/game-server/src/rpc_ptc/babel_tower.rs
Normal file
9
crates/game-server/src/rpc_ptc/babel_tower.rs
Normal 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())
|
||||
}
|
9
crates/game-server/src/rpc_ptc/battle_pass.rs
Normal file
9
crates/game-server/src/rpc_ptc/battle_pass.rs
Normal 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())
|
||||
}
|
12
crates/game-server/src/rpc_ptc/camp_idle.rs
Normal file
12
crates/game-server/src/rpc_ptc/camp_idle.rs
Normal 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(),
|
||||
})
|
||||
}
|
14
crates/game-server/src/rpc_ptc/daily_challenge.rs
Normal file
14
crates/game-server/src/rpc_ptc/daily_challenge.rs
Normal 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()
|
||||
},
|
||||
})
|
||||
}
|
20
crates/game-server/src/rpc_ptc/embattles.rs
Normal file
20
crates/game-server/src/rpc_ptc/embattles.rs
Normal 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())
|
||||
}
|
12
crates/game-server/src/rpc_ptc/gacha.rs
Normal file
12
crates/game-server/src/rpc_ptc/gacha.rs
Normal 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()
|
||||
})
|
||||
}
|
9
crates/game-server/src/rpc_ptc/hadal_zone.rs
Normal file
9
crates/game-server/src/rpc_ptc/hadal_zone.rs
Normal 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())
|
||||
}
|
51
crates/game-server/src/rpc_ptc/interact.rs
Normal file
51
crates/game-server/src/rpc_ptc/interact.rs
Normal 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())
|
||||
}
|
72
crates/game-server/src/rpc_ptc/item.rs
Normal file
72
crates/game-server/src/rpc_ptc/item.rs
Normal 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())
|
||||
}
|
173
crates/game-server/src/rpc_ptc/main_city.rs
Normal file
173
crates/game-server/src/rpc_ptc/main_city.rs
Normal 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())
|
||||
}
|
184
crates/game-server/src/rpc_ptc/mod.rs
Normal file
184
crates/game-server/src/rpc_ptc/mod.rs
Normal 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;
|
||||
};
|
||||
}
|
109
crates/game-server/src/rpc_ptc/player.rs
Normal file
109
crates/game-server/src/rpc_ptc/player.rs
Normal 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}"),
|
||||
})
|
||||
}
|
80
crates/game-server/src/rpc_ptc/quest.rs
Normal file
80
crates/game-server/src/rpc_ptc/quest.rs
Normal 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())
|
||||
}
|
31
crates/game-server/src/rpc_ptc/shop.rs
Normal file
31
crates/game-server/src/rpc_ptc/shop.rs
Normal 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())
|
||||
}
|
25
crates/game-server/src/rpc_ptc/social.rs
Normal file
25
crates/game-server/src/rpc_ptc/social.rs
Normal 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())
|
||||
}
|
65
crates/game-server/src/rpc_ptc/unlock.rs
Normal file
65
crates/game-server/src/rpc_ptc/unlock.rs
Normal 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())
|
||||
}
|
12
crates/game-server/src/rpc_ptc/vhs_store.rs
Normal file
12
crates/game-server/src/rpc_ptc/vhs_store.rs
Normal 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(),
|
||||
})
|
||||
}
|
407
crates/game-server/src/rpc_ptc/world.rs
Normal file
407
crates/game-server/src/rpc_ptc/world.rs
Normal 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())
|
||||
}
|
104
crates/game-server/src/scene_section_util.rs
Normal file
104
crates/game-server/src/scene_section_util.rs
Normal 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, §ion_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()
|
||||
}
|
39
crates/gate-server/Cargo.toml
Normal file
39
crates/gate-server/Cargo.toml
Normal 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
|
8
crates/gate-server/gateserver.toml
Normal file
8
crates/gate-server/gateserver.toml
Normal 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"
|
14
crates/gate-server/kcp/Cargo.toml
Normal file
14
crates/gate-server/kcp/Cargo.toml
Normal 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"
|
56
crates/gate-server/kcp/src/error.rs
Normal file
56
crates/gate-server/kcp/src/error.rs
Normal 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)
|
||||
}
|
||||
}
|
1255
crates/gate-server/kcp/src/kcp.rs
Normal file
1255
crates/gate-server/kcp/src/kcp.rs
Normal file
File diff suppressed because it is too large
Load diff
17
crates/gate-server/kcp/src/lib.rs
Normal file
17
crates/gate-server/kcp/src/lib.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
extern crate bytes;
|
||||
#[macro_use]
|
||||
extern crate log;
|
||||
|
||||
mod error;
|
||||
mod kcp;
|
||||
|
||||
/// The `KCP` prelude
|
||||
pub mod prelude {
|
||||
pub use super::{get_conv, Kcp, KCP_OVERHEAD};
|
||||
}
|
||||
|
||||
pub use error::Error;
|
||||
pub use kcp::{get_conv, get_sn, get_token, set_conv, Kcp, KCP_OVERHEAD};
|
||||
|
||||
/// KCP result
|
||||
pub type KcpResult<T> = Result<T, Error>;
|
85
crates/gate-server/src/database.rs
Normal file
85
crates/gate-server/src/database.rs
Normal 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())
|
||||
}
|
||||
}
|
56
crates/gate-server/src/main.rs
Normal file
56
crates/gate-server/src/main.rs
Normal 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(())
|
||||
}
|
111
crates/gate-server/src/net/kcp_conn_mgr.rs
Normal file
111
crates/gate-server/src/net/kcp_conn_mgr.rs
Normal 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(())
|
||||
}
|
||||
}
|
3
crates/gate-server/src/net/mod.rs
Normal file
3
crates/gate-server/src/net/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod kcp_conn_mgr;
|
||||
pub mod packet_handler;
|
||||
pub mod packet_processor;
|
122
crates/gate-server/src/net/packet_handler.rs
Normal file
122
crates/gate-server/src/net/packet_handler.rs
Normal 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()
|
||||
},
|
||||
);
|
||||
}
|
93
crates/gate-server/src/net/packet_processor.rs
Normal file
93
crates/gate-server/src/net/packet_processor.rs
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
94
crates/gate-server/src/packet/control_packet.rs
Normal file
94
crates/gate-server/src/packet/control_packet.rs
Normal 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))
|
||||
}
|
||||
}
|
5
crates/gate-server/src/packet/mod.rs
Normal file
5
crates/gate-server/src/packet/mod.rs
Normal 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};
|
92
crates/gate-server/src/packet/net_packet.rs
Normal file
92
crates/gate-server/src/packet/net_packet.rs
Normal 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))
|
||||
}
|
||||
}
|
51
crates/gate-server/src/remote_config.rs
Normal file
51
crates/gate-server/src/remote_config.rs
Normal 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,
|
||||
}
|
||||
}
|
158
crates/gate-server/src/session.rs
Normal file
158
crates/gate-server/src/session.rs
Normal 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(())
|
||||
}
|
||||
}
|
158
crates/gate-server/src/udp_server.rs
Normal file
158
crates/gate-server/src/udp_server.rs
Normal 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);
|
||||
}
|
||||
}
|
9
crates/protocol/Cargo.toml
Normal file
9
crates/protocol/Cargo.toml
Normal 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" }
|
12
crates/protocol/protocol-macros/Cargo.toml
Normal file
12
crates/protocol/protocol-macros/Cargo.toml
Normal 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
|
150
crates/protocol/protocol-macros/src/lib.rs
Normal file
150
crates/protocol/protocol-macros/src/lib.rs
Normal 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()
|
||||
}
|
74
crates/protocol/src/action_info.rs
Normal file
74
crates/protocol/src/action_info.rs
Normal 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,
|
||||
);
|
63
crates/protocol/src/dungeon_info.rs
Normal file
63
crates/protocol/src/dungeon_info.rs
Normal 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,
|
||||
}
|
426
crates/protocol/src/enums.rs
Normal file
426
crates/protocol/src/enums.rs
Normal 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,
|
||||
}
|
33
crates/protocol/src/event_graph_info.rs
Normal file
33
crates/protocol/src/event_graph_info.rs
Normal 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,
|
||||
);
|
78
crates/protocol/src/item_info.rs
Normal file
78
crates/protocol/src/item_info.rs
Normal 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
1119
crates/protocol/src/lib.rs
Normal file
File diff suppressed because it is too large
Load diff
887
crates/protocol/src/player_info.rs
Normal file
887
crates/protocol/src/player_info.rs
Normal 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
Loading…
Reference in a new issue