First push

This commit is contained in:
xeon 2024-03-29 09:46:31 +03:00
parent 46a868ca85
commit c0cdf91849
32 changed files with 39006 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
target/
Cargo.lock
proto/StarRail.proto

62
Cargo.toml Normal file
View file

@ -0,0 +1,62 @@
[workspace]
members = [ "gameserver", "proto", "sdkserver"]
resolver = "2"
[workspace.package]
version = "0.1.0"
[workspace.dependencies]
anyhow = "1.0.81"
ansi_term = "0.12.1"
atomic_refcell = "0.1.13"
lazy_static = "1.4.0"
axum = "0.7.4"
axum-server = "0.6.0"
env_logger = "0.11.3"
rbase64 = "2.0.3"
rand = "0.8.5"
rsa = { version = "0.9.6", features = [
"sha1",
"nightly",
"pkcs5",
"serde",
"sha2",
] }
prost = "0.12.3"
prost-types = "0.12.3"
prost-build = "0.12.3"
paste = "1.0.14"
sysinfo = "0.30.7"
hex = "0.4.3"
serde = { version = "1.0.197", features = ["derive"] }
serde_json = "1.0.114"
tokio = { version = "1.36.0", features = ["full"] }
tokio-util = { version = "0.7.10", features = ["io"] }
tracing = "0.1.40"
tracing-futures = "0.2.5"
tracing-log = { version = "0.2.0", features = ["std", "log-tracer"] }
tracing-subscriber = { version = "0.3.18", features = [
"env-filter",
"registry",
"std",
"tracing",
"tracing-log",
] }
tracing-bunyan-formatter = "0.3.9"
proto = { path = "proto/" }
[profile.release]
strip = true # Automatically strip symbols from the binary.
lto = true # Link-time optimization.
opt-level = 3 # Optimize for speed.
codegen-units = 1 # Maximum size reduction optimizations.

30
gameserver/Cargo.toml Normal file
View file

@ -0,0 +1,30 @@
[package]
name = "gameserver"
edition = "2021"
version.workspace = true
[dependencies]
ansi_term.workspace = true
anyhow.workspace = true
atomic_refcell.workspace = true
env_logger.workspace = true
hex.workspace = true
lazy_static.workspace = true
paste.workspace = true
rbase64.workspace = true
sysinfo.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
prost.workspace = true
proto.workspace = true

29
gameserver/src/logging.rs Normal file
View file

@ -0,0 +1,29 @@
#[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"));
}

15
gameserver/src/main.rs Normal file
View file

@ -0,0 +1,15 @@
use anyhow::Result;
mod logging;
mod net;
mod util;
use logging::init_tracing;
#[tokio::main]
async fn main() -> Result<()> {
init_tracing();
net::gateway::listen("0.0.0.0", 23301).await?;
Ok(())
}

View file

@ -0,0 +1,28 @@
use anyhow::Result;
use tokio::net::TcpListener;
use tracing::{info_span, Instrument};
use crate::{log_error, net::PlayerSession};
pub async fn listen(host: &str, port: u16) -> Result<()> {
let listener = TcpListener::bind(format!("{host}:{port}")).await?;
tracing::info!("Listening at {host}:{port}");
loop {
let Ok((client_socket, client_addr)) = listener.accept().await else {
continue;
};
let mut session = PlayerSession::new(client_socket);
tokio::spawn(
async move {
log_error!(
"Session from {client_addr} disconnected",
format!("An error occurred while processing session ({client_addr})"),
Box::pin(session.run()).await
);
}
.instrument(info_span!("session", addr = %client_addr)),
);
}
}

View file

