Split SDK and Dispatch into different servers

This commit is contained in:
xeon 2024-04-16 00:00:57 +03:00
parent 9624a07ad2
commit da466f961b
26 changed files with 363 additions and 191 deletions

1
.gitignore vendored
View file

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

View file

@ -1,5 +1,5 @@
[workspace]
members = ["common", "gameserver", "proto", "sdkserver", "xtask"]
members = ["common", "dispatch", "gameserver", "proto", "sdkserver", "xtask"]
resolver = "2"
[workspace.package]
@ -15,6 +15,8 @@ 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"

View file

@ -18,8 +18,9 @@ A Server emulator for the game [`Honkai: Star Rail`](https://hsr.hoyoverse.com/e
```sh
git clone https://git.xeondev.com/reversedrooms/AcheronSR.git
cd AcheronSR
cargo install --path gameserver
cargo install --path sdkserver
cargo build --bin gameserver
cargo build --bin dispatch
cargo build --bin sdkserver
```
##### Using xtasks (use this if stupid)
@ -43,23 +44,18 @@ page and download the latest release for your platform.
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),

View file

@ -4,6 +4,9 @@ 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

View file

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

View file

@ -1,8 +1,9 @@
pub fn load_or_create_config(path: &str, defaults: &str) -> String {
if let Ok(data) = std::fs::read_to_string(path) {
data
} else {
std::fs::read_to_string(path).map_or_else(
|_| {
std::fs::write(path, defaults).unwrap();
defaults.to_string()
}
},
|data| data,
)
}

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
.iter()
.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: server_config.gateserver_port as u32,
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,13 +1,12 @@
use anyhow::Result;
mod game;
mod logging;
mod net;
mod util;
use common::data::init_assets;
use common::logging::init_tracing;
use game::init_config;
use logging::init_tracing;
#[tokio::main]
async fn main() -> Result<()> {

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

@ -11,6 +11,8 @@ env_logger.workspace = true
axum.workspace = true
axum-server.workspace = true
hyper.workspace = true
hyper-util.workspace = true
dirs.workspace = true
dotenv.workspace = true
@ -30,8 +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,7 +0,0 @@
mod version_config;
pub use version_config::INSTANCE as versions;
pub fn init_config() {
tracing::info!("loaded {} version configs", versions.len());
}

View file

@ -1,23 +0,0 @@
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_VERSIONS: &str = include_str!("../../versions.json");
#[derive(Deserialize)]
pub struct VersionConfig {
pub asset_bundle_url: String,
pub ex_resource_url: String,
pub lua_url: String,
pub lua_version: String,
}
lazy_static! {
pub static ref INSTANCE: HashMap<String, VersionConfig> = {
let data = load_or_create_config("versions.json", DEFAULT_VERSIONS);
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,21 +1,24 @@
use anyhow::Result;
use axum::body::Body;
use axum::extract::Request;
use axum::routing::{get, post};
use axum::{Router, ServiceExt};
use services::{auth, dispatch, errors};
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;
type Client = hyper_util::client::legacy::Client<HttpConnector, Body>;
mod config;
mod logging;
mod services;
use config::init_config;
use logging::init_tracing;
use common::logging::init_tracing;
const PORT: u16 = 21000;
use config::{init_config, CONFIGURATION};
use services::reverse_proxy;
#[tokio::main]
async fn main() -> Result<()> {
@ -25,14 +28,16 @@ async fn main() -> Result<()> {
let span = tracing::span!(Level::DEBUG, "main");
let _ = span.enter();
// 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(
@ -47,11 +52,12 @@ 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 app = NormalizePathLayer::trim_trailing_slash().layer(app);
let addr = format!("0.0.0.0:{PORT}");
let addr = format!("0.0.0.0:{}", CONFIGURATION.http_port);
let server = TcpListener::bind(&addr).await?;
tracing::info!("sdkserver is listening at {addr}");

View file

@ -1,75 +0,0 @@
use crate::config::versions;
use axum::extract::Query;
use prost::Message;
use proto::{Dispatch, Gateserver, RegionInfo};
use serde::Deserialize;
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("AcheronSR"),
title: String::from("AcheronSR"),
env_type: String::from("2"),
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)
}
#[derive(Deserialize, Debug)]
pub struct QueryGatewayParameters {
pub version: String,
}
#[tracing::instrument]
pub async fn query_gateway(parameters: Query<QueryGatewayParameters>) -> String {
let rsp = if let Some(config) = versions.get(&parameters.version) {
Gateserver {
retcode: 0,
ip: String::from("127.0.0.1"),
port: 23301,
asset_bundle_url: config.asset_bundle_url.clone(),
ex_resource_url: config.ex_resource_url.clone(),
lua_url: config.lua_url.clone(),
lua_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,
fbnbbembcgn: false,
dedgfjhbnok: false,
use_tcp: true,
linlaijbboh: false,
ahmbfbkhmgh: false,
nmdccehcdcc: false,
..Default::default()
}
} else {
Gateserver {
retcode: 9,
msg: format!("forbidden version: {} or invalid bind", parameters.version),
..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())
}

View file

@ -1,20 +0,0 @@
{
"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"
}
}

View file

@ -36,6 +36,7 @@ fn spawn_servers(release: bool) -> Result<(), Box<dyn std::error::Error>> {
Ok::<(), io::Error>(())
});
let tx2 = tx.clone();
let handle2 = thread::spawn(move || {
let mut sdkserver = Command::new("cargo")
.arg("run")
@ -46,6 +47,21 @@ fn spawn_servers(release: bool) -> Result<(), Box<dyn std::error::Error>> {
.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>(())
@ -55,6 +71,7 @@ fn spawn_servers(release: bool) -> Result<(), Box<dyn std::error::Error>> {
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(())
}