This commit is contained in:
xeon 2025-02-21 14:03:43 +03:00
parent cbd0a34e09
commit 90e616bc49
210 changed files with 74998 additions and 2 deletions

12
.gitignore vendored Normal file
View file

@ -0,0 +1,12 @@
# Rust build directory
target/
nap.proto
# Service configs
dispatch.toml
gateserver.toml
gameserver.toml
hallserver.toml
battleserver.toml
muipserver.toml

BIN
1321691809.blk Normal file

Binary file not shown.

4504
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

78
Cargo.toml Normal file
View file

@ -0,0 +1,78 @@
[workspace]
members = ["crates/*", "crates/trigger-protobuf/trigger-protobuf-derive", "crates/trigger-database/migration", "crates/trigger-database/entity", "crates/trigger-fileconfig/blockfile"]
resolver = "2"
[workspace.package]
version = "0.0.1"
[workspace.dependencies]
tokio = { version = "1.43.0", features = ["full"] }
tokio-util = "0.7.13"
axum = "0.8.1"
futures = "0.3.31"
zeromq = { version = "0.4.1", features = ["tokio-runtime", "tcp-transport"] }
# Database
sea-orm = { version = "1.1.4", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros"] }
# ECS
bevy_app = { version = "0.15.1", default-features = false }
bevy_ecs = { version = "0.15.1", default-features = false }
bevy_derive = "0.15.1"
# Serialization
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.138"
toml = "0.8.19"
flatbuffers = "24.3.25"
flatc-rust = "0.2.0"
base64 = "0.22.1"
lz4_flex = "0.11.3"
# Debug
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
# Cryptography
rsa = { version = "0.9.7", features = ["sha2"] }
rand = { version = "0.8.5" }
rand_mt = "5.0.0"
aes = "0.8.4"
ecb = { version = "0.1.2", features = ["alloc"] }
# Util
paste = "1.0.15"
thiserror = "2.0.11"
dashmap = "6.1.0"
byteorder = "1.5.0"
num_enum = "0.7.3"
iter-read = "1.1.0"
atomic_enum = "0.3.0"
const_format = "0.2.34"
xxhash-rust = { version = "0.8.15", features = ["const_xxh64"] }
# Protobuf
prost = "0.13.4"
prost-types = "0.13.4"
prost-build = "0.13.4"
# Code generation
proc-macro2 = "1.0.93"
syn = "2.0.96"
quote = "1.0.38"
prettyplease = "0.2.29"
# Internal
trigger-sv = { path = "crates/trigger-sv" }
trigger-logic = { path = "crates/trigger-logic" }
trigger-cryptography = { path = "crates/trigger-cryptography" }
trigger-encoding = { path = "crates/trigger-encoding" }
trigger-codegen = { path = "crates/trigger-codegen" }
trigger-protobuf = { path = "crates/trigger-protobuf" }
trigger-protocol = { path = "crates/trigger-protocol" }
trigger-database = { path = "crates/trigger-database" }
trigger-fileconfig = { path = "crates/trigger-fileconfig" }

View file

@ -0,0 +1,13 @@
{
"GMStringList": [
"addavatar 0",
"addallweapon",
"addallequip",
"UnlockAllHollow",
"UnlockAllHollowBuff",
"UnlockAllCafeItem",
"finishquest 1 0",
"finishquest 3 0",
"addquest 5 4030136"
]
}

View file

@ -0,0 +1,32 @@
{
"blackItemList": [
{
"id": 2011,
"name": "男主"
},
{
"id": 2021,
"name": "玲"
},
{
"id": 50012,
"name": "ZYL的邦布"
},
{
"id": 50013,
"name": "爱心猫猫"
},
{
"id": 6011,
"name": "男主-推荐信"
},
{
"id": 6021,
"name": "玲-推荐信"
},
{
"id": 1011,
"name": "安比"
}
]
}

8399
ConfigScript/MainCity_1.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,53 @@
# trigger-rs
# Trigger-RS
A Set of Servers implemented for the game Zenless Zone Zero
## Introduction
Trigger-RS is a set of servers implemented for the game Zenless Zone Zero.
![Screenshot](screenshot.png)
## Current status
Currently trigger-rs provides these features:
- **Player item management or the inventory**: characters, equipment and currency.
- **Quest management**. Work-in-progress: currently all the quests are unlocked and finished from the beginning and can be replayed using The Archive and Hollow Deep Dive System.
- **Basic battle support**. The server is able to run the combat commissions (including Rally) and the training room.
- **Overworld scene logic**. The server implements the interactions and their systems for overworld (for example: ramen shop, coffee shop and music player)
- **Version-agnostic protocol library**. The server code is not bound to the specific protocol version. Instead, gate-server maps them to standardized structures and uses custom format for internal communication and processing.
- **Server management through MUIP API**.
## Getting started
### Requirements
- [Rust 1.85+](https://www.rust-lang.org/tools/install)
- [PostgreSQL](https://www.postgresql.org/download/)
- [SDK server](https://git.xeondev.com/reversedrooms/hoyo-sdk)
##### NOTE: this server doesn't include the sdk server as it's not specific per game. You can use `hoyo-sdk` with this server.
### Setup
##### a) building from sources
```sh
git clone https://git.xeondev.com/ObolSquad/trigger-rs.git
cd trigger-rs
cargo run --bin trigger-dispatch-server
cargo run --bin trigger-gate-server
cargo run --bin trigger-game-server
cargo run --bin trigger-hall-server
cargo run --bin trigger-battle-server
cargo run --bin trigger-muip-server
```
##### b) using pre-built binaries
Navigate to the [Releases](https://git.xeondev.com/obolsquad/trigger-rs/releases) page and download the latest release for your platform.
Start each service in order from option `a)`.
### Configuration
Most of the configuration (database, encryption keys) is stored in a shared environment configuration file (`environment.toml`). Some of server-specific options are stored in their respective configuration files (which are created upon first startup of each server).
### Logging in
To login to this server, you have to obtain a compatible game client. Currently supported one is `CNBetaWin1.6.0`, you can [get it here](https://git.xeondev.com/xeon/3/raw/branch/3/nap_beta_1.6_reversedrooms.torrent). Next, you have to apply the necessary [client patch](https://git.xeondev.com/ObolSquad/trigger-patch). It allows you to connect to the local server and replaces encryption keys with custom ones.
### Management
You can use the [trigger-muip-tool](https://git.xeondev.com/ObolSquad/trigger-muip-tool) to communicate with MUIP server and execute GM commands.
### 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)!

View file

@ -0,0 +1,20 @@
[package]
name = "trigger-battle-server"
edition = "2024"
version.workspace = true
[dependencies]
tokio.workspace = true
paste.workspace = true
serde.workspace = true
serde_json.workspace = true
dashmap.workspace = true
tracing.workspace = true
trigger-sv.workspace = true
trigger-logic.workspace = true
trigger-encoding.workspace = true
trigger-protocol.workspace = true
trigger-fileconfig.workspace = true

View file

@ -0,0 +1,3 @@
[node]
server_id = 0

View file

@ -0,0 +1,11 @@
use serde::Deserialize;
use trigger_sv::config::{ServerNodeConfiguration, TomlConfig};
#[derive(Deserialize)]
pub struct BattleServerConfig {
pub node: ServerNodeConfiguration,
}
impl TomlConfig for BattleServerConfig {
const DEFAULT_TOML: &str = include_str!("../battleserver.default.toml");
}

View file

@ -0,0 +1,48 @@
use trigger_protocol::DungeonEquipInfo;
pub struct AvatarUnit {
pub avatar_id: u32,
}
pub struct BuddyUnit {
pub buddy_id: u32,
pub buddy_type: i32,
}
pub struct Dungeon {
pub quest_id: u32,
pub avatar_list: Vec<AvatarUnit>,
pub buddy_list: Vec<BuddyUnit>,
pub inner_quests: Vec<u32>,
pub equip: DungeonEquipInfo,
}
impl Dungeon {
pub fn get_protocol_dungeon_info(&self) -> trigger_protocol::DungeonInfo {
use trigger_protocol::*;
DungeonInfo {
quest_id: self.quest_id,
dungeon_equip_info: Some(self.equip.clone()),
avatar_list: self
.avatar_list
.iter()
.map(|unit| AvatarUnitInfo {
avatar_id: unit.avatar_id,
})
.collect(),
buddy_list: self
.buddy_list
.iter()
.map(|unit| BuddyUnitInfo {
buddy_id: unit.buddy_id,
r#type: unit.buddy_type,
})
.collect(),
dungeon_quest_info: Some(DungeonQuestInfo {
inner_quest_id_list: self.inner_quests.clone(),
}),
..Default::default()
}
}
}

View file

@ -0,0 +1,148 @@
mod dungeon;
pub use dungeon::*;
pub mod scene;
use scene::{FightScene, RallyScene, Scene, ScenePerform};
use tracing::debug;
use trigger_fileconfig::NapFileCfg;
use trigger_logic::scene::ELocalPlayType;
use trigger_protocol::{EndBattleCsReq, EndBattleScRsp, FightSettle};
use trigger_sv::message::{GameStateCallback, GameStateData};
pub struct GameState {
#[expect(dead_code)]
filecfg: &'static NapFileCfg<'static>,
pub scene: Scene,
pub dungeon: Dungeon,
}
impl GameState {
pub fn new(filecfg: &'static NapFileCfg<'static>, data: &GameStateData) -> Option<Self> {
Some(match data {
GameStateData::Fight {
quest_id,
play_type,
buddy_id: _,
avatar_id_list,
dungeon_equip,
} => Self {
filecfg,
scene: Scene::Fight(FightScene {
event_id: Self::get_scene_event_id(filecfg, *quest_id, (*play_type).into()),
play_type: (*play_type).into(),
perform: ScenePerform {
time: String::from("Morning"),
weather: String::from("SunShine"),
},
}),
dungeon: Dungeon {
quest_id: *quest_id,
avatar_list: avatar_id_list
.iter()
.map(|&avatar_id| AvatarUnit { avatar_id })
.collect(),
buddy_list: vec![BuddyUnit {
buddy_type: 0,
buddy_id: 50001,
}],
inner_quests: vec![Self::get_scene_event_id(
filecfg,
*quest_id,
(*play_type).into(),
)],
equip: dungeon_equip.clone(),
},
},
GameStateData::Rally {
quest_id,
play_type,
buddy_id: _,
avatar_id_list,
dungeon_equip,
} => Self {
scene: Scene::Rally(RallyScene {
event_id: Self::get_scene_event_id(filecfg, *quest_id, (*play_type).into()),
perform: ScenePerform {
time: String::from("Morning"),
weather: String::from("SunShine"),
},
}),
dungeon: Dungeon {
quest_id: *quest_id,
avatar_list: avatar_id_list
.iter()
.map(|&avatar_id| AvatarUnit { avatar_id })
.collect(),
buddy_list: vec![BuddyUnit {
buddy_type: 0,
buddy_id: 50001,
}],
inner_quests: vec![Self::get_scene_event_id(
filecfg,
*quest_id,
(*play_type).into(),
)],
equip: dungeon_equip.clone(),
},
filecfg,
},
_ => return None,
})
}
fn get_scene_event_id(
filecfg: &NapFileCfg<'static>,
quest_id: u32,
play_type: ELocalPlayType,
) -> u32 {
match play_type {
ELocalPlayType::TrainingRoom => 19800014,
ELocalPlayType::ArchiveBattle => filecfg
.archive_battle_quest_template_tb
.data()
.unwrap()
.iter()
.find(|tmpl| tmpl.id() == quest_id as i32)
.map(|tmpl| tmpl.first_battle_event_id() as u32)
.unwrap_or(0),
_ => filecfg
.battle_group_config_template_tb
.data()
.unwrap()
.iter()
.find(|tmpl| tmpl.quest_id() == quest_id as i32)
.map(|tmpl| tmpl.battle_event_id() as u32)
.unwrap_or(0),
}
}
pub fn on_end_battle(
&self,
request_id: u32,
_request: EndBattleCsReq,
) -> Vec<GameStateCallback> {
debug!(
"the battle is over, quest_id: {}, event_id: {}",
self.dungeon.quest_id,
self.scene.get_event_id()
);
vec![
// TODO: battle rewards
GameStateCallback::PlayerItemsGiven {
changes: Vec::new(),
},
// TODO: FightSettle
GameStateCallback::ClientCmdProcessed {
ack_request_id: request_id,
response: Some(
EndBattleScRsp {
retcode: 0,
fight_settle: Some(FightSettle::default()),
}
.into(),
),
},
]
}
}

View file

@ -0,0 +1,31 @@
use trigger_logic::scene::ELocalPlayType;
use super::ScenePerform;
pub struct FightScene {
pub event_id: u32,
pub play_type: ELocalPlayType,
pub perform: ScenePerform,
}
impl FightScene {
pub fn get_protocol_scene_info(&self) -> trigger_protocol::SceneInfo {
use trigger_protocol::*;
SceneInfo {
scene_type: 3,
local_play_type: self.play_type.into(),
event_id: self.event_id, // or maybe it's actually scene_id ?
fight_scene_info: Some(FightSceneInfo {
level_perform_info: Some(LevelPerformInfo {
time: self.perform.time.clone(),
weather: self.perform.weather.clone(),
}),
level_reward_info: Some(LevelRewardInfo::default()),
perform_type: 0,
end_hollow: true,
}),
..Default::default()
}
}
}

View file

@ -0,0 +1,38 @@
mod fight;
mod rally;
pub use fight::FightScene;
pub use rally::RallyScene;
use trigger_logic::scene::ELocalPlayType;
pub struct ScenePerform {
pub time: String,
pub weather: String,
}
pub enum Scene {
Fight(FightScene),
Rally(RallyScene),
}
impl Scene {
pub fn get_protocol_scene_info(&self) -> trigger_protocol::SceneInfo {
match self {
Self::Fight(scene) => scene.get_protocol_scene_info(),
Self::Rally(scene) => scene.get_protocol_scene_info(),
}
}
pub fn get_event_id(&self) -> u32 {
match self {
Self::Fight(scene) => scene.event_id,
Self::Rally(scene) => scene.event_id,
}
}
pub fn get_play_type(&self) -> ELocalPlayType {
match self {
Self::Fight(scene) => scene.play_type,
Self::Rally(_) => ELocalPlayType::RallyLongFight,
}
}
}

View file

@ -0,0 +1,35 @@
use std::collections::HashMap;
use trigger_logic::scene::ELocalPlayType;
use super::ScenePerform;
pub struct RallyScene {
pub event_id: u32,
pub perform: ScenePerform,
}
impl RallyScene {
pub fn get_protocol_scene_info(&self) -> trigger_protocol::SceneInfo {
use trigger_protocol::*;
SceneInfo {
scene_type: 7,
local_play_type: ELocalPlayType::RallyLongFight.into(),
event_id: self.event_id, // or maybe it's actually scene_id ?
rally_scene_info: Some(RallySceneInfo {
level_perform_info: Some(LevelPerformInfo {
time: self.perform.time.clone(),
weather: self.perform.weather.clone(),
}),
level_reward_info: Some(LevelRewardInfo::default()),
cur_check_point: Some(HollowCheckPoint {
quest_cond_progress: Some(QuestCondProgress {
public_variables: HashMap::new(),
}),
}),
}),
..Default::default()
}
}
}

View file

@ -0,0 +1,88 @@
use std::sync::{LazyLock, OnceLock};
use config::BattleServerConfig;
use dashmap::DashMap;
use session::BattleSession;
use tokio::sync::Mutex;
use tracing::{error, info};
use trigger_fileconfig::{ArchiveFile, NapFileCfg};
use trigger_sv::{
config::{ServerEnvironmentConfiguration, TomlConfig},
die, logging,
net::{ServerNetworkManager, ServerType},
print_banner,
};
mod config;
mod logic;
mod server_message_handler;
mod session;
const BLK_ASSET_FILE: &str = "1321691809.blk";
const CONFIG_FILE: &str = "battleserver.toml";
const SERVER_TYPE: ServerType = ServerType::BattleServer;
struct AppState {
#[expect(unused)]
pub config: &'static BattleServerConfig,
pub filecfg: NapFileCfg<'static>,
pub network_mgr: ServerNetworkManager,
pub sessions: DashMap<u64, Mutex<BattleSession>>,
}
#[tokio::main]
async fn main() {
static APP_STATE: OnceLock<AppState> = OnceLock::new();
static DESIGN_DATA_BLK: OnceLock<ArchiveFile> = OnceLock::new();
static CONFIG: LazyLock<BattleServerConfig> =
LazyLock::new(|| BattleServerConfig::load_or_create(CONFIG_FILE));
print_banner();
logging::init_tracing(tracing::Level::DEBUG);
let environment = ServerEnvironmentConfiguration::load_from_toml("environment.toml")
.unwrap_or_else(|err| {
error!("{err}");
die();
});
let design_data_blk = trigger_fileconfig::read_archive_file(&mut ::std::io::Cursor::new(
&std::fs::read(BLK_ASSET_FILE).unwrap_or_else(|err| {
error!("failed to open design data blk file: {err}");
die();
}),
))
.expect("failed to unpack design data blk file");
let design_data_blk = DESIGN_DATA_BLK.get_or_init(|| design_data_blk);
let network_mgr =
ServerNetworkManager::new(SERVER_TYPE, CONFIG.node.server_id, &environment.servers);
let state = APP_STATE.get_or_init(|| AppState {
config: &CONFIG,
filecfg: NapFileCfg::new(design_data_blk),
network_mgr,
sessions: DashMap::new(),
});
state
.network_mgr
.start_listener(state, server_message_handler::handle_message)
.await
.inspect(|_| {
info!(
"successfully started service {:?}:{}",
SERVER_TYPE, CONFIG.node.server_id
)
})
.unwrap_or_else(|err| {
error!("failed to start network manager: {err}");
die();
})
.await // this will await for entirety of the ServerNetworkManager work (forever)
.unwrap_or_else(|err| {
error!("{err}");
die();
});
}

View file

@ -0,0 +1,166 @@
use tokio::sync::Mutex;
use tracing::{debug, error, warn};
use trigger_protocol::EnterSceneScNotify;
use trigger_sv::{
message::{
BindClientSessionMessage, BindClientSessionOkMessage, ChangeGameStateMessage,
ForwardClientProtocolMessage, GameStateCallback, GameStateCallbackMessage, Header,
UnbindClientSessionMessage, WithOpcode,
},
net::ServerType,
};
use crate::{logic::GameState, session::BattleSession, AppState};
pub async fn handle_message(state: &'static AppState, packet: trigger_sv::message::NetworkPacket) {
match packet.opcode {
BindClientSessionMessage::OPCODE => {
if let Some(message) = packet.get_message() {
on_bind_client_session(state, packet.header, message).await;
}
}
UnbindClientSessionMessage::OPCODE => {
if let Some(message) = packet.get_message() {
on_unbind_client_session(state, packet.header, message).await;
}
}
ChangeGameStateMessage::OPCODE => {
if let Some(message) = packet.get_message() {
on_change_game_state(state, packet.header, message).await;
}
}
ForwardClientProtocolMessage::OPCODE => {
if let Some(message) = packet.get_message() {
on_forward_client_message(state, packet.header, message).await;
}
}
opcode => warn!("unhandled server message, opcode: {opcode}"),
}
}
async fn on_change_game_state(
state: &'static AppState,
_header: Header,
message: ChangeGameStateMessage,
) {
let Some(session) = state.sessions.get(&message.session_id) else {
return;
};
let mut session = session.lock().await;
debug!(
"changing game state for player with uid {}, request: {:?}",
session.player_uid, message
);
let Some(game_state) = GameState::new(&state.filecfg, &message.data) else {
error!("unsupported game state data received: {:?}", message.data);
return;
};
debug!(
"created fight scene for player with uid {}: quest_id: {}, play_type: {:?}",
session.player_uid,
game_state.dungeon.quest_id,
game_state.scene.get_play_type()
);
let enter_scene_notify = EnterSceneScNotify {
scene_info: Some(game_state.scene.get_protocol_scene_info()),
dungeon_info: Some(game_state.dungeon.get_protocol_dungeon_info()),
};
session.game_state = Some(game_state);
state
.network_mgr
.send_to(
ServerType::GameServer,
0,
GameStateCallbackMessage {
session_id: session.id,
protocol_units: vec![enter_scene_notify.into()],
scene_save_data: None,
callback: GameStateCallback::Loaded,
},
)
.await;
}
async fn on_forward_client_message(
state: &'static AppState,
_header: Header,
message: ForwardClientProtocolMessage,
) {
let Some(session) = state.sessions.get(&message.session_id) else {
return;
};
let mut session = session.lock().await;
for callback in crate::session::message::handle_client_message(
&mut session,
message.request_id,
message.message,
) {
state
.network_mgr
.send_to(
ServerType::GameServer,
0,
GameStateCallbackMessage {
session_id: session.id,
protocol_units: Vec::new(),
scene_save_data: None,
callback,
},
)
.await;
}
}
async fn on_unbind_client_session(
state: &'static AppState,
_header: Header,
message: UnbindClientSessionMessage,
) {
if let Some((_, session)) = state.sessions.remove(&message.session_id) {
let session = session.lock().await;
debug!(
"unregistered session with id {} (player_uid: {})",
session.id, session.player_uid
);
}
}
async fn on_bind_client_session(
state: &'static AppState,
header: Header,
message: BindClientSessionMessage,
) {
state.sessions.insert(
message.session_id,
Mutex::new(BattleSession {
id: message.session_id,
player_uid: message.player_uid,
game_state: None,
}),
);
debug!(
"registered new session, id: {}, player_uid: {}",
message.session_id, message.player_uid
);
state
.network_mgr
.send_to(
header.sender_type.try_into().unwrap(),
header.sender_id,
BindClientSessionOkMessage {
session_id: message.session_id,
},
)
.await;
}

View file

@ -0,0 +1,30 @@
use trigger_encoding::Decodeable;
use trigger_protocol::{util::ProtocolUnit, ClientCmdID, EndBattleCsReq};
use trigger_sv::message::GameStateCallback;
use super::BattleSession;
pub fn handle_client_message(
session: &mut BattleSession,
request_id: u32,
message: ProtocolUnit,
) -> Vec<GameStateCallback> {
let mut callbacks = Vec::new();
match message.cmd_id {
EndBattleCsReq::CMD_ID if session.game_state.is_some() => {
if let Ok(message) = EndBattleCsReq::decode(&mut std::io::Cursor::new(&message.blob)) {
callbacks.extend(
session
.game_state
.as_ref()
.unwrap()
.on_end_battle(request_id, message),
);
}
}
_ => (),
}
callbacks
}

View file

@ -0,0 +1,9 @@
use crate::logic::GameState;
pub mod message;
pub struct BattleSession {
pub id: u64,
pub player_uid: u32,
pub game_state: Option<GameState>,
}

View file

@ -0,0 +1,19 @@
[package]
name = "trigger-dispatch-server"
edition = "2024"
version.workspace = true
[dependencies]
tokio.workspace = true
axum.workspace = true
serde.workspace = true
serde_json.workspace = true
base64.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
thiserror.workspace = true
trigger-sv.workspace = true
trigger-cryptography.workspace = true

View file

@ -0,0 +1,20 @@
[network]
http_addr = "127.0.0.1:10100"
[[region]]
name = "trigger_rs"
title = "Trigger-RS"
ping_url = "http://127.0.0.1:10100/ping"
dispatch_url = "http://127.0.0.1:10100/query_gateway"
biz = "nap_global"
env = 2
area = 2
[bound_server]
name = "trigger_rs"
title = "Trigger-RS"
seed = "95dedfc7cf06cdc2"
addr = "127.0.0.1"
port = 20501
is_kcp = false

View file

@ -0,0 +1,42 @@
use std::net::SocketAddr;
use serde::Deserialize;
use trigger_sv::config::TomlConfig;
#[derive(Deserialize)]
pub struct NetworkSetting {
pub http_addr: SocketAddr,
}
#[derive(Deserialize)]
pub struct RegionSetting {
pub name: String,
pub title: String,
pub ping_url: String,
pub dispatch_url: String,
pub biz: String,
pub env: u8,
pub area: u8,
}
#[derive(Deserialize)]
pub struct BoundRegionSetting {
pub name: String,
pub title: String,
pub addr: String,
pub port: u16,
pub is_kcp: bool,
pub seed: String,
}
#[derive(Deserialize)]
pub struct DispatchConfig {
pub network: NetworkSetting,
#[serde(rename = "region")]
pub regions: Vec<RegionSetting>,
pub bound_server: BoundRegionSetting,
}
impl TomlConfig for DispatchConfig {
const DEFAULT_TOML: &str = include_str!("../dispatch.default.toml");
}

View file

@ -0,0 +1,179 @@
use std::borrow::Cow;
use serde::Serialize;
#[derive(Serialize, Default)]
pub struct ServerDispatchData<'ec, 'sv, 'cdn> {
pub retcode: i32,
#[serde(skip_serializing_if = "String::is_empty")]
pub msg: String,
#[serde(skip_serializing_if = "str::is_empty")]
pub title: Cow<'sv, str>,
#[serde(skip_serializing_if = "str::is_empty")]
pub region_name: Cow<'sv, str>,
#[serde(skip_serializing_if = "str::is_empty")]
pub client_secret_key: Cow<'ec, str>,
#[serde(skip_serializing_if = "str::is_empty")]
pub cdn_check_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub gateway: Option<ServerGateway<'sv>>,
#[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<'cdn>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub region_ext: Option<RegionExtension>,
}
#[derive(Serialize)]
pub struct ServerGateway<'ip> {
pub ip: Cow<'ip, str>,
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<'s> {
pub game_res: CdnGameRes<'s>,
pub design_data: CdnDesignData<'s>,
pub silence_data: CdnSilenceData<'s>,
#[serde(skip_serializing_if = "Option::is_none")]
pub pre_download: Option<CdnGameRes<'s>>,
}
#[derive(Serialize)]
pub struct CdnGameRes<'s> {
pub base_url: Cow<'s, str>,
pub res_revision: Cow<'s, str>,
pub audio_revision: Cow<'s, str>,
pub branch: Cow<'s, str>,
pub md5_files: Cow<'s, str>, // Vec<VersionFileInfo> packed as string
}
#[derive(Serialize)]
pub struct CdnDesignData<'s> {
pub base_url: Cow<'s, str>,
pub data_revision: Cow<'s, str>,
pub md5_files: Cow<'s, str>, // Vec<VersionFileInfo> packed as string
}
#[derive(Serialize)]
pub struct CdnSilenceData<'s> {
pub base_url: Cow<'s, str>,
pub silence_revision: Cow<'s, str>,
pub md5_files: Cow<'s, str>, // 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,
}
#[derive(Serialize, Default)]
pub struct ServerListInfo<'s> {
pub area: u8,
pub biz: Cow<'s, str>,
pub dispatch_url: Cow<'s, str>,
pub env: u8,
pub is_recommend: bool,
pub name: Cow<'s, str>,
pub ping_url: Cow<'s, str>,
pub retcode: i32,
pub title: Cow<'s, str>,
}
#[derive(Serialize, Default)]
pub struct QueryDispatchRsp<'rl> {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub region_list: Vec<ServerListInfo<'rl>>,
pub retcode: i32,
#[serde(skip_serializing_if = "String::is_empty")]
pub msg: String,
}