@ -0,0 +1,44 @@
use anyhow::Result;
use proto::*;
use crate::{net::PlayerSession, util};
pub async fn on_player_get_token_cs_req(
session: &mut PlayerSession,
_body: &PlayerGetTokenCsReq,
) -> Result<()> {
session
.send(
CMD_PLAYER_GET_TOKEN_SC_RSP,
PlayerGetTokenScRsp {
retcode: 0,
msg: String::from("OK"),
uid: 1337,
..Default::default()
},
)
.await
}
pub async fn on_player_login_cs_req(
session: &mut PlayerSession,
body: &PlayerLoginCsReq,
) -> Result<()> {
session
.send(
CMD_PLAYER_LOGIN_SC_RSP,
PlayerLoginScRsp {
login_random: body.login_random,
server_timestamp_ms: util::cur_timestamp_ms(),
stamina: 240,
basic_info: Some(PlayerBasicInfo {
nickname: String::from("xeondev"),
level: 5,
stamina: 240,
..Default::default()
}),
..Default::default()
},
)
.await
}

View file

@ -0,0 +1,34 @@
use super::*;
static UNLOCKED_AVATARS: [u32; 49] = [
8001, 1001, 1002, 1003, 1004, 1005, 1006, 1008, 1009, 1013, 1101, 1102, 1103, 1104, 1105, 1106,
1107, 1108, 1109, 1110, 1111, 1112, 1201, 1202, 1203, 1204, 1205, 1206, 1207, 1208, 1209, 1210,
1211, 1212, 1213, 1214, 1215, 1217, 1301, 1302, 1303, 1304, 1305, 1306, 1307, 1308, 1309, 1312,
1315,
];
pub async fn on_get_avatar_data_cs_req(
session: &mut PlayerSession,
body: &GetAvatarDataCsReq,
) -> Result<()> {
session
.send(
CMD_GET_AVATAR_DATA_SC_RSP,
GetAvatarDataScRsp {
retcode: 0,
is_all: body.is_get_all,
avatar_list: UNLOCKED_AVATARS
.iter()
.map(|id| Avatar {
base_avatar_id: *id,
level: 80,
promotion: 6,
rank: 6,
..Default::default()
})
.collect(),
..Default::default()
},
)
.await
}

View file

@ -0,0 +1,65 @@
use super::*;
static BATTLE_LINEUP: [u32; 4] = [1309, 1308, 1307, 1315];
pub async fn on_start_cocoon_stage_cs_req(
session: &mut PlayerSession,
body: &StartCocoonStageCsReq,
) -> Result<()> {
let rsp = StartCocoonStageScRsp {
retcode: 0,
prop_entity_id: body.prop_entity_id,
cocoon_id: body.cocoon_id,
wave: body.wave,
battle_info: Some(SceneBattleInfo {
stage_id: 201012311,
logic_random_seed: 4444,
battle_id: 1,
battle_avatar_list: BATTLE_LINEUP
.iter()
.enumerate()
.map(|(idx, id)| BattleAvatar {
index: idx as u32,
id: *id,
level: 80,
promotion: 6,
rank: 6,
hp: 10000,
avatar_type: 3,
sp: Some(AmountInfo {
cur_amount: 10000,
max_amount: 10000,
}),
..Default::default()
})
.collect(),
monster_wave_list: vec![SceneMonsterWave {
monster_list: vec![SceneMonsterParam {
monster_id: 3013010,
..Default::default()
}],
..Default::default()
}],
..Default::default()
}),
};
session.send(CMD_START_COCOON_STAGE_SC_RSP, rsp).await
}
pub async fn on_pve_battle_result_cs_req(
session: &mut PlayerSession,
body: &PveBattleResultCsReq,
) -> Result<()> {
session
.send(
CMD_P_V_E_BATTLE_RESULT_SC_RSP,
PveBattleResultScRsp {
retcode: 0,
end_status: body.end_status,
battle_id: body.battle_id,
..Default::default()
},
)
.await
}

View file

