refactor: Refactor json loading mechanism & more

- Move json loading into separate crate `common`
- Add new http route for handling SRTools API
- Listen to `freesr-data.json` file change, and sync with client immediately
- Move json loading into `PlayerSession`, instead of load it everytime
- Implement global buff for Castorice
- Implement `GetBigDataAllRecommendCsReq`
This commit is contained in:
amizing25 2025-03-03 08:02:51 +07:00
parent 50a05a5cc2
commit de22105514
28 changed files with 7113 additions and 12024 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
target/
Cargo.lock
proto/StarRail.proto
/prebuild
/assets
.vscode

View File

@ -1,5 +1,5 @@
[workspace]
members = ["gameserver", "proto", "sdkserver"]
members = ["gameserver", "proto", "sdkserver", "common"]
resolver = "3"
[workspace.package]
@ -13,6 +13,7 @@ lazy_static = "1.4.0"
axum = "0.8.1"
axum-server = "0.7.1"
tower-http = "0.6.2"
env_logger = "0.11.3"
@ -32,6 +33,8 @@ prost-build = "0.13.5"
paste = "1.0.14"
sysinfo = "0.33.1"
notify = "8.0.0"
notify-debouncer-mini = "0.6.0"
hex = "0.4.3"
@ -56,6 +59,7 @@ tracing-bunyan-formatter = "0.3.9"
proto = { path = "proto/" }
proto-derive = { path = "proto/proto-derive" }
mhy-kcp = { path = "kcp/", features = ["tokio"] }
common = { path = "common/" }
[profile.release]

View File