View file

@ -0,0 +1,67 @@
use std::{
process::ExitCode,
sync::{LazyLock, OnceLock},
};
use axum::{routing::get, Router};
use config::DispatchConfig;
use tokio::net::TcpListener;
use tracing::error;
use trigger_sv::{
config::{ServerEnvironmentConfiguration, TomlConfig},
die, logging, print_banner,
};
mod config;
mod data;
mod ping;
mod query_dispatch;
mod query_gateway;
const CONFIG_FILE: &str = "dispatch.toml";
#[derive(Clone)]
struct AppState {
pub config: &'static DispatchConfig,
pub environment: &'static ServerEnvironmentConfiguration,
}
#[tokio::main]
async fn main() -> ExitCode {
static APP_STATE: OnceLock<AppState> = OnceLock::new();
static ENVIRONMENT: LazyLock<ServerEnvironmentConfiguration> = LazyLock::new(|| {
ServerEnvironmentConfiguration::load_from_toml("environment.toml").unwrap_or_else(|err| {
error!("{err}");
die();
})
});
static CONFIG: LazyLock<DispatchConfig> =
LazyLock::new(|| DispatchConfig::load_or_create(CONFIG_FILE));
print_banner();
logging::init_tracing(tracing::Level::DEBUG);
let state = APP_STATE.get_or_init(|| AppState {
config: &CONFIG,
environment: &ENVIRONMENT,
});
let app = Router::new()
.route(ping::ROUTE_ENDPOINT, get(ping::process))
.route(query_dispatch::ROUTE_ENDPOINT, get(query_dispatch::process))
.route(query_gateway::ROUTE_ENDPOINT, get(query_gateway::process))
.with_state(state);
let Ok(listener) = TcpListener::bind(CONFIG.network.http_addr).await.inspect_err(|err| {
error!("TcpListener::bind failed. Is another instance of the server already running? Error: {err}");
}) else {
die();
};
axum::serve(listener, app).await.unwrap_or_else(|err| {
error!("axum::serve failed: {err}");
die();
});
ExitCode::SUCCESS
}

View file

@ -0,0 +1,5 @@
pub const ROUTE_ENDPOINT: &str = "/ping";
pub async fn process() -> &'static str {
"success"
}

View file