@ -0,0 +1,89 @@
use super::*;
static STARTING_LINEUP: [u32; 4] = [1309, 1308, 1307, 1315];
pub async fn on_get_all_lineup_data_cs_req(
session: &mut PlayerSession,
_body: &GetAllLineupDataCsReq,
) -> Result<()> {
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: STARTING_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()
}],
},
)
.await
}
pub async fn on_get_cur_lineup_data_cs_req(
session: &mut PlayerSession,
_body: &GetCurLineupDataCsReq,
) -> Result<()> {
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: STARTING_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()
}),
},
)
.await
}
pub async fn on_change_lineup_leader_cs_req(
session: &mut PlayerSession,
body: &ChangeLineupLeaderCsReq,
) -> Result<()> {
session
.send(
CMD_CHANGE_LINEUP_LEADER_SC_RSP,
ChangeLineupLeaderScRsp {
slot: body.slot,
retcode: 0,
},
)
.await
}

View file

@ -0,0 +1,72 @@
use super::*;
static FINISHED_MAIN_MISSIONS: [u32; 365] = [
1000101, 1000111, 1000112, 1000113, 1000114, 1000201, 1000202, 1000203, 1000204, 1000300,
1000301, 1000302, 1000303, 1000304, 1000400, 1000401, 1000402, 1000410, 1000500, 1000501,
1000502, 1000503, 1000504, 1000505, 1000510, 1000511, 1010001, 1010002, 1010101, 1010201,
1010202, 1010203, 1010204, 1010205, 1010206, 1010301, 1010302, 1010303, 1010401, 1010405,
1010402, 1010403, 1010500, 1010501, 1010502, 1010503, 1010601, 1010602, 1010700, 1010701,
1010801, 1010802, 1010901, 1010902, 1011001, 1011002, 1011003, 1011100, 1011101, 1011102,
1011103, 1011201, 1011202, 1011301, 1011400, 1011401, 1011402, 1011403, 1011501, 1011502,
1011503, 1020101, 1020201, 1020302, 1020301, 1020400, 1020401, 1020402, 1020403, 1020501,
1020601, 1020701, 1020702, 1020801, 1020901, 1021001, 1021101, 1021201, 1021301, 1021401,
1021501, 1021601, 1021702, 1021703, 1030101, 1030102, 1030201, 1030202, 1030301, 1030302,
1030303, 1030304, 1030401, 1030402, 1030403, 1030501, 1030601, 1030701, 1030702, 1030801,
2000001, 2000002, 2000003, 2000004, 2000100, 2000101, 2000131, 2000132, 2000133, 2000110,
2000111, 2000301, 2000103, 2000112, 2000108, 2000104, 2000102, 2000105, 2000106, 2000107,
2000313, 2000314, 2000109, 2000113, 2000116, 2000118, 2000119, 2000120, 2000122, 2000302,
2000303, 2000304, 2000305, 2000310, 2000311, 2000312, 2000320, 2000701, 2000702, 2000703,
2000704, 2000705, 2000706, 2000707, 2000801, 2000802, 2000803, 2000901, 2000902, 2000903,
2001001, 2001002, 2001003, 2010005, 2010301, 2010302, 2011103, 2011104, 2011409, 2010401,
2010402, 2010405, 2010502, 2010503, 2010701, 2010708, 2010709, 2010720, 2010730, 2010731,
2010732, 2010733, 2010734, 2010735, 2010904, 2011101, 2011102, 2011105, 2011301, 2011302,
2011303, 2011501, 2011502, 2010909, 2010910, 2011601, 2011701, 2011801, 2011901, 2011902,
2011903, 2011904, 2011905, 2011906, 2020301, 2020302, 2020304, 2020316, 2020317, 2020318,
2020319, 2020401, 2020402, 2020403, 2020404, 2020405, 2020406, 2020407, 2020303, 2020103,
2020104, 2020105, 2020106, 2020107, 2020108, 2020109, 2020110, 2020111, 2020201, 2020202,
2020203, 2020204, 2020205, 2000201, 2000202, 2000203, 2000204, 2000205, 2000206, 2000207,
2000208, 2000209, 2000211, 2000212, 2010201, 2010202, 2010203, 2010204, 2010205, 2010206,
2010500, 2010501, 2010705, 2010706, 2010901, 2010902, 2010903, 2010702, 2010703, 2011400,
2011401, 2011406, 2011402, 2011403, 2011404, 2011405, 2011407, 2011408, 2011410, 2011411,
2011412, 2011413, 2010905, 2010906, 2010907, 2010908, 2010911, 2010912, 2020305, 2020306,
2020309, 2020307, 2020308, 2020701, 2020702, 2020703, 2020313, 2020314, 2020315, 6020101,
6020201, 6020202, 2020501, 2020502, 2020503, 2020504, 2020505, 2020506, 2020507, 2020601,
2020602, 2020603, 2020604, 2020801, 2020802, 2020901, 2021001, 2021002, 2021009, 2021601,
2021602, 2021701, 2021702, 2021703, 2021704, 2021705, 2021801, 2021802, 2021803, 2030001,
2030002, 2030003, 2030101, 2030102, 2030201, 2030202, 2030203, 2030301, 2030302, 3000201,
3000202, 3000203, 3000211, 3000212, 3000213, 3000301, 3000302, 3000303, 3000522, 3000523,
3000524, 3000525, 3000526, 3000527, 3000601, 3000602, 3000603, 3000604, 3000701, 3000702,
3000703, 3000704, 3000705, 3000800, 3000801, 3000802, 3000803, 3010102, 3010103, 3010104,
3010105, 3010201, 3010202, 3010203, 3010204,
];
pub async fn on_get_mission_status_cs_req(
session: &mut PlayerSession,
body: &GetMissionStatusCsReq,
) -> Result<()> {
let rsp = GetMissionStatusScRsp {
retcode: 0,
finished_main_mission_id_list: FINISHED_MAIN_MISSIONS.to_vec(),
sub_mission_status_list: body
.main_mission_id_list
.iter()
.map(|id| Mission {
id: *id,
progress: 0,
status: MissionStatus::MissionFinish.into(),
})
.collect(),
mission_event_status_list: body
.mission_event_id_list
.iter()
.map(|id| Mission {
id: *id,
progress: 0,
status: MissionStatus::MissionFinish.into(),
})
.collect(),
..Default::default()
};
session.send(CMD_GET_MISSION_STATUS_SC_RSP, rsp).await
}