@ -1,4 +1,4 @@
# Supported Version: 2.6.5x
# Supported Version: 3.1.5x
Run the game by clicking run.bat file.
Tool website: [https://srtools.pages.dev](https://srtools.pages.dev)

11
common/Cargo.toml Normal file
View File

@ -0,0 +1,11 @@
[package]
name = "common"
edition = "2024"
version.workspace = true
[dependencies]
proto.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
tracing.workspace = true

View File

@ -1 +1,2 @@
pub mod resources;
pub mod sr_tools;

View File

@ -696,6 +696,10 @@ impl FreesrData {
json
}
pub async fn update(&mut self) {
*self = Self::load().await
}
async fn verify_lineup(&mut self) {
if self.lineups.is_empty() {
self.lineups = BTreeMap::<u32, u32>::from([(0, 8001), (1, 0), (2, 0), (3, 0)])
@ -704,16 +708,10 @@ impl FreesrData {
self.lineups.insert(i as u32, 0);
}
}
self.save().await;
self.save_persistent().await;
}
pub async fn save_lineup(&self) {
self.save().await;
}
pub async fn save(&self) {
let json = serde_json::to_string_pretty(&self).unwrap();
let _ = tokio::fs::write("freesr-data.json", json.as_bytes()).await;
pub async fn save_persistent(&self) {
let _ = tokio::fs::write(
"persistent",
serde_json::to_string_pretty(&Persistent {
@ -723,10 +721,10 @@ impl FreesrData {
scene: self.scene.clone(),
march_type: self.march_type,
})
.unwrap()
.as_bytes(),
.unwrap(),
)
.await;
tracing::info!("persistent saved");
}
pub fn get_multi_path_info(&self) -> Vec<MultiPathAvatarTypeInfo> {

View File

@ -12,7 +12,8 @@ hex.workspace = true
lazy_static.workspace = true
paste.workspace = true
rbase64.workspace = true
sysinfo.workspace = true
notify.workspace = true
notify-debouncer-mini.workspace = true
serde.workspace = true
serde_json.workspace = true
@ -32,3 +33,4 @@ proto-derive.workspace = true
rand.workspace = true
mhy-kcp.workspace = true
common.workspace = true

View File

@ -3,7 +3,6 @@ use anyhow::Result;
mod logging;
mod net;
mod tools;
mod util;
use logging::init_tracing;

View File

@ -1,20 +1,24 @@
use std::{
collections::HashMap,
net::SocketAddr,
path::Path,
sync::{
atomic::{AtomicU32, Ordering},
Arc,
atomic::{AtomicU32, Ordering},
},
time::Duration,
};
use anyhow::Result;
use common::sr_tools::FreesrData;
use rand::RngCore;
use crate::net::PlayerSession;
use tokio::{
net::UdpSocket,
sync::{Mutex, RwLock},
sync::{Mutex, RwLock, mpsc},
};
use crate::net::packet::NetOperation;
@ -74,15 +78,79 @@ impl Gateway {
let (conv_id, session_token) = self.next_conv_pair();
tracing::info!("New connection from addr: {addr} with conv_id: {conv_id}");
self.sessions.lock().await.insert(
let session = Arc::new(RwLock::new(PlayerSession::new(
self.socket.clone(),
addr,
conv_id,
Arc::new(RwLock::new(PlayerSession::new(
self.socket.clone(),
addr,
conv_id,
session_token,
))),
);
session_token,
)));
// Init the json to session
let _ = session
.write()
.await
.json_data
.set(FreesrData::load().await);
let session_ref = session.clone();
// FS watcher
tokio::spawn(async move {
let (tx, mut rx) = mpsc::channel(100);
let mut debouncer =
notify_debouncer_mini::new_debouncer(Duration::from_millis(1000), move |ev| {
let _ = tx.blocking_send(ev);
})
.unwrap();
let path = Path::new("freesr-data.json");
debouncer
.watcher()
.watch(path, notify::RecursiveMode::NonRecursive)
.unwrap();
tracing::info!("watching freesr-data.json changes");
let mut shutdown_rx = session.read().await.shutdown_rx.clone();
loop {
tokio::select! {
res = rx.recv() => {
let Some(res) = res else {
break;
};
match res {
Ok(events) => {
if events
.iter()
.any(|p| p.path.file_name() == path.file_name())
{
let mut session = session.write().await;
if let Some(json) = session.json_data.get_mut() {
let _ = json.update().await;
session.sync_player().await;
tracing::info!("json updated")
}
}
}
Err(e) => eprintln!("json watcher error: {:?}", e),
}
}
_ = shutdown_rx.changed() => {
break;
}
}
}
tracing::info!("unwatch freesr-data.json");
});
let mut sessions = self.sessions.lock().await;
for session in sessions.values_mut() {
let _ = session.write().await.shutdown_tx.send(());
}
sessions.clear();
sessions.insert(conv_id, session_ref);
self.socket
.send_to(
@ -102,38 +170,29 @@ impl Gateway {
async fn drop_kcp_session(&mut self, conv_id: u32, token: u32, addr: SocketAddr) {
tracing::info!("drop_kcp_session {conv_id} {token}");
let Some(session) = self.sessions.lock().await.get(&conv_id).cloned() else {
let mut sessions = self.sessions.lock().await;
let Some(session) = sessions.get(&conv_id) else {
tracing::warn!("drop_kcp_session failed, no session with conv_id {conv_id} was found");
return;
};
if session.read().await.token == token {
self.sessions.lock().await.remove(&conv_id);
let session = session.write().await;
if session.token == token {
let _ = session.shutdown_tx.send(());
drop(session);
sessions.remove(&conv_id);
tracing::info!("Client from {addr} disconnected");
}
}
async fn process_kcp_payload(&mut self, data: Box<[u8]>, addr: SocketAddr) {
let conv_id = mhy_kcp::get_conv(&data);
let mut sessions = self.sessions.lock().await;
let Some(session) = self
.sessions
.lock()
.await
.get_mut(&conv_id)
.map(|s| s.clone())
else {
let Some(session) = sessions.get_mut(&conv_id).map(|s| s.clone()) else {
tracing::warn!("Session with conv_id {conv_id} not found!");
return;
};
// TODO: Temporary fix
if session.read().await.is_destroyed {
drop(session);
self.sessions.lock().await.remove(&conv_id);
return;
}
tokio::spawn(async move {
if let Err(err) = Box::pin(session.write().await.consume(&data)).await {
tracing::error!("An error occurred while processing session ({addr}): {err}");

View File

@ -1,20 +1,21 @@
use crate::net::tools::FreesrData;
use super::*;
static UNLOCKED_AVATARS: [u32; 63] = [
pub static UNLOCKED_AVATARS: [u32; 63] = [
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, 1310,
1314, 1218, 1221, 1220, 1222, 1223, 1317, 1313, 1225, 1402, 1401, 1404, 1403, 1405, 1407
1314, 1218, 1221, 1220, 1222, 1223, 1317, 1313, 1225, 1402, 1401, 1404, 1403, 1405, 1407,
];
pub async fn on_get_avatar_data_cs_req(
_session: &mut PlayerSession,
session: &mut PlayerSession,
body: &GetAvatarDataCsReq,
res: &mut GetAvatarDataScRsp,
) {
let json = FreesrData::load().await;
let Some(json) = session.json_data.get() else {
tracing::error!("data is not set!");
return;
};
// TODO: HARDCODED
let mc_ids = if json.main_character.get_gender() == Gender::Man {

View File

@ -3,19 +3,19 @@ use std::collections::HashMap;
use rand::Rng;
use rogue_magic_battle_unit_info::Item;
use crate::{
net::tools::{self, BattleType, Monster},
tools::resources::GAME_RES,
use common::{
resources::GAME_RES,
sr_tools::{BattleType, Monster, RogueMagicComponentType},
};
use super::*;
pub async fn on_start_cocoon_stage_cs_req(
_session: &mut PlayerSession,
session: &mut PlayerSession,
req: &StartCocoonStageCsReq,
res: &mut StartCocoonStageScRsp,
) {
let battle_info = create_battle_info(0, 0).await;
let battle_info = create_battle_info(session, 0, 0).await;
res.prop_entity_id = req.prop_entity_id;
res.cocoon_id = req.cocoon_id;
@ -24,11 +24,11 @@ pub async fn on_start_cocoon_stage_cs_req(
}
pub async fn on_quick_start_cocoon_stage_cs_req(
_session: &mut PlayerSession,
session: &mut PlayerSession,
req: &QuickStartCocoonStageCsReq,
res: &mut QuickStartCocoonStageScRsp,
) {
let mut battle_info = create_battle_info(0, 0).await;
let mut battle_info = create_battle_info(session, 0, 0).await;
battle_info.world_level = req.world_level;
res.cocoon_id = req.cocoon_id;
@ -46,7 +46,7 @@ pub async fn on_pve_battle_result_cs_req(
}
pub async fn on_scene_cast_skill_cs_req(
_session: &mut PlayerSession,
session: &mut PlayerSession,
req: &SceneCastSkillCsReq,
res: &mut SceneCastSkillScRsp,
) {
@ -55,20 +55,30 @@ pub async fn on_scene_cast_skill_cs_req(
let targets = req
.hit_target_entity_id_list
.iter()
.chain(&req.assist_monster_entity_id_list)
.filter(|id| **id > 30_000 || **id < 1_000)
.collect::<Vec<_>>();
if targets.is_empty() {
tracing::warn!("scene cast skill target is empty!");
return;
}
let battle_info = create_battle_info(req.caster_id, req.skill_index).await;
let battle_info = create_battle_info(session, req.caster_id, req.skill_index).await;
res.attacked_group_id = req.attacked_group_id;
res.battle_info = Some(battle_info);
}
async fn create_battle_info(caster_id: u32, skill_index: u32) -> SceneBattleInfo {
let player = tools::FreesrData::load().await;
async fn create_battle_info(
session: &mut PlayerSession,
caster_id: u32,
skill_index: u32,
) -> SceneBattleInfo {
let Some(player) = session.json_data.get() else {
tracing::error!("data is not set!");
return SceneBattleInfo::default();
};
let mut battle_info = SceneBattleInfo {
stage_id: player.battle_config.stage_id,
@ -207,27 +217,21 @@ async fn create_battle_info(caster_id: u32, skill_index: u32) -> SceneBattleInfo
// pf score object
if player.battle_config.battle_type == BattleType::PF {
if battle_info.stage_id >= 30309011 {
battle_info.battle_target_info.insert(
1,
BattleTargetList {
battle_target_list: vec![BattleTarget {
id: 10003,
progress: 0,
..Default::default()
}],
},
);
battle_info.battle_target_info.insert(1, BattleTargetList {
battle_target_list: vec![BattleTarget {
id: 10003,
progress: 0,
..Default::default()
}],
});
} else {
battle_info.battle_target_info.insert(
1,
BattleTargetList {
battle_target_list: vec![BattleTarget {
id: 10002,
progress: 0,
..Default::default()
}],
},
);
battle_info.battle_target_info.insert(1, BattleTargetList {
battle_target_list: vec![BattleTarget {
id: 10002,
progress: 0,
..Default::default()
}],
});
}
for i in 2..=4 {
@ -236,37 +240,31 @@ async fn create_battle_info(caster_id: u32, skill_index: u32) -> SceneBattleInfo
.insert(i, BattleTargetList::default());
}
battle_info.battle_target_info.insert(
5,
BattleTargetList {
battle_target_list: vec![
BattleTarget {
id: 2001,
progress: 0,
..Default::default()
},
BattleTarget {
id: 2002,
progress: 0,
..Default::default()
},
],
},
);
battle_info.battle_target_info.insert(5, BattleTargetList {
battle_target_list: vec![
BattleTarget {
id: 2001,
progress: 0,
..Default::default()
},
BattleTarget {
id: 2002,
progress: 0,
..Default::default()
},
],
});
}
// Apocalyptic Shadow
if player.battle_config.battle_type == BattleType::AS {
battle_info.battle_target_info.insert(
1,
BattleTargetList {
battle_target_list: vec![BattleTarget {
id: 90005,
progress: 0,
..Default::default()
}],
},
);
battle_info.battle_target_info.insert(1, BattleTargetList {
battle_target_list: vec![BattleTarget {
id: 90005,
progress: 0,
..Default::default()
}],
});
}
// SU
@ -313,9 +311,9 @@ async fn create_battle_info(caster_id: u32, skill_index: u32) -> SceneBattleInfo
for component in &scepter.components {
let (slot_type, locked) = match component.component_type {
tools::RogueMagicComponentType::Passive => (3u32, false),
tools::RogueMagicComponentType::Active => (4, true),
tools::RogueMagicComponentType::Attach => (5, false),
RogueMagicComponentType::Passive => (3u32, false),
RogueMagicComponentType::Active => (4, true),
RogueMagicComponentType::Attach => (5, false),
};
let slot_index = &mut index[slot_type as usize - 3];
@ -339,5 +337,17 @@ async fn create_battle_info(caster_id: u32, skill_index: u32) -> SceneBattleInfo
});
}
// Global Buff
if !battle_info.buff_list.iter().any(|b| b.id == 140703) {
battle_info.buff_list.push(BattleBuff {
id: 140703,
level: 1,
owner_id: u32::MAX,
wave_flag: u32::MAX,
target_index_list: Vec::with_capacity(0),
dynamic_values: HashMap::with_capacity(0),
});
}
battle_info
}

View File

@ -1,10 +1,6 @@
use crate::{
net::{
tools::{FreesrData, MultiPathAvatar},
PlayerSession,
},
util::cur_timestamp_ms,
};
use common::sr_tools::MultiPathAvatar;
use crate::{net::PlayerSession, util::cur_timestamp_ms};
use super::*;
@ -16,7 +12,7 @@ const SERVER_CHAT_HISTORY: [&str; 5] = [
"'mc {mc_id}' mc_id can be set from 8001 to 8008",
"'march {march_id}' march_id can be set 1001 or 1224",
"available commands:",
"visit srtools.pages.dev to configure the PS! (you configure relics, equipment, monsters from there)"
"visit srtools.pages.dev to configure the PS! (you configure relics, equipment, monsters from there)",
];
pub async fn on_get_friend_login_info_cs_req(
@ -76,12 +72,15 @@ pub async fn on_send_msg_cs_req(
body: &SendMsgCsReq,
_res: &mut SendMsgScRsp,
) {
let mut json = FreesrData::load().await;
let Some(json) = session.json_data.get_mut() else {
tracing::error!("data is not set!");
return;
};
if let Some((cmd, args)) = parse_command(&body.text) {
match cmd {
"sync" => {
sync_player(session, json).await;
session.sync_player().await;
session
.send(RevcMsgScNotify {
msg_type: body.msg_type,
@ -90,7 +89,7 @@ pub async fn on_send_msg_cs_req(
from_uid: SERVER_UID,
to_uid: 25,
chat_type: body.chat_type,
hnbepabnbng: body.hnbepabnbng.clone(),
hnbepabnbng: body.hnbepabnbng,
})
.await
.unwrap();
@ -104,7 +103,7 @@ pub async fn on_send_msg_cs_req(
);
json.main_character = mc;
json.save().await;
json.save_persistent().await;
session
.send(AvatarPathChangedNotify {
@ -114,7 +113,7 @@ pub async fn on_send_msg_cs_req(
.await
.unwrap();
sync_player(session, json).await;
session.sync_player().await;
session
.send(RevcMsgScNotify {
@ -124,7 +123,7 @@ pub async fn on_send_msg_cs_req(
from_uid: SERVER_UID,
to_uid: 25,
chat_type: body.chat_type,
hnbepabnbng: body.hnbepabnbng.clone(),
hnbepabnbng: body.hnbepabnbng,
})
.await
.unwrap();
@ -144,7 +143,7 @@ pub async fn on_send_msg_cs_req(
}
json.march_type = march_type;
json.save().await;
json.save_persistent().await;
session
.send(AvatarPathChangedNotify {
@ -162,7 +161,7 @@ pub async fn on_send_msg_cs_req(
from_uid: SERVER_UID,
to_uid: 25,
chat_type: body.chat_type,
hnbepabnbng: body.hnbepabnbng.clone(),
hnbepabnbng: body.hnbepabnbng,
})
.await
.unwrap();
@ -181,70 +180,3 @@ fn parse_command(command: &str) -> Option<(&str, Vec<&str>)> {
Some((parts[0], parts[1..].to_vec()))
}
async fn sync_player(session: &mut PlayerSession, json: FreesrData) {
// clear relics & lightcones
session
.send(PlayerSyncScNotify {
del_equipment_list: (2000..3500).collect(),
del_relic_list: (1..2000).collect(),
..Default::default()
})
.await
.unwrap();
// Sync avatars
session
.send(PlayerSyncScNotify {
avatar_sync: Some(AvatarSync {
avatar_list: json
.avatars
.values()
.map(|avatar| avatar.to_avatar_proto(Option::None, vec![]))
.collect::<Vec<_>>(),
}),
multi_path_avatar_type_info_list: json.get_multi_path_info(),
..Default::default()
})
.await
.unwrap();
// Sync new relics
session
.send(PlayerSyncScNotify {
relic_list: json.relics.iter().map(|v| v.to_relic_proto()).collect(),
equipment_list: json
.lightcones
.iter()
.map(|v| v.to_equipment_proto())
.collect(),
..Default::default()
})
.await
.unwrap();
// Sync new lightcones
session
.send(PlayerSyncScNotify {
avatar_sync: Some(AvatarSync {
avatar_list: json
.avatars
.values()
.map(|avatar| {
avatar.to_avatar_proto(
json.lightcones
.iter()
.find(|v| v.equip_avatar == avatar.avatar_id),
json.relics
.iter()
.filter(|v| v.equip_avatar == avatar.avatar_id)
.collect(),
)
})
.collect(),
}),
..Default::default()
})
.await
.unwrap()
}

View File

@ -1,13 +1,18 @@
use proto::*;
use proto::{get_big_data_all_recommend_sc_rsp::RecommendType, *};
use crate::net::{tools::FreesrData, PlayerSession};
use crate::net::PlayerSession;
use super::UNLOCKED_AVATARS;
pub async fn on_get_bag_cs_req(
_session: &mut PlayerSession,
session: &mut PlayerSession,
_req: &GetBagCsReq,
res: &mut GetBagScRsp,
) {
let player = FreesrData::load().await;
let Some(player) = session.json_data.get() else {
tracing::error!("data is not set!");
return;
};
res.equipment_list = player
.lightcones
@ -65,10 +70,37 @@ pub async fn on_take_off_equipment_cs_req(
) {
}
// pub async fn on_relic_recommend_cs_req(
// _: &mut PlayerSession,
// req: &RelicRecommendCsReq,
// res: &mut RelicRecommendScRsp,
// ) {
// res.avatar_id = req.avatar_id
// }
pub async fn on_get_big_data_all_recommend_cs_req(
_: &mut PlayerSession,
req: &GetBigDataAllRecommendCsReq,
res: &mut GetBigDataAllRecommendScRsp,
) {
res.big_data_recommend_type = req.big_data_recommend_type;
match req.big_data_recommend_type() {
BigDataRecommendType::RelicAvatar => {
res.recommend_type = Some(RecommendType::RelicAvatar(BigDataRecommendRelicAvatar {
recommended_avatar_info_list: UNLOCKED_AVATARS
.into_iter()
.map(|recommend_avatar_id| RecomendedAvatarInfo {
avatar_id_list: vec![],
recommend_avatar_id,
relic_set_id: 0,
})
.collect(),
}))
}
BigDataRecommendType::AvatarRelic => {
res.recommend_type = Some(RecommendType::AvatarRelic(BigDataRecommendAvatarRelic {
recomended_relic_info_list: UNLOCKED_AVATARS
.into_iter()
.map(|avatar_id| RecommendedRelicInfo {
avatar_id,
..Default::default()
})
.collect(),
}))
}
_ => {}
}
}

View File

@ -1,38 +1,45 @@
use common::sr_tools::AvatarJson;
use scene_entity_info::Entity;
use scene_entity_refresh_info::RefreshType;
use crate::net::tools::{self, AvatarJson, FreesrData};
use super::*;
pub async fn on_get_all_lineup_data_cs_req(
_session: &mut PlayerSession,
session: &mut PlayerSession,
_body: &GetAllLineupDataCsReq,
res: &mut GetAllLineupDataScRsp,
) {
let player = tools::FreesrData::load().await;
let Some(player) = session.json_data.get() else {
tracing::error!("data is not set!");
return;
};
res.lineup_list = vec![LineupInfo {
extra_lineup_type: ExtraLineupType::LineupNone.into(),
name: "Squad 1".to_string(),
mp: 5,
max_mp: 5,
avatar_list: AvatarJson::to_lineup_avatars(&player),
avatar_list: AvatarJson::to_lineup_avatars(player),
..Default::default()
}];
}
pub async fn on_get_cur_lineup_data_cs_req(
_session: &mut PlayerSession,
session: &mut PlayerSession,
_body: &GetCurLineupDataCsReq,
res: &mut GetCurLineupDataScRsp,
) {
let player = tools::FreesrData::load().await;
let Some(player) = session.json_data.get() else {
tracing::error!("data is not set!");
return;
};
let lineup = LineupInfo {
extra_lineup_type: ExtraLineupType::LineupNone.into(),
name: "Squad 1".to_string(),
mp: 5,
max_mp: 5,
avatar_list: AvatarJson::to_lineup_avatars(&player),
avatar_list: AvatarJson::to_lineup_avatars(player),
is_virtual: false,
plane_id: 0,
..Default::default()
@ -46,19 +53,26 @@ pub async fn on_join_lineup_cs_req(
body: &JoinLineupCsReq,
_res: &mut JoinLineupScRsp,
) {
let mut player = tools::FreesrData::load().await;
let Some(player) = session.json_data.get_mut() else {
tracing::error!("data is not set!");
return;
};
let lineups = &mut player.lineups;
lineups.insert(body.slot, body.base_avatar_id);
player.save_lineup().await;
refresh_lineup(session, &player).await;
player.save_persistent().await;
refresh_lineup(session).await;
}
pub async fn on_replace_lineup_cs_req(
_session: &mut PlayerSession,
session: &mut PlayerSession,
req: &ReplaceLineupCsReq,
_res: &mut ReplaceLineupScRsp,
) {
let mut player = tools::FreesrData::load().await;
let Some(player) = session.json_data.get_mut() else {
tracing::error!("data is not set!");
return;
};
let lineups = &mut player.lineups;
for (slot, avatar_id) in &mut *lineups {
@ -68,8 +82,8 @@ pub async fn on_replace_lineup_cs_req(
*avatar_id = 0;
}
}
player.save_lineup().await;
refresh_lineup(_session, &player).await;
player.save_persistent().await;
refresh_lineup(session).await;
}
pub async fn on_quit_lineup_cs_req(
@ -79,7 +93,12 @@ pub async fn on_quit_lineup_cs_req(
) {
}
async fn refresh_lineup(session: &mut PlayerSession, player: &FreesrData) {
async fn refresh_lineup(session: &mut PlayerSession) {
let Some(player) = session.json_data.get_mut() else {
tracing::error!("data is not set!");
return;
};
let lineup = LineupInfo {
extra_lineup_type: ExtraLineupType::LineupNone.into(),
name: "Squad 1".to_string(),
@ -89,33 +108,37 @@ async fn refresh_lineup(session: &mut PlayerSession, player: &FreesrData) {
..Default::default()
};
let new_entities = player
.lineups
.iter()
.map(|(idx, v)| SceneEntityRefreshInfo {
refresh_type: Some(RefreshType::AddEntity(SceneEntityInfo {
entity: Some(Entity::Actor(SceneActorInfo {
avatar_type: AvatarType::AvatarFormalType.into(),
base_avatar_id: *v,
map_layer: 0,
uid: 25,
})),
entity_id: idx + 1,
group_id: 0,
inst_id: 0,
..Default::default()
})),
})
.collect();
let floor_id = player.scene.floor_id;
session
.send(SceneGroupRefreshScNotify {
group_refresh_info: vec![SceneGroupRefreshInfo {
group_id: 0,
state: 0,
group_refresh_type: 0,
refresh_entity: player
.lineups
.iter()
.map(|(idx, v)| SceneEntityRefreshInfo {
refresh_type: Some(RefreshType::AddEntity(SceneEntityInfo {
entity: Some(Entity::Actor(SceneActorInfo {
avatar_type: AvatarType::AvatarFormalType.into(),
base_avatar_id: *v,
map_layer: 0,
uid: 25,
})),
entity_id: idx + 1,
group_id: 0,
inst_id: 0,
..Default::default()
})),
})
.collect(),
group_refresh_type: SceneGroupRefreshType::Loaded.into(),
refresh_entity: new_entities,
bccgjihncdn: Vec::with_capacity(0),
}],
floor_id: 0, // TODO!
floor_id,
gfhglffhfbd: 0,
})
.await

View File

@ -1,7 +1,5 @@
use std::collections::HashMap;
use crate::net::tools::FreesrData;
use super::*;
pub async fn on_get_basic_info_cs_req(
@ -56,11 +54,14 @@ pub async fn on_player_login_finish_cs_req(
}
pub async fn on_get_multi_path_avatar_info_cs_req(
_session: &mut PlayerSession,
session: &mut PlayerSession,
_req: &GetMultiPathAvatarInfoCsReq,
res: &mut GetMultiPathAvatarInfoScRsp,
) {
let json = FreesrData::load().await;
let Some(json) = session.json_data.get() else {
tracing::error!("data is not set!");
return;
};
res.current_multi_path_avatar_id = HashMap::from([
(8001, json.main_character.get_type().into()),

View File

@ -1,14 +1,14 @@
use std::collections::HashMap;
use common::{
resources::GAME_RES,
sr_tools::{AvatarJson, Position},
};
use lazy_static::lazy_static;
use scene_entity_info::Entity;
use tokio::sync::Mutex;
use crate::{
net::tools::{AvatarJson, FreesrData, Position},
tools::resources::GAME_RES,
util::{self},
};
use crate::util::{self};
use super::*;
@ -17,21 +17,25 @@ pub async fn on_get_cur_scene_info_cs_req(
_body: &GetCurSceneInfoCsReq,
res: &mut GetCurSceneInfoScRsp,
) {
let mut player = FreesrData::load().await;
let entry = player.scene.entry_id;
let Some(player) = session.json_data.get() else {
tracing::error!("data is not set!");
return;
};
let scene = load_scene(session, &mut player, entry, false, Option::<u32>::None).await;
let default_scene = SceneInfo {
game_mode_type: 3,
entry_id: player.scene.entry_id,
plane_id: player.scene.plane_id,
floor_id: player.scene.floor_id,
..Default::default()
};
let scene = load_scene(session, default_scene.entry_id, false, Option::<u32>::None).await;
res.scene = if let Ok(scene) = scene {
Some(scene)
} else {
Some(SceneInfo {
game_mode_type: 3,
entry_id: player.scene.entry_id,
plane_id: player.scene.plane_id,
floor_id: player.scene.floor_id,
..Default::default()
})
Some(default_scene)
};
}
@ -40,17 +44,9 @@ pub async fn on_enter_scene_cs_req(
req: &EnterSceneCsReq,
res: &mut EnterSceneScRsp,
) {
let mut player = FreesrData::load().await;
if load_scene(
session,
&mut player,
req.entry_id,
true,
Some(req.teleport_id),
)
.await
.is_err()
if load_scene(session, req.entry_id, true, Some(req.teleport_id))
.await
.is_err()
{
res.retcode = Retcode::RetSceneEntryIdNotMatch as u32;
};
@ -126,11 +122,15 @@ lazy_static! {
}
pub async fn on_scene_entity_move_cs_req(
_session: &mut PlayerSession,
session: &mut PlayerSession,
req: &SceneEntityMoveCsReq,
_res: &mut SceneEntityMoveScRsp,
) {
let mut player = FreesrData::load().await;
let Some(player) = session.json_data.get_mut() else {
tracing::error!("data is not set!");
return;
};
let mut timestamp = NEXT_SCENE_SAVE.lock().await;
if util::cur_timestamp_ms() <= *timestamp {
@ -157,7 +157,7 @@ pub async fn on_scene_entity_move_cs_req(
}
}
player.save().await;
player.save_persistent().await;
}
pub async fn on_get_entered_scene_cs_req(
@ -187,11 +187,15 @@ pub async fn on_get_entered_scene_cs_req(
async fn load_scene(
session: &mut PlayerSession,
json: &mut FreesrData,
entry_id: u32,
is_enter_scene: bool,
teleport_id: Option<u32>,
) -> Result<SceneInfo> {
let Some(json) = session.json_data.get_mut() else {
tracing::error!("data is not set!");
return Err(anyhow::format_err!("data is not set!"));
};
let (name, scene) = GAME_RES
.level_output_configs
.get(&entry_id)
@ -375,7 +379,7 @@ async fn load_scene(
.map(|(slot, avatar_id)| SceneEntityInfo {
inst_id: 0,
entity_id: (*slot) + 1,
motion: Some(player_pos.clone()),
motion: Some(player_pos),
entity: Some(Entity::Actor(SceneActorInfo {
avatar_type: AvatarType::AvatarFormalType.into(),
base_avatar_id: *avatar_id,
@ -388,14 +392,6 @@ async fn load_scene(
});
if is_enter_scene {
session
.send(EnterSceneByServerScNotify {
scene: Some(scene_info.clone()),
lineup: Some(lineup_info),
..Default::default()
})
.await?;
json.scene.entry_id = entry_id;
json.scene.floor_id = floor_id;
json.scene.plane_id = plane_id;
@ -403,7 +399,16 @@ async fn load_scene(
json.position.y = json_pos.y;
json.position.z = json_pos.z;
json.position.rot_y = json_pos.rot_y;
json.save().await;
json.save_persistent().await;
session
.send(EnterSceneByServerScNotify {
scene: Some(scene_info.clone()),
lineup: Some(lineup_info),
..Default::default()
})
.await?;
}
Ok(scene_info)

View File

@ -3,7 +3,6 @@ pub mod gateway;
mod handlers;
mod packet;
mod session;
mod tools;
pub use packet::NetPacket;
pub use session::PlayerSession;

View File

@ -27,8 +27,8 @@ use proto::{
CmdWaypointType::*, CmdWolfBroType::*,
};
use super::handlers::*;
use super::PlayerSession;
use super::handlers::*;
const HEAD_MAGIC: u32 = 0x9D74C714;
const TAIL_MAGIC: u32 = 0xD7A152C8;
@ -219,6 +219,6 @@ trait_handler! {
GetGachaInfo;
DoGacha;
PlayerLoginFinish;
// RelicRecommend;
GetBigDataAllRecommend;
// SetClientPaused;
}

View File

@ -2,66 +2,73 @@ use std::{
io::Error,
net::SocketAddr,
pin::Pin,
sync::Arc,
sync::{Arc, OnceLock},
task::{Context, Poll},
};
use anyhow::Result;
use common::sr_tools::FreesrData;
use mhy_kcp::Kcp;
use prost::Message;
use proto::{CmdID, CmdPlayerType};
use tokio::{io::AsyncWrite, net::UdpSocket, sync::Mutex};
use proto::{AvatarSync, CmdID, CmdPlayerType, PlayerSyncScNotify};
use tokio::{
io::AsyncWrite,
net::UdpSocket,
sync::{Mutex, watch},
};
use crate::util;
use super::{packet::CommandHandler, NetPacket};
use super::{NetPacket, packet::CommandHandler};
struct RemoteEndPoint {
socket: Arc<UdpSocket>,
addr: SocketAddr,
}
#[derive(Clone)]
pub struct PlayerSession {
pub token: u32,
kcp: Arc<Mutex<Kcp<RemoteEndPoint>>>,
start_time: u64,
pub is_destroyed: bool,
pub shutdown_tx: watch::Sender<()>,
pub shutdown_rx: watch::Receiver<()>,
pub json_data: OnceLock<FreesrData>,
}
impl PlayerSession {
pub fn new(socket: Arc<UdpSocket>, addr: SocketAddr, conv: u32, token: u32) -> Self {
let (shutdown_tx, shutdown_rx) = watch::channel(());
Self {
token,
kcp: Arc::new(Mutex::new(Kcp::new(
conv,
token,
false,
RemoteEndPoint { socket, addr },
))),
kcp: Arc::new(Mutex::new(Kcp::new(conv, token, false, RemoteEndPoint {
socket,
addr,
}))),
start_time: util::cur_timestamp_secs(),
is_destroyed: false,
json_data: OnceLock::new(),
shutdown_rx,
shutdown_tx,
}
}
pub async fn consume(&mut self, buffer: &[u8]) -> Result<()> {
{
let mut kcp = self.kcp.lock().await;
kcp.input(buffer)?;
kcp.async_update(self.session_time() as u32).await?;
kcp.async_flush().await?;
}
let mut kcp = self.kcp.lock().await;
kcp.input(buffer)?;
kcp.async_update(self.session_time() as u32).await?;
kcp.async_flush().await?;
let mut packets = Vec::new();
let mut buf = [0; 24756];
while let Ok(length) = self.kcp.lock().await.recv(&mut buf) {
while let Ok(length) = kcp.recv(&mut buf) {
packets.push(NetPacket::from(&buf[..length]));
}
drop(kcp);
for packet in packets {
// TODO: Temporary fix
if packet.cmd_type == CmdPlayerType::CmdPlayerLogoutCsReq as u16 {
self.is_destroyed = true;
tracing::info!("Player logged out");
let _ = self.shutdown_tx.send(());
return Ok(());
};
Self::on_message(self, packet.cmd_type, packet.body).await?;
@ -78,6 +85,7 @@ impl PlayerSession {
pub async fn send(&self, body: impl Message + CmdID) -> Result<()> {
let mut buf = Vec::new();
body.encode(&mut buf)?;
tracing::info!("sent packet with CmdID: {}", body.get_cmd_id());
let payload: Vec<u8> = NetPacket {
cmd_type: body.get_cmd_id(),
@ -104,6 +112,74 @@ impl PlayerSession {
Ok(())
}
pub async fn sync_player(&self) {
let Some(json) = self.json_data.get() else {
tracing::error!("data is not init!");
return;
};
// clear relics & lightcones
self.send(PlayerSyncScNotify {
del_equipment_list: (2000..3500).collect(),
del_relic_list: (1..2000).collect(),
..Default::default()
})
.await
.unwrap();
// Sync avatars
self.send(PlayerSyncScNotify {
avatar_sync: Some(AvatarSync {
avatar_list: json
.avatars
.values()
.map(|avatar| avatar.to_avatar_proto(Option::None, vec![]))
.collect::<Vec<_>>(),
}),
multi_path_avatar_type_info_list: json.get_multi_path_info(),
..Default::default()
})
.await
.unwrap();
// Sync new relics
self.send(PlayerSyncScNotify {
relic_list: json.relics.iter().map(|v| v.to_relic_proto()).collect(),
equipment_list: json
.lightcones
.iter()
.map(|v| v.to_equipment_proto())
.collect(),
..Default::default()
})
.await
.unwrap();
// Sync new lightcones
self.send(PlayerSyncScNotify {
avatar_sync: Some(AvatarSync {
avatar_list: json
.avatars
.values()
.map(|avatar| {
avatar.to_avatar_proto(
json.lightcones
.iter()
.find(|v| v.equip_avatar == avatar.avatar_id),
json.relics
.iter()
.filter(|v| v.equip_avatar == avatar.avatar_id)
.collect(),
)
})
.collect(),
}),
..Default::default()
})
.await
.unwrap()
}
fn session_time(&self) -> u64 {
util::cur_timestamp_secs() - self.start_time
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ edition = "2024"
anyhow.workspace = true
env_logger.workspace = true
tower-http = { workspace = true, features = ["cors"]}
axum.workspace = true
axum-server.workspace = true
@ -28,3 +29,4 @@ ansi_term.workspace = true
prost.workspace = true
rbase64.workspace = true
proto.workspace = true
common.workspace = true

View File

@ -1,8 +1,11 @@
use anyhow::Result;
use axum::routing::{get, post};
use axum::Router;
use axum::http::Method;
use axum::http::header::CONTENT_TYPE;
use axum::routing::{get, post};
use logging::init_tracing;
use services::{auth, dispatch, errors};
use services::{auth, dispatch, errors, sr_tools};
use tower_http::cors::{Any, CorsLayer};
use tracing::Level;
mod config;
@ -40,6 +43,16 @@ async fn main() -> Result<()> {
auth::GRANTER_LOGIN_VERIFICATION_ENDPOINT,
post(auth::granter_login_verification),
)
.route(
sr_tools::SRTOOLS_UPLOAD_ENDPOINT,
post(sr_tools::sr_tool_save),
)
.layer(
CorsLayer::new()
.allow_origin(Any)
.allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
.allow_headers([CONTENT_TYPE]),
)
.fallback(errors::not_found);
let addr = format!("0.0.0.0:{PORT}");

View File

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

View File

@ -0,0 +1,50 @@
use axum::Json;
use common::sr_tools::FreesrData;
use serde::{Deserialize, Serialize};
use tokio::fs;
pub const SRTOOLS_UPLOAD_ENDPOINT: &str = "/srtools";
#[derive(Debug, Deserialize)]
pub struct SrToolDataReq {
#[allow(dead_code)]
pub data: Option<FreesrData>,
}
#[derive(Debug, Serialize)]
pub struct SrToolDataRsp {
pub message: String,
pub status: u32,
}
#[tracing::instrument]
pub async fn sr_tool_save(Json(json): Json<SrToolDataReq>) -> Json<SrToolDataRsp> {
let Some(json) = json.data else {
return Json(SrToolDataRsp {
message: String::from("OK"),
status: 200,
});
};
let json = match serde_json::to_string_pretty(&json) {
Ok(json) => json,
Err(err) => {
return Json(SrToolDataRsp {
message: format!("malformed json: {}", err),
status: 200,
});
}
};
if let Err(err) = fs::write("freesr-data.json", json).await {
return Json(SrToolDataRsp {
message: format!("failed to write freesr-data.json: {}", err),
status: 200,
});
};
Json(SrToolDataRsp {
message: String::from("OK"),
status: 200,
})
}

View File

@ -1,20 +1,8 @@
{
"CNBETAWin3.0.51": {
"asset_bundle_url": "https://autopatchcn.bhsr.com/asb/BetaLive/output_9191572_33717c67eee7",
"ex_resource_url": "https://autopatchcn.bhsr.com/design_data/BetaLive/output_9201681_3b7fa40d696e",
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_9188077_6eddb96c0602",
"ifix_url": "https://autopatchcn.bhsr.com/ifix/BetaLive/output_0_40d2ce0253"
},
"OSBETAWin3.0.51": {
"asset_bundle_url": "https://autopatchcn.bhsr.com/asb/BetaLive/output_9191572_33717c67eee7",
"ex_resource_url": "https://autopatchcn.bhsr.com/design_data/BetaLive/output_9194543_a2c963cc027a",
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_9188077_6eddb96c0602",
"ifix_url": "https://autopatchcn.bhsr.com/ifix/BetaLive/output_0_40d2ce0253"
},
"CNBETAWin3.0.53": {
"asset_bundle_url": "https://autopatchcn.bhsr.com/asb/BetaLive/output_9327839_3a7f8c61dd4e",
"ex_resource_url": "https://autopatchcn.bhsr.com/design_data/BetaLive/output_9330527_430d02ffd64d",
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_9327980_89d683a0d346",
"ifix_url": "https://autopatchcn.bhsr.com/ifix/BetaLive/output_0_40d2ce0253"
"OSBETAWin3.1.51": {
"asset_bundle_url": "https://autopatchos.starrails.com/asb/BetaLive/output_9573347_b03981f01966",
"ex_resource_url": "https://autopatchos.starrails.com/design_data/BetaLive/output_9589567_9c50629b0369",
"lua_url": "https://autopatchos.starrails.com/lua/BetaLive/output_9567078_0e2b6acf6a2f",
"ifix_url": "https://autopatchos.starrails.com/ifix/BetaLive/output_0_40d2ce0253"
}
}

View File

@ -1,7 +1,7 @@
{
"OSBETAWin3.1.51": {
"asset_bundle_url": "https://autopatchos.starrails.com/asb/BetaLive/output_9573347_b03981f01966",
"ex_resource_url": "https://autopatchos.starrails.com/design_data/BetaLive/output_9574749_cf833d944ab2",
"ex_resource_url": "https://autopatchos.starrails.com/design_data/BetaLive/output_9589567_9c50629b0369",
"lua_url": "https://autopatchos.starrails.com/lua/BetaLive/output_9567078_0e2b6acf6a2f",
"ifix_url": "https://autopatchos.starrails.com/ifix/BetaLive/output_0_40d2ce0253"
}