Compare commits

..

16 commits

54 changed files with 7381 additions and 406 deletions

2
.cargo/config.toml Normal file
View file

@ -0,0 +1,2 @@
[alias]
xtask = "run --package xtask --"

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
target/
Cargo.lock
proto/StarRail.proto
/*.json

View file

@ -1,5 +1,5 @@
[workspace]
members = [ "gameserver", "proto", "sdkserver"]
members = ["common", "dispatch", "gameserver", "proto", "sdkserver", "xtask"]
resolver = "2"
[workspace.package]
@ -13,6 +13,10 @@ lazy_static = "1.4.0"
axum = "0.7.4"
axum-server = "0.6.0"
tower = "0.4.13"
tower-http = { version = "0.5.2", features = ["normalize-path"] }
hyper = { version = "1.3.0", features = [ "client" ] }
hyper-util = { version = "0.1.3", features = [ "client-legacy" ] }
dirs = "5.0.1"
dotenv = "0.15.0"
@ -49,6 +53,7 @@ tracing-subscriber = { version = "0.3.18", features = [
] }
tracing-bunyan-formatter = "0.3.9"
common = { path = "common/" }
proto = { path = "proto/" }
[profile.release]

View file

@ -208,7 +208,7 @@ If you develop a new program, and you want it to be of the greatest possible use
To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found.
RobinSR
AcheronSR
Copyright (C) 2024 reversedrooms
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@ -221,7 +221,7 @@ Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode:
RobinSR Copyright (C) 2024 reversedrooms
AcheronSR Copyright (C) 2024 reversedrooms
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details.

View file

@ -1,7 +1,7 @@
# RobinSR
# AcheronSR
A Server emulator for the game [`Honkai: Star Rail`](https://hsr.hoyoverse.com/en-us/)
![screenshot](https://git.xeondev.com/reversedrooms/RobinSR/raw/branch/master/screenshot.png)
![screenshot](https://git.xeondev.com/reversedrooms/AcheronSR/raw/branch/master/screenshot.png)
## Installation
@ -11,52 +11,56 @@ A Server emulator for the game [`Honkai: Star Rail`](https://hsr.hoyoverse.com/e
- [Rust](https://www.rust-lang.org/tools/install)
**NOTE**: Nightly Rust is required to build the project. To install it, first install
Rust itself, then run the following command:
```sh
rustup toolchain install nightly
rustup default nightly
```
#### Building
##### Manually
```sh
git clone https://git.xeondev.com/reversedrooms/RobinSR.git
cd RobinSR
cargo install --path gameserver
cargo install --path sdkserver
git clone https://git.xeondev.com/reversedrooms/AcheronSR.git
cd AcheronSR
cargo build --bin gameserver
cargo build --bin dispatch
cargo build --bin sdkserver
```
##### Using xtasks (use this if stupid)
```sh
cargo xtask run
```
##### To run it with automatic recompilation when any Rust files are changed
```sh
cargo xtask watch
```
### From Pre-built Binaries
Navigate to the [Releases](https://git.xeondev.com/reversedrooms/RobinSR/releases)
Navigate to the [Releases](https://git.xeondev.com/reversedrooms/AcheronSR/releases)
page and download the latest release for your platform.
## Usage
To begin using the server, you need to run both the SDK server and the game server.
If you installed from source, Rust's installer should have added .cargo/bin to your
path, so simply run the following:
```sh
gameserver
sdkserver
```
If you installed from pre-built binaries, navigate to the directory where you downloaded
the binaries and either a) double-click on the following executable names or b)
run the following in a terminal:
```sh
./gameserver
./dispatch
./sdkserver
```
##### Note: the `assets` folder should be in the same directory with the `gameserver`, otherwise it will panic.
## Connecting
[Get 2.2 beta client](https://bhrpg-prod.oss-accelerate.aliyuncs.com/client/beta/20240322124944_scfGE0xJXlWtoJ1r/StarRail_2.1.51.zip),
replace [mhypbase.dll](https://git.xeondev.com/reversedrooms/RobinSR/raw/branch/master/mhypbase.dll) file in your game folder, it will redirect game traffic (and also disable in-game censorship)
replace [mhypbase.dll](https://git.xeondev.com/reversedrooms/AcheronSR/raw/branch/master/mhypbase.dll)
file in your game folder, it will redirect game traffic (and disable in-game censorship)
## Contributing

File diff suppressed because it is too large Load diff

13
common/Cargo.toml Normal file
View file

@ -0,0 +1,13 @@
[package]
name = "common"
edition = "2021"
version.workspace = true
[dependencies]
anyhow.workspace = true
ansi_term.workspace = true
env_logger.workspace = true
tracing.workspace = true
lazy_static.workspace = true
serde.workspace = true
serde_json.workspace = true

20
common/src/data/excels.rs Normal file
View file

@ -0,0 +1,20 @@
use serde::Deserialize;
#[derive(Default, Deserialize)]
#[serde(rename_all = "PascalCase")]
#[serde(default)]
pub struct MapEntranceConfig {
#[serde(rename = "BeginMainMissionIDList")]
pub begin_main_mission_idlist: Vec<u32>,
pub entrance_type: String,
#[serde(rename = "FinishMainMissionIDList")]
pub finish_main_mission_idlist: Vec<u32>,
#[serde(rename = "FinishSubMissionIDList")]
pub finish_sub_mission_idlist: Vec<u32>,
#[serde(rename = "FloorID")]
pub floor_id: u32,
#[serde(rename = "ID")]
pub id: u32,
#[serde(rename = "PlaneID")]
pub plane_id: u32,
}

37
common/src/data/mod.rs Normal file
View file

@ -0,0 +1,37 @@
mod excels;
pub use excels::*;
use lazy_static::lazy_static;
use serde_json::from_str;
pub fn init_assets() {
tracing::info!("Loaded {} excel tables", EXCEL_COLLECTION.table_count());
}
lazy_static! {
pub static ref EXCEL_COLLECTION: ExcelCollection = ExcelCollection::new();
}
pub struct ExcelCollection {
pub map_entrance_configs: Vec<MapEntranceConfig>,
}
impl ExcelCollection {
fn new() -> Self {
Self {
map_entrance_configs: from_str(&load_asset(
"assets/ExcelOutput/MapEntranceConfig.json",
))
.unwrap(),
}
}
#[must_use]
pub const fn table_count(&self) -> usize {
1
}
}
fn load_asset(path: &str) -> String {
std::fs::read_to_string(path).unwrap()
}

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

@ -0,0 +1,3 @@
pub mod data;
pub mod logging;
pub mod util;

7
common/src/util.rs Normal file
View file

@ -0,0 +1,7 @@
#[must_use]
pub fn load_or_create_config(path: &str, defaults: &str) -> String {
std::fs::read_to_string(path).unwrap_or_else(|_| {
std::fs::write(path, defaults).unwrap();
defaults.to_string()
})
}

34
dispatch/Cargo.toml Normal file
View file

@ -0,0 +1,34 @@
[package]
name = "dispatch"
edition = "2021"
version.workspace = true
[dependencies]
common.workspace = true
anyhow.workspace = true
env_logger.workspace = true
axum.workspace = true
axum-server.workspace = true
lazy_static.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
tokio-util.workspace = true
tracing.workspace = true
tracing-futures.workspace = true
tracing-log.workspace = true
tracing-subscriber.workspace = true
tracing-bunyan-formatter.workspace = true
ansi_term.workspace = true
prost.workspace = true
rbase64.workspace = true
proto.workspace = true
tower.workspace = true
tower-http.workspace = true

34
dispatch/dispatch.json Normal file
View file

@ -0,0 +1,34 @@
{
"http_port": 21041,
"game_servers": {
"acheron_sr": {
"name": "AcheronSR",
"title": "AcheronSR",
"dispatch_url": "http://127.0.0.1:21041/query_gateway/acheron_sr",
"env_type": "2",
"gateserver_ip": "127.0.0.1",
"gateserver_port": 23301,
"gateserver_protocol": "Tcp"
}
},
"versions": {
"CNBETAWin2.1.51": {
"asset_bundle_url": "https://autopatchcn.bhsr.com/asb/BetaLive/output_6744505_89b2f5dc973e",
"ex_resource_url": "https://autopatchcn.bhsr.com/design_data/BetaLive/output_6759713_b4e0e740f0da",
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_6755976_3c46d7c46e2c",
"lua_version": "6755976"
},
"CNBETAWin2.1.52": {
"asset_bundle_url": "https://autopatchcn.bhsr.com/asb/BetaLive/output_6785106_15237df2ef89",
"ex_resource_url": "https://autopatchcn.bhsr.com/design_data/BetaLive/output_6787319_5f3f1dae4769",
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_6785460_26c4b6c61a8b",
"lua_version": "6785460"
},
"CNBETAWin2.1.53": {
"asset_bundle_url": "https://autopatchcn.bhsr.com/asb/BetaLive/output_6828321_72f2df86102b",
"ex_resource_url": "https://autopatchcn.bhsr.com/design_data/BetaLive/output_6834225_44836493b261",
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_6828764_f749b48347fd",
"lua_version": "6828764"
}
}
}

51
dispatch/src/config.rs Normal file
View file

@ -0,0 +1,51 @@
use std::collections::HashMap;
use common::util::load_or_create_config;
use lazy_static::lazy_static;
use serde::Deserialize;
use serde_json::from_str;
const DEFAULT_CONFIG: &str = include_str!("../dispatch.json");
pub fn init_config() {
let _configuration = &*CONFIGURATION;
}
#[derive(Deserialize)]
pub struct DispatchServerConfiguration {
pub http_port: u16,
pub game_servers: HashMap<String, GameServerConfig>,
pub versions: HashMap<String, VersionConfig>,
}
#[derive(Deserialize)]
pub struct VersionConfig {
pub asset_bundle_url: String,
pub ex_resource_url: String,
pub lua_url: String,
pub lua_version: String,
}
#[derive(Deserialize)]
pub struct GameServerConfig {
pub name: String,
pub title: String,
pub dispatch_url: String,
pub env_type: String,
pub gateserver_ip: String,
pub gateserver_port: u16,
pub gateserver_protocol: GatewayProtocolType,
}
#[derive(Deserialize, Eq, PartialEq)]
pub enum GatewayProtocolType {
Tcp,
Kcp,
}
lazy_static! {
pub static ref CONFIGURATION: DispatchServerConfiguration = {
let data = load_or_create_config("dispatch.json", DEFAULT_CONFIG);
from_str(&data).unwrap()
};
}

85
dispatch/src/handlers.rs Normal file
View file

@ -0,0 +1,85 @@
use crate::config::*;
use axum::extract::{Path, Query};
use prost::Message;
use proto::{Dispatch, Gateserver, RegionInfo};
use serde::Deserialize;
pub const QUERY_DISPATCH_PATH: &str = "/query_dispatch";
pub const QUERY_GATEWAY_PATH: &str = "/query_gateway/:region_name";
#[tracing::instrument]
pub async fn query_dispatch() -> String {
let rsp = Dispatch {
retcode: 0,
region_list: CONFIGURATION
.game_servers
.values()
.map(|c| RegionInfo {
name: c.name.clone(),
title: c.title.clone(),
env_type: c.env_type.clone(),
dispatch_url: c.dispatch_url.clone(),
..Default::default()
})
.collect(),
..Default::default()
};
let mut buff = Vec::new();
rsp.encode(&mut buff).unwrap();
rbase64::encode(&buff)
}
#[derive(Deserialize, Debug)]
pub struct QueryGatewayParameters {
pub version: String,
}
#[tracing::instrument]
pub async fn query_gateway(
Path(region_name): Path<String>,
parameters: Query<QueryGatewayParameters>,
) -> String {
let rsp = if let Some(server_config) = CONFIGURATION.game_servers.get(&region_name) {
if let Some(version_config) = CONFIGURATION.versions.get(&parameters.version) {
Gateserver {
retcode: 0,
ip: server_config.gateserver_ip.clone(),
port: u32::from(server_config.gateserver_port),
asset_bundle_url: version_config.asset_bundle_url.clone(),
ex_resource_url: version_config.ex_resource_url.clone(),
lua_url: version_config.lua_url.clone(),
lua_version: version_config.lua_version.clone(),
ifix_version: String::from("0"),
jblkncaoiao: true,
hjdjakjkdbi: true,
ldknmcpffim: true,
feehapamfci: true,
eebfeohfpph: true,
dfmjjcfhfea: true,
najikcgjgan: true,
giddjofkndm: true,
use_tcp: server_config.gateserver_protocol == GatewayProtocolType::Tcp,
..Default::default()
}
} else {
Gateserver {
retcode: 9,
msg: format!("forbidden version: {} or invalid bind", parameters.version),
..Default::default()
}
}
} else {
Gateserver {
retcode: 9,
msg: format!("server config for {region_name} not found"),
..Default::default()
}
};
let mut buff = Vec::new();
rsp.encode(&mut buff).unwrap();
rbase64::encode(&buff)
}

35
dispatch/src/main.rs Normal file
View file

@ -0,0 +1,35 @@
use anyhow::Result;
use axum::{extract::Request, routing::get, Router, ServiceExt};
use common::logging::init_tracing;
use tokio::net::TcpListener;
use tower::Layer;
use tower_http::normalize_path::NormalizePathLayer;
use tracing::Level;
mod config;
mod handlers;
use config::{init_config, CONFIGURATION};
#[tokio::main]
async fn main() -> Result<()> {
init_tracing();
init_config();
let span = tracing::span!(Level::DEBUG, "main");
let _ = span.enter();
let app = Router::new()
.route(handlers::QUERY_DISPATCH_PATH, get(handlers::query_dispatch))
.route(handlers::QUERY_GATEWAY_PATH, get(handlers::query_gateway));
let app = NormalizePathLayer::trim_trailing_slash().layer(app);
let addr = format!("0.0.0.0:{}", CONFIGURATION.http_port);
let server = TcpListener::bind(&addr).await?;
tracing::info!("dispatch is listening at {addr}");
axum::serve(server, ServiceExt::<Request>::into_make_service(app)).await?;
Ok(())
}

View file

@ -1 +0,0 @@
LINEUP=1309,1308,1307,1315

View file

@ -4,6 +4,8 @@ edition = "2021"
version.workspace = true
[dependencies]
common.workspace = true
ansi_term.workspace = true
anyhow.workspace = true
atomic_refcell.workspace = true

8
gameserver/globals.json Normal file
View file

@ -0,0 +1,8 @@
{
"lineup": [1308, 1309, 1307, 1315],
"monster_wave_list":
[
[3013010, 3013010],
[3024020]
]
}

View file

@ -0,0 +1,19 @@
use common::util::load_or_create_config;
use lazy_static::lazy_static;
use serde::Deserialize;
use serde_json::from_str;
const DEFAULT_GLOBALS: &str = include_str!("../../globals.json");
lazy_static! {
pub static ref INSTANCE: Globals = {
let data = load_or_create_config("globals.json", DEFAULT_GLOBALS);
from_str(&data).unwrap()
};
}
#[derive(Deserialize)]
pub struct Globals {
pub lineup: Vec<u32>,
pub monster_wave_list: Vec<Vec<u32>>,
}

View file

@ -1,16 +0,0 @@
use std::env;
use lazy_static::lazy_static;
lazy_static! {
pub static ref LINEUP: Vec<u32> = {
let lineup_str = env::var("LINEUP").expect("Missing .env parameter: LINEUP");
let mut lineup = Vec::new();
for s in lineup_str.trim().replace(" ", "").split(",") {
lineup.push(s.parse().unwrap());
}
lineup
};
}

View file

@ -1 +1,9 @@
pub mod globals;
mod global_config;
mod player_info;
pub use global_config::INSTANCE as globals;
pub use player_info::PlayerInfo;
pub fn init_config() {
let _globals = &*globals; // this will initialize the static (and create default config)
}

View file

@ -0,0 +1,59 @@
use crate::net::PlayerSession;
use super::globals;
use anyhow::Result;
use proto::*;
pub struct PlayerInfo {
pub uid: u32,
pub lineup: LineupInfo,
}
impl PlayerInfo {
pub fn new() -> Self {
Self {
uid: 1337,
lineup: default_lineup(),
}
}
pub async fn sync_lineup(&self, session: &PlayerSession) -> Result<()> {
session
.send(
CMD_SYNC_LINEUP_NOTIFY,
SyncLineupNotify {
lineup: Some(self.lineup.clone()),
..Default::default()
},
)
.await
}
}
fn default_lineup() -> LineupInfo {
LineupInfo {
plane_id: 10001,
name: String::from("Lineup 1"),
index: 0,
leader_slot: 0,
mp: 5,
mp_max: 5,
avatar_list: globals
.lineup
.iter()
.enumerate()
.map(|(idx, id)| LineupAvatar {
id: *id,
slot: idx as u32,
hp: 10000,
sp: Some(AmountInfo {
cur_amount: 10000,
max_amount: 10000,
}),
satiety: 100,
avatar_type: 3,
})
.collect(),
..Default::default()
}
}

View file

@ -1,42 +1,20 @@
use anyhow::Result;
mod game;
mod logging;
mod net;
mod util;
use logging::init_tracing;
const DEFAULT_DOTENV: &str = include_str!("../.env");
use common::data::init_assets;
use common::logging::init_tracing;
use game::init_config;
#[tokio::main]
async fn main() -> Result<()> {
init_tracing();
init_config()?;
init_config();
init_assets();
net::gateway::listen("0.0.0.0", 23301).await?;
Ok(())
}
fn init_config() -> Result<()> {
let local_dotenv = std::path::Path::new(".env");
if local_dotenv.exists() {
dotenv::dotenv()?;
} else {
let config = dirs::config_dir()
.ok_or_else(|| anyhow::anyhow!("No config directory found"))?
.join("hkrpg-gameserver");
std::fs::create_dir_all(&config)?;
let env = config.join(".env");
if !env.exists() {
std::fs::write(&env, DEFAULT_DOTENV)?;
}
dotenv::from_path(&env)?;
}
Ok(())
}

View file

@ -1,8 +1,9 @@
use anyhow::Result;
use common::log_error;
use tokio::net::TcpListener;
use tracing::{info_span, Instrument};
use crate::{log_error, net::PlayerSession};
use crate::net::PlayerSession;
pub async fn listen(host: &str, port: u16) -> Result<()> {
let listener = TcpListener::bind(format!("{host}:{port}")).await?;

View file

@ -4,7 +4,7 @@ use proto::*;
use crate::{net::PlayerSession, util};
pub async fn on_player_get_token_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
_body: &PlayerGetTokenCsReq,
) -> Result<()> {
session
@ -13,7 +13,7 @@ pub async fn on_player_get_token_cs_req(
PlayerGetTokenScRsp {
retcode: 0,
msg: String::from("OK"),
uid: 1337,
uid: session.player_uid(),
..Default::default()
},
)
@ -21,7 +21,7 @@ pub async fn on_player_get_token_cs_req(
}
pub async fn on_player_login_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
body: &PlayerLoginCsReq,
) -> Result<()> {
session
@ -32,7 +32,7 @@ pub async fn on_player_login_cs_req(
server_timestamp_ms: util::cur_timestamp_ms(),
stamina: 240,
basic_info: Some(PlayerBasicInfo {
nickname: String::from("xeondev"),
nickname: String::from("AcheronSR"),
level: 5,
stamina: 240,
..Default::default()

View file

@ -8,7 +8,7 @@ static UNLOCKED_AVATARS: [u32; 49] = [
];
pub async fn on_get_avatar_data_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
body: &GetAvatarDataCsReq,
) -> Result<()> {
session

View file

@ -2,9 +2,11 @@ use super::*;
use crate::game::globals;
pub async fn on_start_cocoon_stage_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
body: &StartCocoonStageCsReq,
) -> Result<()> {
let player_info = session.player_info();
let rsp = StartCocoonStageScRsp {
retcode: 0,
prop_entity_id: body.prop_entity_id,
@ -14,12 +16,13 @@ pub async fn on_start_cocoon_stage_cs_req(
stage_id: 201012311,
logic_random_seed: 4444,
battle_id: 1,
battle_avatar_list: globals::LINEUP
battle_avatar_list: player_info
.lineup
.avatar_list
.iter()
.enumerate()
.map(|(idx, id)| BattleAvatar {
index: idx as u32,
id: *id,
.map(|avatar| BattleAvatar {
index: avatar.slot,
id: avatar.id,
level: 80,
promotion: 6,
rank: 6,
@ -32,13 +35,20 @@ pub async fn on_start_cocoon_stage_cs_req(
..Default::default()
})
.collect(),
monster_wave_list: vec![SceneMonsterWave {
monster_list: vec![SceneMonsterParam {
monster_id: 3013010,
monster_wave_list: globals
.monster_wave_list
.iter()
.map(|monster_list| SceneMonsterWave {
monster_list: monster_list
.iter()
.map(|id| SceneMonsterParam {
monster_id: *id,
..Default::default()
}],
})
.collect(),
..Default::default()
}],
})
.collect(),
..Default::default()
}),
};
@ -47,7 +57,7 @@ pub async fn on_start_cocoon_stage_cs_req(
}
pub async fn on_pve_battle_result_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
body: &PveBattleResultCsReq,
) -> Result<()> {
session

View file

@ -1,81 +1,47 @@
use super::*;
use crate::game::globals;
pub async fn on_get_all_lineup_data_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
_body: &GetAllLineupDataCsReq,
) -> Result<()> {
let player_info = session.player_info();
session
.send(
CMD_GET_ALL_LINEUP_DATA_SC_RSP,
GetAllLineupDataScRsp {
retcode: 0,
cur_index: 0,
lineup_list: vec![LineupInfo {
plane_id: 10001,
name: String::from("Lineup 1"),
index: 0,
avatar_list: globals::LINEUP
.iter()
.enumerate()
.map(|(idx, id)| LineupAvatar {
id: *id,
slot: idx as u32,
hp: 10000,
sp: Some(AmountInfo {
cur_amount: 10000,
max_amount: 10000,
}),
satiety: 100,
avatar_type: 3,
})
.collect(),
..Default::default()
}],
lineup_list: vec![player_info.lineup.clone()],
},
)
.await
}
pub async fn on_get_cur_lineup_data_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
_body: &GetCurLineupDataCsReq,
) -> Result<()> {
let player_info = session.player_info();
session
.send(
CMD_GET_CUR_LINEUP_DATA_SC_RSP,
GetCurLineupDataScRsp {
retcode: 0,
lineup: Some(LineupInfo {
plane_id: 10001,
name: String::from("Lineup 1"),
index: 0,
avatar_list: globals::LINEUP
.iter()
.enumerate()
.map(|(idx, id)| LineupAvatar {
id: *id,
slot: idx as u32,
hp: 10000,
sp: Some(AmountInfo {
cur_amount: 10000,
max_amount: 10000,
}),
satiety: 100,
avatar_type: 3,
})
.collect(),
..Default::default()
}),
lineup: Some(player_info.lineup.clone()),
},
)
.await
}
pub async fn on_change_lineup_leader_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
body: &ChangeLineupLeaderCsReq,
) -> Result<()> {
let mut player_info = session.player_info_mut();
player_info.lineup.leader_slot = body.slot;
session
.send(
CMD_CHANGE_LINEUP_LEADER_SC_RSP,
@ -86,3 +52,102 @@ pub async fn on_change_lineup_leader_cs_req(
)
.await
}
pub async fn on_join_lineup_cs_req(session: &PlayerSession, body: &JoinLineupCsReq) -> Result<()> {
let mut player_info = session.player_info_mut();
if !(0..4).contains(&body.slot) {
return session
.send(
CMD_JOIN_LINEUP_CS_REQ,
JoinLineupScRsp {
retcode: Retcode::RetLineupInvalidMemberPos as u32,
},
)
.await;
}
if player_info
.lineup
.avatar_list
.iter()
.any(|avatar| avatar.slot == body.slot)
{
return session
.send(
CMD_JOIN_LINEUP_CS_REQ,
JoinLineupScRsp {
retcode: Retcode::RetLineupAvatarAlreadyInit as u32,
},
)
.await;
}
player_info
.lineup
.avatar_list
.push(lineup_avatar(body.base_avatar_id, body.slot));
player_info.sync_lineup(session).await?;
session
.send(CMD_JOIN_LINEUP_SC_RSP, JoinLineupScRsp::default())
.await
}
pub async fn on_replace_lineup_cs_req(
session: &PlayerSession,
body: &ReplaceLineupCsReq,
) -> Result<()> {
let mut player_info = session.player_info_mut();
player_info.lineup.avatar_list.clear();
for slot_info in &body.lineup_slots {
player_info
.lineup
.avatar_list
.push(lineup_avatar(slot_info.id, slot_info.slot));
}
player_info.lineup.leader_slot = body.leader_slot;
player_info.sync_lineup(session).await?;
session
.send(CMD_REPLACE_LINEUP_SC_RSP, ReplaceLineupScRsp::default())
.await
}
pub async fn on_quit_lineup_cs_req(session: &PlayerSession, body: &QuitLineupCsReq) -> Result<()> {
let mut player_info = session.player_info_mut();
player_info
.lineup
.avatar_list
.retain(|avatar| avatar.id != body.base_avatar_id);
player_info.sync_lineup(session).await?;
session
.send(
CMD_QUIT_LINEUP_SC_RSP,
QuitLineupScRsp {
plane_id: body.plane_id,
is_mainline: !body.is_virtual,
is_virtual: body.is_virtual,
base_avatar_id: body.base_avatar_id,
retcode: 0,
},
)
.await
}
#[must_use]
const fn lineup_avatar(id: u32, slot: u32) -> LineupAvatar {
LineupAvatar {
id,
slot,
hp: 10000,
sp: Some(AmountInfo {
cur_amount: 10000,
max_amount: 10000,
}),
satiety: 100,
avatar_type: 3,
}
}

View file

@ -0,0 +1,27 @@
use super::*;
pub async fn on_get_scene_map_info_cs_req(
session: &PlayerSession,
body: &GetSceneMapInfoCsReq,
) -> Result<()> {
session
.send(
CMD_GET_SCENE_MAP_INFO_SC_RSP,
GetSceneMapInfoScRsp {
entry_id: body.entry_id,
cur_map_entry_id: body.entry_id,
scene_map_info: body
.entry_id_list
.iter()
.map(|id| SceneMapInfo {
cur_map_entry_id: body.entry_id,
entry_id: *id,
..Default::default()
})
.collect(),
retcode: 0,
..Default::default()
},
)
.await
}

View file

@ -41,7 +41,7 @@ static FINISHED_MAIN_MISSIONS: [u32; 365] = [
];
pub async fn on_get_mission_status_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
body: &GetMissionStatusCsReq,
) -> Result<()> {
let rsp = GetMissionStatusScRsp {

View file

@ -2,6 +2,7 @@ mod authentication;
mod avatar;
mod battle;
mod lineup;
mod map;
mod mission;
mod player;
mod scene;
@ -19,6 +20,7 @@ pub use authentication::*;
pub use avatar::*;
pub use battle::*;
pub use lineup::*;
pub use map::*;
pub use mission::*;
pub use player::*;
pub use scene::*;
@ -46,7 +48,7 @@ macro_rules! dummy {
}
}
pub async fn send_dummy_response(&mut self, req_id: u16) -> Result<()> {
pub async fn send_dummy_response(&self, req_id: u16) -> Result<()> {
let cmd_type = match req_id {
$(
x if x == [<Cmd $cmd CsReq>] as u16 => [<Cmd $cmd ScRsp>] as u16,
@ -61,7 +63,7 @@ macro_rules! dummy {
}
.into();
self.client_socket.write_all(&payload).await?;
self.client_socket().await.write_all(&payload).await?;
Ok(())
}

View file

@ -3,7 +3,7 @@ use crate::util;
use super::*;
pub async fn on_get_basic_info_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
_body: &GetBasicInfoCsReq,
) -> Result<()> {
session
@ -19,7 +19,7 @@ pub async fn on_get_basic_info_cs_req(
}
pub async fn on_get_hero_basic_type_info_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
_body: &GetHeroBasicTypeInfoCsReq,
) -> Result<()> {
session
@ -40,7 +40,7 @@ pub async fn on_get_hero_basic_type_info_cs_req(
}
pub async fn on_player_heart_beat_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
body: &PlayerHeartBeatCsReq,
) -> Result<()> {
session
@ -53,7 +53,7 @@ pub async fn on_player_heart_beat_cs_req(
download_data: Some(ClientDownloadData {
version: 51,
time: util::cur_timestamp_ms() as i64,
data: rbase64::decode("G0x1YVMBGZMNChoKBAQICHhWAAAAAAAAAAAAAAAod0ABKEBDOlxVc2Vyc1x4ZW9uZGV2XERvd25sb2Fkc1xyYWJzdHZvLmx1YQAAAAAAAAAAAAEEEAAAACQAQAApQEAAKYBAACnAQABWAAEALIAAAR1AQQCkgEEA5ABAAOnAwQHpAMIB6UDCAawAAAEsgAAAH8BChRkAgAAMAAAABANDUwQMVW5pdHlFbmdpbmUEC0dhbWVPYmplY3QEBUZpbmQEKVVJUm9vdC9BYm92ZURpYWxvZy9CZXRhSGludERpYWxvZyhDbG9uZSkEF0dldENvbXBvbmVudEluQ2hpbGRyZW4EB3R5cGVvZgQEUlBHBAdDbGllbnQEDkxvY2FsaXplZFRleHQEBXRleHQURVJvYmluU1IgaXMgYSBmcmVlIGFuZCBvcGVuIHNvdXJjZSBzb2Z0d2FyZS4gZGlzY29yZC5nZy9yZXZlcnNlZHJvb21zAQAAAAEAAAAAABAAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAAAAAAEAAAAFX0VOVg==").unwrap()
data: rbase64::decode("G0x1YVMBGZMNChoKBAQICHhWAAAAAAAAAAAAAAAod0ABKEBDOlxVc2Vyc1x4ZW9uZGV2XERvd25sb2Fkc1xhY2hlcm9uLmx1YQAAAAAAAAAAAAEEHwAAACQAQAApQEAAKYBAACnAQABWAAEALIAAAR1AQQCkgEEA5ABAAOnAwQHpAMIB6UDCAawAAAEsgAAAH8BChSQAQAApQEAAKYBAACnAQABWAAMALIAAAR1AQQCkgEEA5ABAAOnAwQHpAMIB6UDCAawAAAEsgAAAH0BDhRkAgAAOAAAABANDUwQMVW5pdHlFbmdpbmUEC0dhbWVPYmplY3QEBUZpbmQEKVVJUm9vdC9BYm92ZURpYWxvZy9CZXRhSGludERpYWxvZyhDbG9uZSkEF0dldENvbXBvbmVudEluQ2hpbGRyZW4EB3R5cGVvZgQEUlBHBAdDbGllbnQEDkxvY2FsaXplZFRleHQEBXRleHQULUFjaGVyb25TUiBpcyBhIGZyZWUgYW5kIG9wZW4gc291cmNlIHNvZnR3YXJlBAxWZXJzaW9uVGV4dBQuVmlzaXQgZGlzY29yZC5nZy9yZXZlcnNlZHJvb21zIGZvciBtb3JlIGluZm8hAQAAAAEAAAAAAB8AAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAAAAAAEAAAAFX0VOVg==").unwrap()
}),
},
)

View file

@ -1,7 +1,38 @@
use common::data::EXCEL_COLLECTION;
use super::*;
pub async fn on_enter_scene_cs_req(session: &PlayerSession, body: &EnterSceneCsReq) -> Result<()> {
session
.send(CMD_ENTER_SCENE_SC_RSP, EnterSceneScRsp::default())
.await?;
let entrance_config = EXCEL_COLLECTION
.map_entrance_configs
.iter()
.find(|c| c.id == body.entry_id)
.unwrap();
let player = session.player_info();
let enter_scene_by_server = EnterSceneByServerScNotify {
reason: EnterSceneReason::None.into(),
lineup: Some(player.lineup.clone()),
scene: Some(SceneInfo {
plane_id: entrance_config.plane_id,
floor_id: entrance_config.floor_id,
entry_id: entrance_config.id,
game_mode_type: 2, // TODO: EntranceType -> enum repr(u32)
..Default::default()
}),
};
session
.send(CMD_ENTER_SCENE_BY_SERVER_SC_NOTIFY, enter_scene_by_server)
.await
}
pub async fn on_get_cur_scene_info_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
_body: &GetCurSceneInfoCsReq,
) -> Result<()> {
session
@ -26,7 +57,7 @@ pub async fn on_get_cur_scene_info_cs_req(
avatar_type: 3,
base_avatar_id: 1309,
map_layer: 2,
uid: 1337,
uid: session.player_uid(),
}),
motion: Some(MotionInfo {
aomilajjmii: Some(Vector {

View file

@ -8,7 +8,7 @@ static TUTORIAL_IDS: [u32; 55] = [
];
pub async fn on_get_tutorial_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
_body: &GetTutorialCsReq,
) -> Result<()> {
session
@ -29,7 +29,7 @@ pub async fn on_get_tutorial_cs_req(
}
pub async fn on_get_tutorial_guide_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
_body: &GetTutorialGuideCsReq,
) -> Result<()> {
session
@ -44,7 +44,7 @@ pub async fn on_get_tutorial_guide_cs_req(
}
pub async fn on_unlock_tutorial_guide_cs_req(
session: &mut PlayerSession,
session: &PlayerSession,
body: &UnlockTutorialGuideCsReq,
) -> Result<()> {
session

View file

@ -62,13 +62,13 @@ macro_rules! trait_handler {
pub trait CommandHandler {
$(
paste! {
async fn [<on_$name:snake>](session: &mut PlayerSession, body: &$name) -> Result<()> {
async fn [<on_$name:snake>](session: &PlayerSession, body: &$name) -> Result<()> {
[<on_$name:snake>](session, body).await
}
}
)*
async fn on_message(session: &mut PlayerSession, cmd_type: u16, payload: Vec<u8>) -> Result<()> {
async fn on_message(session: &PlayerSession, cmd_type: u16, payload: Vec<u8>) -> Result<()> {
use ::prost::Message;
if PlayerSession::should_send_dummy_rsp(cmd_type) {
session.send_dummy_response(cmd_type).await?;
@ -641,18 +641,18 @@ trait_handler! {
// ExtraLineupDestroyNotify 763;
// GetLineupAvatarDataCsReq 768;
// SwitchLineupIndexScRsp 795;
// JoinLineupCsReq 702;
JoinLineupCsReq 702;
// GetAllLineupDataScRsp 716;
// SetLineupNameCsReq 742;
// ChangeLineupLeaderScRsp 733;
ChangeLineupLeaderCsReq 706;
// ReplaceLineupCsReq 785;
ReplaceLineupCsReq 785;
// SwapLineupCsReq 786;
// QuitLineupScRsp 743;
// GetLineupAvatarDataScRsp 796;
// ReplaceLineupScRsp 756;
// GetStageLineupCsReq 734;
// QuitLineupCsReq 719;
QuitLineupCsReq 719;
// SetLineupNameScRsp 737;
// SwitchLineupIndexCsReq 759;
GetCurLineupDataCsReq 762;
@ -1241,7 +1241,7 @@ trait_handler! {
// SetCurInteractEntityScRsp 1497;
// SceneCastSkillCsReq 1402;
StartCocoonStageCsReq 1408;
// GetSceneMapInfoCsReq 1470;
GetSceneMapInfoCsReq 1470;
// SceneEntityMoveScRsp 1448;
// DeactivateFarmElementScRsp 1467;
// SetCurInteractEntityCsReq 1491;
@ -1263,7 +1263,7 @@ trait_handler! {
// GetSpringRecoverDataCsReq 1466;
// SceneEntityTeleportScRsp 1415;
// SetClientPausedScRsp 1465;
// EnterSceneCsReq 1472;
EnterSceneCsReq 1472;
// GetAllServerPrefsDataScRsp 6148;
// GetServerPrefsDataCsReq 6162;
// UpdateServerPrefsDataScRsp 6109;

View file

@ -1,26 +1,38 @@
use anyhow::Result;
use atomic_refcell::{AtomicRef, AtomicRefCell, AtomicRefMut};
use prost::Message;
use tokio::{io::AsyncWriteExt, net::TcpStream};
use std::sync::Arc;
use tokio::{
io::AsyncWriteExt,
net::TcpStream,
sync::{Mutex, MutexGuard},
};
use crate::game::PlayerInfo;
use super::{packet::CommandHandler, NetPacket};
pub struct PlayerSession {
pub(crate) client_socket: TcpStream,
client_socket: Arc<Mutex<TcpStream>>,
player_info: Arc<AtomicRefCell<PlayerInfo>>,
}
impl PlayerSession {
pub const fn new(client_socket: TcpStream) -> Self {
Self { client_socket }
pub fn new(client_socket: TcpStream) -> Self {
Self {
client_socket: Arc::new(Mutex::new(client_socket)),
player_info: Arc::new(AtomicRefCell::new(PlayerInfo::new())),
}
}
pub async fn run(&mut self) -> Result<()> {
loop {
let net_packet = NetPacket::read(&mut self.client_socket).await?;
let net_packet = NetPacket::read(&mut *self.client_socket().await).await?;
Self::on_message(self, net_packet.cmd_type, net_packet.body).await?;
}
}
pub async fn send(&mut self, cmd_type: u16, body: impl Message) -> Result<()> {
pub async fn send(&self, cmd_type: u16, body: impl Message) -> Result<()> {
let mut buf = Vec::new();
body.encode(&mut buf)?;
@ -31,9 +43,25 @@ impl PlayerSession {
}
.into();
self.client_socket.write_all(&payload).await?;
self.client_socket().await.write_all(&payload).await?;
Ok(())
}
pub async fn client_socket(&self) -> MutexGuard<'_, TcpStream> {
self.client_socket.lock().await
}
pub fn player_uid(&self) -> u32 {
self.player_info().uid
}
pub fn player_info(&self) -> AtomicRef<PlayerInfo> {
self.player_info.borrow()
}
pub fn player_info_mut(&self) -> AtomicRefMut<PlayerInfo> {
self.player_info.borrow_mut()
}
}
// Auto implemented

View file

@ -6,6 +6,7 @@ version.workspace = true
[dependencies]
prost.workspace = true
prost-types.workspace = true
serde.workspace = true
[build-dependencies]
prost-build.workspace = true

View file

@ -1,10 +1,13 @@
use std::path::Path;
pub fn main() {
let proto_file = "StarRail.proto";
if std::path::Path::new(proto_file).exists() {
if Path::new(proto_file).exists() {
println!("cargo:rerun-if-changed={proto_file}");
prost_build::Config::new()
.out_dir("out/")
.type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]")
.compile_protos(&[proto_file], &["."])
.unwrap();
}

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 MiB

After

Width:  |  Height:  |  Size: 2.7 MiB

View file

@ -1,4 +0,0 @@
ASSET_BUNDLE_URL=https://autopatchcn.bhsr.com/asb/BetaLive/output_6744505_89b2f5dc973e
EX_RESOURCE_URL=https://autopatchcn.bhsr.com/design_data/BetaLive/output_6759713_b4e0e740f0da
LUA_URL=https://autopatchcn.bhsr.com/lua/BetaLive/output_6755976_3c46d7c46e2c
LUA_VERSION=6755976

View file

@ -4,15 +4,21 @@ version = "0.1.0"
edition = "2021"
[dependencies]
common.workspace = true
anyhow.workspace = true
env_logger.workspace = true
axum.workspace = true
axum-server.workspace = true
hyper.workspace = true
hyper-util.workspace = true
dirs.workspace = true
dotenv.workspace = true
lazy_static.workspace = true
serde.workspace = true
serde_json.workspace = true
@ -26,6 +32,6 @@ tracing-subscriber.workspace = true
tracing-bunyan-formatter.workspace = true
ansi_term.workspace = true
prost.workspace = true
rbase64.workspace = true
proto.workspace = true
tower.workspace = true
tower-http.workspace = true

4
sdkserver/sdkserver.json Normal file
View file

@ -0,0 +1,4 @@
{
"http_port": 21000,
"dispatch_endpoint": "http://127.0.0.1:21041"
}

23
sdkserver/src/config.rs Normal file
View file

@ -0,0 +1,23 @@
use common::util::load_or_create_config;
use lazy_static::lazy_static;
use serde::Deserialize;
use serde_json::from_str;
const DEFAULT_CONFIG: &str = include_str!("../sdkserver.json");
pub fn init_config() {
let _configuration = &*CONFIGURATION;
}
#[derive(Deserialize)]
pub struct SDKServerConfiguration {
pub http_port: u16,
pub dispatch_endpoint: String,
}
lazy_static! {
pub static ref CONFIGURATION: SDKServerConfiguration = {
let data = load_or_create_config("sdkserver.json", DEFAULT_CONFIG);
from_str(&data).unwrap()
};
}

View file

@ -1,29 +0,0 @@
#[macro_export]
macro_rules! log_error {
($e:expr) => {
if let Err(e) = $e {
tracing::error!(error.message = %format!("{}", &e), "{:?}", e);
}
};
($context:expr, $e:expr $(,)?) => {
if let Err(e) = $e {
let e = format!("{:?}", ::anyhow::anyhow!(e).context($context));
tracing::error!(error.message = %format!("{}", &e), "{:?}", e);
}
};
($ok_context:expr, $err_context:expr, $e:expr $(,)?) => {
if let Err(e) = $e {
let e = format!("{:?}", ::anyhow::anyhow!(e).context($err_context));
tracing::error!(error.message = %format!("{}", &e), "{:?}", e);
} else {
tracing::info!($ok_context);
}
};
}
pub fn init_tracing() {
#[cfg(target_os = "windows")]
ansi_term::enable_ansi_support().unwrap();
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
}

View file

@ -1,33 +1,43 @@
use anyhow::Result;
use axum::body::Body;
use axum::extract::Request;
use axum::routing::{get, post};
use axum::Router;
use logging::init_tracing;
use services::{auth, dispatch, errors};
use std::path::Path;
use axum::{Router, ServiceExt};
use hyper_util::{client::legacy::connect::HttpConnector, rt::TokioExecutor};
use services::{auth, errors};
use tokio::net::TcpListener;
use tower::Layer;
use tower_http::normalize_path::NormalizePathLayer;
use tracing::Level;
mod logging;
type Client = hyper_util::client::legacy::Client<HttpConnector, Body>;
mod config;
mod services;
const PORT: u16 = 21000;
const DEFAULT_DOTENV: &str = include_str!("../.env");
use common::logging::init_tracing;
use config::{init_config, CONFIGURATION};
use services::reverse_proxy;
#[tokio::main]
async fn main() -> Result<()> {
init_tracing();
init_config()?;
init_config();
let span = tracing::span!(Level::DEBUG, "main");
let _ = span.enter();
let router = Router::new()
// For dispatch reverse proxy
let client: Client =
hyper_util::client::legacy::Client::<(), ()>::builder(TokioExecutor::new())
.build(HttpConnector::new());
let app = Router::new()
.route("/query_dispatch", get(reverse_proxy::forward_to_dispatch))
.route(
dispatch::QUERY_DISPATCH_ENDPOINT,
get(dispatch::query_dispatch),
)
.route(
dispatch::QUERY_GATEWAY_ENDPOINT,
get(dispatch::query_gateway),
"/query_gateway/:region_name",
get(reverse_proxy::forward_to_dispatch),
)
.route(auth::RISKY_API_CHECK_ENDPOINT, post(auth::risky_api_check))
.route(
@ -42,36 +52,16 @@ async fn main() -> Result<()> {
auth::GRANTER_LOGIN_VERIFICATION_ENDPOINT,
post(auth::granter_login_verification),
)
.fallback(errors::not_found);
.fallback(errors::not_found)
.with_state(client);
let addr = format!("0.0.0.0:{PORT}");
let server = axum_server::bind(addr.parse()?);
let app = NormalizePathLayer::trim_trailing_slash().layer(app);
let addr = format!("0.0.0.0:{}", CONFIGURATION.http_port);
let server = TcpListener::bind(&addr).await?;
tracing::info!("sdkserver is listening at {addr}");
server.serve(router.into_make_service()).await?;
Ok(())
}
fn init_config() -> Result<()> {
let local_dotenv = Path::new(".env");
if local_dotenv.exists() {
dotenv::dotenv()?;
} else {
let config = dirs::config_dir()
.ok_or_else(|| anyhow::anyhow!("No config directory found"))?
.join("hkrpg-sdkserver");
std::fs::create_dir_all(&config)?;
let env = config.join(".env");
if !env.exists() {
std::fs::write(&env, DEFAULT_DOTENV)?;
}
dotenv::from_path(&env)?;
}
axum::serve(server, ServiceExt::<Request>::into_make_service(app)).await?;
Ok(())
}

View file

@ -1,7 +1,7 @@
use axum::Json;
use serde_json::json;
pub const LOGIN_WITH_PASSWORD_ENDPOINT: &str = "/:product_name/mdk/shield/api/login/";
pub const LOGIN_WITH_PASSWORD_ENDPOINT: &str = "/:product_name/mdk/shield/api/login";
pub const LOGIN_WITH_SESSION_TOKEN_ENDPOINT: &str = "/:product_name/mdk/shield/api/verify";
pub const GRANTER_LOGIN_VERIFICATION_ENDPOINT: &str = "/:product_name/combo/granter/login/v2/login";
pub const RISKY_API_CHECK_ENDPOINT: &str = "/account/risky/api/check";
@ -12,7 +12,7 @@ pub async fn login_with_password() -> Json<serde_json::Value> {
"data": {
"account": {
"area_code": "**",
"email": "ReversedRooms",
"email": "AcheronSR",
"country": "RU",
"is_email_verify": "1",
"token": "mostsecuretokenever",
@ -34,7 +34,7 @@ pub async fn login_with_session_token() -> Json<serde_json::Value> {
"data": {
"account": {
"area_code": "**",
"email": "ReversedRooms",
"email": "AcheronSR",
"country": "RU",
"is_email_verify": "1",
"token": "mostsecuretokenever",

View file

@ -1,61 +0,0 @@
use std::env;
use prost::Message;
use proto::{Dispatch, Gateserver, RegionInfo};
pub const QUERY_DISPATCH_ENDPOINT: &str = "/query_dispatch";
pub const QUERY_GATEWAY_ENDPOINT: &str = "/query_gateway";
#[tracing::instrument]
pub async fn query_dispatch() -> String {
let rsp = Dispatch {
retcode: 0,
region_list: vec![RegionInfo {
name: String::from("RobinSR"),
title: String::from("RobinSR"),
env_type: String::from("9"),
dispatch_url: String::from("http://127.0.0.1:21000/query_gateway"),
..Default::default()
}],
..Default::default()
};
let mut buff = Vec::new();
rsp.encode(&mut buff).unwrap();
rbase64::encode(&buff)
}
#[tracing::instrument]
pub async fn query_gateway() -> String {
let rsp = Gateserver {
retcode: 0,
ip: String::from("127.0.0.1"),
port: 23301,
asset_bundle_url: env::var("ASSET_BUNDLE_URL").unwrap(),
ex_resource_url: env::var("EX_RESOURCE_URL").unwrap(),
lua_url: env::var("LUA_URL").unwrap(),
lua_version: env::var("LUA_VERSION").unwrap(),
ifix_version: String::from("0"),
jblkncaoiao: true,
hjdjakjkdbi: true,
ldknmcpffim: true,
feehapamfci: true,
eebfeohfpph: true,
dfmjjcfhfea: true,
najikcgjgan: true,
giddjofkndm: true,
fbnbbembcgn: false,
dedgfjhbnok: false,
use_tcp: true,
linlaijbboh: false,
ahmbfbkhmgh: false,
nmdccehcdcc: false,
..Default::default()
};
let mut buff = Vec::new();
rsp.encode(&mut buff).unwrap();
rbase64::encode(&buff)
}

View file

@ -1,3 +1,3 @@
pub mod auth;
pub mod dispatch;
pub mod errors;
pub mod reverse_proxy;

View file

@ -0,0 +1,33 @@
use axum::{
body::Body,
extract::{Request, State},
http::uri::{PathAndQuery, Uri},
response::{IntoResponse, Response},
};
use hyper::StatusCode;
use hyper_util::client::legacy::connect::HttpConnector;
use crate::config::CONFIGURATION;
type Client = hyper_util::client::legacy::Client<HttpConnector, Body>;
pub async fn forward_to_dispatch(
State(client): State<Client>,
mut req: Request,
) -> Result<Response, StatusCode> {
let path = req.uri().path();
let path_query = req
.uri()
.path_and_query()
.map_or(path, PathAndQuery::as_str);
let uri = format!("{}{}", CONFIGURATION.dispatch_endpoint, path_query);
*req.uri_mut() = Uri::try_from(uri).unwrap();
Ok(client
.request(req)
.await
.map_err(|_| StatusCode::BAD_REQUEST)?
.into_response())
}

7
xtask/Cargo.toml Normal file
View file

@ -0,0 +1,7 @@
[package]
name = "xtask"
edition = "2021"
version.workspace = true
[dependencies]
notify = "6.1.1"

112
xtask/src/main.rs Normal file
View file

@ -0,0 +1,112 @@
use std::{io, process::Command, sync::mpsc, thread};
fn print_help() {
println!(
"
xtask must specify a task to run.
Usage: `cargo xtask <task>`
Tasks:
run
Run the gameserver and sdkserver.
watch
Watch for changes in the project and restart the servers if any file changes.
"
);
}
// run gameserver and sdkserver, wait till any of them exit
fn spawn_servers(release: bool) -> Result<(), Box<dyn std::error::Error>> {
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
let handle1 = thread::spawn(move || {
let mut gameserver = Command::new("cargo")
.arg("run")
.arg("--bin")
.arg("gameserver")
.args(if release { vec!["--release"] } else { vec![] })
.spawn()
.expect("failed to start gameserver");
gameserver.wait()?;
tx1.send(()).expect("failed to send completion signal");
Ok::<(), io::Error>(())
});
let tx2 = tx.clone();
let handle2 = thread::spawn(move || {
let mut sdkserver = Command::new("cargo")
.arg("run")
.arg("--bin")
.arg("sdkserver")
.args(if release { vec!["--release"] } else { vec![] })
.spawn()
.expect("failed to start sdkserver");
let _ = sdkserver.wait()?;
tx2.send(()).expect("failed to send completion signal");
Ok::<(), io::Error>(())
});
let handle3 = thread::spawn(move || {
let mut dispatch = Command::new("cargo")
.arg("run")
.arg("--bin")
.arg("dispatch")
.args(if release { vec!["--release"] } else { vec![] })
.spawn()
.expect("failed to start dispatch");
let _ = dispatch.wait()?;
tx.send(()).expect("failed to send completion signal");
Ok::<(), io::Error>(())
});
rx.recv().expect("failed to receive from channel");
handle1.join().expect("failed to join gameserver thread")?;
handle2.join().expect("failed to join sdkserver thread")?;
handle3.join().expect("failed to join dispatch thread")?;
Ok(())
}
// watch for changes in the project and restart the servers if any file changes
fn watch(release: bool) -> Result<(), Box<dyn std::error::Error>> {
let mut cmd = std::process::Command::new("cargo");
cmd.arg("watch").arg("-x").arg(format!(
"xtask run {}",
if release { "--release" } else { "" }
));
let mut child = cmd.spawn()?;
child.wait()?;
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let Some(task) = std::env::args().nth(1) else {
print_help();
std::process::exit(0);
};
let release = std::env::args().any(|arg| arg == "--release");
match task.as_str() {
"run" => spawn_servers(release)?,
"watch" => watch(release)?,
_ => {
println!("invalid task: `{task}`, run `cargo xtask` for a list of tasks");
std::process::exit(1);
}
}
Ok(())
}