View file

@ -0,0 +1,119 @@
mod authentication;
mod avatar;
mod battle;
mod lineup;
mod mission;
mod player;
mod scene;
mod tutorial;
use anyhow::Result;
use paste::paste;
use proto::*;
use tokio::io::AsyncWriteExt;
use super::PlayerSession;
use crate::net::NetPacket;
pub use authentication::*;
pub use avatar::*;
pub use battle::*;
pub use lineup::*;
pub use mission::*;
pub use player::*;
pub use scene::*;
pub use tutorial::*;
use proto::{
Aaihejacdpk::*, Achkcddkkkj::*, Bancodieeof::*, CmdActivityType::*, CmdBattleType::*,
CmdItemType::*, CmdPlayerType::*, Cmpepmnekko::*, Cpbdjpocnai::*, Ddhbjcelmjp::*,
Eegmjpcijbc::*, Emhbkpkpjpa::*, Fdkapmfjgjl::*, Gaifgoihffa::*, Galijhmhgcg::*, Gdjpnkniijf::*,
Hfjpennlffa::*, Hmnbojnkleh::*, Ieoildlcdkb::*, Kfmpmaojchm::*, Lopidcokdih::*, Lpegmiilfjm::*,
Mbnnmfkffbo::*, Mkeclbphcol::*, Niinikapdpg::*, Pfokmnnfiap::*, Pjmghcfmmge::*, Pnjfenbhbhg::*,
Pnnbhogkeeh::*,
};
macro_rules! dummy {
($($cmd:ident),* $(,)*) => {
paste! {
impl PlayerSession {
pub const fn should_send_dummy_rsp(cmd_id: u16) -> bool {
match cmd_id {
$(
x if x == [<Cmd $cmd CsReq>] as u16 => true,
)*
_ => false,
}
}
pub async fn send_dummy_response(&mut self, req_id: u16) -> Result<()> {
let cmd_type = match req_id {
$(
x if x == [<Cmd $cmd CsReq>] as u16 => [<Cmd $cmd ScRsp>] as u16,
)*
_ => return Err(anyhow::anyhow!("Invalid request id {req_id:?}")),
};
let payload: Vec<u8> = NetPacket {
cmd_type,
head: Vec::new(),
body: Vec::new(),
}
.into();
self.client_socket.write_all(&payload).await?;
Ok(())
}
}
}
};
}
dummy! {
SceneEntityMove,
GetLevelRewardTakenList,
GetRogueScoreRewardInfo,
GetGachaInfo,
QueryProductInfo,
GetQuestData,
GetQuestRecord,
GetFriendListInfo,
GetFriendApplyListInfo,
GetCurAssist,
GetRogueHandbookData,
GetDailyActiveInfo,
GetFightActivityData,
GetMultipleDropInfo,
GetPlayerReturnMultiDropInfo,
GetShareData,
GetTreasureDungeonActivityData,
PlayerReturnInfoQuery,
GetBag,
GetPlayerBoardData,
GetActivityScheduleConfig,
GetMissionData,
GetMissionEventData,
GetChallenge,
GetCurChallenge,
GetRogueInfo,
GetExpeditionData,
GetRogueDialogueEventData,
GetJukeboxData,
SyncClientResVersion,
DailyFirstMeetPam,
GetMuseumInfo,
GetLoginActivity,
GetRaidInfo,
GetTrialActivityData,
GetBoxingClubInfo,
GetNpcStatus,
TextJoinQuery,
GetSpringRecoverData,
GetChatFriendHistory,
GetSecretKeyInfo,
GetVideoVersionKey,
GetCurBattleInfo,
GetPhoneData,
PlayerLoginFinish,
}