@ -0,0 +1,32 @@
use axum::{extract::State, Json};
use crate::{
data::{QueryDispatchRsp, ServerListInfo},
AppState,
};
pub const ROUTE_ENDPOINT: &str = "/query_dispatch";
pub async fn process(State(state): State<&'static AppState>) -> Json<QueryDispatchRsp<'static>> {
use std::borrow::Cow::Borrowed;
Json(QueryDispatchRsp {
retcode: 0,
msg: String::with_capacity(0),
region_list: state
.config
.regions
.iter()
.map(|rs| ServerListInfo {
name: Borrowed(&rs.name),
title: Borrowed(&rs.title),
ping_url: Borrowed(&rs.ping_url),
dispatch_url: Borrowed(&rs.dispatch_url),
biz: Borrowed(&rs.biz),
area: rs.area,
env: rs.env,
..Default::default()
})
.collect(),
})
}

View file

@ -0,0 +1,186 @@
use axum::{
extract::{Query, State},
response::IntoResponse,
Json,
};
use base64::{display::Base64Display, engine::general_purpose::STANDARD, Engine};
use serde::{Deserialize, Serialize, Serializer};
use tracing::debug;
use trigger_cryptography::rsa;
use trigger_sv::config::RsaSetting;
use crate::{
data::{
CdnConfExt, CdnDesignData, CdnGameRes, CdnSilenceData, RegionExtension, RegionSwitchFunc,
ServerDispatchData, ServerGateway,
},
AppState,
};
pub const ROUTE_ENDPOINT: &str = "/query_gateway";
pub struct Response {
pub data: ServerDispatchData<'static, 'static, 'static>,
pub rsa: Option<&'static RsaSetting>,
}
#[derive(Debug, thiserror::Error)]
pub enum QueryGatewayError {
#[error("invalid rsa version specified: {0}")]
InvalidRsaVer(u32),
#[error("invalid dispatch seed, expected: {0}, got: {1}")]
InvalidDispatchSeed(&'static str, String),
}
impl QueryGatewayError {
pub fn retcode(&self) -> i32 {
match self {
Self::InvalidRsaVer(_) => 74,
Self::InvalidDispatchSeed(_, _) => 75,
}
}
pub fn message(&self) -> String {
String::with_capacity(0)
}
}
#[derive(Serialize)]
struct EncryptedAndSignedData {
#[serde(serialize_with = "to_base64")]
pub content: Vec<u8>,
#[serde(serialize_with = "to_base64")]
pub sign: Vec<u8>,
}
impl IntoResponse for Response {
fn into_response(self) -> axum::response::Response {
let Some(rsa) = self.rsa else {
return Json(self.data).into_response();
};
let json = serde_json::to_string(&self.data).unwrap();
Json(EncryptedAndSignedData {
content: rsa::encrypt(&rsa.client_public_key, json.as_bytes()),
sign: rsa::sign(&rsa.server_private_key, json.as_bytes()).into(),
})
.into_response()
}
}
#[derive(Deserialize)]
pub struct QueryGatewayParam {
pub rsa_ver: u32,
pub seed: String,
}
pub async fn process(
State(state): State<&'static AppState>,
Query(param): Query<QueryGatewayParam>,
) -> Response {
match internal_process(&state, param) {
Ok(response) => response,
Err((rsa, err)) => {
debug!("query_gateway failed: {err}");
Response {
rsa,
data: ServerDispatchData {
retcode: err.retcode(),
msg: err.message(),
..Default::default()
},
}
}
}
}
fn internal_process(
state: &'static AppState,
param: QueryGatewayParam,
) -> Result<Response, (Option<&'static RsaSetting>, QueryGatewayError)> {
use std::borrow::Cow::{Borrowed, Owned};
let rsa = state
.environment
.security
.get_rsa_setting_by_version(param.rsa_ver);
if rsa.is_none() {
return Err((None, QueryGatewayError::InvalidRsaVer(param.rsa_ver)));
};
(param.seed == state.config.bound_server.seed)
.then_some(())
.ok_or((
rsa,
QueryGatewayError::InvalidDispatchSeed(&state.config.bound_server.seed, param.seed),
))?;
let server = &state.config.bound_server;
Ok(Response {
rsa,
data: ServerDispatchData {
retcode: 0,
msg: String::with_capacity(0),
region_name: Borrowed(&server.name),
title: Borrowed(&server.title),
client_secret_key: Owned(base64::engine::general_purpose::STANDARD.encode(&state.environment.security.static_key.seed_buf)),
cdn_check_url: String::with_capacity(0),
gateway: Some(ServerGateway {
ip: Borrowed(&server.addr),
port: server.port,
}),
oaserver_url: String::new(),
force_update_url: String::new(),
stop_jump_url: String::new(),
cdn_conf_ext: Some(CdnConfExt {
// TODO: unhardcode this
design_data: CdnDesignData {
base_url: Borrowed("https://autopatchcn.juequling.com/design_data/beta_live/output_6567607_45953cc7be/client/"),
data_revision: Borrowed("6564324"),
md5_files: Borrowed(r#"[{"fileName": "data_version", "fileSize": 4312, "fileMD5": "7566724726121829372"}]"#),
},
game_res: CdnGameRes {
audio_revision: Borrowed("6549139"),
base_url: Borrowed("https://autopatchcn.juequling.com/game_res/beta_live/output_6567607_45953cc7be/client/"),
branch: Borrowed("beta_live"),
md5_files: Borrowed(r#"[{"fileName": "res_version", "fileSize": 2348948, "fileMD5": "3373372231805782958"}, {"fileName": "audio_version", "fileSize": 30433, "fileMD5": "9430105181932353351"}, {"fileName": "base_revision", "fileSize": 18, "fileMD5": "14955177794877370186"}]"#),
res_revision: Borrowed("6567607"),
},
silence_data: CdnSilenceData {
base_url: Borrowed("https://autopatchcn.juequling.com/design_data/beta_live/output_6567607_45953cc7be/client_silence/"),
md5_files: Borrowed(r#"[{"fileName": "silence_version", "fileSize": 467, "fileMD5": "10455524392801028815"}]"#),
silence_revision: Borrowed("6567607"),
},
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: server.is_kcp as i32,
..Default::default()
},
mtr_nap: String::new(),
mtr_sdk: String::new(),
pgc_webview_method: 1,
url_check_nap: String::new(),
url_check_sdk: String::new(),
}),
}
})
}
pub fn to_base64<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&Base64Display::new(bytes, &STANDARD))
}

View file

@ -0,0 +1,22 @@
[package]
name = "trigger-game-server"
edition = "2024"
version.workspace = true
[dependencies]
tokio.workspace = true
rand.workspace = true
serde.workspace = true
serde_json.workspace = true
tracing.workspace = true
dashmap.workspace = true
thiserror.workspace = true
trigger-sv.workspace = true
trigger-logic.workspace = true
trigger-codegen.workspace = true
trigger-encoding.workspace = true
trigger-protocol.workspace = true
trigger-database.workspace = true
trigger-fileconfig.workspace = true

View file

@ -0,0 +1,3 @@
[node]
server_id = 0

View file

@ -0,0 +1,33 @@
use serde::Deserialize;
use trigger_sv::{
config::{ServerNodeConfiguration, TomlConfig},
gm_command::GMCommand,
};
#[derive(Deserialize)]
pub struct GameServerConfig {
pub node: ServerNodeConfiguration,
}
impl TomlConfig for GameServerConfig {
const DEFAULT_TOML: &str = include_str!("../gameserver.default.toml");
}
#[derive(Debug, Deserialize)]
pub struct GMScript {
#[serde(rename = "GMStringList")]
pub commands: Vec<GMCommand>,
}
#[derive(Debug, Deserialize)]
pub struct BlackListItem {
pub id: i32,
#[expect(dead_code)]
pub name: String,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GMBlackList {
pub black_item_list: Vec<BlackListItem>,
}

View file

@ -0,0 +1,14 @@
pub const MALE_AVATAR_ID: u32 = 2011;
pub const FEMALE_AVATAR_ID: u32 = 2021;
pub fn is_player_avatar(id: u32) -> bool {
id == MALE_AVATAR_ID || id == FEMALE_AVATAR_ID
}
pub fn is_valid_talent_switch(v: &[bool]) -> bool {
v.len() == 6
&& v[0..3]
.iter()
.zip(v[3..6].iter())
.fold(true, |v, (&a, &b)| v && !(a && b))
}

View file

@ -0,0 +1,24 @@
pub trait BitSetExt {
fn is_bit_set(self, idx: usize) -> bool;
fn from_vec(v: Vec<bool>) -> Self;
fn to_vec(self, n: usize) -> Vec<bool>;
}
impl BitSetExt for i16 {
fn is_bit_set(self, idx: usize) -> bool {
(self as u16) & (1 << (idx as u16)) != 0
}
fn from_vec(v: Vec<bool>) -> Self {
v.into_iter()
.take(15)
.enumerate()
.fold(0i16, |v, (i, b)| v | (i16::from(b) << (i as i16)))
}
fn to_vec(self, n: usize) -> Vec<bool> {
(0..std::cmp::min(15, n))
.map(|idx| self.is_bit_set(idx))
.collect()
}
}

View file

@ -0,0 +1,42 @@
use trigger_protocol::*;
use super::NapPlayer;
pub fn build_dungeon_equip_info(player: &NapPlayer, avatar_id_list: &[u32]) -> DungeonEquipInfo {
let avatar_list = player.role_model.get_protocol_avatar_list(avatar_id_list);
let weapon_uid_list = avatar_list
.iter()
.map(|avatar| avatar.cur_weapon_uid as i32)
.filter(|uid| *uid != 0)
.collect::<Vec<i32>>();
let equip_uid_list = avatar_list
.iter()
.flat_map(|avatar| {
avatar
.dressed_equip_list
.iter()
.map(|equip| equip.equip_uid as i32)
})
.collect::<Vec<i32>>();
let weapon_list = (!weapon_uid_list.is_empty())
.then(|| {
player
.equip_model
.get_protocol_weapon_list(&weapon_uid_list)
})
.unwrap_or_default();
let equip_list = (!equip_uid_list.is_empty())
.then(|| player.equip_model.get_protocol_equip_list(&equip_uid_list))
.unwrap_or_default();
DungeonEquipInfo {
avatar_list,
weapon_list,
equip_list,
..Default::default()
}
}

View file

@ -0,0 +1,27 @@
use std::{collections::HashMap, sync::LazyLock};
use rand::RngCore;
static EQUIP_SLOT_PROPERTIES: LazyLock<HashMap<i32, Vec<u32>>> = LazyLock::new(|| {
HashMap::from([
(1, vec![11103]),
(2, vec![12103]),
(3, vec![13103]),
(4, vec![11102, 12102, 13102, 20103, 21103, 31203, 23103]),
(
5,
vec![11102, 12102, 13102, 31803, 31903, 31703, 31603, 31503],
),
(6, vec![11102, 12102, 13102, 31402, 12202, 30502]),
])
});
pub fn random_main_property(equip_type: i32) -> i64 {
let mut rng = rand::thread_rng();
let prop_types = EQUIP_SLOT_PROPERTIES.get(&equip_type).unwrap();
let key = prop_types[(rng.next_u32() as usize) % prop_types.len()] as i64;
let val = 400 + ((rng.next_u32() as i64) % 300);
(key << 32) | (val & 0xFFFF)
}

View file

@ -0,0 +1,405 @@
use tracing::{debug, warn};
use trigger_logic::quest::EQuestType;
use trigger_protocol::{util::ProtocolUnit, AvatarSync, CafeSync, ItemSync, PlayerSyncScNotify};
use trigger_sv::gm_command::GMCommand;
use crate::AppState;
use super::{player::AvatarPropertyChanges, NapPlayer};
pub struct CommandContext<'player> {
pub player: &'player mut NapPlayer,
pub state: &'static AppState,
notifies: Vec<ProtocolUnit>,
}
impl<'player> CommandContext<'player> {
pub fn new(player: &'player mut NapPlayer, state: &'static AppState) -> Self {
Self {
player,
state,
notifies: Vec::new(),
}
}
pub fn add_notify<Notify: Into<ProtocolUnit>>(&mut self, notify: Notify) {
self.notifies.push(notify.into());
}
pub fn remove_notifies(&mut self) -> Vec<ProtocolUnit> {
std::mem::take(&mut self.notifies)
}
}
pub async fn execute_command(context: &mut CommandContext<'_>, command: &GMCommand) {
debug!(
"executing {command:?} for player with uid {}",
context.player.player_uid()
);
match command {
GMCommand::AddAvatar { id } => gm_add_avatar(context, *id).await,
GMCommand::SetAvatarLevel { id, level } => {
gm_modify_avatar_properties(context, *id, Some(*level), None, None).await
}
GMCommand::SetAvatarRank { id, rank } => {
gm_modify_avatar_properties(context, *id, None, Some(*rank), None).await
}
GMCommand::SetAvatarTalent { id, talent } => {
gm_modify_avatar_properties(context, *id, None, None, Some(*talent)).await
}
GMCommand::AddAllWeapon => gm_add_all_weapon(context).await,
GMCommand::AddAllEquip => gm_add_all_equip(context).await,
GMCommand::AddEquip {
equip_id,
level,
star,
property_params,
} => gm_add_equip(context, *equip_id, *level, *star, property_params).await,
GMCommand::AddQuest {
quest_type,
quest_id,
} => gm_add_quest(context, *quest_type, *quest_id).await,
GMCommand::FinishQuest {
quest_type,
quest_id,
} => {
gm_finish_quest(context, *quest_type, *quest_id).await;
}
GMCommand::AddItemByType { .. } => {
warn!("AddItemByType is not implemented yet");
}
GMCommand::UnlockAllHollow => gm_unlock_all_hollow(context).await,
GMCommand::UnlockAllHollowBuff => gm_unlock_all_hollow_buff(context).await,
GMCommand::UnlockAllCafeItem => gm_unlock_all_cafe_item(context).await,
}
}
async fn gm_unlock_all_hollow(context: &mut CommandContext<'_>) {
context
.player
.yorozuya_model
.unlock_hollow(
&context
.state
.filecfg
.hollow_config_template_tb
.data()
.unwrap()
.iter()
.map(|tmpl| tmpl.id())
.collect::<Vec<_>>(),
)
.await;
context.add_notify(PlayerSyncScNotify {
// TODO: HollowData sync!
..Default::default()
});
}
async fn gm_unlock_all_cafe_item(context: &mut CommandContext<'_>) {
context
.player
.cafe_model
.unlock_cafe_item(
&context
.state
.filecfg
.cafe_config_template_tb
.data()
.unwrap()
.iter()
.map(|tmpl| tmpl.id())
.collect::<Vec<_>>(),
)
.await;
context.add_notify(PlayerSyncScNotify {
cafe_sync: Some(CafeSync {
cafe_data: Some(context.player.cafe_model.get_protocol_cafe_data()),
}),
..Default::default()
});
}
async fn gm_unlock_all_hollow_buff(context: &mut CommandContext<'_>) {
context
.player
.ramen_model
.unlock_ramen(
&context
.state
.filecfg
.hollow_buff_template_tb
.data()
.unwrap()
.iter()
.map(|tmpl| tmpl.id())
.collect::<Vec<_>>(),
)
.await;
context.add_notify(PlayerSyncScNotify {
ramen_sync: Some(context.player.ramen_model.get_protocol_ramen_sync(false)),
..Default::default()
});
}
async fn gm_add_quest(context: &mut CommandContext<'_>, quest_type: i32, quest_id: i32) {
if let Ok(ty) = EQuestType::try_from(quest_type) {
match ty {
EQuestType::ArchiveFile => {
context
.player
.main_story_model
.add_archive_files(&[quest_id])
.await;
}
EQuestType::Hollow => {
context
.player
.yorozuya_model
.add_hollow_quest(&[quest_id])
.await;
}
EQuestType::MainCity => {}
_ => {
warn!("quest type {quest_type:?} is not implemented yet");
return;
}
};
context.player.quest_model.add_quest(ty, quest_id).await;
} else {
warn!("gm_add_quest: invalid quest type: {quest_type}");
}
}
async fn gm_finish_quest(context: &mut CommandContext<'_>, quest_type: i32, quest_id: i32) {
if let Ok(quest_type) = EQuestType::try_from(quest_type) {
let quest_id_list = match quest_type {
EQuestType::ArchiveFile => {
let quest_id_list = if quest_id != 0 {
vec![quest_id]
} else {
context
.state
.filecfg
.archive_file_quest_template_tb
.data()
.unwrap()
.iter()
.map(|tmpl| tmpl.id())
.collect::<Vec<_>>()
};
context
.player
.main_story_model
.add_archive_files(&quest_id_list)
.await;
quest_id_list
}
EQuestType::Hollow => {
let quest_id_list = if quest_id != 0 {
vec![quest_id]
} else {
context
.state
.filecfg
.hollow_quest_template_tb
.data()
.unwrap()
.iter()
.map(|tmpl| tmpl.id())
.collect::<Vec<_>>()
};
context
.player
.yorozuya_model
.add_hollow_quest(&quest_id_list)
.await;
quest_id_list
}
_ => {
warn!("quest type {quest_type:?} is not implemented yet");
return;
}
};
context
.player
.quest_model
.finish_quests(quest_type, &quest_id_list)
.await;
} else {
warn!("gm_add_quest: invalid quest type: {quest_type}");
}
}
async fn gm_add_equip(
context: &mut CommandContext<'_>,
equip_id: i32,
level: i16,
star: i16,
params: &[i32],
) {
let equip_uid = context
.player
.equip_model
.add_custom_equip(equip_id, level, star, params)
.await;
context.add_notify(PlayerSyncScNotify {
item_sync: Some(ItemSync {
equip_list: context
.player
.equip_model
.get_protocol_equip_list(&[equip_uid]),
..Default::default()
}),
..Default::default()
});
}
async fn gm_add_all_equip(context: &mut CommandContext<'_>) {
let added_equip_uid_list = context
.player
.equip_model
.add_equip(
&context
.state
.filecfg
.equipment_template_tb
.data()
.unwrap()
.iter()
.map(|tmpl| tmpl.item_id())
.collect::<Vec<_>>(),
)
.await;
context.add_notify(PlayerSyncScNotify {
item_sync: Some(ItemSync {
equip_list: context
.player
.equip_model
.get_protocol_equip_list(&added_equip_uid_list),
..Default::default()
}),
..Default::default()
});
}
async fn gm_add_all_weapon(context: &mut CommandContext<'_>) {
let added_weapon_uid_list = context
.player
.equip_model
.add_weapon(
&context
.state
.filecfg
.weapon_template_tb
.data()
.unwrap()
.iter()
.map(|tmpl| tmpl.item_id())
.collect::<Vec<_>>(),
)
.await;
context.add_notify(PlayerSyncScNotify {
item_sync: Some(ItemSync {
weapon_list: context
.player
.equip_model
.get_protocol_weapon_list(&added_weapon_uid_list),
..Default::default()
}),
..Default::default()
});
}
async fn gm_modify_avatar_properties(
context: &mut CommandContext<'_>,
id: i32,
level: Option<i16>,
rank: Option<i16>,
talent_num: Option<i16>,
) {
if let Some(updated_avatars) = context
.player
.role_model
.change_avatar_properties(AvatarPropertyChanges {
avatar_id: id as u32,
level,
rank,
talent_num,
})
.await
{
context.add_notify(PlayerSyncScNotify {
avatar_sync: Some(AvatarSync {
avatar_list: context
.player
.role_model
.get_protocol_avatar_list(&updated_avatars),
}),
..Default::default()
});
}
}
async fn gm_add_avatar(context: &mut CommandContext<'_>, id: i32) {
if id == 0 {
let added_avatars = context
.player
.role_model
.unlock_avatars(
&context
.state
.filecfg
.avatar_base_template_tb
.data()
.unwrap()
.iter()
.map(|tmpl| tmpl.id())
.filter(|id| {
!context
.state
.gm_blacklist
.black_item_list
.iter()
.any(|item| item.id == *id)
})
.collect::<Vec<_>>(),
)
.await;
context.add_notify(PlayerSyncScNotify {
avatar_sync: Some(AvatarSync {
avatar_list: context
.player
.role_model
.get_protocol_avatar_list(&added_avatars),
}),
..Default::default()
});
} else {
context.player.role_model.unlock_avatars(&[id]).await;
context.add_notify(PlayerSyncScNotify {
avatar_sync: Some(AvatarSync {
avatar_list: context
.player
.role_model
.get_protocol_avatar_list(&[id as u32]),
}),
..Default::default()
});
}
}

View file

@ -0,0 +1,9 @@
pub mod avatar_util;
pub mod bit_util;
pub mod dungeon_util;
pub mod equip_util;
pub mod gm_util;
mod player;
pub mod scene_util;
pub use player::NapPlayer;

View file

@ -0,0 +1,120 @@
use super::NapContext;
use trigger_database::{entity::cafe_data, prelude::*};
use trigger_fileconfig::CafeConfigTemplate;
use trigger_sv::time_util;
pub struct CafeModel {
context: NapContext,
cafe_data: cafe_data::Model,
}
impl CafeModel {
pub async fn init(context: NapContext) -> Self {
let cafe_data = Self::load_or_create_cafe_data(&context).await;
Self { context, cafe_data }
}
pub async fn try_drink_cafe(
&mut self,
cafe_item_id: i32,
) -> Option<(i32, trigger_protocol::CafeSync)> {
if !self.cafe_data.cafe_item_list.contains(&cafe_item_id) {
return None;
}
let Some(config) = self.get_cafe_config(cafe_item_id) else {
return None;
};
// TODO: time check and limits?
let model = cafe_data::ActiveModel {
cur_cafe_item: Set(cafe_item_id),
last_drink_timestamp: Set(time_util::cur_timestamp_seconds()),
..self.cafe_data.clone().into()
};
self.cafe_data = model
.update(self.context.database)
.await
.expect("cafe_data::update failed");
Some((
config.energy_amount(),
trigger_protocol::CafeSync {
cafe_data: Some(self.get_protocol_cafe_data()),
},
))
}
pub async fn unlock_cafe_item(&mut self, cafe_item_id_list: &[i32]) {
let mut cafe_item_list = std::mem::take(&mut self.cafe_data.cafe_item_list);
cafe_item_id_list.iter().for_each(|id| {
if !cafe_item_list.contains(id) {
cafe_item_list.push(*id);
}
});
let model = cafe_data::ActiveModel {
cafe_item_list: Set(cafe_item_list),
..self.cafe_data.clone().into()
};
self.cafe_data = model
.update(self.context.database)
.await
.expect("cafe_data::update failed");
}
pub fn get_cafe_item_price(&self, cafe_item_id: i32) -> i32 {
self.get_cafe_config(cafe_item_id)
.map(|tmpl| tmpl.price())
.unwrap_or(0)
}
pub fn get_protocol_cafe_data(&self) -> trigger_protocol::CafeData {
trigger_protocol::CafeData {
cafe_item_list: self.cafe_data.cafe_item_list.clone(),
cur_cafe_item: self.cafe_data.cur_cafe_item,
today_drink_times: 0,
}
}
async fn load_or_create_cafe_data(context: &NapContext) -> cafe_data::Model {
let player_uid = context.player_uid as i32;
match cafe_data::Entity::find_by_id(player_uid)
.one(context.database)
.await
.expect("cafe_data::find_by_id failed")
{
Some(info) => info,
None => Self::create_default_cafe_data(context)
.insert(context.database)
.await
.expect("cafe_data::insert failed"),
}
}
fn get_cafe_config(&self, cafe_item_id: i32) -> Option<CafeConfigTemplate<'static>> {
self.context
.filecfg
.cafe_config_template_tb
.data()
.unwrap()
.iter()
.find(|tmpl| tmpl.id() == cafe_item_id)
}
fn create_default_cafe_data(context: &NapContext) -> cafe_data::ActiveModel {
cafe_data::Model {
player_uid: context.player_uid as i32,
cafe_item_list: Vec::new(),
cur_cafe_item: 0,
last_drink_timestamp: 0,
}
.into()
}
}

View file

@ -0,0 +1,278 @@
use std::collections::HashMap;
use trigger_database::{
entity::{equipment, player_item_uid, weapon},
prelude::*,
};
use crate::logic::equip_util;
use super::NapContext;
pub struct EquipModel {
context: NapContext,
weapons: HashMap<i32, weapon::Model>,
equipment: HashMap<i32, equipment::Model>,
}
impl EquipModel {
pub async fn init(context: NapContext) -> Self {
let weapons = Self::load_weapon_map(&context).await;
let equipment = Self::load_equipment(&context).await;
Self {
context,
weapons,
equipment,
}
}
pub fn weapon_exists(&self, uid: i32) -> bool {
self.weapons.contains_key(&uid)
}
pub fn equipment_exists(&self, uid: i32) -> bool {
self.equipment.contains_key(&uid)
}
pub fn get_protocol_weapon_list(&self, filter: &[i32]) -> Vec<trigger_protocol::Weapon> {
self.weapons
.values()
.filter(|weapon| filter.is_empty() || filter.contains(&weapon.weapon_uid))
.map(|weapon| trigger_protocol::Weapon {
id: weapon.weapon_id as u32,
uid: weapon.weapon_uid as u32,
level: weapon.level as u32,
exp: weapon.exp as u32,
star: weapon.star as u32,
refine_level: weapon.refine_level as u32,
lock: weapon.lock,
})
.collect()
}
pub fn get_protocol_equip_list(&self, filter: &[i32]) -> Vec<trigger_protocol::Equip> {
use trigger_protocol::*;
self.equipment
.values()
.filter(|equip| filter.is_empty() || filter.contains(&equip.equip_uid))
.map(|equip| Equip {
id: equip.equip_id as u32,
uid: equip.equip_uid as u32,
level: equip.level as u32,
exp: equip.exp as u32,
star: equip.star as u32,
lock: equip.lock,
propertys: equip
.properties
.iter()
.map(|&prop| EquipProperty {
key: (prop >> 32) as u32,
add_value: ((prop >> 16) & 0xFFFF) as u32,
base_value: (prop & 0xFFFF) as u32,
})
.collect(),
sub_propertys: equip
.sub_properties
.iter()
.map(|&prop| EquipProperty {
key: (prop >> 32) as u32,
add_value: ((prop >> 16) & 0xFFFF) as u32,
base_value: (prop & 0xFFFF) as u32,
})
.collect(),
})
.collect()
}
pub async fn add_custom_equip(
&mut self,
equip_id: i32,
level: i16,
star: i16,
property_params: &[i32],
) -> i32 {
let equip_uid = Self::next_item_uid(&self.context).await;
let mut params = property_params.into_iter();
let mut properties = Vec::new();
let mut sub_properties = Vec::new();
if let (Some(&key), Some(&add_value), Some(&base_value)) =
(params.next(), params.next(), params.next())
{
properties
.push(((key as i64) << 32) | ((add_value as i64) << 16) | (base_value as i64));
}
while let (Some(&key), Some(&add_value), Some(&base_value)) =
(params.next(), params.next(), params.next())
{
sub_properties
.push(((key as i64) << 32) | ((add_value as i64) << 16) | (base_value as i64));
}
let model = equipment::Model {
owner_player_uid: self.context.player_uid as i32,
equip_id,
equip_uid,
level,
exp: 0,
star,
lock: false,
properties,
sub_properties,
};
self.equipment.insert(
equip_uid,
equipment::ActiveModel::from(model)
.insert(self.context.database)
.await
.expect("equipment::insert failed"),
);
equip_uid
}
pub async fn add_equip(&mut self, equip_id_list: &[i32]) -> Vec<i32> {
let equipment_template_tb = self.context.filecfg.equipment_template_tb.data().unwrap();
let uids = Self::next_item_uids(&self.context, equip_id_list.len() as u32).await;
let (uids, models): (Vec<_>, Vec<_>) = equip_id_list
.iter()
.map(|id| {
equipment_template_tb
.iter()
.find(|tmpl| tmpl.item_id() == *id)
})
.flatten()
.zip(uids)
.map(|(tmpl, item_uid)| {
(
item_uid,
equipment::Model {
owner_player_uid: self.context.player_uid as i32,
equip_id: tmpl.item_id(),
equip_uid: item_uid,
level: 1,
exp: 0,
star: 1,
lock: false,
properties: vec![equip_util::random_main_property(tmpl.equipment_type())],
sub_properties: Vec::new(),
},
)
})
.unzip();
equipment::Entity::insert_many(models.iter().cloned().map(equipment::ActiveModel::from))
.exec(self.context.database)
.await
.expect("equipment::insert_many failed");
models.into_iter().for_each(|model| {
self.equipment.insert(model.equip_uid, model);
});
uids
}
pub async fn add_weapon(&mut self, weapon_id_list: &[i32]) -> Vec<i32> {
let weapon_template_tb = self.context.filecfg.weapon_template_tb.data().unwrap();
let uids = Self::next_item_uids(&self.context, weapon_id_list.len() as u32).await;
let (uids, models): (Vec<_>, Vec<_>) = weapon_id_list
.iter()
.map(|id| weapon_template_tb.iter().find(|tmpl| tmpl.item_id() == *id))
.flatten()
.zip(uids)
.map(|(tmpl, item_uid)| {
(
item_uid,
weapon::Model {
owner_player_uid: self.context.player_uid as i32,
weapon_uid: item_uid,
weapon_id: tmpl.item_id(),
level: 60,
exp: 0,
star: 1 + tmpl.star_limit() as i16,
refine_level: tmpl.refine_limit() as i16,
lock: false,
},
)
})
.unzip();
weapon::Entity::insert_many(models.iter().cloned().map(weapon::ActiveModel::from))
.exec(self.context.database)
.await
.expect("equipment::insert_many failed");
models.into_iter().for_each(|model| {
self.weapons.insert(model.weapon_uid, model);
});
uids
}
async fn load_equipment(context: &NapContext) -> HashMap<i32, equipment::Model> {
equipment::Entity::find()
.filter(equipment::Column::OwnerPlayerUid.eq(context.player_uid as i32))
.all(context.database)
.await
.expect("equipment::find(all) failed")
.into_iter()
.map(|equip| (equip.equip_uid, equip))
.collect()
}
async fn load_weapon_map(context: &NapContext) -> HashMap<i32, weapon::Model> {
weapon::Entity::find()
.filter(weapon::Column::OwnerPlayerUid.eq(context.player_uid as i32))
.all(context.database)
.await
.expect("weapon::find(all) failed")
.into_iter()
.map(|weapon| (weapon.weapon_uid, weapon))
.collect()
}
async fn next_item_uid(context: &NapContext) -> i32 {
Self::next_item_uids(context, 1).await.nth(0).unwrap()
}
async fn next_item_uids(context: &NapContext, count: u32) -> impl Iterator<Item = i32> {
let count = count as i32;
match player_item_uid::Entity::find_by_id(context.player_uid as i32)
.one(context.database)
.await
.expect("player_item_uid::find_by_id failed")
{
Some(model) => {
let last_item_uid = model.last_item_uid;
player_item_uid::ActiveModel {
last_item_uid: Set(last_item_uid + count),
..model.into()
}
.update(context.database)
.await
.expect("player_item_uid::update failed");
last_item_uid + 1..=last_item_uid + count
}
None => {
player_item_uid::ActiveModel {
player_uid: Set(context.player_uid as i32),
last_item_uid: Set(count),
}
.insert(context.database)
.await
.expect("player_item_uid::insert failed");
1..=count
}
}
}
}

View file

@ -0,0 +1,187 @@
use std::collections::HashMap;
use trigger_database::entity::{auto_recovery_data, material};
use trigger_database::prelude::*;
use trigger_logic::item::ItemStatic;
use super::NapContext;
pub struct ItemModel {
context: NapContext,
material_map: HashMap<u32, i32>,
auto_recovery_map: HashMap<u32, auto_recovery_data::Model>,
}
pub struct MaterialDelta {
pub id: u32,
pub num: i32,
}
impl ItemModel {
const MAX_ENERGY: i32 = 1000;
const MAX_AUTO_RECOVERY_ENERGY: i32 = 240;
const DEFAULT_MATERIALS: &[(ItemStatic, i32)] = &[
(ItemStatic::FrontendGold, 10_000_000),
(ItemStatic::GameDiamond, 10_000_000),
(ItemStatic::Energy, Self::MAX_AUTO_RECOVERY_ENERGY),
];
pub async fn init(context: NapContext) -> Self {
let material_map = match Self::load_material_map(&context).await {
map if map.len() == 0 => Self::create_default_material_map(&context).await,
map => map,
};
let auto_recovery_map = Self::load_auto_recovery_map(&context).await;
Self {
context,
material_map,
auto_recovery_map,
}
}
pub async fn add_energy(&mut self, amount: i32) {
let cur_energy = self
.material_map
.get(&ItemStatic::Energy.into())
.copied()
.unwrap_or(0);
let new_energy = std::cmp::min(cur_energy + amount, Self::MAX_ENERGY);
self.add_or_use_materials(&[MaterialDelta {
id: ItemStatic::Energy.into(),
num: new_energy - cur_energy,
}])
.await;
}
pub fn has_enough_material(&self, id: u32, amount: i32) -> bool {
self.material_map.get(&id).copied().unwrap_or(0) >= amount
}
pub fn has_enough_materials(&self, use_materials: &[MaterialDelta]) -> bool {
use_materials.iter().fold(true, |is_enough, delta| {
is_enough && self.has_enough_material(delta.id, -delta.num)
})
}
pub async fn use_material(&mut self, id: u32, amount: i32) -> bool {
self.add_or_use_materials(&[MaterialDelta { id, num: -amount }])
.await
}
pub async fn add_or_use_materials(&mut self, use_materials: &[MaterialDelta]) -> bool {
if !self.has_enough_materials(&use_materials) {
return false;
}
let updated_models = use_materials
.iter()
.filter(|delta| delta.num != 0)
.map(|delta| {
let num = *self
.material_map
.entry(delta.id)
.and_modify(|num| *num += delta.num)
.or_insert(delta.num);
material::ActiveModel {
owner_player_uid: Set(self.context.player_uid as i32),
id: Set(delta.id as i32),
num: Set(num),
}
})
.collect::<Vec<_>>();
self.context
.database
.transaction::<_, (), DbErr>(|txn| {
Box::pin(async move {
for model in updated_models {
model.update(txn).await?;
}
Ok(())
})
})
.await
.expect("use_materials: update transaction failed");
true
}
pub fn get_protocol_material_list(&self) -> Vec<trigger_protocol::Material> {
self.material_map
.iter()
.map(|(&id, &num)| trigger_protocol::Material { id, num })
.collect()
}
pub fn get_protocol_auto_recovery_info(
&self,
) -> HashMap<u32, trigger_protocol::AutoRecoveryInfo> {
self.auto_recovery_map
.iter()
.map(|(&id, data)| {
(
id,
trigger_protocol::AutoRecoveryInfo {
last_recovery_timestamp: data.last_recovery_timestamp,
buy_times: data.buy_times as u32,
},
)
})
.collect()
}
async fn load_material_map(context: &NapContext) -> HashMap<u32, i32> {
material::Entity::find()
.filter(material::Column::OwnerPlayerUid.eq(context.player_uid as i32))
.all(context.database)
.await
.expect("material::find(all) failed")
.into_iter()
.map(|material| (material.id as u32, material.num))
.collect()
}
async fn load_auto_recovery_map(
context: &NapContext,
) -> HashMap<u32, auto_recovery_data::Model> {
auto_recovery_data::Entity::find()
.filter(auto_recovery_data::Column::OwnerPlayerUid.eq(context.player_uid as i32))
.all(context.database)
.await
.expect("auto_recovery_data::find(all) failed")
.into_iter()
.map(|auto_recovery_data| (auto_recovery_data.id as u32, auto_recovery_data))
.collect()
}
async fn create_default_material_map(context: &NapContext) -> HashMap<u32, i32> {
let materials = Self::DEFAULT_MATERIALS
.into_iter()
.map(|(id, num)| {
material::Model {
owner_player_uid: context.player_uid as i32,
id: (*id).into(),
num: *num,
}
.into()
})
.collect::<Vec<material::ActiveModel>>();
material::Entity::insert_many(materials)
.exec(context.database)
.await
.expect("material::insert_many(default) failed");
Self::DEFAULT_MATERIALS
.into_iter()
.map(|(id, num)| ((*id).into(), *num))
.collect()
}
}

View file

@ -0,0 +1,129 @@
use std::collections::HashMap;
use trigger_database::{
entity::{archive_data, archive_videotape_data},
prelude::*,
};
use super::NapContext;
pub struct MainStoryModel {
context: NapContext,
archive_data: archive_data::Model,
vhs: HashMap<u32, archive_videotape_data::Model>,
}
impl MainStoryModel {
pub async fn init(context: NapContext) -> Self {
let archive_data = Self::load_archive_data(&context).await;
let vhs = Self::load_vhs(&context).await;
Self {
context,
archive_data,
vhs,
}
}
pub async fn add_archive_files(&mut self, archive_file_id_list: &[i32]) {
let mut hollow_archive_id_list =
std::mem::take(&mut self.archive_data.hollow_archive_id_list);
let models = archive_file_id_list
.iter()
.filter(|id| !self.vhs.contains_key(&(**id as u32)))
.map(|id| {
self.context
.filecfg
.archive_file_quest_template_tb
.data()
.unwrap()
.iter()
.find(|tmpl| tmpl.id() == *id)
})
.flatten()
.inspect(|tmpl| {
if !hollow_archive_id_list.contains(&tmpl.archive_id()) {
hollow_archive_id_list.push(tmpl.archive_id());
}
})
.map(|tmpl| archive_videotape_data::Model {
owner_player_uid: self.context.player_uid as i32,
archive_file_id: tmpl.id(),
})
.collect::<Vec<_>>();
models.iter().for_each(|videotape| {
self.vhs
.insert(videotape.archive_file_id as u32, videotape.clone());
});
archive_videotape_data::Entity::insert_many(
models
.into_iter()
.map(archive_videotape_data::ActiveModel::from)
.collect::<Vec<_>>(),
)
.exec(self.context.database)
.await
.expect("archive_videotape_data::insert_many failed");
self.archive_data = archive_data::ActiveModel {
owner_player_uid: Set(self.context.player_uid as i32),
hollow_archive_id_list: Set(hollow_archive_id_list),
}
.update(self.context.database)
.await
.expect("archive_data::update failed");
}
pub fn get_protocol_archive_data(&self) -> trigger_protocol::ArchiveData {
use trigger_protocol::*;
ArchiveData {
hollow_archive_id_list: self
.archive_data
.hollow_archive_id_list
.iter()
.map(|id| *id as u32)
.collect(),
videotaps_info: self
.vhs
.iter()
.map(|(_, videotape)| VideotapeInfo {
archive_file_id: videotape.archive_file_id as u32,
finished: true,
})
.collect(),
}
}
async fn load_archive_data(context: &NapContext) -> archive_data::Model {
if let Some(data) = archive_data::Entity::find_by_id(context.player_uid as i32)
.one(context.database)
.await
.expect("archive_data::find_by_id failed")
{
return data;
}
archive_data::Entity::insert(archive_data::ActiveModel {
owner_player_uid: Set(context.player_uid as i32),
hollow_archive_id_list: Set(Vec::new()),
})
.exec_with_returning(context.database)
.await
.expect("archive_data::insert failed")
}
async fn load_vhs(context: &NapContext) -> HashMap<u32, archive_videotape_data::Model> {
archive_videotape_data::Entity::find()
.filter(archive_videotape_data::Column::OwnerPlayerUid.eq(context.player_uid as i32))
.all(context.database)
.await
.expect("archive_videotape_data::find failed")
.into_iter()
.map(|videotape| (videotape.archive_file_id as u32, videotape))
.collect()
}
}

View file

@ -0,0 +1,144 @@
use cafe::CafeModel;
use equip::EquipModel;
use item::ItemModel;
use main_story::MainStoryModel;
use quest::QuestModel;
use ramen::RamenModel;
use role::RoleModel;
use scene::SceneModel;
use trigger_database::{entity::*, prelude::*, DatabaseConnection};
use trigger_fileconfig::NapFileCfg;
use trigger_logic::scene::ESceneType;
use trigger_protocol::PlayerBasicInfo;
use trigger_sv::message::GameStateData;
use yorozuya::YorozuyaModel;
mod cafe;
mod equip;
mod item;
mod main_story;
pub mod player_util;
mod quest;
mod ramen;
mod role;
mod scene;
mod yorozuya;
pub use role::AvatarPropertyChanges;
#[derive(Clone)]
pub struct NapContext {
pub database: &'static DatabaseConnection,
pub filecfg: &'static NapFileCfg<'static>,
pub player_uid: u32,
}
pub struct NapPlayer {
context: NapContext,
player_basic_info: player_basic_info::Model,
pub is_new_player: bool,
pub active_session_id: Option<u64>,
pub role_model: RoleModel,
pub item_model: ItemModel,
pub equip_model: EquipModel,
pub scene_model: SceneModel,
pub quest_model: QuestModel,
pub yorozuya_model: YorozuyaModel,
pub main_story_model: MainStoryModel,
pub ramen_model: RamenModel,
pub cafe_model: CafeModel,
}
impl NapPlayer {
pub async fn load(
player_uid: u32,
create_if_not_exists: bool,
database: &'static DatabaseConnection,
filecfg: &'static NapFileCfg<'static>,
) -> Option<Self> {
let context = NapContext {
database,
filecfg,
player_uid,
};
let Some((player_basic_info, is_new_player)) =
player_util::load_player_basic_info(database, player_uid, create_if_not_exists).await
else {
return None;
};
let role_model = RoleModel::init(context.clone()).await;
let item_model = ItemModel::init(context.clone()).await;
let equip_model = EquipModel::init(context.clone()).await;
let scene_model = SceneModel::init(context.clone()).await;
let quest_model = QuestModel::init(context.clone()).await;
let yorozuya_model = YorozuyaModel::init(context.clone()).await;
let main_story_model = MainStoryModel::init(context.clone()).await;
let ramen_model = RamenModel::init(context.clone()).await;
let cafe_model = CafeModel::init(context.clone()).await;
Some(Self {
active_session_id: None,
context,
player_basic_info,
is_new_player,
role_model,
item_model,
equip_model,
scene_model,
quest_model,
yorozuya_model,
main_story_model,
ramen_model,
cafe_model,
})
}
pub fn build_state_reentrant_data(&self, scene: &scene_info::Model) -> Option<GameStateData> {
match ESceneType::try_from(scene.scene_type).unwrap() {
ESceneType::Hall => Some(GameStateData::Hall {
player_avatar_id: self.player_basic_info.player_avatar_id as u32,
control_avatar_id: self.player_basic_info.control_avatar_id as u32,
ext: (!scene.ext.is_empty()).then(|| scene.ext.clone()),
}),
_ => None, // only hall reentrance is supported for now
}
}
pub async fn set_control_avatars(&mut self, player_avatar_id: u32, control_avatar_id: u32) {
let mut active_model: player_basic_info::ActiveModel =
self.player_basic_info.clone().into();
active_model.set(
player_basic_info::Column::PlayerAvatarId,
(player_avatar_id as i32).into(),
);
active_model.set(
player_basic_info::Column::ControlAvatarId,
(control_avatar_id as i32).into(),
);
self.player_basic_info = active_model
.update(self.context.database)
.await
.expect("player_basic_info::update failed");
}
pub fn get_protocol_player_basic_info(&self) -> PlayerBasicInfo {
PlayerBasicInfo {
nick_name: self.player_basic_info.nick_name.clone(),
level: self.player_basic_info.level as u32,
exp: self.player_basic_info.exp as u32,
avatar_id: self.player_basic_info.avatar_id as u32,
player_avatar_id: self.player_basic_info.player_avatar_id as u32,
control_avatar_id: self.player_basic_info.control_avatar_id as u32,
last_enter_world_timestamp: self.scene_model.last_enter_world_timestamp(),
}
}
pub fn player_uid(&self) -> u32 {
self.context.player_uid
}
}

View file

@ -0,0 +1,40 @@
use trigger_database::entity::*;
use trigger_database::prelude::*;
use trigger_database::DatabaseConnection;
pub async fn load_player_basic_info(
db: &DatabaseConnection,
player_uid: u32,
create_if_not_exists: bool,
) -> Option<(player_basic_info::Model, bool)> {
let player_uid = player_uid as i32;
match player_basic_info::Entity::find_by_id(player_uid)
.one(db)
.await
.expect("player_basic_info::find_by_id failed")
{
Some(info) => Some((info, false)),
None if create_if_not_exists => Some((
create_default_player_basic_info(player_uid)
.insert(db)
.await
.expect("player_basic_info::insert failed"),
true,
)),
None => None,
}
}
fn create_default_player_basic_info(player_uid: i32) -> player_basic_info::ActiveModel {
player_basic_info::Model {
player_uid,
nick_name: String::from("ReversedRooms"),
level: 60,
exp: 0,
avatar_id: 2021,
player_avatar_id: 2021,
control_avatar_id: 1361,
}
.into()
}

View file

@ -0,0 +1,208 @@
use std::collections::HashMap;
use trigger_database::{
entity::{quest_collection, quest_info},
prelude::*,
};
use trigger_logic::quest::EQuestType;
use trigger_sv::time_util;
use super::NapContext;
pub struct QuestModel {
context: NapContext,
quest_collections: HashMap<EQuestType, quest_collection::Model>,
quest_map: HashMap<EQuestType, Vec<quest_info::Model>>,
}
impl QuestModel {
pub async fn init(context: NapContext) -> Self {
let quest_collections = Self::load_quest_collections(&context).await;
let quest_map = Self::load_quests(&context).await;
Self {
context,
quest_collections,
quest_map,
}
}
#[expect(dead_code)]
pub async fn finish_quest(&mut self, quest_type: EQuestType, quest_id: i32) {
self.finish_quests(quest_type, &[quest_id]).await
}
pub async fn add_quest(&mut self, quest_type: EQuestType, quest_id: i32) {
if !self.quest_collections.contains_key(&quest_type) {
self.quest_collections.insert(
quest_type,
quest_collection::ActiveModel {
owner_player_uid: Set(self.context.player_uid as i32),
quest_type: Set(quest_type.into()),
finished_quest_id_list: Set(Vec::new()),
}
.insert(self.context.database)
.await
.expect("quest_collection::insert failed"),
);
}
if !self
.quest_map
.get(&quest_type)
.map(|list| list.iter().any(|quest| quest.quest_id == quest_id))
.unwrap_or(false)
{
self.quest_map.entry(quest_type).or_default().push(
quest_info::ActiveModel {
owner_player_uid: Set(self.context.player_uid as i32),
quest_type: Set(quest_type.into()),
quest_id: Set(quest_id),
unlock_time: Set(time_util::cur_timestamp_seconds()),
}
.insert(self.context.database)
.await
.expect("quest_info::insert failed"),
);
}
}
pub async fn finish_quests(&mut self, quest_type: EQuestType, quest_id_list: &[i32]) {
let (existed, mut collection) = self
.quest_collections
.remove(&quest_type)
.map(|qc| (true, qc))
.unwrap_or_else(|| {
(
false,
quest_collection::Model {
owner_player_uid: self.context.player_uid as i32,
quest_type: quest_type.into(),
finished_quest_id_list: Vec::with_capacity(quest_id_list.len()),
},
)
});
if let Some(list) = self.quest_map.get_mut(&quest_type) {
list.retain(|q| !quest_id_list.contains(&q.quest_id));
quest_info::Entity::delete_many()
.filter(quest_info::Column::QuestId.is_not_in(quest_id_list.iter().copied()))
.exec(self.context.database)
.await
.expect("quest_info::delete_many failed");
}
let mut finished_quest_id_list = std::mem::take(&mut collection.finished_quest_id_list);
for quest_id in quest_id_list {
if !finished_quest_id_list.contains(quest_id) {
finished_quest_id_list.push(*quest_id);
}
}
let model = quest_collection::ActiveModel {
finished_quest_id_list: Set(finished_quest_id_list),
..collection.into()
};
if existed {
self.quest_collections.insert(
quest_type,
model
.update(self.context.database)
.await
.expect("quest_collection::update failed"),
);
} else {
self.quest_collections.insert(
quest_type,
model
.insert(self.context.database)
.await
.expect("quest_collection::insert failed"),
);
}
}
pub fn get_protocol_quest_data(&self, query_quest_type: u32) -> trigger_protocol::QuestData {
use trigger_protocol::*;
match EQuestType::try_from(query_quest_type as i32) {
Ok(quest_type) => QuestData {
quest_collection_list: vec![self.get_protocol_quest_collection(quest_type)],
},
_ => QuestData {
quest_collection_list: {
let mut list = self
.quest_collections
.keys()
.map(|&ty| self.get_protocol_quest_collection(ty))
.collect::<Vec<_>>();
list.sort_by_key(|qc| qc.quest_type);
list
},
},
}
}
fn get_protocol_quest_collection(&self, ty: EQuestType) -> trigger_protocol::QuestCollection {
use trigger_protocol::*;
self.quest_collections
.get(&ty)
.map(|qc| QuestCollection {
quest_type: qc.quest_type as u32,
finished_quest_id_list: qc
.finished_quest_id_list
.iter()
.map(|id| *id as u32)
.collect(),
quest_list: self
.quest_map
.get(&ty)
.map(|list| {
list.iter()
.map(|quest| QuestInfo {
id: quest.quest_id as u32,
unlock_time: quest.unlock_time,
})
.collect()
})
.unwrap_or_default(),
})
.unwrap_or_default()
}
async fn load_quests(context: &NapContext) -> HashMap<EQuestType, Vec<quest_info::Model>> {
let mut map: HashMap<EQuestType, Vec<quest_info::Model>> = HashMap::new();
quest_info::Entity::find()
.filter(quest_info::Column::OwnerPlayerUid.eq(context.player_uid as i32))
.all(context.database)
.await
.expect("quest_info::find failed")
.into_iter()
.for_each(|quest| {
map.entry(EQuestType::try_from(quest.quest_type).unwrap())
.or_default()
.push(quest);
});
map
}
async fn load_quest_collections(
context: &NapContext,
) -> HashMap<EQuestType, quest_collection::Model> {
quest_collection::Entity::find()
.filter(quest_collection::Column::OwnerPlayerUid.eq(context.player_uid as i32))
.all(context.database)
.await
.expect("quest_collection::find failed")
.into_iter()
.map(|qc| (qc.quest_type.try_into().expect("invalid quest_type"), qc))
.collect()
}
}

View file

@ -0,0 +1,117 @@
use super::NapContext;
use trigger_database::entity::ramen_data;
use trigger_database::prelude::*;
pub struct RamenModel {
context: NapContext,
ramen_data: ramen_data::Model,
}
impl RamenModel {
pub async fn init(context: NapContext) -> Self {
let ramen_data = Self::load_or_create_ramen_data(&context).await;
Self {
context,
ramen_data,
}
}
pub async fn try_eat_ramen(&mut self, ramen: i32) -> Option<trigger_protocol::RamenSync> {
if !self.ramen_data.unlock_ramen_list.contains(&ramen) {
return None;
}
let model = ramen_data::ActiveModel {
cur_ramen: Set(ramen),
eat_ramen_times: Set(self.ramen_data.eat_ramen_times + 1),
..self.ramen_data.clone().into()
};
self.ramen_data = model
.update(self.context.database)
.await
.expect("ramen_data::update failed");
Some(self.get_protocol_ramen_sync(false))
}
pub async fn unlock_ramen(&mut self, unlock_id_list: &[i32]) {
let mut unlock_ramen_list = std::mem::take(&mut self.ramen_data.unlock_ramen_list);
unlock_id_list.iter().for_each(|id| {
if !unlock_ramen_list.contains(id) {
unlock_ramen_list.push(*id);
}
});
let model = ramen_data::ActiveModel {
unlock_ramen_list: Set(unlock_ramen_list),
..self.ramen_data.clone().into()
};
self.ramen_data = model
.update(self.context.database)
.await
.expect("ramen_data::update failed");
}
pub fn get_ramen_price(&self, ramen: i32) -> i32 {
self.context
.filecfg
.hollow_buff_template_tb
.data()
.unwrap()
.iter()
.find(|tmpl| tmpl.id() == ramen)
.map(|tmpl| tmpl.price())
.unwrap_or(0)
}
pub fn get_protocol_ramen_data(&self) -> trigger_protocol::RamenData {
trigger_protocol::RamenData {
cur_ramen: self.ramen_data.cur_ramen as u32,
eat_ramen_times: self.ramen_data.eat_ramen_times as u32,
unlock_ramen_list: self
.ramen_data
.unlock_ramen_list
.iter()
.map(|&id| id as u32)
.collect(),
}
}
pub fn get_protocol_ramen_sync(&self, full_update: bool) -> trigger_protocol::RamenSync {
trigger_protocol::RamenSync {
is_full_update: full_update,
cur_ramen: self.ramen_data.cur_ramen as u32,
eat_ramen_times: self.ramen_data.eat_ramen_times as u32,
}
}
async fn load_or_create_ramen_data(context: &NapContext) -> ramen_data::Model {
let player_uid = context.player_uid as i32;
match ramen_data::Entity::find_by_id(player_uid)
.one(context.database)
.await
.expect("ramen_data::find_by_id failed")
{
Some(info) => info,
None => Self::create_default_ramen_data(context)
.insert(context.database)
.await
.expect("ramen_data::insert failed"),
}
}
fn create_default_ramen_data(context: &NapContext) -> ramen_data::ActiveModel {
ramen_data::Model {
player_uid: context.player_uid as i32,
unlock_ramen_list: Vec::new(),
cur_ramen: 0,
eat_ramen_times: 0,
}
.into()
}
}

View file

@ -0,0 +1,444 @@
use std::collections::{HashMap, HashSet};
use trigger_database::entity::avatar;
use trigger_database::prelude::*;
use trigger_protocol::{Avatar, AvatarSkillLevel, DressedEquip};
use trigger_sv::time_util;
use crate::logic::{avatar_util, bit_util::BitSetExt};
use super::NapContext;
pub struct RoleModel {
context: NapContext,
avatar_map: HashMap<u32, avatar::Model>,
}
pub struct AvatarPropertyChanges {
pub avatar_id: u32,
pub level: Option<i16>,
pub rank: Option<i16>,
pub talent_num: Option<i16>,
}
impl RoleModel {
pub async fn init(context: NapContext) -> Self {
let avatar_map = Self::load_or_create_avatar_map(&context).await;
Self {
context,
avatar_map,
}
}
pub async fn weapon_dress(&mut self, avatar_id: u32, weapon_uid: i32) -> Option<[u32; 2]> {
if !self.avatar_map.contains_key(&avatar_id) {
return None;
}
let mut updated_avatars = [0xFFFFFFFF_u32; 2];
let prev_avatar = if let Some(prev_avatar) = (weapon_uid != 0)
.then(|| {
self.avatar_map
.values_mut()
.find(|avatar| avatar.cur_weapon_uid == weapon_uid)
})
.flatten()
{
prev_avatar.cur_weapon_uid = 0;
prev_avatar.avatar_id as u32
} else {
0
};
let avatar = self.avatar_map.remove(&avatar_id).unwrap();
let prev_weapon = avatar.cur_weapon_uid;
updated_avatars[0] = avatar.avatar_id as u32;
self.avatar_map.insert(
avatar.avatar_id as u32,
avatar::ActiveModel {
cur_weapon_uid: Set(weapon_uid),
..avatar.into()
}
.update(self.context.database)
.await
.expect("avatar::update failed"),
);
if prev_avatar != 0 && prev_weapon != 0 {
if let Some(avatar) = self.avatar_map.remove(&prev_avatar) {
updated_avatars[1] = avatar.avatar_id as u32;
self.avatar_map.insert(
avatar.avatar_id as u32,
avatar::ActiveModel {
cur_weapon_uid: Set(prev_weapon),
..avatar.into()
}
.update(self.context.database)
.await
.expect("avatar::update failed"),
);
}
}
Some(updated_avatars)
}
pub async fn dress_equipment(
&mut self,
avatar_id: u32,
params: &[(u32, u32)],
) -> Option<Vec<u32>> {
if !self.avatar_map.contains_key(&avatar_id) {
return None;
}
let mut updated_avatars = HashSet::new();
for &(equip_uid, _) in params.iter() {
if let Some(avatar_id) = (equip_uid != 0)
.then(|| {
self.avatar_map.values_mut().find(|avatar| {
avatar
.equip_slot_list
.iter()
.any(|slot| ((slot & 0xFFFFFFFF) as u32) == equip_uid)
})
})
.flatten()
.map(|avatar| avatar.avatar_id)
{
if let Some(mut avatar) = self.avatar_map.remove(&(avatar_id as u32)) {
updated_avatars.insert(avatar_id as u32);
avatar
.equip_slot_list
.retain(|slot| ((slot & 0xFFFFFFFF) as u32) != equip_uid);
self.avatar_map.insert(
avatar_id as u32,
avatar::ActiveModel {
equip_slot_list: Set(avatar.equip_slot_list.clone()),
..avatar.into()
}
.update(self.context.database)
.await
.expect("avatar::update failed"),
);
}
}
}
let mut avatar = self.avatar_map.remove(&avatar_id).unwrap();
updated_avatars.insert(avatar.avatar_id as u32);
for &(equip_uid, dress_index) in params.iter() {
let slot = ((dress_index as i64) << 32) | (equip_uid as i64);
if let Some(old_slot) = avatar
.equip_slot_list
.iter_mut()
.find(|slot| ((**slot >> 32) as u32) == dress_index)
{
*old_slot = slot;
} else {
avatar.equip_slot_list.push(slot);
}
}
self.avatar_map.insert(
avatar.avatar_id as u32,
avatar::ActiveModel {
equip_slot_list: Set(avatar.equip_slot_list.clone()),
..avatar.into()
}
.update(self.context.database)
.await
.expect("avatar::update failed"),
);
Some(updated_avatars.into_iter().collect())
}
pub async fn undress_equipment(&mut self, avatar_id: u32, undress_indexes: &[u32]) -> bool {
let Some(mut avatar) = self.avatar_map.remove(&avatar_id) else {
return false;
};
avatar
.equip_slot_list
.retain(|slot| !undress_indexes.contains(&((*slot >> 32) as u32)));
self.avatar_map.insert(
avatar_id,
avatar::ActiveModel {
equip_slot_list: Set(avatar.equip_slot_list.clone()),
..avatar.into()
}
.update(self.context.database)
.await
.expect("avatar::update failed"),
);
true
}
pub async fn talent_switch(&mut self, avatar_id: u32, talent_switch: Vec<bool>) -> bool {
if avatar_util::is_valid_talent_switch(&talent_switch) {
let required_talent_num = talent_switch
.iter()
.enumerate()
.filter(|(_, b)| **b)
.max_by_key(|(i, _)| *i)
.map(|(i, _)| (i + 1) as i16)
.unwrap_or(0);
if self
.avatar_map
.get(&avatar_id)
.map(|avatar| avatar.unlocked_talent_num >= required_talent_num)
.unwrap_or(false)
{
if let Some(avatar) = self.avatar_map.remove(&avatar_id) {
self.avatar_map.insert(
avatar.avatar_id as u32,
avatar::ActiveModel {
talent_switch: Set(BitSetExt::from_vec(talent_switch)),
..avatar.into()
}
.update(self.context.database)
.await
.expect("avatar::update failed"),
);
return true;
}
}
}
false
}
pub async fn change_avatar_properties(
&mut self,
changes: AvatarPropertyChanges,
) -> Option<Vec<u32>> {
if changes.avatar_id == 0 {
// applies to all
let (updated_models, updated_avatar_ids): (Vec<avatar::ActiveModel>, Vec<u32>) = self
.avatar_map
.iter_mut()
.map(|(id, avatar)| {
let mut model = avatar::ActiveModel::from(avatar.clone());
changes.level.inspect(|&level| {
avatar.level = level;
model.level = Set(level);
});
changes.rank.inspect(|&rank| {
avatar.rank = rank;
model.rank = Set(rank);
});
changes.talent_num.inspect(|&num| {
avatar.unlocked_talent_num = num;
model.unlocked_talent_num = Set(num)
});
(model, *id)
})
.unzip();
self.context
.database
.transaction::<_, (), DbErr>(|txn| {
Box::pin(async move {
for model in updated_models {
model.update(txn).await?;
}
Ok(())
})
})
.await
.expect("change_avatar_properties: update transaction failed");
Some(updated_avatar_ids)
} else {
if let Some(avatar) = self.avatar_map.remove(&changes.avatar_id) {
let model = avatar::ActiveModel {
level: changes.level.map(|level| Set(level)).unwrap_or_default(),
rank: changes.rank.map(|rank| Set(rank)).unwrap_or_default(),
unlocked_talent_num: changes.talent_num.map(|num| Set(num)).unwrap_or_default(),
..avatar.into()
};
self.avatar_map.insert(
changes.avatar_id,
model
.update(self.context.database)
.await
.expect("avatar::update failed"),
);
Some(vec![changes.avatar_id])
} else {
None
}
}
}
pub fn is_avatar_unlocked(&self, avatar_id: u32) -> bool {
self.avatar_map.contains_key(&avatar_id)
}
pub async fn unlock_avatars(&mut self, avatar_id_list: &[i32]) -> Vec<u32> {
let models = avatar_id_list
.iter()
.filter(|id| !self.is_avatar_unlocked(**id as u32))
.map(|id| {
self.context
.filecfg
.avatar_base_template_tb
.data()
.unwrap()
.iter()
.find(|tmpl| tmpl.id() == *id)
})
.flatten()
.map(|tmpl| avatar::Model {
owner_player_uid: self.context.player_uid as i32,
avatar_id: tmpl.id(),
level: 60,
exp: 0,
rank: 6,
passive_skill_level: 6,
skill_type_level: (0..=6).map(|_| 12).collect(),
unlocked_talent_num: 6,
talent_switch: 0b111000,
cur_weapon_uid: 0,
taken_rank_up_reward_list: Vec::with_capacity(0),
first_get_time: time_util::cur_timestamp_seconds(),
show_weapon_type: 0,
avatar_skin_id: 0,
equip_slot_list: Vec::with_capacity(0),
})
.collect::<Vec<_>>();
avatar::Entity::insert_many(
models
.iter()
.map(|model| model.clone().into())
.collect::<Vec<avatar::ActiveModel>>(),
)
.exec(self.context.database)
.await
.expect("avatar::insert_many failed");
let mut added_avatars = Vec::with_capacity(models.len());
for model in models {
added_avatars.push(model.avatar_id as u32);
self.avatar_map.insert(model.avatar_id as u32, model);
}
added_avatars
}
pub fn get_protocol_avatar_list(&self, filter: &[u32]) -> Vec<Avatar> {
self.avatar_map
.values()
.filter(|avatar| filter.is_empty() || filter.contains(&(avatar.avatar_id as u32)))
.map(|avatar| Avatar {
id: avatar.avatar_id as u32,
level: avatar.level as u32,
exp: avatar.exp as u32,
rank: avatar.rank as u32,
passive_skill_level: avatar.passive_skill_level as u32,
unlocked_talent_num: avatar.unlocked_talent_num as u32,
cur_weapon_uid: avatar.cur_weapon_uid as u32,
first_get_time: avatar.first_get_time,
show_weapon_type: avatar.show_weapon_type as i32,
avatar_skin_id: avatar.avatar_skin_id as u32,
talent_switch_list: avatar.talent_switch.to_vec(6),
taken_rank_up_reward_list: avatar
.taken_rank_up_reward_list
.iter()
.map(|&i| i as u32)
.collect(),
dressed_equip_list: avatar
.equip_slot_list
.iter()
.map(|&slot| DressedEquip {
index: (slot >> 32) as u32,
equip_uid: (slot & 0xFFFFFFFF) as u32,
})
.collect(),
skill_type_level: avatar
.skill_type_level
.iter()
.enumerate()
.map(|(st, level)| AvatarSkillLevel {
skill_type: st as u32,
level: *level as u32,
})
.collect(),
})
.collect()
}
async fn load_or_create_avatar_map(context: &NapContext) -> HashMap<u32, avatar::Model> {
let player_uid = context.player_uid as i32;
match avatar::Entity::find()
.filter(avatar::Column::OwnerPlayerUid.eq(player_uid))
.all(context.database)
.await
.expect("avatar::find(all) failed")
{
list if list.is_empty() => {
let initial_map = Self::create_initial_avatar_map(context);
avatar::Entity::insert_many(
initial_map
.values()
.cloned()
.map(avatar::ActiveModel::from)
.collect::<Vec<_>>(),
)
.exec(context.database)
.await
.expect("avatar::insert_many(initial_list) failed");
initial_map
}
list => list
.into_iter()
.map(|avatar| (avatar.avatar_id as u32, avatar))
.collect(),
}
}
fn create_initial_avatar_map(context: &NapContext) -> HashMap<u32, avatar::Model> {
HashMap::from([(
1011,
avatar::Model {
owner_player_uid: context.player_uid as i32,
avatar_id: 1011,
level: 60,
exp: 0,
rank: 6,
passive_skill_level: 6,
skill_type_level: (0..=6).map(|_| 12).collect(),
unlocked_talent_num: 6,
talent_switch: 0b111000,
cur_weapon_uid: 0,
taken_rank_up_reward_list: Vec::with_capacity(0),
first_get_time: time_util::cur_timestamp_seconds(),
show_weapon_type: 0,
avatar_skin_id: 0,
equip_slot_list: Vec::with_capacity(0),
},
)])
}
}

View file

@ -0,0 +1,171 @@
use trigger_database::{
entity::{player_world_info, scene_info},
prelude::*,
};
use trigger_logic::scene::ESceneType;
use super::NapContext;
pub struct SceneModel {
context: NapContext,
player_world_info: player_world_info::Model,
}
impl SceneModel {
pub async fn init(context: NapContext) -> Self {
let player_world_info = Self::load_or_create_player_world_info(&context).await;
Self {
context,
player_world_info,
}
}
pub async fn on_leave_scene(&mut self, scene_uid: i64) {
if self
.get_scene_by_uid(scene_uid)
.await
.map_or(false, |sc| sc.to_be_destroyed)
{
scene_info::Entity::delete_by_id(scene_uid)
.exec(self.context.database)
.await
.expect("scene_info::delete_by_id failed");
}
}
pub async fn clear_abandoned_scenes(&self) {
use scene_info::Column;
let world = &self.player_world_info;
scene_info::Entity::delete_many()
.filter(
Condition::all()
.add(Column::ToBeDestroyed.eq(true))
.add(Column::OwnerPlayerUid.eq(self.context.player_uid as i32))
.add(Column::SceneUid.is_not_in([
world.current_scene_uid,
world.back_scene_uid,
world.default_scene_uid,
])),
)
.exec(self.context.database)
.await
.expect("scene_info::delete_many failed");
}
pub async fn update_scene_ext(&mut self, scene_uid: i64, ext: String) {
scene_info::ActiveModel {
scene_uid: Set(scene_uid),
ext: Set(ext),
..Default::default()
}
.update(self.context.database)
.await
.expect("scene_info::update failed");
}
pub async fn set_default_scene(&mut self, scene: &scene_info::Model) {
let mut info: player_world_info::ActiveModel = self.player_world_info.clone().into();
info.default_scene_uid = Set(scene.scene_uid);
self.player_world_info = info
.update(self.context.database)
.await
.expect("player_world_info::update failed");
}
pub async fn set_current_scene(&mut self, scene: &scene_info::Model) {
let mut info: player_world_info::ActiveModel = self.player_world_info.clone().into();
info.current_scene_uid = Set(scene.scene_uid);
self.player_world_info = info
.update(self.context.database)
.await
.expect("player_world_info::update failed");
}
pub async fn push_current_scene(&mut self, scene: &scene_info::Model) {
let mut info: player_world_info::ActiveModel = self.player_world_info.clone().into();
info.back_scene_uid = Set(self.player_world_info.current_scene_uid);
info.current_scene_uid = Set(scene.scene_uid);
self.player_world_info = info
.update(self.context.database)
.await
.expect("player_world_info::update failed");
}
pub async fn create_scene_info(&mut self, scene_type: ESceneType) -> scene_info::Model {
scene_info::Entity::insert(scene_info::ActiveModel {
scene_uid: NotSet,
owner_player_uid: Set(self.context.player_uid as i32),
scene_type: Set(scene_type.into()),
to_be_destroyed: Set(scene_type != ESceneType::Hall),
ext: Set(String::with_capacity(0)),
})
.exec_with_returning(self.context.database)
.await
.expect("scene_basic_info::insert failed")
}
#[expect(dead_code)]
pub async fn get_back_scene(&self) -> Option<scene_info::Model> {
self.get_scene_by_uid(self.player_world_info.back_scene_uid)
.await
}
pub async fn get_current_scene(&self) -> Option<scene_info::Model> {
self.get_scene_by_uid(self.player_world_info.current_scene_uid)
.await
}
pub async fn get_default_scene(&self) -> Option<scene_info::Model> {
self.get_scene_by_uid(self.player_world_info.default_scene_uid)
.await
}
pub async fn get_scene_by_uid(&self, scene_uid: i64) -> Option<scene_info::Model> {
if scene_uid == 0 {
return None;
}
scene_info::Entity::find_by_id(scene_uid)
.one(self.context.database)
.await
.expect("scene_basic_info::find_by_id failed")
}
pub fn last_enter_world_timestamp(&self) -> i64 {
self.player_world_info.last_enter_world_timestamp
}
async fn load_or_create_player_world_info(context: &NapContext) -> player_world_info::Model {
let player_uid = context.player_uid as i32;
match player_world_info::Entity::find_by_id(player_uid)
.one(context.database)
.await
.expect("player_world_info::find_by_id failed")
{
Some(info) => info,
None => Self::create_default_player_world_info(player_uid)
.insert(context.database)
.await
.expect("player_world_info::insert failed"),
}
}
fn create_default_player_world_info(player_uid: i32) -> player_world_info::ActiveModel {
player_world_info::Model {
player_uid,
last_enter_world_timestamp: 0,
default_scene_uid: 0,
current_scene_uid: 0,
back_scene_uid: 0,
}
.into()
}
}

View file

@ -0,0 +1,160 @@
use std::collections::{HashMap, HashSet};
use trigger_database::{
entity::{hollow_data, hollow_info},
prelude::*,
};
use super::NapContext;
pub struct YorozuyaModel {
context: NapContext,
hollow_data: hollow_data::Model,
hollow_map: HashMap<u32, hollow_info::Model>,
}
impl YorozuyaModel {
pub async fn init(context: NapContext) -> Self {
let hollow_data = Self::load_hollow_data(&context).await;
let hollow_map = Self::load_hollows(&context).await;
Self {
context,
hollow_data,
hollow_map,
}
}
pub async fn unlock_hollow(&mut self, hollow_list: &[i32]) {
let mut hollow_group_list = std::mem::take(&mut self.hollow_data.unlock_hollow_group_list);
let mut hollow_id_list = std::mem::take(&mut self.hollow_data.unlock_hollow_id_list);
hollow_list.iter().for_each(|id| {
if !hollow_id_list.contains(id) {
hollow_id_list.push(*id);
if let Some(hollow_template) = self
.context
.filecfg
.hollow_config_template_tb
.data()
.unwrap()
.iter()
.find(|tmpl| tmpl.id() == *id)
{
if !hollow_group_list.contains(&hollow_template.hollow_group()) {
hollow_group_list.push(hollow_template.hollow_group());
}
}
}
});
self.hollow_data = hollow_data::ActiveModel {
unlock_hollow_group_list: Set(hollow_group_list),
unlock_hollow_id_list: Set(hollow_id_list),
..self.hollow_data.clone().into()
}
.update(self.context.database)
.await
.expect("hollow_data::update failed");
}
pub async fn add_hollow_quest(&mut self, hollow_quest_list: &[i32]) {
let models = hollow_quest_list
.iter()
.filter(|id| !self.hollow_map.contains_key(&(**id as u32)))
.collect::<HashSet<_>>()
.into_iter()
.map(|id| {
self.context
.filecfg
.hollow_quest_template_tb
.data()
.unwrap()
.iter()
.find(|tmpl| tmpl.id() == *id)
})
.flatten()
.map(|tmpl| hollow_info::Model {
owner_player_uid: self.context.player_uid as i32,
hollow_id: tmpl.id(),
})
.collect::<Vec<_>>();
models.iter().for_each(|hollow| {
self.hollow_map
.insert(hollow.hollow_id as u32, hollow.clone());
});
hollow_info::Entity::insert_many(
models
.into_iter()
.map(|hollow| hollow_info::ActiveModel {
owner_player_uid: Set(self.context.player_uid as i32),
hollow_id: Set(hollow.hollow_id),
})
.collect::<Vec<_>>(),
)
.exec(self.context.database)
.await
.expect("hollow_info::insert_many failed");
}
pub fn get_protocol_hollow_data(&self) -> trigger_protocol::HollowData {
use trigger_protocol::*;
HollowData {
unlock_hollow_group_list: self
.hollow_data
.unlock_hollow_group_list
.iter()
.map(|&group| group as u32)
.collect(),
unlock_hollow_id_list: self
.hollow_data
.unlock_hollow_id_list
.iter()
.map(|&id| id as u32)
.collect(),
hollow_info_list: self
.hollow_map
.values()
.map(|hollow| HollowInfo {
hollow_quest_id: hollow.hollow_id as u32,
hollow_statistics: Some(HollowStatistics::default()),
})
.collect(),
..Default::default()
}
}
async fn load_hollow_data(context: &NapContext) -> hollow_data::Model {
if let Some(data) = hollow_data::Entity::find_by_id(context.player_uid as i32)
.one(context.database)
.await
.expect("hollow_data::find_by_id failed")
{
return data;
}
hollow_data::Entity::insert(hollow_data::ActiveModel {
owner_player_uid: Set(context.player_uid as i32),
unlock_hollow_group_list: Set(Vec::new()),
unlock_hollow_id_list: Set(Vec::new()),
})
.exec_with_returning(context.database)
.await
.expect("hollow_data::insert failed")
}
async fn load_hollows(context: &NapContext) -> HashMap<u32, hollow_info::Model> {
hollow_info::Entity::find()
.filter(hollow_info::Column::OwnerPlayerUid.eq(context.player_uid as i32))
.all(context.database)
.await
.expect("hollow_info::find failed")
.into_iter()
.map(|hollow| (hollow.hollow_id as u32, hollow))
.collect()
}
}

View file

@ -0,0 +1,14 @@
use trigger_logic::scene::ESceneType;
use trigger_sv::net::ServerType;
pub fn get_scene_logic_simulation_server_type(scene_type: ESceneType) -> Option<ServerType> {
Some(match scene_type {
ESceneType::Hall => ServerType::HallServer,
ESceneType::Fight => ServerType::BattleServer,
_ => return None,
})
}
pub fn persists_on_relogin(scene_type: ESceneType) -> bool {
matches!(scene_type, ESceneType::Hall | ESceneType::Fresh)
}

View file

@ -0,0 +1,112 @@
use std::{
process::ExitCode,
sync::{Arc, LazyLock, OnceLock},
};
use config::{GMBlackList, GMScript, GameServerConfig};
use dashmap::DashMap;
use logic::NapPlayer;
use session::GameSession;
use tokio::sync::Mutex;
use tracing::{error, info};
use trigger_database::DatabaseConnection;
use trigger_fileconfig::{ArchiveFile, NapFileCfg};
use trigger_sv::{
config::{load_json_config, ServerEnvironmentConfiguration, TomlConfig},
die, logging,
net::{ServerNetworkManager, ServerType},
print_banner,
};
mod config;
mod logic;
mod server_message_handler;
mod session;
const GM_DEMO_SCRIPT_PATH: &str = "ConfigScript/GMGroupDemo.json";
const GM_BLACKLIST_PATH: &str = "ConfigScript/Gm_Item_Black_List.json";
const BLK_ASSET_FILE: &str = "1321691809.blk";
const CONFIG_FILE: &str = "gameserver.toml";
const SERVER_TYPE: ServerType = ServerType::GameServer;
struct AppState {
#[expect(unused)]
pub config: &'static GameServerConfig,
pub filecfg: NapFileCfg<'static>,
pub gm_autoexec: GMScript,
pub gm_blacklist: GMBlackList,
pub network_mgr: ServerNetworkManager,
pub sessions: DashMap<u64, Arc<GameSession>>,
pub players: DashMap<u32, Arc<Mutex<NapPlayer>>>,
pub database: DatabaseConnection,
}
#[tokio::main]
async fn main() -> ExitCode {
static APP_STATE: OnceLock<AppState> = OnceLock::new();
static DESIGN_DATA_BLK: OnceLock<ArchiveFile> = OnceLock::new();
static CONFIG: LazyLock<GameServerConfig> =
LazyLock::new(|| GameServerConfig::load_or_create(CONFIG_FILE));
print_banner();
logging::init_tracing(tracing::Level::DEBUG);
let environment = ServerEnvironmentConfiguration::load_from_toml("environment.toml")
.unwrap_or_else(|err| {
error!("{err}");
die();
});
let design_data_blk = trigger_fileconfig::read_archive_file(&mut ::std::io::Cursor::new(
&std::fs::read(BLK_ASSET_FILE).unwrap_or_else(|err| {
error!("failed to open design data blk file: {err}");
die();
}),
))
.expect("failed to unpack design data blk file");
let design_data_blk = DESIGN_DATA_BLK.get_or_init(|| design_data_blk);
let gm_autoexec = load_json_config(GM_DEMO_SCRIPT_PATH, "GMDemo");
let gm_blacklist = load_json_config(GM_BLACKLIST_PATH, "GMBlackList");
let Ok(database) = trigger_database::connect(&environment.database).await else {
die();
};
let network_mgr =
ServerNetworkManager::new(SERVER_TYPE, CONFIG.node.server_id, &environment.servers);
let state = APP_STATE.get_or_init(|| AppState {
config: &CONFIG,
filecfg: NapFileCfg::new(design_data_blk),
gm_autoexec,
gm_blacklist,
network_mgr,
sessions: DashMap::new(),
players: DashMap::new(),
database,
});
state
.network_mgr
.start_listener(state, server_message_handler::handle_message)
.await
.inspect(|_| {
info!(
"successfully started service {:?}:{}",
SERVER_TYPE, CONFIG.node.server_id
)
})
.unwrap_or_else(|err| {
error!("failed to start network manager: {err}");
die();
})
.await // this will await for entirety of the ServerNetworkManager work (forever)
.unwrap_or_else(|err| {
error!("{err}");
die();
});
ExitCode::SUCCESS
}

View file

@ -0,0 +1,294 @@
use std::sync::Arc;
use crate::{
logic::{gm_util, NapPlayer},
session::GameSession,
AppState,
};
use tokio::sync::Mutex;
use tracing::{debug, info, warn};
use trigger_sv::{message::*, net::ServerType};
pub async fn handle_message(state: &'static AppState, packet: trigger_sv::message::NetworkPacket) {
match packet.opcode {
BindClientSessionMessage::OPCODE => {
if let Some(message) = packet.get_message() {
on_bind_client_session(state, packet.header, message).await;
}
}
UnbindClientSessionMessage::OPCODE => {
if let Some(message) = packet.get_message() {
on_unbind_client_session(state, packet.header, message).await;
}
}
BindClientSessionOkMessage::OPCODE => {
if let Some(message) = packet.get_message() {
on_bind_client_session_ok(state, packet.header, message).await;
}
}
ForwardClientProtocolMessage::OPCODE => {
if let Some(message) = packet.get_message() {
on_forward_client_protocol_message(state, packet.header, message).await;
}
}
GameStateCallbackMessage::OPCODE => {
if let Some(message) = packet.get_message() {
on_game_state_callback(state, packet.header, message).await;
}
}
PlayerGmCommandMessage::OPCODE => {
if let Some(message) = packet.get_message() {
on_player_gm_command(state, packet.header, message).await;
}
}
opcode => warn!("unhandled server message, opcode: {opcode}"),
}
}
#[tracing::instrument(skip_all)]
async fn on_player_gm_command(
state: &'static AppState,
_header: Header,
message: PlayerGmCommandMessage,
) {
if let Some(player) = state
.players
.get(&message.player_uid)
.map(|pl| Arc::clone(&pl))
{
debug!(
"player with uid {} is online, emitted notifies will be sent to client after execution",
message.player_uid
);
let mut player = player.lock().await;
let mut context = gm_util::CommandContext::new(&mut *player, state);
gm_util::execute_command(&mut context, &message.command).await;
let notifies = context.remove_notifies();
if let Some(session_id) = player.active_session_id {
state
.network_mgr
.send_to(
ServerType::GateServer,
0,
AvailableServerProtocolMessage {
session_id,
ack_request_id: 0,
notifies,
response: None,
},
)
.await;
}
} else if let Some(mut player) =
NapPlayer::load(message.player_uid, false, &state.database, &state.filecfg).await
{
debug!(
"player with uid {} is offline, changes will be committed to the database",
message.player_uid
);
let mut context = gm_util::CommandContext::new(&mut player, state);
gm_util::execute_command(&mut context, &message.command).await;
} else {
debug!("player with uid {} doesn't exist", message.player_uid);
}
}
#[tracing::instrument(skip_all)]
async fn on_game_state_callback(
state: &'static AppState,
_header: Header,
message: GameStateCallbackMessage,
) {
let Some(session) = state
.sessions
.get(&message.session_id)
.map(|s| Arc::clone(&s))
else {
return;
};
let Some(player) = state
.players
.get(&session.player_uid)
.map(|p| Arc::clone(&p))
else {
return;
};
let mut player = player.lock().await;
info!(
"emitted notifies: {}, callback: {:?}",
message.protocol_units.len(),
message.callback
);
if let Some(ext) = message.scene_save_data {
let cur_scene = player.scene_model.get_current_scene().await.unwrap();
player
.scene_model
.update_scene_ext(cur_scene.scene_uid, ext)
.await;
}
match message.callback {
GameStateCallback::Loaded => {
session.on_game_state_loaded(message.protocol_units).await;
}
GameStateCallback::ClientCmdProcessed {
ack_request_id,
response,
} => {
if ack_request_id != 0 && response.is_some() {
state
.network_mgr
.send_to(
ServerType::GateServer,
0,
AvailableServerProtocolMessage {
session_id: session.id,
ack_request_id,
notifies: message.protocol_units,
response,
},
)
.await;
}
}
GameStateCallback::PlayerItemsGiven { changes: _ } => (), // TODO
}
}
async fn on_bind_client_session_ok(
state: &'static AppState,
header: Header,
message: BindClientSessionOkMessage,
) {
if let Some(session) = state
.sessions
.get(&message.session_id)
.map(|s| Arc::clone(&s))
{
session
.on_server_bound(header.sender_type.try_into().unwrap(), header.sender_id)
.await;
}
}
async fn on_forward_client_protocol_message(
state: &'static AppState,
header: Header,
message: ForwardClientProtocolMessage,
) {
if header.sender_type != u8::from(ServerType::GateServer) {
// Client messages should be forwarded only from the gateway.
return;
}
let Some(session) = state
.sessions
.get(&message.session_id)
.map(|s| Arc::clone(&s))
else {
debug!(
"ForwardClientProtocolMessage: received message for unregistered session with id: {}",
message.session_id
);
return;
};
let Some(player) = state
.players
.get(&session.player_uid)
.map(|p| Arc::clone(&p))
else {
return;
};
let mut player = player.lock().await;
if let Some(available_server_protocol) =
{ crate::session::message::handle_message(state, &session, &mut *player, message).await }
{
state
.network_mgr
.send_to(
ServerType::GateServer,
header.sender_id,
available_server_protocol,
)
.await;
}
}
async fn on_unbind_client_session(
state: &'static AppState,
header: Header,
message: UnbindClientSessionMessage,
) {
if ServerType::GateServer == ServerType::try_from(header.sender_type).unwrap() {
if let Some((id, session)) = state.sessions.remove(&message.session_id) {
session.unbind_all_servers(false).await;
debug!("session with id {id} unregistered");
}
}
}
async fn on_bind_client_session(
state: &'static AppState,
header: Header,
message: BindClientSessionMessage,
) {
if header.sender_type != u8::from(ServerType::GateServer) {
// Only gate server is allowed to request session creation on game server.
return;
}
let mut player = NapPlayer::load(message.player_uid, true, &state.database, &state.filecfg)
.await
.unwrap();
if player.is_new_player {
// Run GM auto-exec commands on first login to unlock items
let mut command_context = gm_util::CommandContext::new(&mut player, state);
for command in state.gm_autoexec.commands.iter() {
gm_util::execute_command(&mut command_context, command).await;
}
}
player.active_session_id = Some(message.session_id);
state.sessions.insert(
message.session_id,
Arc::new(GameSession::new(
&state.network_mgr,
message.session_id,
message.player_uid,
header.sender_id,
)),
);
state
.players
.insert(message.player_uid, Arc::new(Mutex::new(player)));
debug!(
"registered new session, id: {}, player uid: {}",
message.session_id, message.player_uid
);
state
.network_mgr
.send_to(
ServerType::GateServer,
header.sender_id,
BindClientSessionOkMessage {
session_id: message.session_id,
},
)
.await;
}

View file

@ -0,0 +1,24 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod abyss_module {
pub async fn on_abyss_get_data(
_context: &mut MessageContext<'_, '_>,
_request: AbyssGetDataCsReq,
) -> AbyssGetDataScRsp {
AbyssGetDataScRsp {
retcode: 0,
abyss_data: Some(AbyssData::default()),
abyss_group_list: Vec::new(),
abyss_dungeon_list: Vec::new(),
}
}
pub async fn on_abyss_arpeggio_get_data(
_context: &mut MessageContext<'_, '_>,
_request: AbyssArpeggioGetDataCsReq,
) -> AbyssArpeggioGetDataScRsp {
AbyssArpeggioGetDataScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,19 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod activity_module {
pub async fn on_get_activity_data(
_context: &mut MessageContext<'_, '_>,
_request: GetActivityDataCsReq,
) -> GetActivityDataScRsp {
GetActivityDataScRsp { retcode: 0 }
}
pub async fn on_get_web_activity_data(
_context: &mut MessageContext<'_, '_>,
_request: GetWebActivityDataCsReq,
) -> GetWebActivityDataScRsp {
GetWebActivityDataScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,12 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod arcade_module {
pub async fn on_get_arcade_data(
_context: &mut MessageContext<'_, '_>,
_request: GetArcadeDataCsReq,
) -> GetArcadeDataScRsp {
GetArcadeDataScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,171 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod avatar_module {
pub async fn on_get_avatar_data(
context: &mut MessageContext<'_, '_>,
_request: GetAvatarDataCsReq,
) -> GetAvatarDataScRsp {
GetAvatarDataScRsp {
retcode: 0,
avatar_list: context.player.role_model.get_protocol_avatar_list(&[]),
}
}
pub async fn on_get_avatar_recommend_equip(
_context: &mut MessageContext<'_, '_>,
_request: GetAvatarRecommendEquipCsReq,
) -> GetAvatarRecommendEquipScRsp {
GetAvatarRecommendEquipScRsp { retcode: 0 }
}
pub async fn on_weapon_dress(
context: &mut MessageContext<'_, '_>,
request: WeaponDressCsReq,
) -> WeaponDressScRsp {
let equip_model = &context.player.equip_model;
let role_model = &mut context.player.role_model;
if equip_model.weapon_exists(request.weapon_uid as i32) {
if let Some(updated_avatars) = role_model
.weapon_dress(request.avatar_id, request.weapon_uid as i32)
.await
{
let avatar_list = role_model.get_protocol_avatar_list(&updated_avatars);
context.add_notify(PlayerSyncScNotify {
avatar_sync: Some(AvatarSync { avatar_list }),
..Default::default()
});
return WeaponDressScRsp { retcode: 0 };
}
}
WeaponDressScRsp { retcode: 1 }
}
pub async fn on_un_weapon_dress(
context: &mut MessageContext<'_, '_>,
request: WeaponUnDressCsReq,
) -> WeaponUnDressScRsp {
let role_model = &mut context.player.role_model;
if let Some(updated_avatars) = role_model.weapon_dress(request.avatar_id, 0).await {
let avatar_list = role_model.get_protocol_avatar_list(&updated_avatars);
context.add_notify(PlayerSyncScNotify {
avatar_sync: Some(AvatarSync { avatar_list }),
..Default::default()
});
return WeaponUnDressScRsp { retcode: 0 };
}
WeaponUnDressScRsp { retcode: 1 }
}
pub async fn on_dress_equipment(
context: &mut MessageContext<'_, '_>,
request: DressEquipmentCsReq,
) -> DressEquipmentScRsp {
let equip_model = &context.player.equip_model;
let role_model = &mut context.player.role_model;
if equip_model.equipment_exists(request.equip_uid as i32) {
if let Some(updated_avatars) = role_model
.dress_equipment(
request.avatar_id,
&[(request.equip_uid, request.dress_index)],
)
.await
{
let avatar_list = role_model.get_protocol_avatar_list(&updated_avatars);
context.add_notify(PlayerSyncScNotify {
avatar_sync: Some(AvatarSync { avatar_list }),
..Default::default()
});
return DressEquipmentScRsp { retcode: 0 };
}
}
DressEquipmentScRsp { retcode: 1 }
}
pub async fn on_undress_equipment(
context: &mut MessageContext<'_, '_>,
request: UndressEquipmentCsReq,
) -> UndressEquipmentScRsp {
let role_model = &mut context.player.role_model;
if role_model
.undress_equipment(request.avatar_id, &request.undress_index_list)
.await
{
let avatar_list = role_model.get_protocol_avatar_list(&[request.avatar_id]);
context.add_notify(PlayerSyncScNotify {
avatar_sync: Some(AvatarSync { avatar_list }),
..Default::default()
});
return UndressEquipmentScRsp { retcode: 0 };
}
UndressEquipmentScRsp { retcode: 1 }
}
pub async fn on_dress_equipment_suit(
context: &mut MessageContext<'_, '_>,
request: DressEquipmentSuitCsReq,
) -> DressEquipmentSuitScRsp {
let equip_model = &context.player.equip_model;
let role_model = &mut context.player.role_model;
if request.param_list.iter().fold(true, |v, param| {
v && equip_model.equipment_exists(param.equip_uid as i32)
}) {
if let Some(updated_avatars) = role_model
.dress_equipment(
request.avatar_id,
&request
.param_list
.into_iter()
.map(|param| (param.equip_uid, param.dress_index))
.collect::<Vec<_>>(),
)
.await
{
let avatar_list = role_model.get_protocol_avatar_list(&updated_avatars);
context.add_notify(PlayerSyncScNotify {
avatar_sync: Some(AvatarSync { avatar_list }),
..Default::default()
});
return DressEquipmentSuitScRsp { retcode: 0 };
}
}
DressEquipmentSuitScRsp { retcode: 1 }
}
pub async fn on_talent_switch(
context: &mut MessageContext<'_, '_>,
request: TalentSwitchCsReq,
) -> TalentSwitchScRsp {
let role_model = &mut context.player.role_model;
if role_model
.talent_switch(request.avatar_id, request.talent_switch_list)
.await
{
let avatar_list = role_model.get_protocol_avatar_list(&[request.avatar_id]);
context.add_notify(PlayerSyncScNotify {
avatar_sync: Some(AvatarSync { avatar_list }),
..Default::default()
});
return TalentSwitchScRsp { retcode: 0 };
}
TalentSwitchScRsp { retcode: 1 }
}
}

View file

@ -0,0 +1,12 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod babel_tower_module {
pub async fn on_get_babel_tower_data(
_context: &mut MessageContext<'_, '_>,
_request: GetBabelTowerDataCsReq,
) -> GetBabelTowerDataScRsp {
GetBabelTowerDataScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,12 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod bangboo_module {
pub async fn on_get_buddy_data(
_context: &mut MessageContext<'_, '_>,
_request: GetBuddyDataCsReq,
) -> GetBuddyDataScRsp {
GetBuddyDataScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,22 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod battle_event_module {
pub async fn on_get_battle_event_info(
_context: &mut MessageContext<'_, '_>,
_request: GetBattleEventInfoCsReq,
) -> GetBattleEventInfoScRsp {
GetBattleEventInfoScRsp {
retcode: 0,
event_info: Some(BattleEventInfo::default()),
}
}
pub async fn on_report_battle_team(
_context: &mut MessageContext<'_, '_>,
_request: ReportBattleTeamCsReq,
) -> ReportBattleTeamScRsp {
ReportBattleTeamScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,12 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod battle_pass_module {
pub async fn on_get_battle_pass_data(
_context: &mut MessageContext<'_, '_>,
_request: GetBattlePassDataCsReq,
) -> GetBattlePassDataScRsp {
GetBattlePassDataScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,58 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod cafe_module {
use tracing::debug;
use trigger_logic::item::ItemStatic;
pub async fn on_get_cafe_data(
context: &mut MessageContext<'_, '_>,
_request: GetCafeDataCsReq,
) -> GetCafeDataScRsp {
GetCafeDataScRsp {
retcode: 0,
cafe_data: Some(context.player.cafe_model.get_protocol_cafe_data()),
}
}
pub async fn on_drink_cafe(
context: &mut MessageContext<'_, '_>,
request: DrinkCafeCsReq,
) -> DrinkCafeScRsp {
debug!("{request:?}");
let item_model = &mut context.player.item_model;
let cafe_model = &mut context.player.cafe_model;
let price = cafe_model.get_cafe_item_price(request.cafe_item_id);
if item_model.has_enough_material(ItemStatic::FrontendGold.into(), price) {
if let Some((add_energy, cafe_sync)) =
cafe_model.try_drink_cafe(request.cafe_item_id).await
{
item_model.add_energy(add_energy).await;
item_model
.use_material(ItemStatic::FrontendGold.into(), price)
.await;
// TODO: RewardBuff
let material_list = item_model.get_protocol_material_list();
context.add_notify(PlayerSyncScNotify {
cafe_sync: Some(cafe_sync),
item_sync: Some(ItemSync {
material_list,
..Default::default()
}),
..Default::default()
});
return DrinkCafeScRsp { retcode: 0 };
}
}
DrinkCafeScRsp { retcode: 1 }
}
}

View file

@ -0,0 +1,15 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod camp_idle_module {
pub async fn on_get_camp_idle_data(
_context: &mut MessageContext<'_, '_>,
_request: GetCampIdleDataCsReq,
) -> GetCampIdleDataScRsp {
GetCampIdleDataScRsp {
retcode: 0,
camp_idle_data: Some(CampIdleData::default()),
}
}
}

View file

@ -0,0 +1,12 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod character_quest_module {
pub async fn on_get_character_quest_list(
_context: &mut MessageContext<'_, '_>,
_request: GetCharacterQuestListCsReq,
) -> GetCharacterQuestListScRsp {
GetCharacterQuestListScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,185 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod client_systems_module {
use std::collections::HashMap;
use tracing::debug;
pub async fn on_video_get_info(
_context: &mut MessageContext<'_, '_>,
_request: VideoGetInfoCsReq,
) -> VideoGetInfoScRsp {
VideoGetInfoScRsp {
retcode: 0,
video_key_map: HashMap::default(),
}
}
pub async fn on_save_player_system_setting(
_context: &mut MessageContext<'_, '_>,
_request: SavePlayerSystemSettingCsReq,
) -> SavePlayerSystemSettingScRsp {
SavePlayerSystemSettingScRsp { retcode: 0 }
}
pub async fn on_player_operation(
_context: &mut MessageContext<'_, '_>,
_request: PlayerOperationCsReq,
) -> PlayerOperationScRsp {
PlayerOperationScRsp { retcode: 0 }
}
pub async fn on_get_tips_info(
_context: &mut MessageContext<'_, '_>,
_request: GetTipsInfoCsReq,
) -> GetTipsInfoScRsp {
GetTipsInfoScRsp {
retcode: 0,
tips_info: Some(TipsInfo::default()),
}
}
pub async fn on_get_client_systems_data(
context: &mut MessageContext<'_, '_>,
_request: GetClientSystemsDataCsReq,
) -> GetClientSystemsDataScRsp {
GetClientSystemsDataScRsp {
retcode: 0,
data: Some(ClientSystemsData {
unlock_data: Some(UnlockData {
unlocked_list: context
.state
.filecfg
.unlock_config_template_tb
.data()
.unwrap()
.iter()
.map(|tmpl| tmpl.id())
.collect(),
..Default::default()
}),
post_girl_data: Some(PostGirlData {
post_girl_item_list: context
.state
.filecfg
.post_girl_config_template_tb
.data()
.unwrap()
.iter()
.map(|tmpl| PostGirlItem {
id: tmpl.id() as u32,
unlock_time: 0,
})
.collect(),
selected_post_girl_id_list: vec![3510027],
show_random_selected: false,
}),
music_player_data: Some(MusicPlayerData {
music_list: context
.state
.filecfg
.music_player_config_template_tb
.data()
.unwrap()
.iter()
.map(|tmpl| MusicPlayerItem {
id: tmpl.id() as u32,
unlock_time: 1,
seen_time: 2,
})
.collect(),
}),
..Default::default()
}),
}
}
pub async fn on_get_news_stand_data(
_context: &mut MessageContext<'_, '_>,
_request: GetNewsStandDataCsReq,
) -> GetNewsStandDataScRsp {
GetNewsStandDataScRsp {
retcode: 0,
news_stand_data: Some(NewsStandData::default()),
}
}
pub async fn on_get_trashbin_hermit_data(
_context: &mut MessageContext<'_, '_>,
_request: GetTrashbinHermitDataCsReq,
) -> GetTrashbinHermitDataScRsp {
GetTrashbinHermitDataScRsp {
retcode: 0,
trashbin_hermit_data: Some(TrashbinHermitData::default()),
}
}
pub async fn on_get_exploration_data(
_context: &mut MessageContext<'_, '_>,
_request: GetExplorationDataCsReq,
) -> GetExplorationDataScRsp {
GetExplorationDataScRsp { retcode: 0 }
}
pub async fn on_get_journey_data(
_context: &mut MessageContext<'_, '_>,
_request: GetJourneyDataCsReq,
) -> GetJourneyDataScRsp {
GetJourneyDataScRsp {
retcode: 0,
journey_data: Some(JourneyData::default()),
}
}
pub async fn on_get_red_dot_list(
_context: &mut MessageContext<'_, '_>,
_request: GetRedDotListCsReq,
) -> GetRedDotListScRsp {
GetRedDotListScRsp { retcode: 0 }
}
pub async fn on_report_ui_layout_platform(
_context: &mut MessageContext<'_, '_>,
_request: ReportUiLayoutPlatformCsReq,
) -> ReportUiLayoutPlatformScRsp {
ReportUiLayoutPlatformScRsp { retcode: 0 }
}
pub async fn on_game_log_report(
_context: &mut MessageContext<'_, '_>,
request: GameLogReportCsReq,
) -> GameLogReportScRsp {
debug!("{request:?}");
GameLogReportScRsp { retcode: 0 }
}
pub async fn on_trigger_interact(
_context: &mut MessageContext<'_, '_>,
_request: TriggerInteractCsReq,
) -> TriggerInteractScRsp {
TriggerInteractScRsp { retcode: 0 }
}
pub async fn on_battle_report(
_context: &mut MessageContext<'_, '_>,
_request: BattleReportCsReq,
) -> BattleReportScRsp {
BattleReportScRsp { retcode: 0 }
}
pub async fn on_play_song(
_context: &mut MessageContext<'_, '_>,
_request: PlaySongCsReq,
) -> PlaySongScRsp {
PlaySongScRsp { retcode: 0 }
}
pub async fn on_set_music_player_mode(
_context: &mut MessageContext<'_, '_>,
_request: SetMusicPlayerModeCsReq,
) -> SetMusicPlayerModeScRsp {
SetMusicPlayerModeScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,35 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod collections_module {
pub async fn on_get_collect_map(
_context: &mut MessageContext<'_, '_>,
_request: GetCollectMapCsReq,
) -> GetCollectMapScRsp {
GetCollectMapScRsp {
retcode: 0,
collect_map: Some(CollectMap::default()),
}
}
pub async fn on_workbench_get_data(
_context: &mut MessageContext<'_, '_>,
_request: WorkbenchGetDataCsReq,
) -> WorkbenchGetDataScRsp {
WorkbenchGetDataScRsp {
retcode: 0,
workbench_data: Some(WorkbenchData::default()),
}
}
pub async fn on_get_abyss_reward_data(
_context: &mut MessageContext<'_, '_>,
_request: GetAbyssRewardDataCsReq,
) -> GetAbyssRewardDataScRsp {
GetAbyssRewardDataScRsp {
retcode: 0,
abyss_reward_data: Some(AbyssRewardData::default()),
}
}
}

View file

@ -0,0 +1,15 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod daily_challenge_module {
pub async fn on_get_daily_challenge_data(
_context: &mut MessageContext<'_, '_>,
_request: GetDailyChallengeDataCsReq,
) -> GetDailyChallengeDataScRsp {
GetDailyChallengeDataScRsp {
retcode: 0,
data: Some(DailyChallengeData::default()),
}
}
}

View file

@ -0,0 +1,15 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod fairy_module {
pub async fn on_get_fairy_data(
_context: &mut MessageContext<'_, '_>,
_request: GetFairyDataCsReq,
) -> GetFairyDataScRsp {
GetFairyDataScRsp {
retcode: 0,
data: Some(FairyData::default()),
}
}
}

View file

@ -0,0 +1,12 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod fishing_contest_module {
pub async fn on_get_fishing_contest_data(
_context: &mut MessageContext<'_, '_>,
_request: GetFishingContestDataCsReq,
) -> GetFishingContestDataScRsp {
GetFishingContestDataScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,15 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod gacha_module {
pub async fn on_get_gacha_data(
_context: &mut MessageContext<'_, '_>,
request: GetGachaDataCsReq,
) -> GetGachaDataScRsp {
GetGachaDataScRsp {
retcode: 0,
gacha_type: request.gacha_type,
}
}
}

View file

@ -0,0 +1,12 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod hadal_zone_module {
pub async fn on_get_hadal_zone_data(
_context: &mut MessageContext<'_, '_>,
_request: GetHadalZoneDataCsReq,
) -> GetHadalZoneDataScRsp {
GetHadalZoneDataScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,45 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod inventory_module {
pub async fn on_get_weapon_data(
context: &mut MessageContext<'_, '_>,
_request: GetWeaponDataCsReq,
) -> GetWeaponDataScRsp {
GetWeaponDataScRsp {
retcode: 0,
weapon_list: context.player.equip_model.get_protocol_weapon_list(&[]),
}
}
pub async fn on_get_equip_data(
context: &mut MessageContext<'_, '_>,
_request: GetEquipDataCsReq,
) -> GetEquipDataScRsp {
GetEquipDataScRsp {
retcode: 0,
equip_list: context.player.equip_model.get_protocol_equip_list(&[]),
}
}
pub async fn on_get_resource_data(
context: &mut MessageContext<'_, '_>,
_request: GetResourceDataCsReq,
) -> GetResourceDataScRsp {
let item_model = &context.player.item_model;
GetResourceDataScRsp {
retcode: 0,
material_list: item_model.get_protocol_material_list(),
auto_recovery_info: item_model.get_protocol_auto_recovery_info(),
}
}
pub async fn on_get_wishlist_data(
_context: &mut MessageContext<'_, '_>,
_request: GetWishlistDataCsReq,
) -> GetWishlistDataScRsp {
GetWishlistDataScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,15 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod land_revive_module {
pub async fn on_get_main_city_revival_data(
_context: &mut MessageContext<'_, '_>,
_request: GetMainCityRevivalDataCsReq,
) -> GetMainCityRevivalDataScRsp {
GetMainCityRevivalDataScRsp {
retcode: 0,
main_city_revival_data: Some(MainCityRevivalData::default()),
}
}
}

View file

@ -0,0 +1,12 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod mail_module {
pub async fn on_get_player_mails(
_context: &mut MessageContext<'_, '_>,
_request: GetPlayerMailsCsReq,
) -> GetPlayerMailsScRsp {
GetPlayerMailsScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,12 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod miniscape_entrust_module {
pub async fn on_get_miniscape_entrust_data(
_context: &mut MessageContext<'_, '_>,
_request: GetMiniscapeEntrustDataCsReq,
) -> GetMiniscapeEntrustDataScRsp {
GetMiniscapeEntrustDataScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,128 @@
use trigger_encoding::Encodeable;
use trigger_protocol::{util::ProtocolUnit, ClientCmdID};
use super::GameSession;
use crate::AppState;
use crate::NapPlayer;
modules! {
player,
scene,
avatar,
inventory,
quest,
abyss,
bangboo,
client_systems,
gacha,
mail,
ramen,
social,
cafe,
reward_buff,
arcade,
daily_challenge,
fairy,
activity,
land_revive,
collections,
perform,
battle_event,
vhs_store,
month_card,
battle_pass,
hadal_zone,
photo_wall,
character_quest,
shop,
babel_tower,
camp_idle,
miniscape_entrust,
fishing_contest,
ridus_got_boo,
qa_game
}
client_message_forwarding! {
HallServer <- [SavePosInMainCityCsReq, InteractWithUnitCsReq, RunEventGraphCsReq, EnterSectionCsReq]
BattleServer <- [EndBattleCsReq]
}
struct MessageContext<'s, 'pl> {
pub state: &'static AppState,
pub session: &'s GameSession,
pub player: &'pl mut NapPlayer,
pub request_id: u32,
notify_list: Vec<ProtocolUnit>,
}
impl<'s, 'pl> MessageContext<'s, 'pl> {
pub fn new(
state: &'static AppState,
session: &'s GameSession,
player: &'pl mut NapPlayer,
request_id: u32,
) -> Self {
Self {
state,
session,
player,
request_id,
notify_list: Vec::with_capacity(0),
}
}
pub fn add_notify<Message: Encodeable + ClientCmdID>(&mut self, message: Message) {
self.notify_list.push(message.into());
}
pub fn remove_notifies(&mut self) -> Vec<ProtocolUnit> {
std::mem::take(&mut self.notify_list)
}
}
macro_rules! modules {
($($name:ident),*) => {
$(mod $name;)*
pub async fn handle_message(state: &'static AppState, session: &GameSession, player: &mut NapPlayer, message: ::trigger_sv::message::ForwardClientProtocolMessage) -> Option<::trigger_sv::message::AvailableServerProtocolMessage> {
let Some(message) = forward_client_message(session, message).await else {
return None;
};
let cmd_id = message.message.cmd_id;
$(
if $name::supports_message(cmd_id) {
return $name::handle_message(state, session, player, message).await;
}
)*
::tracing::warn!("couldn't find handler module for message with id {cmd_id}");
None
}
};
}
macro_rules! client_message_forwarding {
($($server_type:ident <- [$($cmd_type:ident),*])*) => {
pub async fn forward_client_message(session: &GameSession, message: ::trigger_sv::message::ForwardClientProtocolMessage) -> Option<::trigger_sv::message::ForwardClientProtocolMessage> {
use std::sync::LazyLock;
use std::collections::HashMap;
use trigger_sv::net::ServerType;
static FORWARD_SERVER_MAP: LazyLock<HashMap<u16, ServerType>> = LazyLock::new(|| HashMap::from([$($((::trigger_protocol::$cmd_type::CMD_ID, ServerType::$server_type),)*)*]));
if let Some(server_type) = FORWARD_SERVER_MAP.get(&message.message.cmd_id) {
session.forward_client_message(message.message, *server_type, message.request_id).await;
None
}
else {
Some(message)
}
}
};
}
use client_message_forwarding;
use modules;

View file

@ -0,0 +1,12 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod month_card_module {
pub async fn on_get_month_card_reward_list(
_context: &mut MessageContext<'_, '_>,
_request: GetMonthCardRewardListCsReq,
) -> GetMonthCardRewardListScRsp {
GetMonthCardRewardListScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,41 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod perform_module {
use rand::RngCore;
use tracing::debug;
// TODO: actual perform trigger/jump/end tracking
pub async fn on_perform_trigger(
_context: &mut MessageContext<'_, '_>,
request: PerformTriggerCsReq,
) -> PerformTriggerScRsp {
debug!("{request:?}");
PerformTriggerScRsp {
retcode: 0,
perform_uid: ((request.perform_id as i64) << 32)
| (rand::thread_rng().next_u32() as i64),
}
}
pub async fn on_perform_jump(
_context: &mut MessageContext<'_, '_>,
request: PerformJumpCsReq,
) -> PerformJumpScRsp {
debug!("{request:?}");
PerformJumpScRsp { retcode: 0 }
}
pub async fn on_perform_end(
_context: &mut MessageContext<'_, '_>,
request: PerformEndCsReq,
) -> PerformEndScRsp {
debug!("{request:?}");
PerformEndScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,12 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod photo_wall_module {
pub async fn on_get_photo_wall_data(
_context: &mut MessageContext<'_, '_>,
_request: GetPhotoWallDataCsReq,
) -> GetPhotoWallDataScRsp {
GetPhotoWallDataScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,111 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod player_module {
use tracing::debug;
use trigger_sv::{net::ServerType, time_util};
use crate::logic::avatar_util;
pub async fn on_get_player_basic_info(
context: &mut MessageContext<'_, '_>,
_request: GetPlayerBasicInfoCsReq,
) -> GetPlayerBasicInfoScRsp {
GetPlayerBasicInfoScRsp {
retcode: 0,
basic_info: Some(context.player.get_protocol_player_basic_info()),
}
}
pub async fn on_get_server_timestamp(
_context: &mut MessageContext<'_, '_>,
_request: GetServerTimestampCsReq,
) -> GetServerTimestampScRsp {
GetServerTimestampScRsp {
retcode: 0,
utc_offset: 3, // TODO
timestamp: time_util::cur_timestamp_seconds() as u64,
}
}
pub async fn on_get_player_transaction(
context: &mut MessageContext<'_, '_>,
_request: GetPlayerTransactionCsReq,
) -> GetPlayerTransactionScRsp {
GetPlayerTransactionScRsp {
retcode: 0,
transaction: format!("{}-{}", context.session.player_uid, 100),
}
}
pub async fn on_get_authkey(
context: &mut MessageContext<'_, '_>,
request: GetAuthkeyCsReq,
) -> GetAuthkeyScRsp {
debug!("{request:?}");
let offline_verify_value = context.session.calc_offline_verify_value(0);
if request.offline_verify_value != offline_verify_value {
debug!(
"offline verify value mismatch! Expected: {}, got: {}",
offline_verify_value, request.offline_verify_value
);
return GetAuthkeyScRsp {
retcode: 1,
..Default::default()
};
}
GetAuthkeyScRsp {
retcode: 0,
auth_appid: request.auth_appid,
authkey_ver: request.authkey_ver,
authkey: String::from("TODO_Authkey"),
}
}
pub async fn on_switch_role(
context: &mut MessageContext<'_, '_>,
request: SwitchRoleCsReq,
) -> SwitchRoleScRsp {
if avatar_util::is_player_avatar(request.player_avatar_id)
&& (avatar_util::is_player_avatar(request.control_avatar_id)
|| context
.player
.role_model
.is_avatar_unlocked(request.control_avatar_id))
{
context
.player
.set_control_avatars(request.player_avatar_id, request.control_avatar_id)
.await;
// hall-server also should know controlled avatar
// (for predicates in EventGraph such as 'Share.CConfigEventByMainCharacter')
context
.session
.forward_client_message(request, ServerType::HallServer, 0)
.await;
context.add_notify(PlayerSyncScNotify {
basic_info: Some(context.player.get_protocol_player_basic_info()),
..Default::default()
});
SwitchRoleScRsp { retcode: 0 }
} else {
debug!("invalid avatars specified: {request:?}");
SwitchRoleScRsp { retcode: 1 }
}
}
pub async fn on_player_logout(
context: &mut MessageContext<'_, '_>,
_request: PlayerLogoutCsReq,
) -> PlayerLogoutScRsp {
context.session.unbind_all_servers(true).await;
PlayerLogoutScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,12 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod qa_game_module {
pub async fn on_get_questions_answer_game_data(
_context: &mut MessageContext<'_, '_>,
_request: GetQuestionsAnswerGameDataCsReq,
) -> GetQuestionsAnswerGameDataScRsp {
GetQuestionsAnswerGameDataScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,199 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod quest_module {
use trigger_logic::{
quest::EHollowQuestType,
scene::{ELocalPlayType, ESceneType},
};
use trigger_sv::message::GameStateData;
use crate::logic::dungeon_util;
pub async fn on_get_quest_data(
context: &mut MessageContext<'_, '_>,
request: GetQuestDataCsReq,
) -> GetQuestDataScRsp {
GetQuestDataScRsp {
retcode: 0,
quest_type: request.quest_type,
quest_data: Some(
context
.player
.quest_model
.get_protocol_quest_data(request.quest_type),
),
}
}
pub async fn on_get_archive_data(
context: &mut MessageContext<'_, '_>,
_request: GetArchiveDataCsReq,
) -> GetArchiveDataScRsp {
GetArchiveDataScRsp {
retcode: 0,
archive_data: Some(context.player.main_story_model.get_protocol_archive_data()),
}
}
pub async fn on_get_hollow_data(
context: &mut MessageContext<'_, '_>,
_request: GetHollowDataCsReq,
) -> GetHollowDataScRsp {
GetHollowDataScRsp {
retcode: 0,
hollow_data: Some(context.player.yorozuya_model.get_protocol_hollow_data()),
}
}
pub async fn on_get_private_message_data(
_context: &mut MessageContext<'_, '_>,
_request: GetPrivateMessageDataCsReq,
) -> GetPrivateMessageDataScRsp {
GetPrivateMessageDataScRsp {
retcode: 0,
private_message_data: Some(PrivateMessageData::default()),
}
}
pub async fn on_hollow_data_refresh(
_context: &mut MessageContext<'_, '_>,
_request: HollowDataRefreshCsReq,
) -> HollowDataRefreshScRsp {
HollowDataRefreshScRsp { retcode: 0 }
}
pub async fn on_finish_archive_perform(
_context: &mut MessageContext<'_, '_>,
request: FinishArchivePerformCsReq,
) -> FinishArchivePerformScRsp {
FinishArchivePerformScRsp {
retcode: 0,
quest_id: request.quest_id,
sub_id: request.sub_id,
}
}
pub async fn on_begin_training_course_battle(
context: &mut MessageContext<'_, '_>,
request: BeginTrainingCourseBattleCsReq,
) {
let scene_model = &mut context.player.scene_model;
let scene_info = scene_model.create_scene_info(ESceneType::Fight).await;
let dungeon_equip =
dungeon_util::build_dungeon_equip_info(&context.player, &request.avatar_id_list);
context
.session
.change_game_state(
context.request_id,
BeginTrainingCourseBattleScRsp { retcode: 0 },
GameStateData::Fight {
play_type: ELocalPlayType::TrainingRoom.into(),
quest_id: request.quest_id,
buddy_id: request.buddy_id,
avatar_id_list: request.avatar_id_list,
dungeon_equip,
},
&scene_info,
context.player,
true,
)
.await;
}
pub async fn on_begin_archive_battle_quest(
context: &mut MessageContext<'_, '_>,
request: BeginArchiveBattleQuestCsReq,
) {
let scene_model = &mut context.player.scene_model;
let scene_info = scene_model.create_scene_info(ESceneType::Fight).await;
let dungeon_equip =
dungeon_util::build_dungeon_equip_info(&context.player, &request.avatar_id_list);
context
.session
.change_game_state(
context.request_id,
BeginArchiveBattleQuestScRsp {
retcode: 0,
quest_id: request.quest_id,
},
GameStateData::Fight {
play_type: ELocalPlayType::ArchiveBattle.into(),
quest_id: request.quest_id,
buddy_id: request.buddy_id,
avatar_id_list: request.avatar_id_list,
dungeon_equip,
},
&scene_info,
context.player,
true,
)
.await;
}
pub async fn on_start_hollow_quest(
context: &mut MessageContext<'_, '_>,
request: StartHollowQuestCsReq,
) {
let scene_model = &mut context.player.scene_model;
let scene_info = scene_model.create_scene_info(ESceneType::Fight).await;
let dungeon_equip =
dungeon_util::build_dungeon_equip_info(&context.player, &request.avatar_id_list);
let quest_template = context
.state
.filecfg
.hollow_quest_template_tb
.data()
.unwrap()
.iter()
.find(|quest| quest.id() == request.quest_id as i32)
.unwrap();
let game_state_data =
match EHollowQuestType::try_from(quest_template.hollow_quest_type()).unwrap() {
EHollowQuestType::RallyBattle => GameStateData::Rally {
play_type: ELocalPlayType::RallyLongFight.into(),
quest_id: request.quest_id,
buddy_id: 0,
avatar_id_list: request.avatar_id_list,
dungeon_equip,
},
_ => GameStateData::Fight {
play_type: ELocalPlayType::PureHollowBattle.into(),
quest_id: request.quest_id,
buddy_id: 0,
avatar_id_list: request.avatar_id_list,
dungeon_equip,
},
};
context
.session
.change_game_state(
context.request_id,
StartHollowQuestScRsp {
retcode: 0,
quest_id: request.quest_id,
},
game_state_data,
&scene_info,
context.player,
true,
)
.await;
}
pub async fn on_click_hollow_system(
_context: &mut MessageContext<'_, '_>,
_request: ClickHollowSystemCsReq,
) -> ClickHollowSystemScRsp {
ClickHollowSystemScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,62 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod ramen_module {
use tracing::debug;
use trigger_logic::item::ItemStatic;
pub async fn on_get_ramen_data(
context: &mut MessageContext<'_, '_>,
_request: GetRamenDataCsReq,
) -> GetRamenDataScRsp {
GetRamenDataScRsp {
retcode: 0,
ramen_data: Some(context.player.ramen_model.get_protocol_ramen_data()),
}
}
pub async fn on_eat_ramen(
context: &mut MessageContext<'_, '_>,
request: EatRamenCsReq,
) -> EatRamenScRsp {
debug!("{request:?}");
let item_model = &mut context.player.item_model;
let ramen_model = &mut context.player.ramen_model;
let price = ramen_model.get_ramen_price(request.ramen);
if item_model.has_enough_material(ItemStatic::FrontendGold.into(), price) {
if let Some(ramen_sync) = ramen_model.try_eat_ramen(request.ramen).await {
item_model
.use_material(ItemStatic::FrontendGold.into(), price)
.await;
let material_list = item_model.get_protocol_material_list();
context.add_notify(PlayerSyncScNotify {
ramen_sync: Some(ramen_sync),
item_sync: Some(ItemSync {
material_list,
..Default::default()
}),
..Default::default()
});
return EatRamenScRsp { retcode: 0 };
}
}
EatRamenScRsp { retcode: 1 }
}
pub async fn on_del_new_ramen(
_context: &mut MessageContext<'_, '_>,
request: DelNewRamenCsReq,
) -> DelNewRamenScRsp {
debug!("{request:?}");
DelNewRamenScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,15 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod reward_buff_module {
pub async fn on_get_reward_buff_data(
_context: &mut MessageContext<'_, '_>,
_request: GetRewardBuffDataCsReq,
) -> GetRewardBuffDataScRsp {
GetRewardBuffDataScRsp {
retcode: 0,
data: Some(RewardBuffData::default()),
}
}
}

View file

@ -0,0 +1,12 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod ridus_got_module {
pub async fn on_get_ridus_got_boo_data(
_context: &mut MessageContext<'_, '_>,
_request: GetRidusGotBooDataCsReq,
) -> GetRidusGotBooDataScRsp {
GetRidusGotBooDataScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,131 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod scene_module {
use tracing::debug;
use trigger_logic::scene::ESceneType;
use crate::logic::scene_util;
pub async fn on_enter_world(context: &mut MessageContext<'_, '_>, _request: EnterWorldCsReq) {
let scene_model = &mut context.player.scene_model;
let scene_to_enter = match scene_model.get_current_scene().await {
Some(current_scene)
if scene_util::persists_on_relogin(
ESceneType::try_from(current_scene.scene_type).unwrap(),
) =>
{
Some(current_scene)
}
_ => scene_model.get_default_scene().await,
};
let scene_to_enter = if scene_to_enter.is_none() {
// TODO: first scene to be created should be the 'Fresh' scene (beginner procedure)
debug!(
"player with uid {} has no scene to enter, default hall scene will be created",
context.session.player_uid
);
let scene = scene_model.create_scene_info(ESceneType::Hall).await;
scene_model.set_default_scene(&scene).await;
scene
} else {
scene_to_enter.unwrap()
};
context
.session
.change_game_state(
context.request_id,
EnterWorldScRsp { retcode: 0 },
context
.player
.build_state_reentrant_data(&scene_to_enter)
.unwrap(),
&scene_to_enter,
context.player,
false,
)
.await;
context.player.scene_model.clear_abandoned_scenes().await;
}
pub async fn on_post_enter_world(
_context: &mut MessageContext<'_, '_>,
_request: PostEnterWorldCsReq,
) -> PostEnterWorldScRsp {
PostEnterWorldScRsp { retcode: 0 }
}
pub async fn on_scene_transition(
_context: &mut MessageContext<'_, '_>,
request: SceneTransitionCsReq,
) -> SceneTransitionScRsp {
debug!("{request:?}");
SceneTransitionScRsp { retcode: 0 }
}
pub async fn on_enter_section_complete(
_context: &mut MessageContext<'_, '_>,
_request: EnterSectionCompleteCsReq,
) -> EnterSectionCompleteScRsp {
EnterSectionCompleteScRsp { retcode: 0 }
}
pub async fn on_refresh_section(
_context: &mut MessageContext<'_, '_>,
_request: RefreshSectionCsReq,
) -> RefreshSectionScRsp {
RefreshSectionScRsp {
retcode: 0,
refresh_status: 0,
}
}
pub async fn on_leave_cur_scene(
context: &mut MessageContext<'_, '_>,
_request: LeaveCurSceneCsReq,
) {
if let Some(scene_uid) = context.session.get_cur_scene_uid() {
context.player.scene_model.on_leave_scene(scene_uid).await;
}
let default_scene = context
.player
.scene_model
.get_default_scene()
.await
.unwrap();
context
.session
.change_game_state(
context.request_id,
LeaveCurSceneScRsp { retcode: 0 },
context
.player
.build_state_reentrant_data(&default_scene)
.unwrap(),
&default_scene,
context.player,
false,
)
.await;
}
pub async fn on_active_hollow_check_point(
_context: &mut MessageContext<'_, '_>,
request: ActiveHollowCheckPointCsReq,
) -> ActiveHollowCheckPointScRsp {
// TODO: forward it to battle-server and actually handle
debug!("{:?}", request.check_point);
ActiveHollowCheckPointScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,32 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod shop_module {
pub async fn on_get_fashion_store_data(
_context: &mut MessageContext<'_, '_>,
_request: GetFashionStoreDataCsReq,
) -> GetFashionStoreDataScRsp {
GetFashionStoreDataScRsp {
retcode: 0,
data: Some(FashionStoreData::default()),
}
}
pub async fn on_get_shopping_mall_info(
_context: &mut MessageContext<'_, '_>,
_request: GetShoppingMallInfoCsReq,
) -> GetShoppingMallInfoScRsp {
GetShoppingMallInfoScRsp {
retcode: 0,
shopping_mall_info: Some(ShoppingMallInfo::default()),
}
}
pub async fn on_recharge_get_item_list(
_context: &mut MessageContext<'_, '_>,
_request: RechargeGetItemListCsReq,
) -> RechargeGetItemListScRsp {
RechargeGetItemListScRsp { retcode: 0 }
}
}

View file

@ -0,0 +1,60 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod social_module {
pub async fn on_get_friend_list(
_context: &mut MessageContext<'_, '_>,
_request: GetFriendListCsReq,
) -> GetFriendListScRsp {
GetFriendListScRsp { retcode: 0 }
}
pub async fn on_get_online_friends_list(
_context: &mut MessageContext<'_, '_>,
_request: GetOnlineFriendsListCsReq,
) -> GetOnlineFriendsListScRsp {
GetOnlineFriendsListScRsp { retcode: 0 }
}
pub async fn on_get_role_card_data(
_context: &mut MessageContext<'_, '_>,
_request: GetRoleCardDataCsReq,
) -> GetRoleCardDataScRsp {
GetRoleCardDataScRsp {
retcode: 0,
role_card_data: Some(RoleCardData {
signature: String::from("discord.gg/reversedrooms"),
}),
}
}
pub async fn on_get_chat_emoji_list(
_context: &mut MessageContext<'_, '_>,
_request: GetChatEmojiListCsReq,
) -> GetChatEmojiListScRsp {
GetChatEmojiListScRsp { retcode: 0 }
}
pub async fn on_get_display_case_data(
_context: &mut MessageContext<'_, '_>,
_request: GetDisplayCaseDataCsReq,
) -> GetDisplayCaseDataScRsp {
GetDisplayCaseDataScRsp { retcode: 0 }
}
pub async fn on_get_player_display_data(
_context: &mut MessageContext<'_, '_>,
_request: GetPlayerDisplayDataCsReq,
) -> GetPlayerDisplayDataScRsp {
GetPlayerDisplayDataScRsp {
retcode: 0,
player_display_data: Some(PlayerDisplayData {
signature: String::from("discord.gg/reversedrooms"),
display_item_group: Some(DisplayItemGroup::default()),
avatar_data_package: Some(AvatarDataPackage::default()),
photo_wall_network_data: Some(PhotoWallNetworkData::default()),
}),
}
}
}

View file

@ -0,0 +1,15 @@
use super::MessageContext;
use trigger_codegen::handlers;
#[handlers]
mod vhs_store_module {
pub async fn on_get_vhs_store_data(
_context: &mut MessageContext<'_, '_>,
_request: GetVhsStoreDataCsReq,
) -> GetVhsStoreDataScRsp {
GetVhsStoreDataScRsp {
retcode: 0,
data: Some(VhsStoreData::default()),
}
}
}

View file

@ -0,0 +1,280 @@
use std::{
collections::HashMap,
sync::{Mutex, RwLock},
};
use tracing::warn;
use trigger_database::entity::scene_info;
use trigger_logic::scene::ESceneType;
use trigger_protocol::util::ProtocolUnit;
use trigger_sv::{
message::{
AvailableServerProtocolMessage, BindClientSessionMessage, ChangeGameStateMessage,
ForwardClientProtocolMessage, GameStateData, UnbindClientSessionMessage,
},
net::{ServerNetworkManager, ServerType},
};
use crate::logic::{scene_util, NapPlayer};
pub mod message;
pub struct GameSession {
network_mgr: &'static ServerNetworkManager,
bound_server_map: RwLock<HashMap<ServerType, u32>>,
pub id: u64,
pub player_uid: u32,
pub current_state: Mutex<Option<SessionGameStateInfo>>,
}
pub struct SessionGameStateInfo {
pub scene_uid: i64,
pub state_server: ServerType,
pub prepared_game_state_data: Option<GameStateData>,
pub state_confirm_response: Option<AvailableServerProtocolMessage>,
}
impl GameSession {
pub fn new(
network_mgr: &'static ServerNetworkManager,
id: u64,
player_uid: u32,
gate_id: u32,
) -> Self {
Self {
network_mgr,
bound_server_map: RwLock::new(HashMap::from([(ServerType::GateServer, gate_id)])),
id,
player_uid,
current_state: Mutex::new(None),
}
}
pub async fn change_game_state(
&self,
ack_request_id: u32,
response: impl Into<ProtocolUnit>,
game_state_data: GameStateData,
scene: &scene_info::Model,
player: &mut NapPlayer,
set_back_scene: bool,
) {
self.unload_current_game_state().await;
let scene_type = ESceneType::try_from(scene.scene_type).expect("invalid scene type");
let state_server = scene_util::get_scene_logic_simulation_server_type(scene_type)
.unwrap_or_else(|| todo!("scene type {scene_type:?}"));
let response = AvailableServerProtocolMessage {
session_id: self.id,
ack_request_id,
notifies: Vec::new(),
response: Some(response.into()),
};
*self.current_state.lock().unwrap() = Some(SessionGameStateInfo {
scene_uid: scene.scene_uid,
state_server,
prepared_game_state_data: Some(game_state_data),
state_confirm_response: Some(response),
});
if set_back_scene {
player.scene_model.push_current_scene(&scene).await;
} else {
player.scene_model.set_current_scene(&scene).await;
}
self.bind_server(state_server, 0).await;
}
pub async fn on_game_state_loaded(&self, notifies: Vec<ProtocolUnit>) {
if let Some(mut response) = self.get_awaiting_state_response() {
response.notifies = notifies;
self.network_mgr
.send_to(ServerType::GateServer, 0, response)
.await;
}
}
fn get_awaiting_state_response(&self) -> Option<AvailableServerProtocolMessage> {
self.current_state
.lock()
.unwrap()
.as_mut()
.map(|state| state.state_confirm_response.take())
.flatten()
}
pub fn get_cur_scene_uid(&self) -> Option<i64> {
self.current_state
.lock()
.unwrap()
.as_ref()
.map(|state| state.scene_uid)
}
pub fn get_cur_state_server(&self) -> Option<ServerType> {
self.current_state
.lock()
.unwrap()
.as_ref()
.map(|state| state.state_server)
}
async fn unload_current_game_state(&self) {
if let Some(server_type) = self.get_cur_state_server() {
self.unbind_server(server_type).await;
}
}
async fn on_game_state_server_bound(&self) {
if let Some(state_server) = self.get_cur_state_server() {
if let Some(message) = self.build_change_game_state_message() {
self.network_mgr
.send_to(
state_server,
self.get_bound_server_of_type(state_server).unwrap(),
message,
)
.await;
}
}
}
fn build_change_game_state_message(&self) -> Option<ChangeGameStateMessage> {
let mut current_state = self.current_state.lock().unwrap();
let Some(state) = current_state.as_mut() else {
return None;
};
let data = match state.prepared_game_state_data.take() {
Some(data) => data,
None => {
panic!(
"simulation server {:?} is bound, but game state data is not set!",
state.state_server
)
}
};
Some(ChangeGameStateMessage {
session_id: self.id,
scene_uid: state.scene_uid,
data,
})
}
pub async fn bind_server(&self, server_type: ServerType, server_id: u32) {
self.network_mgr
.send_to(
server_type,
server_id,
BindClientSessionMessage {
session_id: self.id,
player_uid: self.player_uid,
},
)
.await;
}
pub async fn unbind_server(&self, server_type: ServerType) {
if let Some(server_id) = self.get_bound_server_of_type(server_type) {
self.bound_server_map.write().unwrap().remove(&server_type);
self.network_mgr
.send_to(
server_type,
server_id,
UnbindClientSessionMessage {
session_id: self.id,
},
)
.await;
}
}
pub async fn unbind_all_servers(&self, unbind_gateway: bool) {
let server_map = std::mem::take(&mut *self.bound_server_map.write().unwrap());
for (server_type, server_id) in server_map.into_iter() {
if server_type != ServerType::GateServer || unbind_gateway {
self.network_mgr
.send_to(
server_type,
server_id,
UnbindClientSessionMessage {
session_id: self.id,
},
)
.await;
}
}
}
pub async fn on_server_bound(&self, server_type: ServerType, server_id: u32) {
self.bound_server_map
.write()
.unwrap()
.insert(server_type, server_id);
if let Some(state_server) = self.get_cur_state_server() {
if state_server == server_type {
self.on_game_state_server_bound().await;
}
}
}
pub async fn forward_client_message<Message: Into<ProtocolUnit>>(
&self,
message: Message,
destination: ServerType,
request_id: u32,
) {
let server_id = self
.bound_server_map
.read()
.unwrap()
.get(&destination)
.copied();
if let Some(server_id) = server_id {
self.network_mgr
.send_to(
destination,
server_id,
ForwardClientProtocolMessage {
session_id: self.id,
request_id,
message: message.into(),
},
)
.await;
} else {
warn!(
"forward_client_message: server {:?} is not bound! session_id: {}, request_id: {}",
destination, self.id, request_id
);
}
}
pub fn calc_offline_verify_value(&self, login_random: u32) -> u32 {
5_u32
.wrapping_mul(rotate_right(
self.player_uid ^ login_random,
2 * (self.player_uid & 1) + 17,
))
.wrapping_sub(430675100)
}
pub fn get_bound_server_of_type(&self, server_type: ServerType) -> Option<u32> {
self.bound_server_map
.read()
.unwrap()
.get(&server_type)
.copied()
}
}
#[inline(always)]
const fn rotate_right(value: u32, count: u32) -> u32 {
(value >> count) | (value << (32 - count))
}

View file

@ -0,0 +1,27 @@
[package]
name = "trigger-gate-server"
edition = "2024"
version.workspace = true
[dependencies]
tokio.workspace = true
tokio-util.workspace = true
serde.workspace = true
serde_json.workspace = true
dashmap.workspace = true
atomic_enum.workspace = true
rand.workspace = true
base64.workspace = true
byteorder.workspace = true
tracing.workspace = true
thiserror.workspace = true
trigger-sv.workspace = true
trigger-encoding.workspace = true
trigger-cryptography.workspace = true
trigger-database.workspace = true
trigger-protobuf.workspace = true

View file

@ -0,0 +1,6 @@
[node]
server_id = 0
[network]
tcp_addr = "127.0.0.1:20501"

View file

@ -0,0 +1,19 @@
use std::net::SocketAddr;
use serde::Deserialize;
use trigger_sv::config::{ServerNodeConfiguration, TomlConfig};
#[derive(Deserialize)]
pub struct NetworkSetting {
pub tcp_addr: SocketAddr,
}
#[derive(Deserialize)]
pub struct GateServerConfig {
pub node: ServerNodeConfiguration,
pub network: NetworkSetting,
}
impl TomlConfig for GateServerConfig {
const DEFAULT_TOML: &str = include_str!("../gateserver.default.toml");
}

View file

@ -0,0 +1,230 @@
use base64::Engine;
use rand::RngCore;
use tracing::{debug, error, info, warn};
use trigger_protobuf::{
CmdID, KeepAliveNotify, PacketHead, PlayerGetTokenCsReq, PlayerGetTokenScRsp, PlayerLoginCsReq,
ProtobufMessage, XorFields,
};
use trigger_sv::{
config::RsaSetting,
message::{BindClientSessionMessage, ForwardClientProtocolMessage},
net::ServerType,
};
use crate::{
net::{Connection, NetPacket},
session::SessionState,
util::BinExt,
AppState,
};
pub async fn handle_message(connection: &Connection, state: &'static AppState, packet: NetPacket) {
let head = packet.decode_head();
match packet.cmd_id {
PlayerGetTokenCsReq::CMD_ID => {
on_player_get_token(
connection,
state,
PlayerGetTokenCsReq::decode(&*packet.body).unwrap_or_default(),
)
.await
}
PlayerLoginCsReq::CMD_ID => {
on_player_login(
connection,
state,
PlayerLoginCsReq::decode(&*packet.body).unwrap_or_default(),
)
.await
}
KeepAliveNotify::CMD_ID => {
on_keep_alive(
connection,
state,
KeepAliveNotify::decode(&*packet.body).unwrap_or_default()
).await
}
cmd_id if connection.session.is_logged_in() => {
match trigger_protobuf::pb_to_common_protocol_unit(cmd_id, &packet.body) {
Ok(Some(unit)) => state.network_mgr.send_to(ServerType::GameServer, 0, ForwardClientProtocolMessage {
session_id: connection.session.id,
request_id: head.packet_id,
message: unit,
}).await,
Ok(None) => warn!("ignoring message with unknown cmd_id: {cmd_id}"),
Err(err) => error!(
"failed to decode a message with cmd_id: {} from {} (player_uid: {}), error: {}",
cmd_id, connection.addr(), connection.session.player_uid(), err
),
}
}
unknown => warn!("received unknown cmd_id and not yet logged in: {unknown}"),
}
}
async fn on_keep_alive(
connection: &Connection,
_state: &'static AppState,
_notify: KeepAliveNotify,
) {
connection.session.refresh_keep_alive_time();
}
async fn on_player_login(
connection: &Connection,
state: &'static AppState,
mut req: PlayerLoginCsReq,
) {
if connection.session.state() != SessionState::Login {
return;
}
req.xor_fields();
// TODO: check client capabilities. Asset version and etc.
info!("PlayerLogin: {req:?}");
state
.network_mgr
.send_to(
ServerType::GameServer,
0,
BindClientSessionMessage {
session_id: connection.session.id,
player_uid: connection.session.player_uid(),
},
)
.await;
}
async fn on_player_get_token(
connection: &Connection,
state: &'static AppState,
mut req: PlayerGetTokenCsReq,
) {
if connection.session.state() != SessionState::GetToken {
return;
}
req.xor_fields();
info!(
"PlayerGetToken: account_uid: {}, rsa_ver: {}, client_rand_key: {}",
req.account_uid, req.rsa_ver, req.client_rand_key
);
let Some(rsa) = state
.environment
.security
.get_rsa_setting_by_version(req.rsa_ver)
else {
debug!(
"client from {} (account_uid: {}) tries to login with unsupported rsa version ({})",
connection.addr(),
req.account_uid,
req.rsa_ver
);
login_failed!(connection, PlayerGetTokenScRsp, 1);
return;
};
let Some(client_rand_key) = decrypt_client_rand_key(&req.client_rand_key, rsa) else {
debug!(
"failed to decrypt client_rand_key given by peer at {} (account_uid: {})",
connection.addr(),
req.account_uid
);
login_failed!(connection, PlayerGetTokenScRsp, 1);
return;
};
let server_rand_key = rand::thread_rng().next_u64();
connection.set_secret_key(client_rand_key ^ server_rand_key);
let server_rand_key = server_rand_key.to_le_bytes();
// TODO: account_uid-token pair verification via http request to sdk.
let Some(player_uid) = fetch_player_uid(state, &req.account_uid).await else {
login_failed!(connection, PlayerGetTokenScRsp, 1);
return;
};
connection.session.on_player_get_token_ok(player_uid);
connection
.send_pb(
PacketHead::default(),
PlayerGetTokenScRsp {
uid: player_uid,
server_rand_key: trigger_cryptography::rsa::encrypt(
&rsa.client_public_key,
&server_rand_key,
)
.to_base64(),
sign: trigger_cryptography::rsa::sign(&rsa.server_private_key, &server_rand_key)
.to_base64(),
..Default::default()
},
)
.await;
}
async fn fetch_player_uid(state: &'static AppState, account_uid: &str) -> Option<u32> {
use trigger_database::entity::*;
use trigger_database::prelude::*;
match account_uid::Entity::find()
.filter(account_uid::Column::AccountUid.eq(account_uid))
.one(&state.database)
.await
{
Ok(Some(uid)) => return Some(uid.player_uid as u32),
Err(err) => {
error!("account_uid::find() failed: {err}");
return None;
}
Ok(None) => (),
}
match account_uid::Entity::insert(account_uid::ActiveModel {
account_uid: Set(account_uid.to_string()),
..Default::default()
})
.exec(&state.database)
.await
{
Ok(result) => Some(result.last_insert_id as u32),
Err(err) => {
error!("account_uid::insert() failed: {err}");
None
}
}
}
fn decrypt_client_rand_key(client_rand_key: &str, rsa_setting: &RsaSetting) -> Option<u64> {
let cipher = base64::engine::general_purpose::STANDARD
.decode(client_rand_key)
.ok()?;
Some(u64::from_le_bytes(
trigger_cryptography::rsa::decrypt(&rsa_setting.server_private_key, &cipher)?
.try_into()
.ok()?,
))
}
macro_rules! login_failed {
($conn:expr, $rsp:ident, $retcode:expr) => {
$conn.session.set_login_failed();
$conn
.send_pb(
PacketHead::default(),
$rsp {
retcode: $retcode,
..Default::default()
},
)
.await;
};
}
use login_failed;

View file

@ -0,0 +1,2 @@
pub mod client;
pub mod server;

View file

@ -0,0 +1,122 @@
use tracing::{debug, info, warn};
use trigger_protobuf::{CmdID, PacketHead};
use trigger_sv::{
message::{
AvailableServerProtocolMessage, BindClientSessionOkMessage, Header,
UnbindClientSessionMessage, WithOpcode,
},
net::ServerType,
};
use crate::AppState;
pub async fn handle_message(state: &'static AppState, packet: trigger_sv::message::NetworkPacket) {
match packet.opcode {
BindClientSessionOkMessage::OPCODE => {
if let Some(message) = packet.get_message() {
on_bind_client_session_ok(state, packet.header, message).await;
}
}
AvailableServerProtocolMessage::OPCODE => {
if let Some(message) = packet.get_message() {
on_available_server_protocol(state, packet.header, message).await;
}
}
UnbindClientSessionMessage::OPCODE => {
if let Some(message) = packet.get_message() {
on_unbind_client_session(state, packet.header, message).await;
}
}
opcode => warn!("unhandled server message, opcode: {opcode}"),
}
}
async fn on_unbind_client_session(
state: &'static AppState,
header: Header,
message: UnbindClientSessionMessage,
) {
if ServerType::GameServer == ServerType::try_from(header.sender_type).unwrap() {
if let Some(connection) = state.connection_mgr.get(message.session_id) {
connection.shutdown();
}
}
}
async fn on_available_server_protocol(
state: &'static AppState,
_header: Header,
message: AvailableServerProtocolMessage,
) {
if let Some(connection) = state.connection_mgr.get(message.session_id) {
for notify in message.notifies {
if let Ok(Some((cmd_id, body))) = trigger_protobuf::common_protocol_unit_to_pb(&notify)
{
let head = PacketHead {
packet_id: connection.next_packet_id(),
..Default::default()
};
debug!("sending notify (cmd_id: {cmd_id})");
connection.send(head, cmd_id, body).await;
}
}
if let Some(response) = message.response {
if let Ok(Some((cmd_id, body))) =
trigger_protobuf::common_protocol_unit_to_pb(&response)
{
let head = PacketHead {
packet_id: connection.next_packet_id(),
request_id: message.ack_request_id,
..Default::default()
};
debug!(
"sending response (ack_request_id: {}, rsp_cmd_id: {})",
message.ack_request_id, cmd_id
);
connection.send(head, cmd_id, body).await;
} else {
let head = PacketHead {
packet_id: connection.next_packet_id(),
request_id: message.ack_request_id,
..Default::default()
};
debug!(
"sending response (using fallback, ack_request_id: {}, common_protocol_cmd_id: {})",
message.ack_request_id, response.cmd_id
);
connection
.send(
head,
trigger_protobuf::FallbackRsp::CMD_ID,
Vec::with_capacity(0),
)
.await;
}
}
}
}
async fn on_bind_client_session_ok(
state: &'static AppState,
header: Header,
message: BindClientSessionOkMessage,
) {
if header.sender_type != u8::from(ServerType::GameServer) {
return;
}
if let Some(connection) = state.connection_mgr.get(message.session_id) {
info!(
"successfully bound Game Server to client with session id {}",
message.session_id
);
connection.session.on_game_server_bound(&connection).await;
}
}

View file

@ -0,0 +1,90 @@
use std::{
process::ExitCode,
sync::{LazyLock, OnceLock},
};
use config::GateServerConfig;
use message_handler::MessageHandler;
use net::ConnectionManager;
use tracing::{error, info};
use trigger_database::DatabaseConnection;
use trigger_sv::{
config::{ServerEnvironmentConfiguration, TomlConfig},
die, logging,
net::{ServerNetworkManager, ServerType},
print_banner,
};
mod config;
mod handlers;
mod message_handler;
mod net;
mod session;
mod util;
const CONFIG_FILE: &str = "gateserver.toml";
const SERVER_TYPE: ServerType = ServerType::GateServer;
struct AppState {
pub environment: ServerEnvironmentConfiguration,
pub connection_mgr: ConnectionManager,
pub network_mgr: ServerNetworkManager,
pub database: DatabaseConnection,
}
#[tokio::main]
async fn main() -> ExitCode {
static APP_STATE: OnceLock<AppState> = OnceLock::new();
static CONFIG: LazyLock<GateServerConfig> =
LazyLock::new(|| GateServerConfig::load_or_create(CONFIG_FILE));
print_banner();
logging::init_tracing(tracing::Level::DEBUG);
let environment = ServerEnvironmentConfiguration::load_from_toml("environment.toml")
.unwrap_or_else(|err| {
error!("{err}");
die();
});
let Ok(database) = trigger_database::connect(&environment.database).await else {
die();
};
let network_mgr =
ServerNetworkManager::new(SERVER_TYPE, CONFIG.node.server_id, &environment.servers);
let connection_mgr = ConnectionManager::new(CONFIG.node.server_id);
let state = APP_STATE.get_or_init(|| AppState {
environment,
connection_mgr,
network_mgr,
database,
});
state
.network_mgr
.start_listener(state, handlers::server::handle_message)
.await
.inspect(|_| {
info!(
"successfully started service {:?}:{}",
SERVER_TYPE, CONFIG.node.server_id
)
})
.unwrap_or_else(|err| {
error!("failed to start network manager: {err}");
die();
});
let message_handler = MessageHandler::new(state);
if let Err(err) = net::serve(CONFIG.network.tcp_addr, state, message_handler).await {
error!(
"failed to serve at tcp://{}. Is another instance of server already running? Error: {}",
CONFIG.network.tcp_addr, err
);
die();
}
ExitCode::SUCCESS
}

View file

@ -0,0 +1,33 @@
use tokio::sync::mpsc;
use tracing::warn;
use crate::{net::NetPacket, AppState};
#[derive(Clone)]
pub struct MessageHandler(mpsc::UnboundedSender<(u64, NetPacket)>);
impl MessageHandler {
pub fn new(state: &'static AppState) -> Self {
let (tx, rx) = mpsc::unbounded_channel();
tokio::spawn(Self::handler_loop(state, rx));
Self(tx)
}
pub fn enqueue(&self, session_id: u64, packet: NetPacket) {
let _ = self.0.send((session_id, packet));
}
async fn handler_loop(
state: &'static AppState,
mut rx: mpsc::UnboundedReceiver<(u64, NetPacket)>,
) {
while let Some((session_id, packet)) = rx.recv().await {
if let Some(connection) = state.connection_mgr.get(session_id) {
crate::handlers::client::handle_message(connection.as_ref(), state, packet).await;
} else {
warn!("connection with session_id {session_id} doesn't exist");
}
}
}
}

View file

@ -0,0 +1,276 @@
use std::{
io,
net::SocketAddr,
sync::{
atomic::{AtomicU32, Ordering::SeqCst},
Arc, OnceLock,
},
time::Duration,
};
use dashmap::DashMap;
use tokio::{io::AsyncReadExt, net::TcpStream, select, sync::mpsc};
use tokio::{
io::{AsyncRead, AsyncWriteExt},
net::tcp::{OwnedReadHalf, OwnedWriteHalf},
};
use tokio_util::sync::CancellationToken;
use tracing::debug;
use trigger_protobuf::{CmdID, NapMessage, PacketHead, PlayerGetTokenScRsp};
use trigger_sv::net::ServerNetworkManager;
use crate::{message_handler::MessageHandler, session::ClientSession};
use super::packet::NetPacket;
pub struct Connection {
pub session: ClientSession,
send_packet_tx: OnceLock<mpsc::Sender<NetPacket>>,
net_variables: Arc<NetVariables>,
io_cancellation: OnceLock<CancellationToken>,
}
#[derive(Debug, thiserror::Error)]
enum RecvError {
#[error("{0}")]
Io(#[from] io::Error),
#[error("packet magic const mismatch, stream is corrupted")]
MagicMismatch,
}
struct NetVariables {
pub addr: SocketAddr,
pub session_id: u64,
pub incremental_packet_id: AtomicU32,
pub initial_key: &'static [u8],
pub secret_key: OnceLock<[u8; 4096]>,
pub recv_loop_exited: OnceLock<()>,
}
impl NetVariables {
pub fn key(&self) -> &[u8] {
self.secret_key.get().map_or(self.initial_key, |v| v)
}
}
impl Connection {
pub fn new(
network_mgr: &'static ServerNetworkManager,
session_id: u64,
initial_key: &'static [u8],
addr: SocketAddr,
) -> Self {
Self {
session: ClientSession::new(network_mgr, session_id),
send_packet_tx: OnceLock::new(),
io_cancellation: OnceLock::new(),
net_variables: Arc::new(NetVariables {
addr,
session_id,
initial_key,
secret_key: OnceLock::new(),
incremental_packet_id: AtomicU32::new(0),
recv_loop_exited: OnceLock::new(),
}),
}
}
pub fn is_connected(&self) -> bool {
self.net_variables.recv_loop_exited.get().is_none() && self.session.is_alive()
}
pub fn addr(&self) -> SocketAddr {
self.net_variables.addr
}
pub fn start(&self, stream: TcpStream, message_handler: MessageHandler) {
let (r, w) = stream.into_split();
let (tx, rx) = mpsc::channel(32);
let _ = self.send_packet_tx.set(tx);
let io_cancellation = CancellationToken::new();
tokio::spawn(Self::send_loop(w, rx, io_cancellation.clone()));
tokio::spawn(Self::recv_loop(
r,
Arc::clone(&self.net_variables),
message_handler,
io_cancellation.clone(),
));
let _ = self.io_cancellation.set(io_cancellation);
}
pub fn next_packet_id(&self) -> u32 {
self.net_variables
.incremental_packet_id
.fetch_add(1, SeqCst)
+ 1
}
pub fn set_secret_key(&self, seed: u64) {
let _ = self
.net_variables
.secret_key
.set(trigger_cryptography::gen_xorpad(seed));
}
pub async fn send_pb(&self, head: PacketHead, mut message: impl NapMessage) {
use trigger_protobuf::ProtobufMessage;
message.xor_fields();
self.internal_send(NetPacket {
cmd_id: message.get_cmd_id(),
head: head.encode_to_vec(),
body: message.encode_to_vec(),
})
.await;
}
pub async fn send(&self, head: PacketHead, cmd_id: u16, body: Vec<u8>) {
use trigger_protobuf::ProtobufMessage;
self.internal_send(NetPacket {
cmd_id,
head: head.encode_to_vec(),
body,
})
.await;
}
async fn internal_send(&self, mut packet: NetPacket) {
if packet.cmd_id == PlayerGetTokenScRsp::CMD_ID {
packet.xor(&self.net_variables.initial_key);
} else {
packet.xor(self.net_variables.key());
}
let _ = self.send_packet_tx.get().unwrap().send(packet).await;
}
async fn send_loop(
mut w: OwnedWriteHalf,
mut rx: mpsc::Receiver<NetPacket>,
io_cancellation: CancellationToken,
) {
loop {
select! {
packet = rx.recv() => {
match packet {
Some(packet) => w.write_all(&Vec::from(packet)).await.unwrap(),
None => break,
}
},
_ = io_cancellation.cancelled() => break,
}
}
}
async fn recv_loop(
mut r: OwnedReadHalf,
variables: Arc<NetVariables>,
message_handler: MessageHandler,
io_cancellation: CancellationToken,
) {
while let Ok(mut packet) = Self::recv(&mut r).await {
packet.xor(variables.key());
message_handler.enqueue(variables.session_id, packet);
}
let _ = variables.recv_loop_exited.set(());
io_cancellation.cancel();
debug!("client from {} disconnected", variables.addr);
}
async fn recv<R: AsyncRead + Unpin>(r: &mut R) -> Result<NetPacket, RecvError> {
let mut buf = [0u8; 12];
r.read_exact(&mut buf).await?;
(buf[0..4] == NetPacket::HEAD_MAGIC)
.then_some(())
.ok_or(RecvError::MagicMismatch)?;
let cmd_id = u16::from_be_bytes(buf[4..6].try_into().unwrap());
let head_len = u16::from_be_bytes(buf[6..8].try_into().unwrap()) as usize;
let body_len = u32::from_be_bytes(buf[8..12].try_into().unwrap()) as usize;
let mut payload = vec![0u8; head_len + body_len + 4];
r.read_exact(&mut payload).await?;
(payload[payload.len() - 4..payload.len()] == NetPacket::TAIL_MAGIC)
.then_some(())
.ok_or(RecvError::MagicMismatch)?;
Ok(NetPacket {
cmd_id,
head: payload[..head_len].to_vec(),
body: payload[head_len..head_len + body_len].to_vec(),
})
}
pub fn shutdown(&self) {
if let Some(io_cancellation) = self.io_cancellation.get() {
io_cancellation.cancel();
}
}
}
pub struct ConnectionManager {
connections: Arc<DashMap<u64, Arc<Connection>>>,
counter: AtomicU32,
server_id: u32,
}
impl ConnectionManager {
pub fn new(server_id: u32) -> Self {
let connections = Arc::new(DashMap::new());
tokio::spawn(Self::alive_check_loop(Arc::clone(&connections)));
Self {
connections,
counter: AtomicU32::default(),
server_id,
}
}
pub fn create(
&self,
network_mgr: &'static ServerNetworkManager,
addr: SocketAddr,
initial_key: &'static [u8],
) -> Arc<Connection> {
let session_id =
((self.server_id as u64) << 32) | ((self.counter.fetch_add(1, SeqCst) + 1) as u64);
let connection = Arc::new(Connection::new(network_mgr, session_id, initial_key, addr));
self.connections.insert(session_id, Arc::clone(&connection));
connection
}
pub fn get(&self, session_id: u64) -> Option<Arc<Connection>> {
self.connections
.get(&session_id)
.map(|kv| Arc::clone(&kv.value()))
}
async fn alive_check_loop(connections: Arc<DashMap<u64, Arc<Connection>>>) {
loop {
let to_remove = connections
.iter()
.filter(|pair| !pair.value().is_connected())
.map(|pair| *pair.key())
.collect::<Vec<_>>();
for id in to_remove {
if let Some((_, connection)) = connections.remove(&id) {
connection.session.stop().await;
connection.shutdown();
}
}
tokio::time::sleep(Duration::from_secs(5)).await;
}
}
}

View file

@ -0,0 +1,7 @@
mod connection;
mod packet;
mod tcp_gateway;
pub use connection::{Connection, ConnectionManager};
pub use packet::NetPacket;
pub use tcp_gateway::serve;

View file

@ -0,0 +1,45 @@
use std::io::{Cursor, Write};
use byteorder::{WriteBytesExt, BE};
use trigger_protobuf::PacketHead;
pub struct NetPacket {
pub cmd_id: u16,
pub head: Vec<u8>,
pub body: Vec<u8>,
}
impl NetPacket {
pub const HEAD_MAGIC: [u8; 4] = 0x01234567_u32.to_be_bytes();
pub const TAIL_MAGIC: [u8; 4] = 0x89ABCDEF_u32.to_be_bytes();
pub fn xor(&mut self, xorpad: &[u8]) {
self.body
.iter_mut()
.enumerate()
.for_each(|(i, b)| *b ^= xorpad[i % xorpad.len()]);
}
pub fn decode_head(&self) -> PacketHead {
use ::trigger_protobuf::ProtobufMessage;
PacketHead::decode(&*self.head).unwrap_or_default()
}
}
impl From<NetPacket> for Vec<u8> {
fn from(value: NetPacket) -> Self {
let mut buf = Vec::with_capacity(16 + value.head.len() + value.body.len());
let mut w = Cursor::new(&mut buf);
w.write_all(&NetPacket::HEAD_MAGIC).unwrap();
w.write_u16::<BE>(value.cmd_id).unwrap();
w.write_u16::<BE>(value.head.len() as u16).unwrap();
w.write_u32::<BE>(value.body.len() as u32).unwrap();
w.write_all(&value.head).unwrap();
w.write_all(&value.body).unwrap();
w.write_all(&NetPacket::TAIL_MAGIC).unwrap();
buf
}
}

View file

@ -0,0 +1,30 @@
use std::{io, net::SocketAddr};
use tokio::net::TcpListener;
use tracing::info;
use crate::{message_handler::MessageHandler, AppState};
pub async fn serve(
addr: SocketAddr,
state: &'static AppState,
message_handler: MessageHandler,
) -> io::Result<()> {
let listener = TcpListener::bind(addr).await?;
info!("listening at tcp://{addr}");
loop {
let Ok((stream, addr)) = listener.accept().await else {
continue;
};
let connection = state.connection_mgr.create(
&state.network_mgr,
addr,
&state.environment.security.static_key.xorpad,
);
connection.start(stream, message_handler.clone());
info!("new connection from {addr}");
}
}

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