56 Commits

Author SHA1 Message Date
amizing25 243df2b03e feat: add gitea prebuilt actions
Prebuilt release / prebuilt (push) Successful in 4m27s
2026-07-17 06:03:33 +07:00
amizing25 592ad690d9 feat: Add support for 4.4.5x 2026-07-17 06:03:18 +07:00
amizing25 99e1e09707 feat: Add support for 4.3.5x 2026-07-16 19:54:13 +07:00
yuvlian a2e81c40fc update res 2026-07-16 19:54:13 +07:00
yuvlian 701a78e9dc move global buff toggle logic in commands, add compile flags for playerheartbeat data 2026-07-16 19:53:57 +07:00
yuvlian 6aa7670ca8 add toggle for global buffs in persistent 2026-05-01 14:07:18 +07:00
yuvlian 80754bbfea FINAL FIX FOR HARDCODED BUFFS ON GOD 2026-05-01 13:54:15 +07:00
yuvlian 137db60000 remove debugging & add note for dahlia's hardcode 2026-05-01 13:08:49 +07:00
yuvlian 676bd63dac bump deps ver, use result instead of panic for fs watcher, hardcode dahlia tech to 1st unit 2026-04-30 20:31:00 +07:00
amizing25 5e0f2a9673 feat: Add support for 4.2.5x 2026-04-30 14:15:28 +07:00
yuvlian 26956e3193 custom lineup support 2026-03-18 07:37:21 +07:00
amizing25 cc389bd368 feat: Add support for 4.1.5x 2026-03-17 21:24:16 +07:00
amizing25 c697c799d6 feat: Support SceneEnterStageC and remove avatar red DOM 2026-03-01 01:14:45 +08:00
amizing25 b34e7af824 feat: Add support for 4.0.5x 2026-02-25 13:25:25 +07:00
amizing25 9d856e76c8 feat: lua executor via chat 2026-02-09 14:19:32 +07:00
amizing25 5e5a90cb50 fix: swapped del_relic_list & del_equipment_list and increase inv limit 2026-01-08 06:57:27 +07:00
amizing25 0208899832 feat: account plat native auth handling 2026-01-07 17:22:02 +07:00
amizing25 5ad0ac3556 fix: loading bar when clicking quick lineup 2026-01-07 17:20:41 +07:00
amizing25 7807973f93 fix: chat history & chat send 2026-01-07 17:20:21 +07:00
amizing25 c2b3bb4dd9 fix: main character switching 2026-01-07 17:19:54 +07:00
amizing25 779efd2771 feat: update res to 3.8.51 2026-01-07 05:29:50 +07:00
amizing25 14692dc78e feat: Add support for 3.8.5x 2026-01-06 23:57:37 +07:00
amizing25 56bdb1abc1 feat: Add support for 3.7.5x 2025-11-04 23:16:14 +07:00
amizing25 f1ef8d8da2 feat: Add support for 3.6.5x 2025-09-25 20:02:12 +07:00
amizing25 77f4bdf686 feat: hardcode cerydra & dh-pt technique 2025-08-15 17:01:11 +07:00
amizing25 aed52a523e feat: Add support for 3.5.5x 2025-08-14 11:52:14 +07:00
Keirakoro 14dd31c11f add avatars 2025-07-01 10:53:23 +07:00
Keirakoro 8c0d4fdc34 env type change
22 fuck up
2025-07-01 10:40:58 +07:00
amizing25 f6717b3157 feat: Add support for 3.4.5x 2025-07-01 09:53:08 +07:00
amizing25 bd40dc058f chore: cargo clippy 2025-06-30 15:10:50 +07:00
amizing25 8d8686692e feat: deobf field 2025-05-24 11:14:43 +07:00
amizing25 04f517daed fix: handle unhandled packet for handbook teleport 2025-05-24 11:04:49 +07:00
amizing25 6e72b9b509 feat: some changes 2025-05-24 06:03:24 +07:00
amizing25 2d995ef7d0 feat: update hotfix 2025-05-24 05:52:03 +07:00
amizing25 42545bbc53 fix: fix relic recommend & some scene not appearing 2025-05-24 05:50:58 +07:00
amizing25 81a783a078 feat: handle avatar enhanced state 2025-05-24 05:50:15 +07:00
amizing25 0cb4138562 fix: vector proto 2025-05-22 20:24:25 +07:00
amizing25 d1c8eeaa52 fix: multi path 2025-05-22 13:47:28 +07:00
amizing25 ac5ef9706f feat: Initial support for 3.3.5x 2025-05-22 13:47:15 +07:00
amizing25 284e5cd48e fix(deps): use rustls-tls for reqwest to fix openssl error on android target 2025-04-15 18:48:31 +07:00
amizing25 d14bc79fcc feat: lighten map sections 2025-04-15 18:46:35 +07:00
aorus22 90c2f332a5 fix: fix vector proto 2025-04-08 15:46:09 +07:00
supriyadimarseto e69e3c4e61 feat: new res 3.2.51 2025-04-08 14:05:16 +07:00
supriyadimarseto 38ee98cea2 feat: 3.2.5x proto 2025-04-08 11:29:20 +07:00
supriyadimarseto 74455e23f8 fix(sdk): fix hotfix retcode false-positive 2025-04-08 11:27:33 +07:00
amizing25 6c5ba8b6e4 feat(sdk): add automatic hotfix downloader 2025-03-12 13:32:32 +07:00
amizing25 608775b428 feat: 3.1.53 res 2025-03-11 17:46:32 +07:00
amizing25 d3af43fb06 refactor: some restructuring
- move structs inside `sr_tools.rs` into individual file
- implement in-game equip handler (relic & lightcone)
2025-03-11 17:37:35 +07:00
amizing25 fff8fc803e chore: expose sdkserver & gameserver as lib 2025-03-09 05:20:26 +07:00
amizing25 ea640e7992 feat: update res & version 3.1.52 2025-03-09 05:16:08 +07:00
amizing25 505c446a53 chore: split executable into lib and main 2025-03-09 05:11:16 +07:00
amizing25 c034f8a2f9 fix: modify file listener to ensure the file is modified 2025-03-09 05:10:33 +07:00
amizing25 6d9bf58a85 perf: remove lazy_static 2025-03-09 05:09:09 +07:00
amizing25 7d233dc614 chore: remove unused fields 2025-03-09 05:07:06 +07:00
amizing25 6d2489de47 chore: better response message & status 2025-03-08 22:06:16 +07:00
Keirakoro 6435d4e30b support IOS ( ig?) 2025-03-03 12:00:36 +07:00
55 changed files with 47355 additions and 104793 deletions
+109
View File
@@ -0,0 +1,109 @@
name: Prebuilt release
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
releases: write
jobs:
prebuilt:
runs-on: ubuntu-latest
env:
CARGO_TERM_COLOR: always
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Install nightly Rust
uses: dtolnay/rust-toolchain@nightly
with:
targets: x86_64-pc-windows-gnu
- name: Install native build tools
run: |
sudo apt-get update
sudo apt-get install -y mingw-w64 protobuf-compiler zip unzip jq
- name: Build release binaries
env:
CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER: x86_64-w64-mingw32-gcc
run: cargo build --release --target x86_64-pc-windows-gnu --bin gameserver --bin sdkserver
- name: Package prebuilt release
run: |
set -euo pipefail
rm -rf dist
mkdir -p dist
cp target/x86_64-pc-windows-gnu/release/gameserver.exe dist/gameserver.exe
cp target/x86_64-pc-windows-gnu/release/sdkserver.exe dist/sdkserver.exe
cp persistent res.json freesr-data.json versions.json dist/
for file in gameserver.exe sdkserver.exe persistent res.json freesr-data.json versions.json; do
test -f "dist/${file}"
done
(
cd dist
zip -9 -q ../prebuilt-win64.zip \
gameserver.exe \
sdkserver.exe \
persistent \
res.json \
freesr-data.json \
versions.json
)
unzip -l prebuilt-win64.zip
- name: Publish prebuilt release
env:
GITEA_API_URL: ${{ gitea.server_url }}/api/v1
GITEA_REPOSITORY: ${{ gitea.repository }}
GITEA_SHA: ${{ gitea.sha }}
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
run: |
set -euo pipefail
api="${GITEA_API_URL}"
repo="${GITEA_REPOSITORY}"
tag="prebuilt"
auth=(-H "Authorization: token ${GITEA_TOKEN}" -H "Accept: application/json")
release_status="$(curl -sS "${auth[@]}" -o release.json -w '%{http_code}' \
"${api}/repos/${repo}/releases/tags/${tag}" || true)"
case "${release_status}" in
200)
;;
404)
curl -fsS -X POST "${auth[@]}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\":\"${tag}\",\"target_commitish\":\"${GITEA_SHA}\",\"name\":\"${tag}\",\"body\":\"Automated prebuilt binaries for ${GITEA_SHA}\",\"draft\":false,\"prerelease\":false}" \
"${api}/repos/${repo}/releases" > release.json
;;
*)
cat release.json
exit 1
;;
esac
release_id="$(jq -er '.id' release.json)"
assets="$(curl -fsS "${auth[@]}" \
"${api}/repos/${repo}/releases/${release_id}/assets")"
while read -r asset_id; do
test -z "${asset_id}" || curl -fsS -X DELETE "${auth[@]}" \
"${api}/repos/${repo}/releases/${release_id}/assets/${asset_id}"
done < <(printf '%s' "${assets}" | jq -r '.[] | select(.name == "prebuilt-win64.zip") | .id')
curl -fsS -X POST "${auth[@]}" \
-F "attachment=@prebuilt-win64.zip;type=application/zip" \
"${api}/repos/${repo}/releases/${release_id}/assets?name=prebuilt-win64.zip"
+1 -1
View File
@@ -1,6 +1,6 @@
target/
Cargo.lock
proto/StarRail.proto
/prebuild
/assets
.vscode
.prebuilt
+42 -41
View File
@@ -6,64 +6,65 @@ resolver = "3"
version = "0.1.0"
[workspace.dependencies]
anyhow = "1.0.81"
# Framework
tokio = { version = "1.52.1", features = ["full"] }
axum = "0.8.9"
axum-server = "0.8.0"
tower-http = "0.6.8"
reqwest = { version = "0.13.3", default-features = false, features = [
"json",
"multipart",
"cookies",
"gzip",
"brotli",
"deflate",
"rustls",
"charset",
"http2",
"system-proxy",
] }
# JSON
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149"
# Logging
tracing = "0.1.44"
ansi_term = "0.12.1"
atomic_refcell = "0.1.13"
lazy_static = "1.4.0"
env_logger = "0.11.10"
axum = "0.8.1"
axum-server = "0.7.1"
tower-http = "0.6.2"
env_logger = "0.11.3"
rbase64 = "2.0.3"
rand = "0.9.0"
rsa = { version = "0.9.6", features = [
# Cryptography
rsa = { version = "0.9.10", features = [
"sha1",
"nightly",
"pkcs5",
"serde",
"sha2",
] }
rand = "0.10.1"
prost = "0.13.5"
prost-types = "0.13.5"
prost-build = "0.13.5"
# Serialization
prost = "0.14.3"
prost-types = "0.14.3"
prost-build = "0.14.3"
rbase64 = "2.0.3"
paste = "1.0.14"
sysinfo = "0.33.1"
notify = "8.0.0"
notify-debouncer-mini = "0.6.0"
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"
# Utilities
paste = "1.0.15"
notify = "8.2.0"
notify-debouncer-mini = "0.7.0"
anyhow = "1.0.102"
# Local
proto = { path = "proto/" }
proto-derive = { path = "proto/proto-derive" }
mhy-kcp = { path = "kcp/", features = ["tokio"] }
common = { path = "common/" }
sdkserver = { path = "sdkserver/" }
gameserver = { path = "gameserver/" }
[profile.release]
strip = true # Automatically strip symbols from the binary.
opt-level = "z" # Optimize for size.
strip = true # Automatically strip symbols from the binary.
lto = true
codegen-units = 1
+5 -3
View File
@@ -1,13 +1,15 @@
# Supported Version: 3.1.5x
# Supported Version: 4.4.5x
Run the game by clicking run.bat file.
Tool website: [https://srtools.pages.dev](https://srtools.pages.dev)
Tool website: [https://srtools.neonteam.dev](https://srtools.neonteam.dev)
Start battle by entering any calyx in the map, DON'T ATTACK THE ENEMIES, IT WON'T WORK (maybe)
Some scenes might not loaded properly. If you stuck at loading screen, remove `persistent` file.
# RobinSR
Original:
[https://git.xeondev.com/reversedrooms/RobinSR](https://git.xeondev.com/reversedrooms/RobinSR)
@@ -32,4 +34,4 @@ rustup default nightly
To begin using the server, you need to run both the SDK server and the game server. Just run the `run.bat` file.
# Special thanks for keiracoder for updating the repo 🔥
# Special thanks to keiracoder for updating the repo! 🔥
+1
View File
@@ -9,3 +9,4 @@ serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
tracing.workspace = true
anyhow.workspace = true
+1
View File
@@ -1,2 +1,3 @@
pub mod resources;
pub mod sr_tools;
pub mod structs;
+7 -1
View File
@@ -114,6 +114,9 @@ pub struct SceneData {
pub props: Vec<ScenePropInfo>,
pub monsters: Vec<SceneMonsterInfo>,
pub teleports: HashMap<u32, TeleportInfo>,
pub finished_sub_missions: Vec<u32>,
pub finished_main_missions: Vec<u32>,
pub chests: Vec<u32>,
}
#[derive(Deserialize)]
@@ -123,6 +126,8 @@ pub struct LevelOutputConfig {
pub scenes: HashMap<u32, SceneData>,
pub plane_type: u32,
pub world_id: u32,
pub sections: Vec<u32>,
pub saved_values: HashMap<String, i32>,
}
#[derive(Deserialize)]
@@ -138,7 +143,8 @@ pub struct JsonConfig {
/// `entryid` -> `P[planeId]_F[floorId]` -> `groupId`
pub level_output_configs: HashMap<u32, HashMap<String, LevelOutputConfig>>,
pub avatar_configs: HashMap<u32, AvatarConfig>,
pub map_default_entrance_map: HashMap<u32, u32>
pub map_default_entrance_map: HashMap<u32, u32>,
pub relic_avatar_recommend: HashMap<u32, Vec<u32>>,
}
pub static GAME_RES: LazyLock<JsonConfig> = LazyLock::new(|| {
+94 -706
View File
@@ -1,517 +1,17 @@
use proto::*;
use proto::{Avatar, AvatarPathData};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, HashMap};
// AVATAR
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AvatarJson {
#[serde(alias = "ownerUid")]
pub owner_uid: u32,
#[serde(alias = "avatarId")]
pub avatar_id: u32,
pub data: AvatarData,
pub level: u32,
pub promotion: u32,
#[serde(alias = "use_technique")]
#[serde(alias = "useTechnique")]
pub techniques: Vec<u32>,
#[serde(alias = "spValue")]
pub sp_value: Option<u32>,
#[serde(alias = "spMax")]
pub sp_max: Option<u32>,
}
use crate::structs::{
// AllowedLanguages,
avatar::{AvatarJson, MultiPathAvatar},
battle::BattleConfig,
lightcone::Lightcone,
persistent::Persistent,
relic::Relic,
scene::{Position, Scene},
};
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AvatarData {
pub rank: u32,
pub skills: HashMap<u32, u32>,
}
impl AvatarJson {
pub fn to_avatar_proto(&self, lightcone: Option<&Lightcone>, relics: Vec<&Relic>) -> Avatar {
// TODO: HARDCODED
let base_avatar_id = if self.avatar_id > 8000 {
8001
} else if self.avatar_id == 1001 || self.avatar_id == 1224 {
1001
} else {
self.avatar_id
};
Avatar {
base_avatar_id,
level: self.level,
promotion: self.promotion,
rank: self.data.rank,
skilltree_list: self
.data
.skills
.iter()
.map(|v| AvatarSkillTree {
point_id: *v.0,
level: *v.1,
})
.collect::<Vec<_>>(),
equipment_unique_id: if let Some(lc) = lightcone {
// TODO: HARDCODED LIGHTCONE ID
2000 + lc.internal_uid
} else {
0
},
first_met_timestamp: 1712924677,
equip_relic_list: relics
.iter()
.map(|v| v.to_equipment_relic_proto())
.collect::<Vec<_>>(),
..Default::default()
}
}
pub fn to_battle_avatar_proto(
&self,
index: u32,
lightcone: Option<&Lightcone>,
relics: Vec<&Relic>,
) -> (BattleAvatar, Vec<BattleBuff>) {
let battle_avatar = BattleAvatar {
index,
avatar_type: AvatarType::AvatarUpgradeAvailableType.into(),
id: self.avatar_id,
level: self.level,
rank: self.data.rank,
skilltree_list: self
.data
.skills
.iter()
.map(|v| AvatarSkillTree {
point_id: *v.0,
level: *v.1,
})
.collect::<Vec<_>>(),
equipment_list: if let Some(lc) = lightcone {
vec![lc.to_battle_equipment_proto()]
} else {
vec![]
},
hp: 10_000,
promotion: self.promotion,
relic_list: relics
.iter()
.map(|v| v.to_battle_relic_proto())
.collect::<Vec<_>>(),
world_level: 6,
sp_bar: Some(SpBarInfo {
cur_sp: self.sp_value.unwrap_or(10_000),
max_sp: self.sp_max.unwrap_or(10_000),
}),
..Default::default()
};
let mut battle_buff = Vec::<BattleBuff>::new();
for buff_id in &self.techniques {
battle_buff.push(BattleBuff {
wave_flag: 0xffffffff,
owner_id: index,
level: 1,
id: *buff_id,
dynamic_values: HashMap::from([(String::from("SkillIndex"), 2.0)]),
..Default::default()
});
}
(battle_avatar, battle_buff)
}
pub fn to_lineup_avatar_proto(&self, slot: u32) -> LineupAvatar {
LineupAvatar {
id: self.avatar_id,
hp: 10_000,
satiety: 100,
avatar_type: AvatarType::AvatarFormalType.into(),
sp_bar: Some(SpBarInfo {
cur_sp: self.sp_value.unwrap_or(10_000),
max_sp: self.sp_max.unwrap_or(10_000),
}),
slot,
}
}
pub fn to_lineup_avatars(player: &FreesrData) -> Vec<LineupAvatar> {
let avatar_ids = player
.avatars
.values()
.map(|v| &v.avatar_id)
.collect::<Vec<_>>();
player
.lineups
.iter()
.filter(|(slot, v)| **slot < 4 && v > &&0 && avatar_ids.contains(v))
.map(|(slot, avatar_id)| {
player
.avatars
.get(avatar_id)
.unwrap()
.to_lineup_avatar_proto(*slot)
})
.collect::<Vec<LineupAvatar>>()
}
pub fn to_lineup_info(lineups: &BTreeMap<u32, u32>) -> LineupInfo {
let mut lineup_info = LineupInfo {
extra_lineup_type: ExtraLineupType::LineupNone.into(),
name: "Squad 1".to_string(),
mp: 5,
max_mp: 5,
..Default::default()
};
for id in lineups.values() {
if *id == 0 {
continue;
}
lineup_info.avatar_list.push(LineupAvatar {
id: *id,
hp: 10_000,
satiety: 100,
avatar_type: AvatarType::AvatarFormalType.into(),
sp_bar: Some(SpBarInfo {
cur_sp: 10_000,
max_sp: 10_000,
}),
slot: lineup_info.avatar_list.len() as u32,
});
}
lineup_info
}
}
// LIGHTCONE
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Lightcone {
pub level: u32,
#[serde(alias = "itemId")]
pub item_id: u32,
#[serde(alias = "equipAvatar")]
pub equip_avatar: u32,
pub rank: u32,
pub promotion: u32,
#[serde(alias = "internalUid")]
pub internal_uid: u32,
}
impl Lightcone {
pub fn to_equipment_proto(&self) -> Equipment {
Equipment {
equip_avatar_id: self.equip_avatar,
exp: 0,
is_protected: false,
level: self.level,
promotion: self.promotion,
rank: self.rank,
tid: self.item_id,
// ?
unique_id: 2000 + self.internal_uid,
}
}
pub fn to_battle_equipment_proto(&self) -> BattleEquipment {
BattleEquipment {
id: self.item_id,
level: self.level,
promotion: self.promotion,
rank: self.rank,
}
}
}
// RELIC
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Relic {
pub level: u32,
#[serde(alias = "relicId")]
pub relic_id: u32,
#[serde(alias = "relicSetId")]
pub relic_set_id: u32,
#[serde(alias = "mainAffixId")]
pub main_affix_id: u32,
#[serde(alias = "subAffixes")]
pub sub_affixes: Vec<SubAffix>,
#[serde(alias = "internalUid")]
pub internal_uid: u32,
#[serde(alias = "equipAvatar")]
pub equip_avatar: u32,
}
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct SubAffix {
#[serde(alias = "subAffixId")]
pub sub_affix_id: u32,
pub count: u32,
pub step: u32,
}
impl Relic {
pub fn to_relic_proto(&self) -> proto::Relic {
proto::Relic {
equip_avatar_id: self.equip_avatar,
exp: 0,
is_protected: false,
level: self.level,
main_affix_id: self.main_affix_id,
tid: self.relic_id,
// ?
unique_id: 1 + self.internal_uid,
sub_affix_list: self
.sub_affixes
.iter()
.map(|v| RelicAffix {
affix_id: v.sub_affix_id,
cnt: v.count,
step: v.step,
})
.collect::<Vec<_>>(),
..Default::default()
}
}
pub fn to_battle_relic_proto(&self) -> BattleRelic {
BattleRelic {
id: self.relic_id,
level: self.level,
main_affix_id: self.main_affix_id,
unique_id: self.internal_uid,
sub_affix_list: self
.sub_affixes
.iter()
.map(|v| RelicAffix {
affix_id: v.sub_affix_id,
cnt: v.count,
step: v.step,
})
.collect::<Vec<_>>(),
..Default::default()
}
}
pub fn to_equipment_relic_proto(&self) -> EquipRelic {
EquipRelic {
slot: self.relic_id % 10,
// ?
relic_unique_id: 1 + self.internal_uid,
}
}
}
// MONSTER
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct Monster {
pub level: u32,
#[serde(alias = "monsterId")]
pub monster_id: u32,
#[serde(default)]
pub max_hp: u32,
}
impl Monster {
fn to_scene_monster_info(&self) -> SceneMonster {
SceneMonster {
monster_id: self.monster_id,
max_hp: self.max_hp,
cur_hp: self.max_hp,
}
}
pub fn to_scene_monster_wave(wave_index: u32, monsters: &[Self]) -> SceneMonsterWave {
let mut wave_index = wave_index;
if wave_index < 1 {
wave_index += 1;
}
SceneMonsterWave {
wave_id: wave_index, // wave indexx??
wave_param: Some(SceneMonsterWaveParam {
// monster param
level: monsters.iter().map(|v| v.level).max().unwrap_or(95),
..Default::default()
}),
monster_list: monsters
.iter()
.map(|v| v.to_scene_monster_info())
.collect::<Vec<_>>(),
..Default::default()
}
}
pub fn to_scene_monster_waves(monsters: &[Vec<Self>]) -> Vec<SceneMonsterWave> {
monsters
.iter()
.enumerate()
.map(|(i, v)| Self::to_scene_monster_wave(i as u32, v))
.collect::<_>()
}
}
// BATTLE CONFIG
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BattleConfig {
pub battle_type: BattleType,
pub monsters: Vec<Vec<Monster>>,
pub blessings: Vec<BattleBuffJson>,
pub stage_id: u32,
pub cycle_count: u32,
pub path_resonance_id: u32,
pub custom_stats: Vec<SubAffix>,
#[serde(default)]
pub scepters: Vec<RogueMagicScepter>,
}
impl Default for BattleConfig {
fn default() -> Self {
Self {
battle_type: Default::default(),
monsters: vec![vec![Monster {
level: 60,
monster_id: 3014022,
max_hp: 0,
}]],
stage_id: 201012311,
blessings: Default::default(),
cycle_count: Default::default(),
path_resonance_id: Default::default(),
custom_stats: Default::default(),
scepters: Default::default(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum BattleType {
#[serde(alias = "DEFAULT")]
Default = 0,
#[serde(alias = "MOC")]
Moc = 1,
PF = 2,
SU = 3,
AS = 4,
}
impl Default for BattleType {
fn default() -> Self {
Self::Default
}
}
// BATTLE BUFFS
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct BattleBuffJson {
pub level: u32,
pub id: u32,
pub dynamic_key: Option<DynamicKey>,
#[serde(default)]
pub dynamic_values: Vec<DynamicKey>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct DynamicKey {
pub key: String,
pub value: u32,
}
#[allow(dead_code)]
impl BattleBuffJson {
pub fn to_battle_buff_proto(&self) -> proto::BattleBuff {
proto::BattleBuff {
id: self.id,
level: self.level,
wave_flag: 0xffffffff,
owner_id: 0xffffffff,
dynamic_values: if let Some(dyn_key) = &self.dynamic_key {
HashMap::from([(dyn_key.key.clone(), dyn_key.value as f32)])
} else {
Default::default()
},
..Default::default()
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Default)]
#[repr(u32)]
pub enum RogueMagicComponentType {
Passive = 3,
#[default]
Active = 4,
Attach = 5,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct RogueMagicScepter {
pub level: u32,
pub id: u32,
pub components: Vec<RogueMagicComponent>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct RogueMagicComponent {
pub id: u32,
pub level: u32,
pub component_type: RogueMagicComponentType,
}
// SCENE
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Scene {
pub plane_id: u32,
pub floor_id: u32,
pub entry_id: u32,
}
impl Default for Scene {
fn default() -> Self {
Self {
entry_id: 2032101,
plane_id: 20321,
floor_id: 20321001,
}
}
}
// Position
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct Position {
pub x: i32,
pub y: i32,
pub z: i32,
pub rot_y: i32,
}
impl Position {
#[allow(unused)]
pub fn is_empty(&self) -> bool {
self.x == 0 && self.y == 0 && self.z == 0
}
pub fn to_motion(&self) -> MotionInfo {
MotionInfo {
rot: Some(Vector {
x: 0,
y: self.rot_y,
z: 0,
}),
pos: Some(Vector {
x: self.x,
y: self.y,
z: self.z,
}),
}
}
}
// FREESR-DATA.json
#[derive(Debug, Serialize, Deserialize)]
pub struct FreesrData {
pub lightcones: Vec<Lightcone>,
@@ -520,186 +20,115 @@ pub struct FreesrData {
#[serde(default)]
pub battle_config: BattleConfig,
#[serde(default, skip_serializing)]
// Non freesr-data.json fields
#[serde(skip_serializing, skip_deserializing)]
pub lineups: BTreeMap<u32, u32>,
#[serde(default, skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub position: Position,
#[serde(default, skip_serializing)]
#[serde(skip_serializing, skip_deserializing)]
pub scene: Scene,
#[serde(skip_serializing, skip_deserializing)]
pub main_character: MultiPathAvatar,
#[serde(skip_serializing, skip_deserializing)]
pub march_type: MultiPathAvatar,
#[serde(skip_serializing, skip_deserializing)]
pub enable_sw_global: Option<bool>,
#[serde(skip_serializing, skip_deserializing)]
pub enable_castorice_global: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Persistent {
#[serde(default)]
pub lineups: BTreeMap<u32, u32>,
#[serde(default)]
pub position: Position,
#[serde(default)]
pub scene: Scene,
pub main_character: MultiPathAvatar,
pub march_type: MultiPathAvatar,
}
impl Default for Persistent {
fn default() -> Self {
let mut lineups = BTreeMap::<u32, u32>::new();
lineups.insert(0, 8001);
lineups.insert(1, 0);
lineups.insert(2, 0);
lineups.insert(3, 0);
Self {
lineups,
position: Default::default(),
main_character: MultiPathAvatar::FemaleRememberance,
scene: Default::default(),
march_type: MultiPathAvatar::MarchHunt,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Copy, PartialEq, Eq, Default)]
#[repr(u32)]
pub enum MultiPathAvatar {
MalePyhsical = 8001,
FemalePhysical = 8002,
MalePreservation = 8003,
FemalePreservation = 8004,
MaleHarmony = 8005,
FemaleHarmony = 8006,
MaleRememberance = 8007,
FemaleRememberance = 8008,
MarchHunt = 1224,
MarchPreservation = 1001,
#[default]
Unk = 0,
}
impl From<u32> for MultiPathAvatar {
fn from(value: u32) -> Self {
match value {
8001 => Self::MalePyhsical,
8002 => Self::FemalePhysical,
8003 => Self::MalePreservation,
8004 => Self::FemalePreservation,
8005 => Self::MaleHarmony,
8006 => Self::FemaleHarmony,
8007 => Self::MaleRememberance,
8008 => Self::FemaleRememberance,
1224 => Self::MarchHunt,
1001 => Self::MarchPreservation,
_ => Self::Unk,
}
}
}
impl From<MultiPathAvatar> for u32 {
fn from(value: MultiPathAvatar) -> Self {
match value {
MultiPathAvatar::MalePyhsical => 8001,
MultiPathAvatar::FemalePhysical => 8002,
MultiPathAvatar::MalePreservation => 8003,
MultiPathAvatar::FemalePreservation => 8004,
MultiPathAvatar::MaleHarmony => 8005,
MultiPathAvatar::FemaleHarmony => 8006,
MultiPathAvatar::MaleRememberance => 8007,
MultiPathAvatar::FemaleRememberance => 8008,
MultiPathAvatar::MarchHunt => 1224,
MultiPathAvatar::MarchPreservation => 1001,
_ => 8006,
}
}
}
impl MultiPathAvatar {
#[allow(unused)]
pub fn get_gender(&self) -> Gender {
if (*self as u32) < 8000 {
Gender::None
} else if *self as u32 % 2 == 1 {
Gender::Man
} else {
Gender::Woman
}
impl FreesrData {
pub fn get_avatar_proto(&self, avatar_id: u32, mc_id: u32, march_id: u32) -> Option<Avatar> {
let avatar = self.avatars.get(&avatar_id)?;
let lightcone = self.lightcones.iter().find(|l| l.equip_avatar == avatar_id);
Some(avatar.to_avatar_proto(lightcone, mc_id, march_id))
}
pub fn get_type(&self) -> MultiPathAvatarType {
match *self {
MultiPathAvatar::MalePyhsical => MultiPathAvatarType::BoyWarriorType,
MultiPathAvatar::FemalePhysical => MultiPathAvatarType::GirlWarriorType,
MultiPathAvatar::MalePreservation => MultiPathAvatarType::BoyKnightType,
MultiPathAvatar::FemalePreservation => MultiPathAvatarType::GirlKnightType,
MultiPathAvatar::MaleHarmony => MultiPathAvatarType::BoyShamanType,
MultiPathAvatar::FemaleHarmony => MultiPathAvatarType::GirlShamanType,
MultiPathAvatar::MarchHunt => MultiPathAvatarType::Mar7thRogueType,
MultiPathAvatar::MarchPreservation => MultiPathAvatarType::Mar7thKnightType,
MultiPathAvatar::Unk => MultiPathAvatarType::None,
MultiPathAvatar::MaleRememberance => MultiPathAvatarType::BoyMemoryType,
MultiPathAvatar::FemaleRememberance => MultiPathAvatarType::GirlMemoryType,
}
}
pub fn get_avatar_path_data_proto(&self, avatar_id: u32) -> Option<AvatarPathData> {
let avatar = self.avatars.get(&avatar_id)?;
pub fn is_mc(&self) -> bool {
(*self as u32) > 8000
}
pub fn to_vec() -> Vec<MultiPathAvatar> {
vec![
Self::MalePyhsical,
Self::FemalePhysical,
Self::MalePreservation,
Self::FemalePreservation,
Self::MaleHarmony,
Self::FemaleHarmony,
Self::MaleRememberance,
Self::FemaleRememberance,
Self::MarchHunt,
Self::MarchPreservation,
]
Some(
avatar.to_avatar_path_data_proto(
self.lightcones.iter().find(|v| v.equip_avatar == avatar_id),
self.relics
.iter()
.filter(|relic| relic.equip_avatar == avatar_id)
.collect(),
),
)
}
}
impl FreesrData {
pub async fn load() -> Self {
let mut json: FreesrData = tokio::fs::read_to_string("freesr-data.json")
pub async fn load() -> anyhow::Result<Self> {
let mut freesr_data: FreesrData = tokio::fs::read_to_string("freesr-data.json")
.await
.map(|v| {
.map_err(|e| anyhow::anyhow!("failed to read freesr-data.json: {e}"))
.and_then(|v| {
serde_json::from_str::<FreesrData>(&v)
.expect("freesr-data.json is broken, pls redownload")
})
.expect("failed to read freesr-data.json");
.map_err(|e| anyhow::anyhow!("freesr-data.json is broken: {e}, pls redownload"))
})?;
let json2: Persistent = serde_json::from_str(
let persistent: Persistent = serde_json::from_str(
&tokio::fs::read_to_string("persistent")
.await
.unwrap_or_default(),
)
.unwrap_or_default();
json.lineups = json2.lineups;
json.position = json2.position;
json.scene = json2.scene;
json.main_character = json2.main_character;
json.march_type = json2.march_type;
freesr_data.lineups = persistent.lineups;
freesr_data.position = persistent.position;
freesr_data.scene = persistent.scene;
freesr_data.main_character = persistent.main_character;
freesr_data.march_type = persistent.march_type;
freesr_data.enable_sw_global = persistent.enable_sw_global;
freesr_data.enable_castorice_global = persistent.enable_castorice_global;
// freesr_data.game_language = persistent.game_language;
// freesr_data.voice_langauge = persistent.voice_language;
json.verify_lineup().await;
if json.march_type as u32 > 8000 {
json.march_type = MultiPathAvatar::MarchHunt;
// remove unequipped lightcones
if freesr_data.lightcones.len() > 1500 {
freesr_data.lightcones.retain(|v| v.equip_avatar != 0);
}
json
// remove unequipped relics
if freesr_data.relics.len() > 3000 {
freesr_data.relics.retain(|v| v.equip_avatar != 0);
}
pub async fn update(&mut self) {
*self = Self::load().await
freesr_data.verify_lineup().await;
if freesr_data.march_type as u32 > 8000 {
freesr_data.march_type = MultiPathAvatar::MarchHunt;
}
Ok(freesr_data)
}
pub async fn update(&mut self) -> anyhow::Result<()> {
*self = Self::load().await?;
Ok(())
}
// pub async fn refresh_persistent(&mut self) -> anyhow::Result<()> {
// let persistent: Persistent = serde_json::from_str(
// &tokio::fs::read_to_string("persistent")
// .await
// .unwrap_or_default(),
// )
// .unwrap_or_default();
// self.lineups = persistent.lineups;
// self.position = persistent.position;
// self.scene = persistent.scene;
// self.main_character = persistent.main_character;
// self.march_type = persistent.march_type;
// self.enable_sw_global = persistent.enable_sw_global;
// self.enable_castorice_global = persistent.enable_castorice_global;
// Ok(())
// }
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)])
@@ -712,60 +141,19 @@ impl FreesrData {
}
pub async fn save_persistent(&self) {
let _ = tokio::fs::write(
"persistent",
serde_json::to_string_pretty(&Persistent {
if let Ok(data) = serde_json::to_string_pretty(&Persistent {
lineups: self.lineups.clone(),
main_character: self.main_character,
position: self.position.clone(),
scene: self.scene.clone(),
march_type: self.march_type,
})
.unwrap(),
)
.await;
enable_sw_global: self.enable_sw_global,
enable_castorice_global: self.enable_castorice_global,
// game_language: self.game_language,
// voice_language: self.voice_langauge,
}) {
let _ = tokio::fs::write("persistent", data).await;
tracing::info!("persistent saved");
}
pub fn get_multi_path_info(&self) -> Vec<MultiPathAvatarTypeInfo> {
MultiPathAvatar::to_vec()
.iter()
.filter_map(|mp_type| {
if mp_type.is_mc() && mp_type.get_gender() != self.main_character.get_gender() {
return Option::None;
}
let avatar_info = self.avatars.get(&((*mp_type) as u32))?;
Some(MultiPathAvatarTypeInfo {
avatar_id: *mp_type as i32,
rank: avatar_info.data.rank,
equip_relic_list: self
.relics
.iter()
.filter(|relic| relic.equip_avatar == *mp_type as u32)
.map(|relic| relic.to_equipment_relic_proto())
.collect(),
skilltree_list: avatar_info
.data
.skills
.iter()
.map(|(point_id, level)| AvatarSkillTree {
point_id: *point_id,
level: *level,
})
.collect(),
path_equipment_id: self
.lightcones
.iter()
.find(|v| v.equip_avatar == *mp_type as u32)
.map(|v| {
// TODO: HARDCODED LIGHTCONE ID
2000 + v.internal_uid
})
.unwrap_or_default(),
dressed_skin_id: 0,
})
})
.collect()
}
}
+334
View File
@@ -0,0 +1,334 @@
use std::collections::{BTreeMap, HashMap};
use proto::{
Avatar, AvatarPathData, AvatarPathSkillTree, AvatarSkillTree, AvatarType, BattleAvatar,
BattleBuff, ExtraLineupType, Gender, LineupAvatar, LineupInfo, MultiPathAvatarType, SpBarInfo,
};
use serde::{Deserialize, Serialize};
use crate::sr_tools::FreesrData;
use super::{lightcone::Lightcone, relic::Relic};
#[derive(Serialize, Deserialize, Clone, Debug, Copy, PartialEq, Eq, Default)]
#[repr(u32)]
pub enum MultiPathAvatar {
MalePyhsical = 8001,
FemalePhysical = 8002,
MalePreservation = 8003,
FemalePreservation = 8004,
MaleHarmony = 8005,
FemaleHarmony = 8006,
MaleRemembrance = 8007,
FemaleRemembrance = 8008,
MaleElation = 8009,
FemaleElation = 8010,
MarchHunt = 1224,
MarchPreservation = 1001,
#[default]
Unk = 0,
}
impl From<u32> for MultiPathAvatar {
fn from(value: u32) -> Self {
match value {
8001 => Self::MalePyhsical,
8002 => Self::FemalePhysical,
8003 => Self::MalePreservation,
8004 => Self::FemalePreservation,
8005 => Self::MaleHarmony,
8006 => Self::FemaleHarmony,
8007 => Self::MaleRemembrance,
8008 => Self::FemaleRemembrance,
8009 => Self::MaleElation,
8010 => Self::FemaleElation,
1224 => Self::MarchHunt,
1001 => Self::MarchPreservation,
_ => Self::Unk,
}
}
}
impl From<MultiPathAvatar> for u32 {
fn from(value: MultiPathAvatar) -> Self {
match value {
MultiPathAvatar::MalePyhsical => 8001,
MultiPathAvatar::FemalePhysical => 8002,
MultiPathAvatar::MalePreservation => 8003,
MultiPathAvatar::FemalePreservation => 8004,
MultiPathAvatar::MaleHarmony => 8005,
MultiPathAvatar::FemaleHarmony => 8006,
MultiPathAvatar::MaleRemembrance => 8007,
MultiPathAvatar::FemaleRemembrance => 8008,
MultiPathAvatar::MaleElation => 8009,
MultiPathAvatar::FemaleElation => 8010,
MultiPathAvatar::MarchHunt => 1224,
MultiPathAvatar::MarchPreservation => 1001,
MultiPathAvatar::Unk => 8006,
}
}
}
impl MultiPathAvatar {
#[allow(unused)]
pub fn get_gender(&self) -> Gender {
if (*self as u32) < 8000 {
Gender::None
} else if *self as u32 % 2 == 1 {
Gender::Man
} else {
Gender::Woman
}
}
pub fn get_type(&self) -> MultiPathAvatarType {
match *self {
MultiPathAvatar::MalePyhsical => MultiPathAvatarType::BoyWarriorType,
MultiPathAvatar::FemalePhysical => MultiPathAvatarType::GirlWarriorType,
MultiPathAvatar::MalePreservation => MultiPathAvatarType::BoyKnightType,
MultiPathAvatar::FemalePreservation => MultiPathAvatarType::GirlKnightType,
MultiPathAvatar::MaleHarmony => MultiPathAvatarType::BoyShamanType,
MultiPathAvatar::FemaleHarmony => MultiPathAvatarType::GirlShamanType,
MultiPathAvatar::MarchHunt => MultiPathAvatarType::Mar7thRogueType,
MultiPathAvatar::MarchPreservation => MultiPathAvatarType::Mar7thKnightType,
MultiPathAvatar::Unk => MultiPathAvatarType::None,
MultiPathAvatar::MaleRemembrance => MultiPathAvatarType::BoyMemoryType,
MultiPathAvatar::FemaleRemembrance => MultiPathAvatarType::GirlMemoryType,
MultiPathAvatar::MaleElation => MultiPathAvatarType::BoyElationType,
MultiPathAvatar::FemaleElation => MultiPathAvatarType::GirlElationType,
}
}
pub fn is_mc(&self) -> bool {
(*self as u32) > 8000
}
pub fn to_vec() -> Vec<MultiPathAvatar> {
vec![
Self::MalePyhsical,
Self::FemalePhysical,
Self::MalePreservation,
Self::FemalePreservation,
Self::MaleHarmony,
Self::FemaleHarmony,
Self::MaleRemembrance,
Self::FemaleRemembrance,
Self::MaleElation,
Self::FemaleElation,
Self::MarchHunt,
Self::MarchPreservation,
]
}
}
// AVATAR
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AvatarJson {
// #[serde(alias = "ownerUid")]
// pub owner_uid: u32,
#[serde(alias = "avatarId")]
pub avatar_id: u32,
pub data: AvatarData,
pub level: u32,
pub promotion: u32,
#[serde(alias = "use_technique")]
#[serde(alias = "useTechnique")]
pub techniques: Vec<u32>,
#[serde(alias = "spValue")]
pub sp_value: Option<u32>,
#[serde(alias = "spMax")]
pub sp_max: Option<u32>,
pub enhanced_id: Option<u32>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AvatarData {
pub rank: u32,
pub skills: HashMap<u32, u32>,
#[serde(default)]
pub skills_by_anchor_type: HashMap<u32, u32>,
}
impl AvatarJson {
/// This should only be used to serialize "BaseAvatar". For example, only 8001 for MC and 1001 for March
pub fn to_avatar_proto(
&self,
lightcone: Option<&Lightcone>,
mc_id: u32,
march_id: u32,
) -> Avatar {
// TODO: HARDCODED
let base_avatar_id = if self.avatar_id > 8000 {
8001
} else if self.avatar_id == 1001 || self.avatar_id == 1224 {
1001
} else {
self.avatar_id
};
// TODO: HARDCODED
let cur_multi_path_avatar_type = if base_avatar_id == 8001 {
mc_id
} else if base_avatar_id == 1001 {
march_id
} else {
base_avatar_id
};
Avatar {
base_avatar_id,
level: self.level,
promotion: self.promotion,
equipment_unique_id: lightcone.map(|v| v.get_unique_id()).unwrap_or_default(),
first_met_time_stamp: 1712924677,
cur_multi_path_avatar_type,
has_taken_promotion_reward_list: vec![1, 3, 5],
is_marked: false,
exp: 0,
}
}
pub fn to_avatar_path_data_proto(
&self,
lightcone: Option<&Lightcone>,
relics: Vec<&Relic>,
) -> AvatarPathData {
AvatarPathData {
avatar_id: self.avatar_id,
rank: self.data.rank,
equip_relic_list: relics
.iter()
.filter(|relic| relic.equip_avatar == self.avatar_id)
.map(|&relic| relic.into())
.collect(),
avatar_path_skill_tree: self
.data
.skills_by_anchor_type
.iter()
.map(|(&anchor_type, &level)| AvatarPathSkillTree { anchor_type, level })
.collect(),
path_equipment_id: lightcone.map(|v| v.get_unique_id()).unwrap_or_default(),
unk_enhanced_id: self.enhanced_id.unwrap_or_default(),
unlock_timestamp: 0,
dressed_skin_id: 0,
}
}
pub fn to_battle_avatar_proto(
&self,
index: u32,
lightcone: Option<&Lightcone>,
relics: Vec<&Relic>,
) -> (BattleAvatar, Vec<BattleBuff>) {
let battle_avatar = BattleAvatar {
index,
avatar_type: AvatarType::AvatarUpgradeAvailableType.into(),
id: self.avatar_id,
level: self.level,
rank: self.data.rank,
skilltree_list: self
.data
.skills
.iter()
.map(|v| AvatarSkillTree {
point_id: *v.0,
level: *v.1,
})
.collect::<Vec<_>>(),
equipment_list: if let Some(lc) = lightcone {
vec![lc.into()]
} else {
vec![]
},
hp: 10_000,
promotion: self.promotion,
relic_list: relics.iter().map(|v| (*v).into()).collect::<Vec<_>>(),
world_level: 6,
sp_bar: Some(SpBarInfo {
cur_sp: self.sp_value.unwrap_or(10_000),
max_sp: self.sp_max.unwrap_or(10_000),
}),
enhanced_id: self.enhanced_id.unwrap_or_default(),
..Default::default()
};
let mut battle_buff = Vec::<BattleBuff>::new();
for buff_id in &self.techniques {
battle_buff.push(BattleBuff {
wave_flag: 0xffffffff,
owner_index: index,
level: 1,
id: *buff_id,
dynamic_values: HashMap::from([(String::from("SkillIndex"), 2.0)]),
..Default::default()
});
}
(battle_avatar, battle_buff)
}
pub fn to_lineup_avatar_proto(&self, slot: u32) -> LineupAvatar {
LineupAvatar {
id: self.avatar_id,
hp: 10_000,
satiety: 100,
avatar_type: AvatarType::AvatarFormalType.into(),
sp_bar: Some(SpBarInfo {
cur_sp: self.sp_value.unwrap_or(10_000),
max_sp: self.sp_max.unwrap_or(10_000),
}),
slot,
}
}
pub fn to_lineup_avatars(player: &FreesrData) -> Vec<LineupAvatar> {
let avatar_ids = player
.avatars
.values()
.map(|v| &v.avatar_id)
.collect::<Vec<_>>();
player
.lineups
.iter()
.filter(|(slot, v)| **slot < 4 && v > &&0 && avatar_ids.contains(v))
.map(|(slot, avatar_id)| {
player
.avatars
.get(avatar_id)
.unwrap()
.to_lineup_avatar_proto(*slot)
})
.collect::<Vec<LineupAvatar>>()
}
pub fn to_lineup_info(lineups: &BTreeMap<u32, u32>) -> LineupInfo {
let max_mp = if lineups.contains_key(&1408) { 8 } else { 5 };
let mut lineup_info = LineupInfo {
extra_lineup_type: ExtraLineupType::LineupNone.into(),
name: "Squad 1".to_string(),
mp: max_mp,
max_mp,
..Default::default()
};
for id in lineups.values() {
if *id == 0 {
continue;
}
lineup_info.avatar_list.push(LineupAvatar {
id: *id,
hp: 10_000,
satiety: 100,
avatar_type: AvatarType::AvatarFormalType.into(),
sp_bar: Some(SpBarInfo {
cur_sp: 10_000,
max_sp: 10_000,
}),
slot: lineup_info.avatar_list.len() as u32,
});
}
lineup_info
}
}
+111
View File
@@ -0,0 +1,111 @@
use std::collections::{BTreeMap, HashMap};
use serde::{Deserialize, Serialize};
use super::{monster::Monster, relic::SubAffix};
// BATTLE CONFIG
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct BattleConfig {
pub battle_type: BattleType,
pub monsters: Vec<Vec<Monster>>,
pub blessings: Vec<BattleBuffJson>,
pub stage_id: u32,
pub cycle_count: u32,
pub path_resonance_id: u32,
pub custom_stats: Vec<SubAffix>,
#[serde(default)]
pub scepters: Vec<RogueMagicScepter>,
#[serde(default)]
pub custom_battle_lineup: Option<BTreeMap<u32, u32>>,
}
impl Default for BattleConfig {
fn default() -> Self {
Self {
battle_type: Default::default(),
monsters: vec![vec![Monster {
level: 60,
monster_id: 3014022,
max_hp: 0,
}]],
stage_id: 201012311,
blessings: Default::default(),
cycle_count: Default::default(),
path_resonance_id: Default::default(),
custom_stats: Default::default(),
scepters: Default::default(),
custom_battle_lineup: Default::default(),
}
}
}
#[derive(Default, Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum BattleType {
#[serde(alias = "DEFAULT")]
#[default]
Default = 0,
#[serde(alias = "MOC")]
Moc = 1,
PF = 2,
SU = 3,
AS = 4,
AA = 5,
}
// BATTLE BUFFS
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct BattleBuffJson {
pub level: u32,
pub id: u32,
pub dynamic_key: Option<DynamicKey>,
#[serde(default)]
pub dynamic_values: Vec<DynamicKey>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct DynamicKey {
pub key: String,
pub value: u32,
}
#[allow(dead_code)]
impl BattleBuffJson {
pub fn to_battle_buff_proto(&self) -> proto::BattleBuff {
proto::BattleBuff {
id: self.id,
level: self.level,
wave_flag: 0xffffffff,
owner_index: 0xffffffff,
dynamic_values: if let Some(dyn_key) = &self.dynamic_key {
HashMap::from([(dyn_key.key.clone(), dyn_key.value as f32)])
} else {
Default::default()
},
..Default::default()
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq, Default)]
#[repr(u32)]
pub enum RogueMagicComponentType {
Passive = 3,
#[default]
Active = 4,
Attach = 5,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct RogueMagicScepter {
pub level: u32,
pub id: u32,
pub components: Vec<RogueMagicComponent>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct RogueMagicComponent {
pub id: u32,
pub level: u32,
pub component_type: RogueMagicComponentType,
}
+45
View File
@@ -0,0 +1,45 @@
use proto::{BattleEquipment, Equipment, ItemType};
use serde::{Deserialize, Serialize};
use crate::impl_from;
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Lightcone {
pub level: u32,
#[serde(alias = "itemId")]
pub item_id: u32,
#[serde(alias = "equipAvatar")]
pub equip_avatar: u32,
pub rank: u32,
pub promotion: u32,
#[serde(alias = "internalUid")]
pub internal_uid: u32,
}
impl_from!(Lightcone, Equipment, |value| {
Equipment {
dress_avatar_id: value.equip_avatar,
exp: 0,
is_protected: false,
level: value.level,
promotion: value.promotion,
rank: value.rank,
tid: value.item_id,
unique_id: value.get_unique_id(),
}
});
impl_from!(Lightcone, BattleEquipment, |value| {
BattleEquipment {
id: value.item_id,
level: value.level,
promotion: value.promotion,
rank: value.rank,
}
});
impl Lightcone {
pub fn get_unique_id(&self) -> u32 {
super::get_item_unique_id(self.internal_uid, ItemType::ItemEquipment)
}
}
+48
View File
@@ -0,0 +1,48 @@
use proto::ItemType;
pub mod avatar;
pub mod battle;
pub mod lightcone;
pub mod monster;
pub mod persistent;
pub mod relic;
pub mod scene;
pub use avatar::*;
pub use battle::*;
pub use lightcone::*;
pub use monster::*;
pub use persistent::*;
pub use scene::*;
pub fn get_item_unique_id(internal_uid: u32, item_type: ItemType) -> u32 {
// srtools start internal_uid from 1
if item_type == ItemType::ItemEquipment {
3000 + internal_uid // 3001..3500
} else {
internal_uid // 1..2000 ->
}
}
#[macro_export]
macro_rules! impl_from {
($from:ty, $to:ty, |$param:ident| $body:block) => {
impl From<$from> for $to {
fn from($param: $from) -> Self {
$body
}
}
impl From<&$from> for $to {
fn from($param: &$from) -> Self {
$body
}
}
impl From<&mut $from> for $to {
fn from($param: &mut $from) -> Self {
$body
}
}
};
}
+49
View File
@@ -0,0 +1,49 @@
use proto::{SceneMonster, SceneMonsterWave, SceneMonsterWaveParam};
use serde::{Deserialize, Serialize};
use crate::impl_from;
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct Monster {
pub level: u32,
#[serde(alias = "monsterId")]
pub monster_id: u32,
#[serde(default)]
pub max_hp: u32,
}
impl_from!(Monster, SceneMonster, |value| {
SceneMonster {
monster_id: value.monster_id,
max_hp: value.max_hp,
cur_hp: value.max_hp,
extra_info: None,
}
});
impl Monster {
pub fn to_scene_monster_wave(mut wave_id: u32, monsters: &[Self]) -> SceneMonsterWave {
if wave_id < 1 {
wave_id += 1;
}
SceneMonsterWave {
battle_wave_id: wave_id,
monster_param: Some(SceneMonsterWaveParam {
level: monsters.iter().map(|v| v.level).max().unwrap_or(95),
..Default::default()
}),
monster_list: monsters.iter().map(|v| v.into()).collect(),
..Default::default()
}
}
pub fn to_scene_monster_waves(monsters: &[Vec<Self>]) -> Vec<SceneMonsterWave> {
monsters
.iter()
.enumerate()
.map(|(i, v)| Self::to_scene_monster_wave(i as u32, v))
.collect::<_>()
}
}
+70
View File
@@ -0,0 +1,70 @@
use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::{
avatar::MultiPathAvatar,
scene::{Position, Scene},
};
// #[derive(Serialize, Deserialize, Clone, Debug, Copy, PartialEq, Eq, Default)]
// #[repr(u32)]
// pub enum AllowedLanguages {
// #[default]
// En,
// Jp,
// Kr,
// Cn,
// }
// impl FromStr for AllowedLanguages {
// type Err = ();
// fn from_str(input: &str) -> Result<AllowedLanguages, Self::Err> {
// match input {
// "en" => Ok(AllowedLanguages::En),
// "kr" => Ok(AllowedLanguages::Kr),
// "jp" => Ok(AllowedLanguages::Jp),
// "cn" => Ok(AllowedLanguages::Cn),
// _ => Ok(AllowedLanguages::En),
// }
// }
// }
#[derive(Debug, Serialize, Deserialize)]
pub struct Persistent {
#[serde(default)]
pub lineups: BTreeMap<u32, u32>,
#[serde(default)]
pub position: Position,
#[serde(default)]
pub scene: Scene,
pub main_character: MultiPathAvatar,
pub march_type: MultiPathAvatar,
// pub game_language: AllowedLanguages,
// pub voice_language: AllowedLanguages,
#[serde(default = "default_true")]
pub enable_sw_global: Option<bool>,
#[serde(default = "default_true")]
pub enable_castorice_global: Option<bool>,
}
fn default_true() -> Option<bool> {
Some(true)
}
impl Default for Persistent {
fn default() -> Self {
Self {
lineups: BTreeMap::from([(0, 1313), (1, 1006), (2, 8001), (3, 1405)]),
position: Default::default(),
main_character: MultiPathAvatar::FemaleRemembrance,
scene: Default::default(),
march_type: MultiPathAvatar::MarchHunt,
// game_language: AllowedLanguages::En,
// voice_language: AllowedLanguages::Jp,
enable_sw_global: Some(true),
enable_castorice_global: Some(true),
}
}
}
+95
View File
@@ -0,0 +1,95 @@
use proto::{BattleRelic, EquipRelic, ItemType, RelicAffix};
use serde::{Deserialize, Serialize};
use crate::impl_from;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Relic {
pub level: u32,
#[serde(alias = "relicId")]
pub relic_id: u32,
// #[serde(alias = "relicSetId")]
// pub relic_set_id: u32,
#[serde(alias = "mainAffixId")]
pub main_affix_id: u32,
#[serde(alias = "subAffixes")]
pub sub_affixes: Vec<SubAffix>,
#[serde(alias = "internalUid")]
pub internal_uid: u32,
#[serde(alias = "equipAvatar")]
pub equip_avatar: u32,
}
#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct SubAffix {
#[serde(alias = "subAffixId")]
pub sub_affix_id: u32,
pub count: u32,
pub step: u32,
}
impl Relic {
pub fn is_matching_slot(&self, slot: u32) -> bool {
self.get_slot() == slot
}
pub fn get_slot(&self) -> u32 {
self.relic_id % 10
}
pub fn get_unique_id(&self) -> u32 {
super::get_item_unique_id(self.internal_uid, ItemType::ItemRelic)
}
}
impl_from!(SubAffix, RelicAffix, |v| {
RelicAffix {
affix_id: v.sub_affix_id,
cnt: v.count,
step: v.step,
}
});
impl_from!(Relic, BattleRelic, |value| {
BattleRelic {
id: value.relic_id,
level: value.level,
main_affix_id: value.main_affix_id,
unique_id: value.get_unique_id(),
sub_affix_list: value
.sub_affixes
.iter()
.map(|v| v.into())
.collect::<Vec<_>>(),
..Default::default()
}
});
impl_from!(Relic, EquipRelic, |value| {
EquipRelic {
r#type: value.get_slot(),
relic_unique_id: value.get_unique_id(),
}
});
impl_from!(Relic, proto::Relic, |value| {
proto::Relic {
dress_avatar_id: value.equip_avatar,
exp: 0,
is_protected: false,
level: value.level,
main_affix_id: value.main_affix_id,
tid: value.relic_id,
unique_id: value.get_unique_id(),
sub_affix_list: value
.sub_affixes
.iter()
.map(|v| RelicAffix {
affix_id: v.sub_affix_id,
cnt: v.count,
step: v.step,
})
.collect::<Vec<_>>(),
..Default::default()
}
});
+56
View File
@@ -0,0 +1,56 @@
use proto::{MotionInfo, Vector};
use serde::{Deserialize, Serialize};
use crate::impl_from;
// SCENE
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Scene {
pub plane_id: u32,
pub floor_id: u32,
pub entry_id: u32,
}
impl Default for Scene {
fn default() -> Self {
Self {
plane_id: 20411,
floor_id: 20411001,
entry_id: 2041101,
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Position {
pub x: i32,
pub y: i32,
pub z: i32,
pub rot_y: i32,
}
impl Default for Position {
fn default() -> Self {
Position {
x: -26968,
y: 78953,
z: 14457,
rot_y: 11858,
}
}
}
impl_from!(Position, MotionInfo, |value| {
MotionInfo {
rot: Some(Vector {
x: 0,
y: value.rot_y,
z: 0,
}),
pos: Some(Vector {
x: value.x,
y: value.y,
z: value.z,
}),
}
});
+20730 -38657
View File
File diff suppressed because it is too large Load Diff
+21 -19
View File
@@ -4,33 +4,35 @@ edition = "2024"
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
notify.workspace = true
notify-debouncer-mini.workspace = true
# Framework
tokio.workspace = true
# JSON
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
tokio-util.workspace = true
# Logging
tracing.workspace = true
tracing-futures.workspace = true
tracing-log.workspace = true
tracing-subscriber.workspace = true
tracing-bunyan-formatter.workspace = true
ansi_term.workspace = true
env_logger.workspace = true
# Encoding / Serialization
prost.workspace = true
proto.workspace = true
proto-derive.workspace = true
rbase64.workspace = true
# Cryptography
rand.workspace = true
# Utilities
paste.workspace = true
notify.workspace = true
notify-debouncer-mini.workspace = true
# Error handling
anyhow.workspace = true
# Local
mhy-kcp.workspace = true
common.workspace = true
proto.workspace = true
proto-derive.workspace = true
+11
View File
@@ -0,0 +1,11 @@
use anyhow::Result;
mod net;
mod util;
use net::gateway::Gateway;
pub async fn start_gameserver() -> Result<()> {
let mut gateway = Gateway::new("0.0.0.0", 23301).await?;
Box::pin(gateway.listen()).await
}
-29
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"));
}
+6 -8
View File
@@ -1,16 +1,14 @@
#![feature(let_chains)]
use anyhow::Result;
mod logging;
mod net;
mod util;
pub fn init_tracing() {
#[cfg(target_os = "windows")]
ansi_term::enable_ansi_support().unwrap();
use logging::init_tracing;
use net::gateway::Gateway;
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
}
#[tokio::main]
async fn main() -> Result<()> {
init_tracing();
let mut gateway = Gateway::new("0.0.0.0", 23301).await?;
Box::pin(gateway.listen()).await
gameserver::start_gameserver().await
}
+37 -17
View File
@@ -6,13 +6,13 @@ use std::{
Arc,
atomic::{AtomicU32, Ordering},
},
time::Duration,
time::{Duration, SystemTime},
};
use anyhow::Result;
use common::sr_tools::FreesrData;
use rand::RngCore;
use rand::Rng;
use crate::net::PlayerSession;
@@ -86,11 +86,11 @@ impl Gateway {
)));
// Init the json to session
let _ = session
.write()
.await
.json_data
.set(FreesrData::load().await);
if let Ok(data) = FreesrData::load().await {
let _ = session.write().await.json_data.set(data);
} else {
tracing::error!("Failed to load initial freesr-data.json");
}
let session_ref = session.clone();
@@ -98,21 +98,34 @@ impl Gateway {
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| {
match notify_debouncer_mini::new_debouncer(Duration::from_millis(1000), move |ev| {
let _ = tx.blocking_send(ev);
})
.unwrap();
}) {
Ok(d) => d,
Err(e) => {
tracing::error!("debouncer init err: {e}");
return;
}
};
let path = Path::new("freesr-data.json");
debouncer
if let Err(e) = debouncer
.watcher()
.watch(path, notify::RecursiveMode::NonRecursive)
.unwrap();
{
tracing::error!("failed to watch {path:?}: {e}");
return;
}
tracing::info!("watching freesr-data.json changes");
let mut shutdown_rx = session.read().await.shutdown_rx.clone();
let mut last_modified = std::fs::metadata(path)
.and_then(|meta| meta.modified())
.unwrap_or(SystemTime::UNIX_EPOCH);
loop {
tokio::select! {
res = rx.recv() => {
@@ -124,16 +137,23 @@ impl Gateway {
if events
.iter()
.any(|p| p.path.file_name() == path.file_name())
{
&& let Ok(metadata) = std::fs::metadata(path)
&& let Ok(modified) = metadata.modified()
&& modified > last_modified {
last_modified = modified;
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")
if let Err(e) = json.update().await {
tracing::error!("Failed to update json: {e}");
} else {
let _ = session.sync_player().await;
tracing::info!("json updated");
}
}
}
Err(e) => eprintln!("json watcher error: {:?}", e),
}
Err(e) => tracing::error!("json watcher error: {e:?}"),
}
}
_ = shutdown_rx.changed() => {
@@ -7,7 +7,6 @@ pub async fn on_player_get_token_cs_req(
_body: &PlayerGetTokenCsReq,
res: &mut PlayerGetTokenScRsp,
) {
res.msg = String::from("OK");
res.uid = 25;
}
+60 -27
View File
@@ -1,10 +1,14 @@
use super::*;
pub static UNLOCKED_AVATARS: [u32; 63] = [
pub static BASE_AVATAR_IDS: [u32; 87] = [
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, 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, 1406,
1409, 1014, 1015, 1408, 1410, 1412, 1413, 1414, 1415, 1321, 1501, 1502, 1504, 1505, 1506, 1507,
1508, 1509, 1510, 1512, 1513,
];
pub async fn on_get_avatar_data_cs_req(
@@ -17,46 +21,75 @@ pub async fn on_get_avatar_data_cs_req(
return;
};
// TODO: HARDCODED
let mc_ids = if json.main_character.get_gender() == Gender::Man {
[8001, 8003, 8005, 8007]
} else {
[8002, 8004, 8006, 8008]
};
let march_ids = [1001, 1224];
res.is_get_all = body.is_get_all;
res.avatar_list = UNLOCKED_AVATARS
res.avatar_list = BASE_AVATAR_IDS
.into_iter()
.chain(mc_ids.iter().copied())
.chain(march_ids.iter().copied())
.map(|id| {
json.avatars
.get(&id)
.map(|v| {
v.to_avatar_proto(
json.lightcones.iter().find(|v| v.equip_avatar == id),
json.relics
.iter()
.filter(|v| v.equip_avatar == id)
.collect(),
json.main_character as u32,
json.march_type as u32,
)
})
.unwrap_or(Avatar {
base_avatar_id: id,
level: 80,
promotion: 6,
rank: 6,
skilltree_list: (1..=4)
.map(|m| AvatarSkillTree {
point_id: id * 1000 + m,
level: 1,
})
.collect(),
first_met_timestamp: 1712924677,
..Default::default()
first_met_time_stamp: 1712924677,
cur_multi_path_avatar_type: 0,
equipment_unique_id: 0,
has_taken_promotion_reward_list: vec![1, 3, 5],
is_marked: false,
exp: 0,
})
})
.collect();
res.avatar_path_data_info_list = json
.avatars
.values()
.map(|avatar| {
avatar.to_avatar_path_data_proto(
json.lightcones
.iter()
.find(|l| l.equip_avatar == avatar.avatar_id),
json.relics
.iter()
.filter(|r| r.equip_avatar == avatar.avatar_id)
.collect(),
)
})
.collect();
}
pub async fn on_take_promotion_reward_cs_req(
_session: &mut PlayerSession,
_req: &TakePromotionRewardCsReq,
_res: &mut TakePromotionRewardScRsp,
) {
// retcode defaults to 0, reward_list defaults to empty
// All avatars already report has_taken_promotion_reward_list = [0..=6] in GetAvatarData
}
pub async fn on_set_avatar_enhanced_id_cs_req(
_session: &mut PlayerSession,
_req: &SetAvatarEnhancedIdCsReq,
_res: &mut SetAvatarEnhancedIdScRsp,
) {
// let Some(json) = session.json_data.get_mut() else {
// return;
// };
// let Some(avatar) = json.avatars.get_mut(&req.avatar_id) else {
// return;
// };
// avatar.enhanced_id = if req.enhanced_id == 0 {
// Option::<u32>::None
// } else {
// Some(req.enhanced_id)
// };
// res.growth_avatar_id = avatar.avatar_id;
// res.unk_enhanced_id = req.enhanced_id;
}
+198 -77
View File
@@ -1,11 +1,10 @@
use std::collections::HashMap;
use rand::Rng;
use rogue_magic_battle_unit_info::Item;
use rand::RngExt;
use common::{
resources::GAME_RES,
sr_tools::{BattleType, Monster, RogueMagicComponentType},
structs::{BattleType, Monster},
};
use super::*;
@@ -36,6 +35,16 @@ pub async fn on_quick_start_cocoon_stage_cs_req(
res.battle_info = Some(battle_info);
}
pub async fn on_scene_enter_stage_cs_req(
session: &mut PlayerSession,
_req: &SceneEnterStageCsReq,
res: &mut SceneEnterStageScRsp,
) {
let battle_info = create_battle_info(session, 0, 0).await;
res.battle_info = Some(battle_info);
}
pub async fn on_pve_battle_result_cs_req(
_session: &mut PlayerSession,
req: &PveBattleResultCsReq,
@@ -50,7 +59,7 @@ pub async fn on_scene_cast_skill_cs_req(
req: &SceneCastSkillCsReq,
res: &mut SceneCastSkillScRsp,
) {
res.attacked_group_id = req.attacked_group_id;
res.cast_entity_id = req.cast_entity_id;
let targets = req
.hit_target_entity_id_list
@@ -64,9 +73,9 @@ pub async fn on_scene_cast_skill_cs_req(
return;
}
let battle_info = create_battle_info(session, req.caster_id, req.skill_index).await;
let battle_info = create_battle_info(session, req.attacked_by_entity_id, req.skill_index).await;
res.attacked_group_id = req.attacked_group_id;
res.cast_entity_id = req.cast_entity_id;
res.battle_info = Some(battle_info);
}
@@ -75,6 +84,10 @@ async fn create_battle_info(
caster_id: u32,
skill_index: u32,
) -> SceneBattleInfo {
// if let Some(player) = session.json_data.get_mut() {
// let _ = player.refresh_persistent().await;
// }
let Some(player) = session.json_data.get() else {
tracing::error!("data is not set!");
return SceneBattleInfo::default();
@@ -89,8 +102,27 @@ async fn create_battle_info(
..Default::default()
};
let is_calyx = caster_id == 0 && skill_index == 0;
let mut has_dahlia = false;
let mut first_avatar_id = 0;
let mut first_avatar_idx = 9999;
let lineup_uwu = if let Some(custom) = &player.battle_config.custom_battle_lineup {
custom.iter()
} else {
player.lineups.iter()
};
// avatars
for (avatar_index, avatar_id) in player.lineups.iter() {
for (avatar_index, avatar_id) in lineup_uwu {
if first_avatar_id == 0 {
first_avatar_id = *avatar_id;
first_avatar_idx = *avatar_index;
}
if !has_dahlia {
has_dahlia = *avatar_id == 1321;
}
let is_trailblazer = *avatar_id == 8001;
let is_march = *avatar_id == 1001;
@@ -115,9 +147,8 @@ async fn create_battle_info(
.filter(|v| v.equip_avatar == avatar.avatar_id)
.collect::<Vec<_>>(),
);
for tech in techs {
battle_info.buff_list.push(tech);
}
battle_info.buff_list.extend(techs);
if caster_id > 0
&& *avatar_index == (caster_id - 1)
@@ -127,7 +158,7 @@ async fn create_battle_info(
battle_info.buff_list.push(BattleBuff {
id: avatar_config.weakness_buff_id,
level: 1,
owner_id: *avatar_index,
owner_index: *avatar_index,
wave_flag: 0xffffffff,
dynamic_values: HashMap::from([(
String::from("SkillIndex"),
@@ -141,23 +172,79 @@ async fn create_battle_info(
// hardcoded march
if avatar.avatar_id == 1224 {
let buffs = BattleBuff {
battle_info.buff_list.push(BattleBuff {
id: 122401,
level: 3,
wave_flag: 0xffffffff,
owner_id: *avatar_index,
owner_index: *avatar_index,
dynamic_values: HashMap::from([
(String::from("#ADF_1"), 3f32),
(String::from("#ADF_2"), 3f32),
]),
target_index_list: vec![0],
};
battle_info.buff_list.push(buffs);
});
}
};
}
// Hardcoded Cerydra & Danheng PT technique
// and hardcode dahlia dance partner to 1st in lineup
let first_avatar_attack_id = if let Some(c) = GAME_RES.avatar_configs.get(&first_avatar_id) {
c.weakness_buff_id
} else {
0
};
let len = battle_info.buff_list.len();
let mut has_replaced = false;
let mut dahlia_buffs = Vec::new();
for (i, buff) in battle_info.buff_list.iter_mut().enumerate() {
let is_last = i == len - 1;
if buff.id == 141202 || buff.id == 141403 {
buff.owner_index = first_avatar_idx;
continue;
}
// this is actually useless because there is 0 attack buff id in calyx but idc
if has_dahlia
&& is_calyx
&& let 1000111..=1000117 = buff.id
&& !has_replaced
&& first_avatar_attack_id != 0
{
buff.id = first_avatar_attack_id;
buff.owner_index = first_avatar_idx;
has_replaced = true;
}
if has_dahlia && is_calyx && !has_replaced && first_avatar_attack_id != 0 && is_last {
dahlia_buffs.push(BattleBuff {
id: 1000121,
level: 1,
wave_flag: u32::MAX,
target_index_list: vec![first_avatar_idx],
..Default::default()
});
dahlia_buffs.push(BattleBuff {
id: first_avatar_attack_id,
level: 1,
wave_flag: u32::MAX,
target_index_list: vec![first_avatar_idx],
dynamic_values: HashMap::from([(
String::from("SkillIndex"),
first_avatar_idx as f32,
)]),
..Default::default()
});
}
}
if !dahlia_buffs.is_empty() {
battle_info.buff_list.extend(dahlia_buffs);
}
// custom stats for avatars
for stat in &player.battle_config.custom_stats {
for avatar in &mut battle_info.battle_avatar_list {
@@ -192,7 +279,7 @@ async fn create_battle_info(
id: blessing.id,
level: blessing.level,
wave_flag: 0xffffffff,
owner_id: 0xffffffff,
owner_index: 0xffffffff,
..Default::default()
};
@@ -217,21 +304,27 @@ async fn create_battle_info(
// 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_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_info.battle_target_info.insert(
1,
BattleTargetList {
battle_target_list: vec![BattleTarget {
id: 10002,
progress: 0,
..Default::default()
}],
});
},
);
}
for i in 2..=4 {
@@ -240,7 +333,9 @@ async fn create_battle_info(
.insert(i, BattleTargetList::default());
}
battle_info.battle_target_info.insert(5, BattleTargetList {
battle_info.battle_target_info.insert(
5,
BattleTargetList {
battle_target_list: vec![
BattleTarget {
id: 2001,
@@ -253,27 +348,29 @@ async fn create_battle_info(
..Default::default()
},
],
});
},
);
}
// Apocalyptic Shadow
if player.battle_config.battle_type == BattleType::AS {
battle_info.battle_target_info.insert(1, BattleTargetList {
battle_info.battle_target_info.insert(
1,
BattleTargetList {
battle_target_list: vec![BattleTarget {
id: 90005,
progress: 0,
..Default::default()
}],
});
},
);
}
// SU
if player.battle_config.battle_type == BattleType::SU {
battle_info
.event_battle_info_list
.push(BattleEventBattleInfo {
battle_info.battle_event.push(BattleEventBattleInfo {
battle_event_id: player.battle_config.path_resonance_id,
status: Some(BattleEventInitedData {
status: Some(BattleEventProperty {
sp_bar: Some(SpBarInfo {
cur_sp: 10_000,
max_sp: 10_000,
@@ -287,62 +384,86 @@ async fn create_battle_info(
battle_info.monster_wave_list = Monster::to_scene_monster_waves(&player.battle_config.monsters);
// Rogue Magic
// TODO: i dont need these shit
if !player.battle_config.scepters.is_empty() {
battle_info.rogue_magic_battle_info = Some(RogueMagicBattleInfo {
player_detail_info: Some(RogueMagicBattleUnitInfo {
item: Some(Item::BattleRogueMagicData(BattleRogueMagicData {
round_cnt: Some(BattleRogueMagicRoundCount {
gpojenhaiba: 3,
kljklbmlefo: 0,
}),
battle_scepter_list: player
.battle_config
.scepters
.iter()
.map(|scepter| {
let mut battle_scepter = BattleRogueMagicScepter {
level: scepter.level,
scepter_id: scepter.id,
magic_list: Vec::new(),
trench_count: HashMap::from([(3, 0), (4, 0), (5, 0)]),
};
let mut index = [0u32; 3];
for component in &scepter.components {
let (slot_type, locked) = match component.component_type {
RogueMagicComponentType::Passive => (3u32, false),
RogueMagicComponentType::Active => (4, true),
RogueMagicComponentType::Attach => (5, false),
};
let slot_index = &mut index[slot_type as usize - 3];
battle_scepter.magic_list.push(BattleRogueMagicUnit {
level: component.level,
unit_id: component.id,
slot_id: *slot_index,
locked,
counter_map: Default::default(),
});
*slot_index += 1;
*battle_scepter.trench_count.get_mut(&slot_type).unwrap() += 1;
// battle_info.battle_rogue_magic_info = Some(BattleRogueMagicInfo {
// detail_info: Some(BattleRogueMagicDetailInfo {
// paiigoggofj: Some(Item::BattleRogueMagicData(BattleRogueMagicData {
// round_cnt: Some(BattleRogueMagicRoundCount {
// gpojenhaiba: 3,
// kljklbmlefo: 0,
// }),
// battle_scepter_list: player
// .battle_config
// .scepters
// .iter()
// .map(|scepter| {
// let mut battle_scepter = BattleRogueMagicScepter {
// level: scepter.level,
// scepter_id: scepter.id,
// magic_list: Vec::new(),
// trench_count: HashMap::from([(3, 0), (4, 0), (5, 0)]),
// };
//
// let mut index = [0u32; 3];
//
// for component in &scepter.components {
// let (slot_type, locked) = match component.component_type {
// RogueMagicComponentType::Passive => (3u32, false),
// RogueMagicComponentType::Active => (4, true),
// RogueMagicComponentType::Attach => (5, false),
// };
//
// let slot_index = &mut index[slot_type as usize - 3];
// battle_scepter.magic_list.push(BattleRogueMagicUnit {
// level: component.level,
// unit_id: component.id,
// slot_id: *slot_index,
// locked,
// counter_map: Default::default(),
// });
// *slot_index += 1;
// *battle_scepter.trench_count.get_mut(&slot_type).unwrap() += 1;
// }
//
// battle_scepter
// })
// .collect(),
// })),
// }),
// modifier_content: Some(BattleRogueMagicModifierInfo {
// rogue_magic_battle_const: 5,
// }),
// });
}
battle_scepter
})
.collect(),
})),
}),
scepter: Some(Plgjihifpag { egmebanhhnf: 5 }),
});
// Global buffs
let (mut has_castorice_global_buff, mut has_silver_wolf_global_buff) = (false, false);
for buff in &battle_info.buff_list {
if buff.id == 140703 {
has_castorice_global_buff = true;
}
if buff.id == 150602 {
has_silver_wolf_global_buff = true;
}
}
// Global Buff
if !battle_info.buff_list.iter().any(|b| b.id == 140703) {
if !has_castorice_global_buff && player.enable_castorice_global.unwrap_or(true) {
battle_info.buff_list.push(BattleBuff {
id: 140703,
level: 1,
owner_id: u32::MAX,
owner_index: u32::MAX,
wave_flag: u32::MAX,
target_index_list: Vec::with_capacity(0),
dynamic_values: HashMap::with_capacity(0),
});
}
if !has_silver_wolf_global_buff && player.enable_sw_global.unwrap_or(true) {
battle_info.buff_list.push(BattleBuff {
id: 150602,
level: 1,
owner_index: u32::MAX,
wave_flag: u32::MAX,
target_index_list: Vec::with_capacity(0),
dynamic_values: HashMap::with_capacity(0),
+212 -46
View File
@@ -1,18 +1,27 @@
use common::sr_tools::MultiPathAvatar;
use common::structs::MultiPathAvatar;
use proto::chat_data::ExtendType;
use std::path::Path;
use tokio::fs;
use crate::{net::PlayerSession, util::cur_timestamp_ms};
use crate::{
net::PlayerSession,
util::{self, cur_timestamp_ms},
};
use super::*;
const SERVER_UID: u32 = 727;
const SERVER_HEAD_ICON: u32 = 201402;
const SERVER_CHAT_BUBBLE_ID: u32 = 220005;
const SERVER_CHAT_HISTORY: [&str; 5] = [
const SERVER_CHAT_HISTORY: &[&str] = &[
"'lua {path_to_lua_script}' execute lua script",
"'sw {on/off}' enable/disable silver wolf global buff",
"'castorice {on/off}' enable/disable castorice global buff",
"'sync' to synchronize stats between json and in-game view",
"'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.neonteam.dev to configure the PS! (you configure relics, equipment, monsters from there)",
];
pub async fn on_get_friend_login_info_cs_req(
@@ -20,6 +29,7 @@ pub async fn on_get_friend_login_info_cs_req(
_req: &GetFriendLoginInfoCsReq,
res: &mut GetFriendLoginInfoScRsp,
) {
res.black_uid_list = vec![SERVER_UID];
res.friend_uid_list = vec![SERVER_UID];
}
@@ -28,11 +38,11 @@ pub async fn on_get_friend_list_info_cs_req(
_req: &GetFriendListInfoCsReq,
res: &mut GetFriendListInfoScRsp,
) {
res.friend_list = vec![FriendListInfo {
friend_name: String::from("RobinSR"),
simple_info: Some(SimpleInfo {
res.friend_list = vec![FriendSimpleInfo {
remark_name: String::from("RobinSR"),
player_info: Some(PlayerSimpleInfo {
uid: SERVER_UID,
platform_type: PlatformType::Pc.into(),
platform: PlatformType::Pc.into(),
online_status: FriendOnlineStatus::Online.into(),
head_icon: SERVER_HEAD_ICON,
chat_bubble_id: SERVER_CHAT_BUBBLE_ID,
@@ -42,7 +52,7 @@ pub async fn on_get_friend_list_info_cs_req(
..Default::default()
}),
is_marked: true,
sent_time: 0,
create_time: 0,
..Default::default()
}];
}
@@ -53,18 +63,29 @@ pub async fn on_get_private_chat_history_cs_req(
res: &mut GetPrivateChatHistoryScRsp,
) {
let cur_time = cur_timestamp_ms();
res.chat_list = SERVER_CHAT_HISTORY
res.chat_message_list = SERVER_CHAT_HISTORY
.iter()
.map(|text| Chat {
msg_type: MsgType::CustomText.into(),
sent_time: cur_time,
text: String::from(*text),
sender_uid: SERVER_UID,
.map(|text| ChatMessageData {
create_time: cur_time,
ckhpffenobe: Some(Eknabklpeel {
kpobmnlklok: 1,
role_id: SERVER_UID,
}),
bkoalkhdlob: Some(Eknabklpeel {
kpobmnlklok: 1,
role_id: SERVER_UID,
}),
message_datas: vec![MessageChatData {
message_type: 1,
chat_data: Some(ChatData {
extend_type: Some(ExtendType::MessageText(text.to_string())),
}),
}],
..Default::default()
})
.collect();
res.to_uid = req.to_uid;
res.from_uid = SERVER_UID;
res.target_side = req.target_side;
res.contact_side = SERVER_UID;
}
pub async fn on_send_msg_cs_req(
@@ -77,20 +98,66 @@ pub async fn on_send_msg_cs_req(
return;
};
if let Some((cmd, args)) = parse_command(&body.text) {
let msg = body
.message_datas
.as_ref()
.and_then(|x| x.chat_data.as_ref())
.and_then(|x| x.extend_type.as_ref())
.and_then(|x| match x {
ExtendType::MessageText(s) => Some(s.as_str()),
_ => Option::<&str>::None,
})
.unwrap_or("");
if let Some((cmd, args)) = parse_command(msg) {
match cmd {
"sync" => {
session.sync_player().await;
let _ = session.sync_player().await;
session
.send(RevcMsgScNotify {
msg_type: body.msg_type,
text: String::from("Inventory Synced"),
emote: body.emote,
from_uid: SERVER_UID,
to_uid: 25,
chat_type: body.chat_type,
hnbepabnbng: body.hnbepabnbng,
})
.send(create_send_message(
25,
SERVER_UID,
body.message_datas
.as_ref()
.map(|v| v.message_type)
.unwrap_or_default(),
body.chat_type,
String::from("Inventory Synced"),
))
.await
.unwrap();
}
"sw" | "castorice" => {
let status = args.first().unwrap_or(&"on").to_lowercase();
let enabled = match status.as_str() {
"on" | "1" | "true" => true,
"off" | "0" | "false" => false,
_ => true,
};
if cmd == "sw" {
json.enable_sw_global = Some(enabled);
} else {
json.enable_castorice_global = Some(enabled);
}
json.save_persistent().await;
session
.send(create_send_message(
25,
SERVER_UID,
body.message_datas
.as_ref()
.map(|v| v.message_type)
.unwrap_or_default(),
body.chat_type,
format!(
"{} Global Buff: {}",
if cmd == "sw" { "SW" } else { "Castorice" },
if enabled { "Enabled" } else { "Disabled" }
),
))
.await
.unwrap();
}
@@ -113,18 +180,19 @@ pub async fn on_send_msg_cs_req(
.await
.unwrap();
session.sync_player().await;
let _ = session.sync_player().await;
session
.send(RevcMsgScNotify {
msg_type: body.msg_type,
text: format!("Success change mc to {:#?}", mc),
emote: body.emote,
from_uid: SERVER_UID,
to_uid: 25,
chat_type: body.chat_type,
hnbepabnbng: body.hnbepabnbng,
})
.send(create_send_message(
25,
SERVER_UID,
body.message_datas
.as_ref()
.map(|v| v.message_type)
.unwrap_or_default(),
body.chat_type,
format!("Success change mc to {mc:#?}"),
))
.await
.unwrap();
}
@@ -154,17 +222,84 @@ pub async fn on_send_msg_cs_req(
.unwrap();
session
.send(RevcMsgScNotify {
msg_type: body.msg_type,
text: format!("Success change march to {:#?}", march_type),
emote: body.emote,
from_uid: SERVER_UID,
to_uid: 25,
chat_type: body.chat_type,
hnbepabnbng: body.hnbepabnbng,
.send(create_send_message(
25,
SERVER_UID,
body.message_datas
.as_ref()
.map(|v| v.message_type)
.unwrap_or_default(),
body.chat_type,
format!("Success change march to {march_type:#?}"),
))
.await
.unwrap();
}
"lua" => {
let path = Path::new(args.first().unwrap_or(&""));
if !path.is_file() {
session
.send(create_send_message(
25,
SERVER_UID,
body.message_datas
.as_ref()
.map(|v| v.message_type)
.unwrap_or_default(),
body.chat_type,
format!("File {path:?} does not exist!"),
))
.await
.unwrap();
}
let data = match fs::read(&path).await {
Ok(file) => file,
Err(err) => {
session
.send(create_send_message(
25,
SERVER_UID,
body.message_datas
.as_ref()
.map(|v| v.message_type)
.unwrap_or_default(),
body.chat_type,
format!("Failed to read file: {err:?}"),
))
.await
.unwrap();
return;
}
};
session
.send(ClientDownloadDataScNotify {
download_data: Some(ClientDownloadData {
version: 51,
time: util::cur_timestamp_ms() as i64,
data,
..Default::default()
}),
})
.await
.unwrap();
session
.send(create_send_message(
25,
SERVER_UID,
body.message_datas
.as_ref()
.map(|v| v.message_type)
.unwrap_or_default(),
body.chat_type,
format!("Executed {path:?}"),
))
.await
.unwrap();
}
_ => {}
}
@@ -180,3 +315,34 @@ fn parse_command(command: &str) -> Option<(&str, Vec<&str>)> {
Some((parts[0], parts[1..].to_vec()))
}
fn create_send_message(
to: u32,
from: u32,
message_type: i32,
chat_type: i32,
msg: String,
) -> RevcMsgScNotify {
RevcMsgScNotify {
chat_type,
pffpfkoglpo: to,
recv_message_data: Some(ChatMessageData {
create_time: cur_timestamp_ms(),
ckhpffenobe: Some(Eknabklpeel {
kpobmnlklok: 1,
role_id: from,
}),
bkoalkhdlob: Some(Eknabklpeel {
kpobmnlklok: 1,
role_id: from,
}),
message_datas: vec![MessageChatData {
message_type,
chat_data: Some(ChatData {
extend_type: Some(chat_data::ExtendType::MessageText(msg)),
}),
}],
..Default::default()
}),
}
}
+2 -2
View File
@@ -9,8 +9,8 @@ pub async fn on_get_gacha_info_cs_req(
end_time: 1924992000,
begin_time: 0,
gacha_ceiling: Some(GachaCeiling::default()),
up_info: vec![23002, 1003, 1101, 1104, 23000, 23003],
featured: vec![
prize_item_list: vec![23002, 1003, 1101, 1104, 23000, 23003],
item_detail_list: vec![
1001, 1002, 1008, 1009, 1013, 1103, 1105, 1106, 1108, 1109, 1110, 1111, 1201, 1202,
1206, 1207, 1210, 1003, 1004, 1101, 1107, 1104, 1209, 1211, 21000, 21001, 21002, 21003,
21004, 21005, 21006, 21007, 21008, 21009, 21010, 21011, 21012, 21013, 21014, 21015,
+270 -27
View File
@@ -1,8 +1,9 @@
use common::{resources::GAME_RES, sr_tools::FreesrData};
use proto::{get_big_data_all_recommend_sc_rsp::RecommendType, *};
use crate::net::PlayerSession;
use super::UNLOCKED_AVATARS;
use super::BASE_AVATAR_IDS;
pub async fn on_get_bag_cs_req(
session: &mut PlayerSession,
@@ -14,12 +15,8 @@ pub async fn on_get_bag_cs_req(
return;
};
res.equipment_list = player
.lightcones
.iter()
.map(|v| v.to_equipment_proto())
.collect();
res.relic_list = player.relics.iter().map(|v| v.to_relic_proto()).collect();
res.equipment_list = player.lightcones.iter().map(|v| v.into()).collect();
res.relic_list = player.relics.iter().map(|v| v.into()).collect();
res.material_list = vec![
Material {
tid: 101, // Normal Pass
@@ -43,31 +40,63 @@ pub async fn on_get_archive_data_cs_req(
}
pub async fn on_dress_relic_avatar_cs_req(
_session: &mut PlayerSession,
_req: &DressRelicAvatarCsReq,
_res: &mut DressRelicAvatarScRsp,
session: &mut PlayerSession,
req: &DressRelicAvatarCsReq,
_: &mut DressRelicAvatarScRsp,
) {
let Some(player) = session.json_data.get_mut() else {
tracing::error!("data is not set!");
return;
};
if let Some(pkt) = equip_relic(player, req) {
let _ = session.send(pkt).await;
};
}
pub async fn on_take_off_relic_cs_req(
_session: &mut PlayerSession,
_req: &TakeOffRelicCsReq,
_res: &mut TakeOffRelicScRsp,
session: &mut PlayerSession,
req: &TakeOffRelicCsReq,
_: &mut TakeOffRelicScRsp,
) {
let Some(player) = session.json_data.get_mut() else {
tracing::error!("data is not set!");
return;
};
if let Some(pkt) = unequip_relic(player, req) {
let _ = session.send(pkt).await;
};
}
pub async fn on_dress_avatar_cs_req(
_session: &mut PlayerSession,
_req: &DressAvatarCsReq,
_res: &mut DressAvatarScRsp,
session: &mut PlayerSession,
req: &DressAvatarCsReq,
_: &mut DressAvatarScRsp,
) {
let Some(player) = session.json_data.get_mut() else {
tracing::error!("data is not set!");
return;
};
if let Some(pkt) = set_lightcone_equipper(player, req.avatar_id, req.equipment_unique_id) {
let _ = session.send(pkt).await;
};
}
pub async fn on_take_off_equipment_cs_req(
_session: &mut PlayerSession,
_req: &TakeOffEquipmentCsReq,
_res: &mut TakeOffEquipmentScRsp,
session: &mut PlayerSession,
req: &TakeOffEquipmentCsReq,
_: &mut TakeOffEquipmentScRsp,
) {
let Some(player) = session.json_data.get_mut() else {
tracing::error!("data is not set!");
return;
};
if let Some(pkt) = set_lightcone_equipper(player, req.avatar_id, 0) {
let _ = session.send(pkt).await;
};
}
pub async fn on_get_big_data_all_recommend_cs_req(
@@ -80,21 +109,22 @@ pub async fn on_get_big_data_all_recommend_cs_req(
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,
recommended_avatar_info_list: GAME_RES
.relic_avatar_recommend
.iter()
.map(|(set_id, avatar_list)| RecomendedAvatarInfo {
avatar_id_list: avatar_list.clone(),
recommend_avatar_id: avatar_list.first().copied().unwrap_or_default(),
relic_set_id: *set_id,
})
.collect(),
}))
}
BigDataRecommendType::AvatarRelic => {
res.recommend_type = Some(RecommendType::AvatarRelic(BigDataRecommendAvatarRelic {
recomended_relic_info_list: UNLOCKED_AVATARS
recomended_relic_info_list: BASE_AVATAR_IDS
.into_iter()
.map(|avatar_id| RecommendedRelicInfo {
.map(|avatar_id| BigDataAvatarRelicRecommend {
avatar_id,
..Default::default()
})
@@ -104,3 +134,216 @@ pub async fn on_get_big_data_all_recommend_cs_req(
_ => {}
}
}
pub async fn on_rank_up_avatar_cs_req(
session: &mut PlayerSession,
req: &RankUpAvatarCsReq,
_: &mut RankUpAvatarScRsp,
) -> Option<()> {
let player = session.json_data.get_mut()?;
let avatar = player.avatars.get_mut(&req.avatar_id)?;
avatar.data.rank = req.rank;
let avatar_id = avatar.avatar_id;
let mut ret = PlayerSyncScNotify::default();
build_sync(
player,
&mut ret,
vec![avatar_id],
Vec::with_capacity(0),
Vec::with_capacity(0),
);
let _ = session.send(ret).await;
Some(())
}
// TODO: move these somewhere else?
fn set_lightcone_equipper(
player: &mut FreesrData,
target_avatar: u32,
target_lightcone_uid: u32,
) -> Option<PlayerSyncScNotify> {
let mut ret = PlayerSyncScNotify::default();
let target_avatar = player.avatars.get(&target_avatar)?;
let cur_avatar_lc_idx = player
.lightcones
.iter()
.position(|l| l.equip_avatar == target_avatar.avatar_id);
// undress
if target_lightcone_uid == 0
&& let Some(cur_avatar_lc_idx) = cur_avatar_lc_idx
{
player.lightcones[cur_avatar_lc_idx].equip_avatar = 0;
build_sync(
player,
&mut ret,
vec![target_avatar.avatar_id],
vec![cur_avatar_lc_idx],
Vec::with_capacity(0),
);
return Some(ret);
}
let target_lightcone_idx = player
.lightcones
.iter()
.position(|l| l.get_unique_id() == target_lightcone_uid)?;
// jika avatar sekarang sedang pakai LC, kita tukar pemiliknya dengan pemilik dari LC target
if let Some(cur_avatar_lc_idx) = cur_avatar_lc_idx {
player.lightcones[cur_avatar_lc_idx].equip_avatar =
player.lightcones[target_lightcone_idx].equip_avatar;
}
let avatars_sync = vec![
player.lightcones[target_lightcone_idx].equip_avatar, // old
target_avatar.avatar_id, // cur
];
// set kepemilikan lightcone barunya ke avatar sekarang
player.lightcones[target_lightcone_idx].equip_avatar = target_avatar.avatar_id;
build_sync(
player,
&mut ret,
avatars_sync,
[Some(target_lightcone_idx), cur_avatar_lc_idx]
.into_iter()
.flatten()
.collect(),
Vec::with_capacity(0),
);
Some(ret)
}
fn equip_relic(player: &mut FreesrData, req: &DressRelicAvatarCsReq) -> Option<PlayerSyncScNotify> {
let mut ret = PlayerSyncScNotify::default();
let target_avatar = player.avatars.get(&req.avatar_id)?;
let mut avatar_ids_to_sy = vec![];
let mut relic_index_to_sync = vec![];
for param in &req.switch_list {
let Some(target_relic_idx) = player
.relics
.iter()
.position(|v| v.get_unique_id() == param.relic_unique_id)
else {
continue;
};
let cur_avatar_relic_idx = player.relics.iter().position(|r| {
r.equip_avatar == target_avatar.avatar_id && r.get_slot() == param.relic_type
});
// jika avatar sekarang sedang pakai LC, kita tukar pemiliknya dengan pemilik dari LC target
if let Some(cur_avatar_relic_idx) = cur_avatar_relic_idx {
avatar_ids_to_sy.push(player.relics[cur_avatar_relic_idx].equip_avatar);
player.relics[cur_avatar_relic_idx].equip_avatar =
player.relics[target_relic_idx].equip_avatar;
relic_index_to_sync.push(cur_avatar_relic_idx);
}
// old owner
avatar_ids_to_sy.push(player.relics[target_relic_idx].equip_avatar);
// set kepemilikan relic barunya ke avatar sekarang
player.relics[target_relic_idx].equip_avatar = target_avatar.avatar_id;
// new owner
avatar_ids_to_sy.push(player.relics[target_relic_idx].equip_avatar);
relic_index_to_sync.push(target_relic_idx);
}
build_sync(
player,
&mut ret,
avatar_ids_to_sy,
Vec::with_capacity(0),
relic_index_to_sync,
);
Some(ret)
}
fn unequip_relic(player: &mut FreesrData, req: &TakeOffRelicCsReq) -> Option<PlayerSyncScNotify> {
let mut ret = PlayerSyncScNotify::default();
let target_avatar = player.avatars.get(&req.avatar_id)?;
let mut relic_index_to_sync = vec![];
for slot in &req.relic_type_list {
let relics = player
.relics
.iter()
.enumerate()
.filter(|(_, r)| r.equip_avatar == target_avatar.avatar_id && r.get_slot() == *slot)
.map(|(i, _)| i)
.collect::<Vec<_>>();
for relic_idx in &relics {
player.relics[*relic_idx].equip_avatar = 0;
}
relic_index_to_sync.extend(relics);
}
build_sync(
player,
&mut ret,
vec![req.avatar_id],
Vec::with_capacity(0),
relic_index_to_sync,
);
Some(ret)
}
fn build_sync(
player: &mut FreesrData,
ret: &mut PlayerSyncScNotify,
avatar_ids: Vec<u32>,
lightcone_indexes: Vec<usize>,
relic_indexes: Vec<usize>,
) {
let avatar_list = avatar_ids
.iter()
.filter_map(|id| {
player.get_avatar_proto(*id, player.main_character as u32, player.march_type as u32)
})
.collect::<Vec<_>>();
let avatar_path_data_info_list = avatar_ids
.into_iter()
.filter_map(|id| player.get_avatar_path_data_proto(id))
.collect();
ret.avatar_sync = Some(AvatarSync {
avatar_list,
avatar_path_data_info_list,
});
ret.relic_list = relic_indexes
.into_iter()
.map(|id| (&player.relics[id]).into())
.collect();
ret.equipment_list = lightcone_indexes
.into_iter()
.map(|id| (&player.lightcones[id]).into())
.collect();
}
+6 -6
View File
@@ -1,4 +1,4 @@
use common::sr_tools::AvatarJson;
use common::structs::AvatarJson;
use scene_entity_info::Entity;
use scene_entity_refresh_info::RefreshType;
@@ -76,7 +76,7 @@ pub async fn on_replace_lineup_cs_req(
let lineups = &mut player.lineups;
for (slot, avatar_id) in &mut *lineups {
if let Some(lineup) = req.slots.get(*slot as usize) {
if let Some(lineup) = req.lineup_slot_list.get(*slot as usize) {
*avatar_id = lineup.id;
} else {
*avatar_id = 0;
@@ -131,15 +131,15 @@ async fn refresh_lineup(session: &mut PlayerSession) {
session
.send(SceneGroupRefreshScNotify {
group_refresh_info: vec![SceneGroupRefreshInfo {
group_refresh_list: vec![GroupRefreshInfo {
group_id: 0,
state: 0,
group_refresh_type: SceneGroupRefreshType::Loaded.into(),
refresh_type: SceneGroupRefreshType::Loaded.into(),
refresh_entity: new_entities,
bccgjihncdn: Vec::with_capacity(0),
..Default::default()
}],
floor_id,
gfhglffhfbd: 0,
dimension_id: 0,
})
.await
.unwrap();
+1 -1
View File
@@ -23,7 +23,7 @@ pub async fn on_get_mail_cs_req(
..Default::default()
}],
}),
mail_type: MailType::Normal.into(),
mail_type: 0,
template_id: 0,
}];
}
+2 -2
View File
@@ -5,9 +5,9 @@ pub async fn on_get_mission_status_cs_req(
body: &GetMissionStatusCsReq,
res: &mut GetMissionStatusScRsp,
) {
res.finished_main_mission_id_list = body.main_mission_id_list.clone();
res.finished_main_mission_id_list = body.sub_mission_id_list.clone();
res.sub_mission_status_list = body
.sub_mission_id_list
.main_mission_id_list
.iter()
.map(|id| Mission {
id: *id,
+23 -39
View File
@@ -29,30 +29,6 @@ pub use mission::*;
pub use player::*;
pub use scene::*;
#[allow(unused_imports)]
use proto::{
CmdActivityType::*, CmdAdventureType::*, CmdAetherDivideType::*, CmdAlleyType::*,
CmdArchiveType::*, CmdAvatarType::*, CmdBattleCollegeType::*, CmdBattlePassType::*,
CmdBattleType::*, CmdBoxingClubType::*, CmdChallengeType::*, CmdChatType::*,
CmdChessRogueType::*, CmdClockParkType::*, CmdContentPackageType::*, CmdDailyActiveType::*,
CmdDrinkMakerType::*, CmdExpeditionType::*, CmdFantasticStoryActivityType::*,
CmdFeverTimeActivityType::*, CmdFightActivityType::*, CmdFightMathc3Type::*, CmdFightType::*,
CmdFriendType::*, CmdGachaType::*, CmdHeartdialType::*, CmdHeliobusType::*, CmdItemType::*,
CmdJukeboxType::*, CmdLineupType::*, CmdLobbyType::*, CmdMailType::*, CmdMapRotationType::*,
CmdMatchThreeModuleType::*, CmdMatchType::*, CmdMessageType::*, CmdMiscModuleType::*,
CmdMissionType::*, CmdMonopolyType::*, CmdMultiplayerType::*, CmdMultipleDropType::*,
CmdMuseumType::*, CmdOfferingType::*, CmdPamMissionType::*, CmdPhoneType::*,
CmdPlayerBoardType::*, CmdPlayerReturnType::*, CmdPlayerSync::*, CmdPlayerType::*,
CmdPlotType::*, CmdPunkLordType::*, CmdQuestType::*, CmdRaidCollectionType::*, CmdRaidType::*,
CmdRechargeGiftType::*, CmdRecommendType::*, CmdRedDotType::*, CmdReplayType::*,
CmdRndOptionType::*, CmdRogueCommonType::*, CmdRogueEndless::*, CmdRogueModifierType::*,
CmdRogueTournType::*, CmdRogueType::*, CmdRollShopType::*, CmdSceneType::*,
CmdServerPrefsType::*, CmdShopType::*, CmdSpaceZooType::*, CmdStarFightType::*,
CmdStoryLineType::*, CmdStrongChallengeActivityType::*, CmdTalkRewardType::*,
CmdTelevisionActivityType::*, CmdTextJoinType::*, CmdTrainVisitorType::*,
CmdTreasureDungeonType::*, CmdTutorialType::*, CmdWaypointType::*, CmdWolfBroType::*,
};
macro_rules! dummy {
($($cmd:ident),* $(,)*) => {
paste! {
@@ -60,7 +36,7 @@ macro_rules! dummy {
pub const fn should_send_dummy_rsp(cmd_id: u16) -> bool {
match cmd_id {
$(
x if x == [<Cmd $cmd CsReq>] as u16 => true,
x if x == <proto::[<$cmd CsReq>] as proto::CmdID>::CMD_ID => true,
)*
_ => false,
}
@@ -69,7 +45,9 @@ macro_rules! dummy {
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,
x if x == <proto::[<$cmd CsReq>] as proto::CmdID>::CMD_ID => {
<proto::[<$cmd ScRsp>] as proto::CmdID>::CMD_ID
},
)*
_ => return Err(anyhow::anyhow!("Invalid request id {req_id:?}")),
};
@@ -90,7 +68,7 @@ macro_rules! dummy {
dummy! {
// SceneEntityMove,
GetLevelRewardTakenList,
GetRogueScoreRewardInfo,
// GetRogueScoreRewardInfo, // ?3.7.51
// GetGachaInfo,
QueryProductInfo,
GetQuestData,
@@ -98,33 +76,33 @@ dummy! {
// GetFriendListInfo,
// GetFriendApplyListInfo,
GetCurAssist,
GetRogueHandbookData,
// GetRogueHandbookData, // ?3.7.51
GetDailyActiveInfo,
GetFightActivityData,
GetMultipleDropInfo,
GetPlayerReturnMultiDropInfo,
GetShareData,
GetTreasureDungeonActivityData,
PlayerReturnInfoQuery,
// GetMultipleDropInfo, // ?3.7.51
// GetPlayerReturnMultiDropInfo, // ?3.7.51
// GetShareData, // ?3.7.51
// GetTreasureDungeonActivityData, // ?3.7.51
// PlayerReturnInfoQuery, // ?3.7.51
// GetBag,
GetPlayerBoardData,
GetActivityScheduleConfig,
GetMissionData,
GetChallenge,
GetCurChallenge,
GetRogueInfo,
// GetRogueInfo, // ?3.7.51
GetExpeditionData,
// GetRogueDialogueEventData,
GetJukeboxData,
SyncClientResVersion,
DailyFirstMeetPam,
GetMuseumInfo,
// DailyFirstMeetPam, // ?3.7.51
// GetMuseumInfo, // ?3.7.51
GetLoginActivity,
GetRaidInfo,
GetTrialActivityData,
GetBoxingClubInfo,
// GetBoxingClubInfo, // ?3.7.51
GetNpcStatus,
TextJoinQuery,
// TextJoinQuery, // ?3.7.51
// GetSpringRecoverData, // Removed 2.7.51
// GetChatFriendHistory,
GetSecretKeyInfo,
@@ -134,6 +112,12 @@ dummy! {
// PlayerLoginFinish,
InteractProp,
FinishTalkMission,
GetRechargeGiftInfo
GetRechargeGiftInfo,
// RelicRecommend
GetPreAvatarGrowthInfo,
GetPreAvatarActivityList,
// GetUnreleasedBlockInfo,
GetFriendAssistList,
GetAssistList,
B51RacingGetData
}
+12 -29
View File
@@ -1,5 +1,3 @@
use std::collections::HashMap;
use super::*;
pub async fn on_get_basic_info_cs_req(
@@ -22,8 +20,8 @@ pub async fn on_player_heart_beat_cs_req(
res.download_data = Some(ClientDownloadData {
version: 51,
time: res.server_time_ms as i64,
data: rbase64::decode("bG9jYWwgZnVuY3Rpb24gYmV0YV90ZXh0KG9iaikKICAgIGxvY2FsIGdhbWVPYmplY3QgPSBDUy5Vbml0eUVuZ2luZS5HYW1lT2JqZWN0LkZpbmQoIlVJUm9vdC9BYm92ZURpYWxvZy9CZXRhSGludERpYWxvZyhDbG9uZSkiKQogICAgaWYgZ2FtZU9iamVjdCB0aGVuCiAgICAgICAgbG9jYWwgdGV4dENvbXBvbmVudCA9IGdhbWVPYmplY3Q6R2V0Q29tcG9uZW50SW5DaGlsZHJlbih0eXBlb2YoQ1MuUlBHLkNsaWVudC5Mb2NhbGl6ZWRUZXh0KSkKICAgICAgICBpZiB0ZXh0Q29tcG9uZW50IHRoZW4KICAgICAgICAgICAgdGV4dENvbXBvbmVudC50ZXh0ID0gIiIKICAgICAgICBlbmQKICAgIGVuZAplbmQKCmxvY2FsIGZ1bmN0aW9uIHZlcnNpb25fdGV4dChvYmopCiAgICBsb2NhbCBnYW1lT2JqZWN0ID0gQ1MuVW5pdHlFbmdpbmUuR2FtZU9iamVjdC5GaW5kKCJWZXJzaW9uVGV4dCIpCiAgICBpZiBnYW1lT2JqZWN0IHRoZW4KICAgICAgICBsb2NhbCB0ZXh0Q29tcG9uZW50ID0gZ2FtZU9iamVjdDpHZXRDb21wb25lbnRJbkNoaWxkcmVuKHR5cGVvZihDUy5SUEcuQ2xpZW50LkxvY2FsaXplZFRleHQpKQogICAgICAgIGlmIHRleHRDb21wb25lbnQgdGhlbgogICAgICAgICAgICB0ZXh0Q29tcG9uZW50LnRleHQgPSAiPGNvbG9yPSMwMGUxZmY+Um9iaW5TUiE8L2NvbG9yPiIKICAgICAgICBlbmQKICAgIGVuZAplbmQKCnZlcnNpb25fdGV4dCgpCmJldGFfdGV4dCgpCg==").unwrap(),
haehhcpoapp: 0
data: rbase64::decode("BAEcA2xvZwJDUwtVbml0eUVuZ2luZQtBcHBsaWNhdGlvbg90YXJnZXRGcmFtZVJhdGUPUXVhbGl0eVNldHRpbmdzCnZTeW5jQ291bnQIcGF0Y2hGcHMKR2FtZU9iamVjdARGaW5kKFVJUm9vdC9BYm92ZURpYWxvZy9CZXRhSGludERpYWxvZyhDbG9uZSkDUlBHBkNsaWVudA1Mb2NhbGl6ZWRUZXh0BnR5cGVvZhZHZXRDb21wb25lbnRJbkNoaWxkcmVuJjxjb2xvcj0jMDBlMWZmPnQubWUvbmVvbnRlYW0yNTwvY29sb3I+BHRleHQLVmVyc2lvblRleHQiPGNvbG9yPSMwMGUxZmY+Um9iaW5TUiEgfCA8L2NvbG9yPg88Y29sb3I9IzAwZTFmZj4KR2xvYmFsVmFycw1zX1ZlcnNpb25EYXRhF0dldFNlcnZlclBha1R5cGVWZXJzaW9uCDwvY29sb3I+C3BhdGNoQmV0YVdtBG1haW4GeHBjYWxsBQEBAAAAAAEWAAEAAAAEAQEYAAUAAAAAAgAAAAAACwwAAwACBADABAFoARABAAMEAAAADAAGAAUEAMAEAQAAEAEA0AcAAAAWAAEACAMCAwMDBAQCBADAAwUDBgQFBADAAwcABwgBGAAAAAAAAQAAAAABCAAAAAAJAAAAAAA2DAEDAAIEAMAPAAF2BAAAAAUBBQAVAAICDAQIAAcYAMAPAwQUCQAAAE0sAwIMAgsAAACgQBUCAgAUAACxDAAAABUAAAIFAQ0AEAEA1Q4AAAAMAQMAAgQAwA8AAXYEAAAABQEPABUAAgIMBAgABxgAwA8DBBQJAAAATSwDAgwCCwAAAKBAFQICABQAALEMAAAAFQAAAgUCEAAFAxEADAgIAAcYAMAPBwisEgAAAA8GByMTAAAAFAYGkRQAAAAVBgICBgQGAAUFFQA1AQIFEAEA1Q4AAAAWAAEAFgMCAwMDCQQCBADAAwoDCwMMAw0EBxgAwAMOAw8EAACgQAMQAxEDEgMTAxQDFQMWAxcDGAMZAAwaARgAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAAAAAAAAAAAAAAAAQEBAAAAAAAAAAAAAQD8AAUOAAAAAAEAAQAAAAMJAAAAFQABARYAAQAAABkbARgAAAEaAAAAAAcAAAEAAAxFAAAARAAAAEQBAQBEAgIARAMDAEoAAgAMBAUAAABAQAYFAwAGBgAAFQQDARYAAQAGBgAGAQYCBgMDHAQAAEBABAABAgMBAAEYAAMDBQ0ABAAAAAAAAQAAAAAE").unwrap(),
..Default::default()
});
}
@@ -34,15 +32,18 @@ pub async fn on_player_login_finish_cs_req(
) -> Result<()> {
session
.send(ContentPackageSyncDataScNotify {
data: Some(PackageData {
info_list: [
200001, 200002, 200003, 200004, 150017, 150015, 150021, 150018, 130011, 130012,
130013,
data: Some(ContentPackageData {
content_package_list: [
200001, 200002, 200003, 200004, 200005, 200006, 200007, 200008, 200009, 200010,
200011, 200012, 150017, 150015, 150021, 150018, 130011, 130012, 130013, 150025,
140006, 150026, 130014, 150034, 150029, 150035, 150041, 150039, 150045, 150057,
150042, 150067, 150064, 150063, 150024, 171002, 150068, 150070, 150071, 150073,
150074, 150075, 150076, 150077, 150078, 150079,
]
.iter()
.map(|v| ContentInfo {
.into_iter()
.map(|v| ContentPackageInfo {
status: ContentPackageStatus::Finished.into(),
content_id: *v,
content_id: v,
})
.collect(),
..Default::default()
@@ -52,21 +53,3 @@ pub async fn on_player_login_finish_cs_req(
Ok(())
}
pub async fn on_get_multi_path_avatar_info_cs_req(
session: &mut PlayerSession,
_req: &GetMultiPathAvatarInfoCsReq,
res: &mut GetMultiPathAvatarInfoScRsp,
) {
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()),
(1001, json.march_type.get_type().into()),
]);
res.multi_path_avatar_type_info_list = json.get_multi_path_info();
}
+86 -46
View File
@@ -2,11 +2,9 @@ use std::collections::HashMap;
use common::{
resources::GAME_RES,
sr_tools::{AvatarJson, Position},
structs::{AvatarJson, Position},
};
use lazy_static::lazy_static;
use scene_entity_info::Entity;
use tokio::sync::Mutex;
use crate::util::{self};
@@ -44,11 +42,11 @@ pub async fn on_enter_scene_cs_req(
req: &EnterSceneCsReq,
res: &mut EnterSceneScRsp,
) {
if load_scene(session, req.entry_id, true, Some(req.teleport_id))
if load_scene(session, req.entry_id, true, Some(req.entry_id2))
.await
.is_err()
{
res.retcode = Retcode::RetSceneEntryIdNotMatch as u32;
res.retcode = 2605;
};
}
@@ -57,34 +55,33 @@ pub async fn on_get_scene_map_info_cs_req(
req: &GetSceneMapInfoCsReq,
res: &mut GetSceneMapInfoScRsp,
) {
for floor_id in &req.floor_id_list {
let mut map_info = MazeMapData {
retcode: 0,
unlocked_chest_list: vec![
MazeChest {
map_info_chest_type: MapInfoChestType::Normal.into(),
for floor_id in req.scene_identifiers.iter().map(|v| v.floor_id) {
let mut map_info = SceneMapInfo {
chest_list: vec![
ChestInfo {
chest_type: 101,
..Default::default()
},
MazeChest {
map_info_chest_type: MapInfoChestType::Puzzle.into(),
ChestInfo {
chest_type: 102,
..Default::default()
},
MazeChest {
map_info_chest_type: MapInfoChestType::Challenge.into(),
ChestInfo {
chest_type: 104,
..Default::default()
},
],
floor_id: *floor_id,
floor_id: floor_id,
scene_identifier: Some(SceneIdentifier {
floor_id,
..Default::default()
}),
..Default::default()
};
for i in 0..100 {
map_info.lighten_section_list.push(i)
}
let group_config = GAME_RES
let floor_configs = GAME_RES
.map_default_entrance_map
.get(floor_id)
.get(&floor_id)
.and_then(|v| {
GAME_RES
.level_output_configs
@@ -92,33 +89,47 @@ pub async fn on_get_scene_map_info_cs_req(
.and_then(|v| v.iter().next())
});
if let Some((_, group_config)) = group_config {
for (group_id, group) in group_config.scenes.iter() {
map_info.maze_group_list.push(MazeGroup {
if let Some((_, floor_config)) = floor_configs {
for (group_id, group) in floor_config.scenes.iter() {
map_info.group_list.push(MapInfoGroup {
group_id: *group_id,
..Default::default()
});
for teleport in group.teleports.keys() {
map_info.unlocked_teleport_list.push(*teleport)
map_info.unlock_teleport_list.push(*teleport)
}
for prop in &group.props {
map_info.maze_prop_list.push(MazeProp {
map_info.map_info_prop_list.push(MazePropState {
group_id: prop.group_id,
state: prop.prop_state,
config_id: prop.inst_id,
extra_info: Option::<PropExtraInfo>::None,
});
}
// map_info.maze_group_list.push(MazeGroup {
// group_id: prop.group_id,
// state: prop.prop_state,
// config_id: prop.inst_id,
// extra_info: Option::None,
// });
}
}
res.map_list.push(map_info)
map_info.lighten_section_list = floor_config.sections.clone();
map_info.floor_saved_value_map = floor_config.saved_values.clone();
// #TODO!
// map_info
// .chest_unlock_progress_list
// .push(ChestUnlockProgress {
// r#type: 0,
// total_chest_count: 25,
// unlocked_chest_count: 25,
// });
}
}
lazy_static! {
static ref NEXT_SCENE_SAVE: Mutex<u64> = Mutex::new(0);
res.scene_map_info_list.push(map_info)
}
}
pub async fn on_scene_entity_move_cs_req(
@@ -131,14 +142,12 @@ pub async fn on_scene_entity_move_cs_req(
return;
};
let mut timestamp = NEXT_SCENE_SAVE.lock().await;
if util::cur_timestamp_ms() <= *timestamp {
if util::cur_timestamp_ms() <= session.next_scene_save {
return;
}
// save every 5 sec
*timestamp = util::cur_timestamp_ms() + (5 * 1000);
session.next_scene_save = util::cur_timestamp_ms() + (5 * 1000);
for entity in &req.entity_motion_list {
if entity.entity_id != 0 {
@@ -165,7 +174,7 @@ pub async fn on_get_entered_scene_cs_req(
_req: &GetEnteredSceneCsReq,
res: &mut GetEnteredSceneScRsp,
) {
res.entered_scene_info = GAME_RES
res.entered_scene_info_list = GAME_RES
.level_output_configs
.iter()
.flat_map(|(_, v)| {
@@ -243,6 +252,36 @@ async fn load_scene(
} else {
scene.world_id
},
lighten_section_list: scene.sections.clone(),
opened_chests_list: scene
.scenes
.values()
.flat_map(|v| v.chests.clone())
.collect::<Vec<_>>(),
scene_mission_info: Some(MissionStatusBySceneInfo {
finished_main_mission_id_list: scene
.scenes
.values()
.flat_map(|s| s.finished_main_missions.clone())
.collect::<Vec<_>>(),
sub_mission_status_list: scene
.scenes
.values()
.flat_map(|s| {
s.finished_sub_missions.iter().map(|sm| Mission {
id: *sm,
status: MissionStatus::MissionFinish.into(),
progress: 0,
})
})
.collect::<Vec<_>>(),
..Default::default()
}),
floor_saved_data: scene.saved_values.clone(),
scene_identifier: Some(SceneIdentifier {
floor_id,
..Default::default()
}),
..Default::default()
};
@@ -285,7 +324,7 @@ async fn load_scene(
let entity_info = SceneEntityInfo {
inst_id: prop.inst_id,
group_id: prop.group_id,
motion: Some(prop_position.to_motion()),
motion: Some(prop_position.into()),
entity: Some(Entity::Prop(ScenePropInfo {
prop_state: prop.prop_state,
prop_id: prop.prop_id,
@@ -316,7 +355,7 @@ async fn load_scene(
inst_id: npc.inst_id,
group_id: npc.group_id,
entity_id: npc_entity_id,
motion: Some(npc_position.to_motion()),
motion: Some(npc_position.into()),
entity: Some(Entity::Npc(SceneNpcInfo {
npc_id: npc.npc_id,
..Default::default()
@@ -347,7 +386,7 @@ async fn load_scene(
inst_id: monster.inst_id,
group_id: monster.group_id,
entity_id: monster_entity_id,
motion: Some(monster_position.to_motion()),
motion: Some(monster_position.into()),
entity: Some(Entity::NpcMonster(npc_monster)),
};
@@ -361,18 +400,19 @@ async fn load_scene(
scene_info.entity_group_list.push(group_info);
scene_info.group_state_list.push(SceneGroupState {
group_id: *group_id,
is_default: true,
state: 0,
});
// TODO: ?
// scene_info.group_state_list.push(SceneGroupState {
// group_id: *group_id,
// is_default: true,
// state: 0,
// });
}
// load player entity
scene_info.entity_group_list.push(SceneEntityGroupInfo {
state: 0,
group_id: 0,
hejamoojbcj: HashMap::with_capacity(0),
banbecdcdhg: HashMap::with_capacity(0),
entity_list: json
.lineups
.iter()
+11 -32
View File
@@ -3,29 +3,6 @@ use paste::paste;
use tracing::Instrument;
use proto::*;
#[allow(unused_imports)]
use proto::{
CmdActivityType::*, CmdAdventureType::*, CmdAetherDivideType::*, CmdAlleyType::*,
CmdArchiveType::*, CmdAvatarType::*, CmdBattleCollegeType::*, CmdBattlePassType::*,
CmdBattleType::*, CmdBoxingClubType::*, CmdChallengeType::*, CmdChatType::*,
CmdChessRogueType::*, CmdClockParkType::*, CmdContentPackageType::*, CmdDailyActiveType::*,
CmdDrinkMakerType::*, CmdExpeditionType::*, CmdFantasticStoryActivityType::*,
CmdFeverTimeActivityType::*, CmdFightActivityType::*, CmdFightMathc3Type::*, CmdFightType::*,
CmdFriendType::*, CmdGachaType::*, CmdHeartdialType::*, CmdHeliobusType::*, CmdItemType::*,
CmdJukeboxType::*, CmdLineupType::*, CmdLobbyType::*, CmdMailType::*, CmdMapRotationType::*,
CmdMatchThreeModuleType::*, CmdMatchType::*, CmdMessageType::*, CmdMiscModuleType::*,
CmdMissionType::*, CmdMonopolyType::*, CmdMultiplayerType::*, CmdMultipleDropType::*,
CmdMuseumType::*, CmdOfferingType::*, CmdPamMissionType::*, CmdPhoneType::*,
CmdPlayerBoardType::*, CmdPlayerReturnType::*, CmdPlayerSync::*, CmdPlayerType::*,
CmdPlotType::*, CmdPunkLordType::*, CmdQuestType::*, CmdRaidCollectionType::*, CmdRaidType::*,
CmdRecommendType::*, CmdRedDotType::*, CmdReplayType::*, CmdRndOptionType::*,
CmdRogueCommonType::*, CmdRogueEndless::*, CmdRogueModifierType::*, CmdRogueTournType::*,
CmdRogueType::*, CmdRollShopType::*, CmdSceneType::*, CmdServerPrefsType::*, CmdShopType::*,
CmdSpaceZooType::*, CmdStarFightType::*, CmdStoryLineType::*,
CmdStrongChallengeActivityType::*, CmdTalkRewardType::*, CmdTelevisionActivityType::*,
CmdTextJoinType::*, CmdTrainVisitorType::*, CmdTreasureDungeonType::*, CmdTutorialType::*,
CmdWaypointType::*, CmdWolfBroType::*,
};
use super::PlayerSession;
use super::handlers::*;
@@ -138,27 +115,27 @@ macro_rules! trait_handler {
}
)*
async fn on_message(session: &mut PlayerSession, cmd_type: u16, payload: Vec<u8>) -> Result<()> {
async fn on_message(session: &mut PlayerSession, cmd_id: u16, payload: Vec<u8>) -> Result<()> {
use ::prost::Message;
if PlayerSession::should_send_dummy_rsp(cmd_type) {
session.send_dummy_response(cmd_type).await?;
if PlayerSession::should_send_dummy_rsp(cmd_id) {
session.send_dummy_response(cmd_id).await?;
return Ok(());
}
match cmd_type {
match cmd_id {
$(
cmd_type if cmd_type == paste! { [<Cmd$name CsReq>] as u16 } => {
cmd_id if cmd_id == paste! { <proto::[<$name CsReq>] as proto::CmdID>::CMD_ID } => {
let body = paste! { proto::[<$name CsReq>]::decode(&mut &payload[..])? };
paste! {
Self::[<on_$name:snake _cs_req>](session, &body)
.instrument(tracing::info_span!(stringify!([<on_$name:snake>]), cmd_type = cmd_type))
.instrument(tracing::info_span!(stringify!([<on_$name:snake>]), cmd_id = cmd_id))
.await
}
}
)*
_ => {
tracing::warn!("Unknown command type: {cmd_type}");
tracing::warn!("Unknown command ID: {cmd_id}");
Ok(())
},
}
@@ -172,13 +149,13 @@ trait_handler! {
PlayerLogin;
GetMissionStatus;
GetBasicInfo;
GetMultiPathAvatarInfo;
GetAvatarData;
GetAllLineupData;
GetCurLineupData;
GetCurSceneInfo;
PlayerHeartBeat;
SetAvatarEnhancedId;
TakePromotionReward;
// Entity move (dummy!)
SceneEntityMove;
@@ -190,6 +167,7 @@ trait_handler! {
TakeOffEquipment;
DressRelicAvatar;
TakeOffRelic;
RankUpAvatar;
// Chat (dummy!)
SendMsg;
@@ -208,6 +186,7 @@ trait_handler! {
PveBattleResult;
SceneCastSkill;
QuickStartCocoonStage;
SceneEnterStage;
// Teleport
GetEnteredScene;
+65 -43
View File
@@ -17,7 +17,7 @@ use tokio::{
sync::{Mutex, watch},
};
use crate::util;
use crate::{net::handlers::BASE_AVATAR_IDS, util};
use super::{NetPacket, packet::CommandHandler};
@@ -33,6 +33,7 @@ pub struct PlayerSession {
pub shutdown_tx: watch::Sender<()>,
pub shutdown_rx: watch::Receiver<()>,
pub json_data: OnceLock<FreesrData>,
pub next_scene_save: u64,
}
impl PlayerSession {
@@ -40,14 +41,17 @@ impl PlayerSession {
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(),
json_data: OnceLock::new(),
shutdown_rx,
shutdown_tx,
next_scene_save: 0,
}
}
@@ -112,72 +116,90 @@ impl PlayerSession {
Ok(())
}
pub async fn sync_player(&self) {
pub async fn sync_player(&self) -> Result<()> {
let Some(json) = self.json_data.get() else {
tracing::error!("data is not init!");
return;
return Ok(());
};
// clear relics & lightcones
self.send(PlayerSyncScNotify {
del_equipment_list: (2000..3500).collect(),
del_relic_list: (1..2000).collect(),
del_relic_list: (1..=3000).collect(),
del_equipment_list: (3001..=3500).collect(),
..Default::default()
})
.await
.unwrap();
.await?;
// Sync avatars
// clear all avatars equip item
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()
avatar_list: BASE_AVATAR_IDS
.into_iter()
.filter_map(|id| {
json.avatars.get(&id).map(|v| {
v.to_avatar_proto(
Option::None,
json.main_character as u32,
json.march_type as u32,
)
})
})
.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
avatar_path_data_info_list: json
.avatars
.values()
.map(|avatar| {
avatar.to_avatar_proto(
avatar.to_avatar_path_data_proto(Option::None, Vec::with_capacity(0))
})
.collect::<Vec<_>>(),
}),
..Default::default()
})
.await?;
// Sync new relics & lightcones
self.send(PlayerSyncScNotify {
relic_list: json.relics.iter().map(|v| v.into()).collect(),
equipment_list: json.lightcones.iter().map(|v| v.into()).collect(),
..Default::default()
})
.await?;
// Sync new avatars equip item
self.send(PlayerSyncScNotify {
avatar_sync: Some(AvatarSync {
avatar_list: BASE_AVATAR_IDS
.into_iter()
.filter_map(|avatar_id| {
json.avatars.get(&avatar_id).map(|v| {
v.to_avatar_proto(
json.lightcones.iter().find(|v| v.equip_avatar == avatar_id),
json.main_character as u32,
json.march_type as u32,
)
})
})
.collect(),
avatar_path_data_info_list: json
.avatars
.values()
.map(|avatar| {
avatar.to_avatar_path_data_proto(
json.lightcones
.iter()
.find(|v| v.equip_avatar == avatar.avatar_id),
json.relics
.iter()
.filter(|v| v.equip_avatar == avatar.avatar_id)
.filter(|r| r.equip_avatar == avatar.avatar_id)
.collect(),
)
})
.collect(),
.collect::<Vec<_>>(),
}),
..Default::default()
})
.await
.unwrap()
}
fn session_time(&self) -> u64 {
+2 -6
View File
@@ -16,7 +16,7 @@ use bytes::{Buf, BufMut, BytesMut};
#[cfg(feature = "tokio")]
use tokio::io::{AsyncWrite, AsyncWriteExt};
use crate::{error::Error, KcpResult};
use crate::{KcpResult, error::Error};
const KCP_RTO_NDL: u32 = 20; // no delay min rto
const KCP_RTO_MIN: u32 = 100; // normal min rto
@@ -528,11 +528,7 @@ impl<Output> Kcp<Output> {
self.rx_srtt = rtt;
self.rx_rttval = rtt / 2;
} else {
let delta = if rtt > self.rx_srtt {
rtt - self.rx_srtt
} else {
self.rx_srtt - rtt
};
let delta = rtt.abs_diff(self.rx_srtt);
self.rx_rttval = (3 * self.rx_rttval + delta) / 4;
self.rx_srtt = ((7 * u64::from(self.rx_srtt) + u64::from(rtt)) / 8) as u32;
if self.rx_srtt < 1 {
+2 -2
View File
@@ -7,11 +7,11 @@ mod kcp;
/// The `KCP` prelude
pub mod prelude {
pub use super::{get_conv, Kcp, KCP_OVERHEAD};
pub use super::{KCP_OVERHEAD, Kcp, get_conv};
}
pub use error::Error;
pub use kcp::{get_conv, get_sn, set_conv, Kcp, KCP_OVERHEAD};
pub use kcp::{KCP_OVERHEAD, Kcp, get_conv, get_sn, set_conv};
/// KCP result
pub type KcpResult<T> = Result<T, Error>;
+15 -13
View File
@@ -1,21 +1,23 @@
{
"lineups": {
"0": 1403,
"1": 1407,
"2": 8001,
"3": 1405
"0": 1513,
"1": 1308,
"2": 1403,
"3": 1409
},
"position": {
"x": -26968,
"y": 78953,
"z": 14457,
"rot_y": 11858
"x": 674182,
"y": 75562,
"z": -16482,
"rot_y": 204639
},
"scene": {
"plane_id": 20411,
"floor_id": 20411001,
"entry_id": 2041101
"plane_id": 20511,
"floor_id": 20511001,
"entry_id": 2051101
},
"main_character": "FemaleRememberance",
"march_type": "MarchHunt"
"main_character": "FemaleElation",
"march_type": "MarchHunt",
"enable_sw_global": true,
"enable_castorice_global": true
}
+5841
View File
File diff suppressed because it is too large Load Diff
+12344 -58815
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
use proc_macro::TokenStream;
use quote::{quote, ToTokens};
use syn::{parse_macro_input, DeriveInput, Meta, MetaList};
use quote::{ToTokens, quote};
use syn::{DeriveInput, Meta, MetaList, parse_macro_input};
#[proc_macro_derive(CmdID, attributes(cmdid))]
pub fn message_id_derive(input: TokenStream) -> TokenStream {
+1 -1
View File
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
ignore = [
"proto/"
]
+14 -14
View File
@@ -4,29 +4,29 @@ version = "0.1.0"
edition = "2024"
[dependencies]
anyhow.workspace = true
env_logger.workspace = true
tower-http = { workspace = true, features = ["cors"]}
# Framework
tokio.workspace = true
tower-http = { workspace = true, features = ["cors"] }
axum.workspace = true
axum-server.workspace = true
reqwest.workspace = true
lazy_static.workspace = true
# JSON
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
tokio-util.workspace = true
# Logging
tracing.workspace = true
tracing-futures.workspace = true
tracing-log.workspace = true
tracing-subscriber.workspace = true
tracing-bunyan-formatter.workspace = true
ansi_term.workspace = true
env_logger.workspace = true
# Encoding / Serialization
prost.workspace = true
rbase64.workspace = true
proto.workspace = true
# Error handling
anyhow.workspace = true
# Local
common.workspace = true
proto.workspace = true
+1 -3
View File
@@ -1,3 +1 @@
mod version_config;
pub use version_config::INSTANCE as versions;
pub mod version_config;
+112 -14
View File
@@ -1,29 +1,127 @@
use std::collections::HashMap;
use lazy_static::lazy_static;
use serde::Deserialize;
use serde_json::from_str;
use anyhow::{Result, anyhow};
use prost::Message as _;
use proto::GateServer;
use serde::{Deserialize, Serialize};
use tokio::fs;
const DEFAULT_VERSIONS: &str = include_str!("../../versions.json");
const DEFAULT_VERSIONS: &str = include_str!("../../../versions.json");
#[derive(Deserialize)]
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct VersionConfig {
pub asset_bundle_url: String,
pub ex_resource_url: String,
pub lua_url: String,
// pub lua_version: String,
pub ifix_url: String,
}
lazy_static! {
pub static ref INSTANCE: HashMap<String, VersionConfig> = {
let local_config = std::path::Path::new("versions.json");
let data = if local_config.exists() {
std::fs::read_to_string("versions.json").unwrap()
} else {
std::fs::write("versions.json", DEFAULT_VERSIONS).unwrap();
impl VersionConfig {
const CNPROD_HOST: &str = "prod-gf-cn-dp01.bhsr.com";
const CNBETA_HOST: &str = "beta-release01-cn.bhsr.com";
const OSPROD_HOST: &str = "prod-official-asia-dp01.starrails.com";
const OSBETA_HOST: &str = "beta-release01-asia.starrails.com";
const PROXY_HOST: &str = "proxy1.neonteam.dev"; // we used this because PS users usually have a proxy enabled
pub async fn load_hotfix() -> HashMap<String, VersionConfig> {
const CONFIG_PATH: &str = "versions.json";
let data = match fs::read_to_string(CONFIG_PATH).await {
Ok(data) => data,
Err(_) => {
fs::write(CONFIG_PATH, DEFAULT_VERSIONS)
.await
.expect("Failed to create versions file");
DEFAULT_VERSIONS.to_string()
}
};
from_str(&data).unwrap()
match serde_json::from_str(&data) {
Ok(data) => data,
Err(_) => {
tracing::error!("malformed versions.json. replacing it with default one");
let _ = fs::write(CONFIG_PATH, DEFAULT_VERSIONS).await;
serde_json::from_str(DEFAULT_VERSIONS).unwrap()
}
}
}
pub async fn fetch_hotfix(version: &str, dispatch_seed: &str) -> Result<VersionConfig> {
let (region, branch, _, _) = Self::parse_version_string(version)?;
let host = match (region.as_str(), branch.as_str()) {
("OS", "BETA") => Self::OSBETA_HOST,
("OS", "PROD") => Self::OSPROD_HOST,
("CN", "BETA") => Self::CNBETA_HOST,
("CN", "PROD") => Self::CNPROD_HOST,
// TODO: Support more host, or use query_dispatch result to determine the urls
_ => Self::OSBETA_HOST,
};
let url = format!(
"https://{}/{host}/query_gateway?version={}&platform_type=1&language_type=3&dispatch_seed={}&channel_id=1&sub_channel_id=1&is_need_url=1",
Self::PROXY_HOST,
version,
dispatch_seed
);
tracing::info!("fetching hotfix: {url}");
let res = reqwest::get(url).await?.text().await?;
tracing::info!("raw gateway response: {}", res);
let bytes = rbase64::decode(&res)?;
let decoded = GateServer::decode(bytes.as_slice())?;
if decoded.asset_bundle_url.is_empty() && decoded.ex_resource_url.is_empty() {
return Err(anyhow::format_err!(
"asset_bundle_url and ex_resource_url are empty! gateway result code: {} message: {}",
decoded.retcode,
decoded.msg
));
}
tracing::info!("{:#?}", decoded);
Ok(VersionConfig {
asset_bundle_url: decoded.asset_bundle_url,
ex_resource_url: decoded.ex_resource_url,
lua_url: decoded.lua_url,
ifix_url: decoded.ifix_url,
})
}
fn parse_version_string(s: &str) -> Result<(String, String, String, String)> {
const BRANCHES: [&str; 7] = ["PREbeta", "BETA", "PROD", "DEV", "PRE", "GM", "CECREATION"];
const OS: [&str; 3] = ["Android", "Win", "iOS"];
let region = s
.get(0..2)
.ok_or_else(|| anyhow!("version parse failed. reason: invalid region url: {s}"))?;
let after_region = s.get(2..).unwrap_or(s);
let branch = BRANCHES
.iter()
.find(|&&b| after_region.starts_with(b))
.map(|&b| b.to_string())
.ok_or_else(|| anyhow!("version parse failed. reason: invalid branch url: {s}"))?;
let branch_len = branch.len();
let after_branch = after_region.get(branch_len..).unwrap_or(after_region);
let os = OS
.iter()
.find(|&&o| after_branch.starts_with(o))
.map(|&o| o.to_string())
.ok_or_else(|| anyhow!("version parse failed. reason: invalid os url: {s}"))?;
let os_len = os.len();
let version = after_branch
.get(os_len..)
.ok_or_else(|| anyhow!("version parse failed. reason: invalid version url: {s}"))?;
Ok((region.to_string(), branch, os, version.to_string()))
}
}
+114
View File
@@ -0,0 +1,114 @@
use std::collections::HashMap;
use std::sync::Arc;
use anyhow::Result;
use axum::Router;
use axum::http::Method;
use axum::http::header::CONTENT_TYPE;
use axum::routing::{get, post};
use config::version_config::VersionConfig;
use services::{auth, dispatch, errors, sr_tools};
use tokio::fs;
use tokio::sync::RwLock;
use tower_http::cors::{Any, CorsLayer};
use tracing::Level;
mod config;
mod services;
const PORT: u16 = 21000;
#[derive(Clone)]
struct AppState {
hotfix_map: HashMap<String, VersionConfig>,
}
impl AppState {
async fn get_or_insert_hotfix(&mut self, version: &str, dispatch_seed: &str) -> &VersionConfig {
if self.hotfix_map.contains_key(version) {
return &self.hotfix_map[version];
}
tracing::info!(
"trying to fetch hotfix for version {version} with dispatch seed {dispatch_seed}"
);
let hotfix = match VersionConfig::fetch_hotfix(version, dispatch_seed).await {
Ok(hotfix) => hotfix,
Err(err) => {
tracing::error!("failed to fetch hotfix. reason: {err}");
VersionConfig::default()
}
};
self.hotfix_map.insert(version.to_string(), hotfix);
if let Ok(serialized) = serde_json::to_string_pretty(&self.hotfix_map) {
let _ = fs::write("versions.json", serialized).await;
}
&self.hotfix_map[version]
}
}
pub async fn start_sdkserver() -> Result<()> {
let span = tracing::span!(Level::DEBUG, "main");
let _ = span.enter();
let hotfix_map = VersionConfig::load_hotfix().await;
tracing::info!(
"loaded {} hotfix versions. supported versions: {:?}",
hotfix_map.len(),
hotfix_map.keys()
);
let state = Arc::new(RwLock::new(AppState { hotfix_map }));
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),
)
.route(
sr_tools::SRTOOLS_UPLOAD_ENDPOINT,
post(sr_tools::sr_tool_save),
)
.route(
auth::APN_LOGIN_BY_PASSWORD_ENDPOINT,
post(auth::apn_login_with_password),
)
.route(auth::APN_VERIFY_TOKEN_ENDPOINT, post(auth::apn_veriy_token))
.layer(
CorsLayer::new()
.allow_origin(Any)
.allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
.allow_headers([CONTENT_TYPE]),
)
.with_state(state)
.fallback(errors::not_found);
let addr = format!("0.0.0.0:{PORT}");
let server = axum_server::bind(addr.parse::<std::net::SocketAddr>()?);
tracing::info!("sdkserver is listening at {addr}");
server.serve(router.into_make_service()).await?;
Ok(())
}
-29
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"));
}
+6 -57
View File
@@ -1,65 +1,14 @@
use anyhow::Result;
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, sr_tools};
use tower_http::cors::{Any, CorsLayer};
use tracing::Level;
mod config;
mod logging;
mod services;
pub fn init_tracing() {
#[cfg(target_os = "windows")]
ansi_term::enable_ansi_support().unwrap();
const PORT: u16 = 21000;
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
}
#[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),
)
.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}");
let server = axum_server::bind(addr.parse()?);
tracing::info!("sdkserver is listening at {addr}");
server.serve(router.into_make_service()).await?;
Ok(())
sdkserver::start_sdkserver().await
}
+52 -1
View File
@@ -3,8 +3,11 @@ 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 GRANTER_LOGIN_VERIFICATION_ENDPOINT: &str =
"/{product_name}/combo/granter/login/v2/login";
pub const RISKY_API_CHECK_ENDPOINT: &str = "/account/risky/api/check";
pub const APN_LOGIN_BY_PASSWORD_ENDPOINT: &str = "/account/ma-cn-passport/app/loginByPassword";
pub const APN_VERIFY_TOKEN_ENDPOINT: &str = "/account/ma-cn-session/app/verify";
#[tracing::instrument]
pub async fn login_with_password() -> Json<serde_json::Value> {
@@ -78,3 +81,51 @@ pub async fn risky_api_check() -> Json<serde_json::Value> {
"retcode": 0
}))
}
#[tracing::instrument]
pub async fn apn_login_with_password() -> Json<serde_json::Value> {
Json(json!({
"data": {
"token": {
"token": "mostsecuretokenever",
"token_type": 1
},
"user_info": {
"aid": "1337",
"mid": "1337",
"is_email_verify": 1,
"area_code": "**",
"country": "US",
"is_adult": 1,
"email": "motorized@wheel.chair"
}
},
"message": "OK",
"retcode": 0
}))
}
#[tracing::instrument]
pub async fn apn_veriy_token() -> Json<serde_json::Value> {
Json(json!({
"data": {
"tokens": [
{
"token": "mostsecuretokenever",
"token_type": 1
}
],
"user_info": {
"aid": "1337",
"mid": "1337",
"is_email_verify": 1,
"area_code": "**",
"country": "US",
"is_adult": 1,
"email": "motorized@wheel.chair"
}
},
"message": "OK",
"retcode": 0
}))
}
+30 -28
View File
@@ -1,20 +1,24 @@
use crate::config::versions;
use axum::extract::Query;
use std::sync::Arc;
use crate::AppState;
use axum::extract::{Query, State};
use prost::Message;
use proto::{DispatchRegionData, Gateserver, RegionEntry};
use proto::{Dispatch, GateServer, RegionInfo};
use serde::Deserialize;
use tokio::sync::RwLock;
use tracing::instrument;
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 = DispatchRegionData {
let rsp = Dispatch {
retcode: 0,
region_list: vec![RegionEntry {
region_list: vec![RegionInfo {
name: String::from("RobinSR"),
title: String::from("RobinSR"),
env_type: String::from("21"),
env_type: String::from("9"),
dispatch_url: String::from("http://127.0.0.1:21000/query_gateway"),
..Default::default()
}],
@@ -30,37 +34,35 @@ pub async fn query_dispatch() -> String {
#[derive(Deserialize, Debug)]
pub struct QueryGatewayParameters {
pub version: String,
pub dispatch_seed: 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,
#[instrument(skip(state))]
pub async fn query_gateway(
State(state): State<Arc<RwLock<AppState>>>,
parameters: Query<QueryGatewayParameters>,
) -> String {
let mut lock = state.write().await;
let config = lock
.get_or_insert_hotfix(&parameters.version, &parameters.dispatch_seed)
.await;
let rsp = GateServer {
ip: String::from("127.0.0.1"),
port: 23301,
asset_bundle_url: config.asset_bundle_url.clone(),
asset_bundle_url_android: config.asset_bundle_url.clone(),
ex_resource_url: config.ex_resource_url.clone(),
lua_url: config.lua_url.clone(),
ifix_version: String::from("0"),
enable_design_data_version_update: true,
enable_version_update: true,
enable_upload_battle_log: true,
network_diagnostic: true,
close_redeem_code: true,
enable_android_middle_package: true,
enable_watermark: true,
event_tracking_open: true,
enable_cdn_ipv6: 1,
enable_save_replay_file: true,
unk1: true,
unk2: true,
unk3: true,
unk4: true,
unk5: true,
unk6: true,
unk7: true,
..Default::default()
}
} else {
Gateserver {
retcode: 9,
login_white_msg: format!("forbidden version: {} or invalid bind", parameters.version),
..Default::default()
}
};
let mut buff = Vec::new();
+12 -5
View File
@@ -1,3 +1,5 @@
use std::path::Path;
use axum::Json;
use common::sr_tools::FreesrData;
use serde::{Deserialize, Serialize};
@@ -29,16 +31,21 @@ pub async fn sr_tool_save(Json(json): Json<SrToolDataReq>) -> Json<SrToolDataRsp
Ok(json) => json,
Err(err) => {
return Json(SrToolDataRsp {
message: format!("malformed json: {}", err),
status: 200,
message: format!("malformed json: {err}"),
status: 500,
});
}
};
if let Err(err) = fs::write("freesr-data.json", json).await {
let path = Path::new("freesr-data.json");
let env = std::env::current_dir();
if let Err(err) = fs::write(&path, json).await {
return Json(SrToolDataRsp {
message: format!("failed to write freesr-data.json: {}", err),
status: 200,
message: format!(
"failed to write freesr-data.json: {err} at path: {path:#?} env: {env:#?}"
),
status: 500,
});
};
-8
View File
@@ -1,8 +0,0 @@
{
"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"
}
}
+53 -5
View File
@@ -1,8 +1,56 @@
{
"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"
"CNBETAWin3.5.55": {
"asset_bundle_url": "https://autopatchcn.bhsr.com/asb/BetaLive/output_11797411_74704086c081_d40a71b41ba492",
"ex_resource_url": "https://autopatchcn.bhsr.com/design_data/BetaLive/output_11823033_c9e8c870bcc7_b78f37c36487d0",
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_11798340_8c6543a1d6c5_2d60cd9b1584b8",
"ifix_url": "https://autopatchcn.bhsr.com/ifix/BetaLive/output_0_40d2ce0253_c61ba99f70b885"
},
"CNBETAWin3.6.51": {
"asset_bundle_url": "https://autopatchcn.bhsr.com/asb/BetaLive/output_12066992_f083970b907e_999074cab6dce6",
"ex_resource_url": "https://autopatchcn-bhsr.neonteam.dev/design_data/BetaLive/output_12114942_e99cbde25134_e63a6b835f17f9",
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_12103115_ee78155e9867_3626f0948d93e2",
"ifix_url": "https://autopatchcn.bhsr.com/ifix/BetaLive/output_12118783_55113408814f_c874267d04c04a"
},
"CNBETAWin3.6.52": {
"asset_bundle_url": "https://autopatchcn.bhsr.com/asb/BetaLive/output_12150127_00d6d096d968_cd76a04beb7ba6",
"ex_resource_url": "https://autopatchcn.bhsr.com/design_data/BetaLive/output_12178965_e246796e0bb6_05bcce36cd648b",
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_12150614_5279f6d8029a_bdecff99d2d817",
"ifix_url": "https://autopatchcn.bhsr.com/ifix/BetaLive/output_12164593_8e3fba5163df_b2b6fc46de4c06"
},
"CNBETAWin3.7.51": {
"asset_bundle_url": "https://autopatchcn.bhsr.com/asb/BetaLive/output_12579793_48327ff319b5_1b794dd1071e3a",
"ex_resource_url": "https://autopatchcn.bhsr.com/design_data/BetaLive/output_12611332_5f583f2f54ae_c04979f13c950d",
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_12579929_9566349ee5fb_c6341faaf9b027",
"ifix_url": "https://autopatchcn.bhsr.com/ifix/BetaLive/output_0_40d2ce0253_c61ba99f70b885"
},
"OSBETAWin3.8.51": {
"asset_bundle_url": "https://autopatchos.starrails.com/asb/BetaLive/output_13490885_bf25e1553b38_e9adc23a6b6710",
"ex_resource_url": "https://autopatchos.starrails.com/design_data/BetaLive/output_13532881_eb1de9bedabd_c74ab2513059ad",
"lua_url": "https://autopatchos.starrails.com/lua/BetaLive/output_13493894_ec7d6d879305_8b2cfd39d6e545",
"ifix_url": "https://autopatchos.starrails.com/ifix/BetaLive/output_13491130_ecfa51828143_4bd221b76c0d9a"
},
"OSBETAWin4.0.51": {
"asset_bundle_url": "https://autopatchos.starrails.com/asb/BetaLive/output_14026982_55a517a26691_89300298bf2b2e",
"ex_resource_url": "https://autopatchos.starrails.com/design_data/BetaLive/output_14036546_28a134460a9a_5805431f75f0ff",
"lua_url": "https://autopatchos.starrails.com/lua/BetaLive/output_14028456_7088b08fd431_59194837d90977",
"ifix_url": "https://autopatchos.starrails.com/ifix/BetaLive/output_0_40d2ce0253_6d871f8bca6eb4"
},
"CNBETAWin4.1.51": {
"asset_bundle_url": "https://autopatchcn.bhsr.com/asb/BetaLive/output_14372365_fa78e03ad599_36972b4e3bb553",
"ex_resource_url": "https://autopatchcn.bhsr.com/design_data/BetaLive/output_14372319_01febed213ae_b1f343e62353c2",
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_14358961_e055a63b3c34_b199a0234548b5",
"ifix_url": "https://autopatchcn.bhsr.com/ifix/BetaLive/output_14374543_04d971175704_ab00cb3f54bb00"
},
"CNBETAWin4.2.51": {
"asset_bundle_url": "https://autopatchcn.bhsr.com/asb/BetaLive/output_14851990_9aa8ee8de0fa_ec7d1e42f851f1",
"ex_resource_url": "https://autopatchcn.bhsr.com/design_data/BetaLive/output_14890490_fcc969a5461f_1f9258ee9068a5",
"lua_url": "https://autopatchcn.bhsr.com/lua/BetaLive/output_14852119_8789f55c1c30_0c0cf5d3c9089b",
"ifix_url": "https://autopatchcn.bhsr.com/ifix/BetaLive/output_14887825_65010135064b_71536d98b8abbf"
},
"OSBETAWin4.4.51": {
"asset_bundle_url": "https://autopatchos.starrails.com/asb/BetaLive/output_15711835_1986f76c287e_9c3d058458edf0",
"ex_resource_url": "https://autopatchos.starrails.com/design_data/BetaLive/output_15753142_3bb8238470fc_a7cec92c089da7",
"lua_url": "https://autopatchos.starrails.com/lua/BetaLive/output_15712109_65eeab393c89_0c116c2699518d",
"ifix_url": "https://autopatchos.starrails.com/ifix/BetaLive/output_0_40d2ce0253_6d871f8bca6eb4"
}
}