View file

@ -0,0 +1,61 @@
use crate::util;
use super::*;
pub async fn on_get_basic_info_cs_req(
session: &mut PlayerSession,
_body: &GetBasicInfoCsReq,
) -> Result<()> {
session
.send(
CMD_GET_BASIC_INFO_SC_RSP,
GetBasicInfoScRsp {
retcode: 0,
player_setting_info: Some(PlayerSettingInfo::default()),
..Default::default()
},
)
.await
}
pub async fn on_get_hero_basic_type_info_cs_req(
session: &mut PlayerSession,
_body: &GetHeroBasicTypeInfoCsReq,
) -> Result<()> {
session
.send(
CMD_GET_HERO_BASIC_TYPE_INFO_SC_RSP,
GetHeroBasicTypeInfoScRsp {
retcode: 0,
gender: Gender::Man.into(),
cur_basic_type: HeroBasicType::BoyWarrior.into(),
basic_type_info_list: vec![HeroBasicTypeInfo {
basic_type: HeroBasicType::BoyWarrior.into(),
..Default::default()
}],
..Default::default()
},
)
.await
}
pub async fn on_player_heart_beat_cs_req(
session: &mut PlayerSession,
body: &PlayerHeartBeatCsReq,
) -> Result<()> {
session
.send(
CMD_PLAYER_HEART_BEAT_SC_RSP,
PlayerHeartBeatScRsp {
retcode: 0,
client_time_ms: body.client_time_ms,
server_time_ms: util::cur_timestamp_ms(),
download_data: Some(ClientDownloadData {
version: 51,
time: util::cur_timestamp_ms() as i64,
data: rbase64::decode("G0x1YVMBGZMNChoKBAQICHhWAAAAAAAAAAAAAAAod0ABKEBDOlxVc2Vyc1x4ZW9uZGV2XERvd25sb2Fkc1xyYWJzdHZvLmx1YQAAAAAAAAAAAAEEEAAAACQAQAApQEAAKYBAACnAQABWAAEALIAAAR1AQQCkgEEA5ABAAOnAwQHpAMIB6UDCAawAAAEsgAAAH8BChRkAgAAMAAAABANDUwQMVW5pdHlFbmdpbmUEC0dhbWVPYmplY3QEBUZpbmQEKVVJUm9vdC9BYm92ZURpYWxvZy9CZXRhSGludERpYWxvZyhDbG9uZSkEF0dldENvbXBvbmVudEluQ2hpbGRyZW4EB3R5cGVvZgQEUlBHBAdDbGllbnQEDkxvY2FsaXplZFRleHQEBXRleHQURVJvYmluU1IgaXMgYSBmcmVlIGFuZCBvcGVuIHNvdXJjZSBzb2Z0d2FyZS4gZGlzY29yZC5nZy9yZXZlcnNlZHJvb21zAQAAAAEAAAAAABAAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAAAAAAEAAAAFX0VOVg==").unwrap()
}),
},
)
.await
}

View file

@ -0,0 +1,79 @@
use super::*;
pub async fn on_get_cur_scene_info_cs_req(
session: &mut PlayerSession,
_body: &GetCurSceneInfoCsReq,
) -> Result<()> {
session
.send(
CMD_GET_CUR_SCENE_INFO_SC_RSP,
GetCurSceneInfoScRsp {
retcode: 0,
scene: Some(SceneInfo {
plane_id: 20101,
floor_id: 20101001,
entry_id: 2010101,
game_mode_type: 2,
chhmmbdhjpg: vec![
Dhkacjhaoid {
state: 1,
group_id: 0,
entity_list: vec![SceneEntityInfo {
group_id: 0,
inst_id: 0,
entity_id: 0,
actor: Some(SceneActorInfo {
avatar_type: 3,
base_avatar_id: 1309,
map_layer: 2,
uid: 1337,
}),
motion: Some(MotionInfo {
aomilajjmii: Some(Vector {
bagloppgnpb: 4480,
bemlopmcgch: 19364,
baimdminomk: -550,
}),
eiaoiankefd: Some(Vector {
bagloppgnpb: 4480,
bemlopmcgch: 19364,
baimdminomk: -550,
}),
}),
..Default::default()
}],
},
Dhkacjhaoid {
state: 1,
group_id: 19,
entity_list: vec![SceneEntityInfo {
group_id: 19,
inst_id: 300001,
entity_id: 228,
prop: Some(ScenePropInfo {
prop_id: 808,
prop_state: 1,
..Default::default()
}),
motion: Some(MotionInfo {
aomilajjmii: Some(Vector {
bagloppgnpb: 4480,
bemlopmcgch: 19364,
baimdminomk: -570,
}),
eiaoiankefd: Some(Vector {
bagloppgnpb: 4480,
bemlopmcgch: 19364,
baimdminomk: -570,
}),
}),
..Default::default()
}],
},
],
..Default::default()
}),
},
)
.await
}

View file

@ -0,0 +1,62 @@
use super::*;
static TUTORIAL_IDS: [u32; 55] = [
1001, 1002, 1003, 1004, 1005, 1007, 1008, 1010, 1011, 2001, 2002, 2003, 2004, 2005, 2008, 2009,
2010, 2011, 2012, 2013, 2014, 2015, 3001, 3002, 3003, 3004, 3005, 3006, 4002, 4003, 4004, 4005,
4006, 4007, 4008, 4009, 5001, 5002, 5003, 5004, 5005, 5006, 5007, 5008, 5009, 5010, 5011, 5012,
7001, 9001, 9002, 9003, 9004, 9005, 9006,
];
pub async fn on_get_tutorial_cs_req(
session: &mut PlayerSession,
_body: &GetTutorialCsReq,
) -> Result<()> {
session
.send(
CMD_GET_TUTORIAL_SC_RSP,
GetTutorialScRsp {
retcode: 0,
tutorial_list: TUTORIAL_IDS
.iter()
.map(|id| Tutorial {
id: *id,
status: TutorialStatus::TutorialFinish.into(),
})
.collect(),
},
)
.await
}
pub async fn on_get_tutorial_guide_cs_req(
session: &mut PlayerSession,
_body: &GetTutorialGuideCsReq,
) -> Result<()> {
session
.send(
CMD_GET_TUTORIAL_GUIDE_SC_RSP,
GetTutorialGuideScRsp {
retcode: 0,
tutorial_guide_list: vec![],
},
)
.await
}
pub async fn on_unlock_tutorial_guide_cs_req(
session: &mut PlayerSession,
body: &UnlockTutorialGuideCsReq,
) -> Result<()> {
session
.send(
CMD_UNLOCK_TUTORIAL_GUIDE_SC_RSP,
UnlockTutorialGuideScRsp {
retcode: 0,
tutorial_guide: Some(TutorialGuide {
id: body.group_id,
status: TutorialStatus::TutorialUnlock.into(),
}),
},
)
.await
}

View file

@ -0,0 +1,8 @@
pub mod gateway;
mod handlers;
mod packet;
mod session;
pub use packet::NetPacket;
pub use session::PlayerSession;

1422
gameserver/src/net/packet.rs Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,40 @@
use anyhow::Result;
use prost::Message;
use tokio::{io::AsyncWriteExt, net::TcpStream};
use super::{packet::CommandHandler, NetPacket};
pub struct PlayerSession {
pub(crate) client_socket: TcpStream,
}
impl PlayerSession {
pub const fn new(client_socket: TcpStream) -> Self {
Self { client_socket }
}
pub async fn run(&mut self) -> Result<()> {
loop {
let net_packet = NetPacket::read(&mut self.client_socket).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<()> {
let mut buf = Vec::new();
body.encode(&mut buf)?;
let payload: Vec<u8> = NetPacket {
cmd_type,
head: Vec::new(),
body: buf,
}
.into();
self.client_socket.write_all(&payload).await?;
Ok(())
}
}
// Auto implemented
impl CommandHandler for PlayerSession {}

8
gameserver/src/util.rs Normal file
View file

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

11
proto/Cargo.toml Normal file
View file

@ -0,0 +1,11 @@
[package]
name = "proto"
edition = "2021"
version.workspace = true
[dependencies]
prost.workspace = true
prost-types.workspace = true
[build-dependencies]
prost-build.workspace = true

11
proto/build.rs Normal file
View file

@ -0,0 +1,11 @@
pub fn main() {
let proto_file = "StarRail.proto";
if std::path::Path::new(proto_file).exists() {
println!("cargo:rerun-if-changed={proto_file}");
prost_build::Config::new()
.out_dir("out/")
.compile_protos(&[proto_file], &["."])
.unwrap();
}
}

0
proto/out/.gitkeep Normal file
View file

35124
proto/out/_.rs Normal file

File diff suppressed because it is too large Load diff

1323
proto/src/cmd_types.rs Normal file

File diff suppressed because it is too large Load diff

4
proto/src/lib.rs Normal file
View file

@ -0,0 +1,4 @@
mod cmd_types;
pub use cmd_types::*;
include!("../out/_.rs");

28
sdkserver/Cargo.toml Normal file
View file

@ -0,0 +1,28 @@
[package]
name = "sdkserver"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow.workspace = true
env_logger.workspace = true
axum.workspace = true
axum-server.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

29
sdkserver/src/logging.rs Normal file
View file

@ -0,0 +1,29 @@
#[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"));
}

51
sdkserver/src/main.rs Normal file
View file

@ -0,0 +1,51 @@
use anyhow::Result;
use axum::routing::{get, post};
use axum::Router;
use logging::init_tracing;
use services::{auth, dispatch, errors};
use tracing::Level;
mod logging;
mod services;
const PORT: u16 = 21000;
#[tokio::main]
async fn main() -> Result<()> {
init_tracing();
let span = tracing::span!(Level::DEBUG, "main");
let _ = span.enter();
let router = Router::new()
.route(
dispatch::QUERY_DISPATCH_ENDPOINT,
get(dispatch::query_dispatch),
)
.route(
dispatch::QUERY_GATEWAY_ENDPOINT,
get(dispatch::query_gateway),
)
.route(auth::RISKY_API_CHECK_ENDPOINT, post(auth::risky_api_check))
.route(
auth::LOGIN_WITH_PASSWORD_ENDPOINT,
post(auth::login_with_password),
)
.route(
auth::LOGIN_WITH_SESSION_TOKEN_ENDPOINT,
post(auth::login_with_session_token),
)
.route(
auth::GRANTER_LOGIN_VERIFICATION_ENDPOINT,
post(auth::granter_login_verification),
)
.fallback(errors::not_found);
let addr = format!("0.0.0.0:{PORT}");
let server = axum_server::bind(addr.parse()?);
tracing::info!("sdkserver is listening at {addr}");
server.serve(router.into_make_service()).await?;
Ok(())
}

View file

@ -0,0 +1,80 @@
use axum::Json;
use serde_json::json;
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";
#[tracing::instrument]
pub async fn login_with_password() -> Json<serde_json::Value> {
Json(json!({
"data": {
"account": {
"area_code": "**",
"email": "ReversedRooms",
"country": "RU",
"is_email_verify": "1",
"token": "mostsecuretokenever",
"uid": "1337"
},
"device_grant_required": false,
"reactivate_required": false,
"realperson_required": false,
"safe_mobile_required": false
},
"message": "OK",
"retcode": 0
}))
}
#[tracing::instrument]
pub async fn login_with_session_token() -> Json<serde_json::Value> {
Json(json!({
"data": {
"account": {
"area_code": "**",
"email": "ReversedRooms",
"country": "RU",
"is_email_verify": "1",
"token": "mostsecuretokenever",
"uid": "1337"
},
"device_grant_required": false,
"reactivate_required": false,
"realperson_required": false,
"safe_mobile_required": false
},
"message": "OK",
"retcode": 0
}))
}
#[tracing::instrument]
pub async fn granter_login_verification() -> Json<serde_json::Value> {
Json(json!({
"data": {
"account_type": 1,
"combo_id": "1337",
"combo_token": "9065ad8507d5a1991cb6fddacac5999b780bbd92",
"data": "{\"guest\":false}",
"heartbeat": false,
"open_id": "1337"
},
"message": "OK",
"retcode": 0
}))
}
#[tracing::instrument]
pub async fn risky_api_check() -> Json<serde_json::Value> {
Json(json!({
"data": {
"id": "06611ed14c3131a676b19c0d34c0644b",
"action": "ACTION_NONE",
"geetest": null
},
"message": "OK",
"retcode": 0
}))
}

View file

@ -0,0 +1,65 @@
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: String::from(
"https://autopatchcn.bhsr.com/asb/BetaLive/output_6744505_89b2f5dc973e",
),
lua_url: String::from(
"https://autopatchcn.bhsr.com/lua/BetaLive/output_6755976_3c46d7c46e2c",
),
ex_resource_url: String::from(
"https://autopatchcn.bhsr.com/design_data/BetaLive/output_6759713_b4e0e740f0da",
),
ifix_version: String::from("0"),
lua_version: String::from("6755976"),
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

@ -0,0 +1,7 @@
use axum::http::{StatusCode, Uri};
use axum::response::IntoResponse;
pub async fn not_found(uri: Uri) -> impl IntoResponse {
tracing::warn!("unhandled http request: {uri}");
StatusCode::NOT_FOUND
}

View file

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