Skip to content

Commit dc914ee

Browse files
committed
feat: 全国对战读取游戏曲库求交集
hook 游戏曲库加载函数,中序遍历 std::map 红黑树导出 music id 与难度掩码, 替换原先写死的空 Music 帧,使服务端曲库交集生效。
1 parent b0e0751 commit dc914ee

2 files changed

Lines changed: 266 additions & 1 deletion

File tree

src/national_match/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use std::sync::Mutex;
55
use once_cell::sync::OnceCell;
66
use windows_sys::Win32::Networking::WinSock::{SOCKADDR, SOCKADDR_IN, WSABUF};
77

8+
mod music;
9+
810
use crate::config::Config;
911
use crate::util::api::Api;
1012
use crate::util::iat_hook::hook_iat;
@@ -81,6 +83,8 @@ pub fn init(api: &Api, config: &Config) {
8183
}
8284

8385
api.log_info("national match: enabled (UDP<->TCP relay)");
86+
87+
music::init(api, config);
8488
}
8589

8690
unsafe fn collect_payload(buffers: *const WSABUF, count: u32) -> Vec<u8> {
@@ -181,7 +185,8 @@ unsafe extern "system" fn hooked_sendto(
181185
was
182186
};
183187
if !already {
184-
let music = build_frame(TYPE_MUSIC, &[0, 0]);
188+
let payload = music::music_payload();
189+
let music = build_frame(TYPE_MUSIC, &payload);
185190
send_to_reflector(&music);
186191
}
187192

src/national_match/music.rs

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
//! 全国对战曲库读取:hook 游戏曲库加载函数,遍历 std::map 红黑树导出曲库给 reflector 求交集。
2+
//!
3+
//! 机制对照 duolinguo(闭源,经 Ghidra 逆向)在 SDHD245 上验证:
4+
//! - 签名 `50 E8 ?? ?? ?? ?? 8D 8D E8 FE FF FF E8` 定位曲库加载 CALL 点
5+
//! - 解 E8 rel32 得加载函数地址,inline hook 之
6+
//! - detour 先调原函数,再遍历 std::map<id, MusicInfo> 红黑树
7+
//! - 每曲:id = node+0x10 (u16),难度 vector = node+0x208..0x20c(每项 0x40 字节,[+0]=难度id [+1]=enable)
8+
//! - 难度掩码 mask |= enable << difficulty_id(bit0=BASIC..bit4=ULTIMA)
9+
//! - 输出 count(2B LE) + 每曲[id(2B LE) + mask(1B)]
10+
11+
use std::mem::transmute;
12+
use std::sync::Mutex;
13+
14+
use once_cell::sync::OnceCell;
15+
16+
use crate::config::Config;
17+
use crate::util::api::Api;
18+
use crate::util::pattern;
19+
20+
const SECTION: &str = "NationalMatch";
21+
22+
/// 曲库加载 CALL 点签名:`PUSH EAX; CALL <load>; LEA ECX,[EBP-0x118]; CALL ...`
23+
const MUSIC_LOAD_SIG: &str = "50 E8 ?? ?? ?? ?? 8D 8D E8 FE FF FF E8";
24+
25+
// MSVC std::_Tree_node 节点偏移
26+
const NODE_PARENT: usize = 0x4;
27+
const NODE_RIGHT: usize = 0x8;
28+
const NODE_ISNIL: usize = 0xd;
29+
const NODE_LEFT: usize = 0x0;
30+
// MusicInfo(节点 _Myval)字段偏移
31+
const MUSIC_ID: usize = 0x10;
32+
const DIFF_VEC_FIRST: usize = 0x208;
33+
const DIFF_VEC_LAST: usize = 0x20c;
34+
const DIFF_STRIDE: usize = 0x40;
35+
const DIFF_TYPE_ID: usize = 0x0;
36+
const DIFF_ENABLE: usize = 0x1;
37+
38+
/// 红黑树遍历安全上限(曲库远小于此,纯防御野指针导致的死循环)
39+
const MAX_NODES: usize = 100_000;
40+
41+
type LoadFn = unsafe extern "stdcall" fn(usize, usize, usize, usize, *const usize);
42+
43+
static API: OnceCell<Api> = OnceCell::new();
44+
static TRAMPOLINE: OnceCell<usize> = OnceCell::new();
45+
static MUSIC_PAYLOAD: Mutex<Option<Vec<u8>>> = Mutex::new(None);
46+
47+
pub fn init(api: &Api, config: &Config) {
48+
if !config.is_enabled(SECTION) {
49+
return;
50+
}
51+
52+
let text_base = api.text_base();
53+
let text_size = api.text_size();
54+
if text_base == 0 || text_size == 0 {
55+
api.log_warn("national match music: invalid text section");
56+
return;
57+
}
58+
59+
let hit = pattern::scan_range(api, text_base, text_size, MUSIC_LOAD_SIG);
60+
if hit == 0 {
61+
api.log_warn("national match music: load signature not found");
62+
return;
63+
}
64+
65+
// hit 指向 PUSH EAX;hit+1 是 E8,hit+2 是 rel32。
66+
let Some(rel) = read_i32(api, hit + 2) else {
67+
api.log_warn("national match music: failed to read call rel32");
68+
return;
69+
};
70+
// CALL 目标 = (E8 下一条指令地址) + rel = (hit + 1 + 5) + rel
71+
let mut load_fn = (hit + 6).wrapping_add(rel as usize);
72+
// 跳过 thunk:若目标是 E9 jmp,再解一跳。
73+
load_fn = resolve_thunk(api, load_fn);
74+
75+
let _ = API.set(*api);
76+
77+
let Some(trampoline) = api.hook_create(load_fn, music_load_detour as *const () as usize) else {
78+
api.log_warn("national match music: failed to create hook");
79+
return;
80+
};
81+
let _ = TRAMPOLINE.set(trampoline);
82+
83+
if !api.hook_enable(load_fn) {
84+
api.log_warn("national match music: failed to enable hook");
85+
return;
86+
}
87+
88+
api.log_info(&format!(
89+
"national match music: hooked load fn @ 0x{load_fn:08X}"
90+
));
91+
}
92+
93+
/// 取缓存的曲库帧 payload;未就绪时回退空列表(count=0)。
94+
pub fn music_payload() -> Vec<u8> {
95+
MUSIC_PAYLOAD
96+
.lock()
97+
.ok()
98+
.and_then(|guard| guard.clone())
99+
.unwrap_or_else(|| vec![0, 0])
100+
}
101+
102+
unsafe extern "stdcall" fn music_load_detour(
103+
p1: usize,
104+
p2: usize,
105+
p3: usize,
106+
p4: usize,
107+
container: *const usize,
108+
) {
109+
if let Some(&trampoline) = TRAMPOLINE.get() {
110+
let orig: LoadFn = transmute(trampoline);
111+
orig(p1, p2, p3, p4, container);
112+
}
113+
114+
let Some(api) = API.get() else {
115+
return;
116+
};
117+
118+
match dump_music(api, container) {
119+
Some(list) => {
120+
let payload = encode(&list);
121+
api.log_info(&format!(
122+
"national match music: loaded {} musics",
123+
list.len()
124+
));
125+
if let Ok(mut guard) = MUSIC_PAYLOAD.lock() {
126+
*guard = Some(payload);
127+
}
128+
}
129+
None => api.log_warn("national match music: dump failed"),
130+
}
131+
}
132+
133+
/// 中序遍历 std::map 红黑树,返回 (music_id, 难度掩码) 列表。
134+
fn dump_music(api: &Api, container: *const usize) -> Option<Vec<(u16, u8)>> {
135+
if container.is_null() {
136+
return None;
137+
}
138+
// container -> _Myhead -> _Left(最小节点,中序起点)
139+
let head = read_usize(api, container as usize)?;
140+
let mut node = read_usize(api, head + NODE_LEFT)?;
141+
142+
let mut out = Vec::new();
143+
let mut guard = 0usize;
144+
145+
while guard < MAX_NODES {
146+
guard += 1;
147+
// _Isnil != 0 表示走到哨兵,结束。
148+
if read_u8(api, node + NODE_ISNIL)? != 0 {
149+
break;
150+
}
151+
152+
if let (Some(id), Some(mask)) = (read_u16(api, node + MUSIC_ID), difficulty_mask(api, node))
153+
{
154+
out.push((id, mask));
155+
}
156+
157+
node = inorder_successor(api, node, head)?;
158+
if node == head {
159+
break;
160+
}
161+
}
162+
163+
Some(out)
164+
}
165+
166+
/// 计算单曲难度掩码:遍历难度 vector,mask |= enable << difficulty_id。
167+
fn difficulty_mask(api: &Api, node: usize) -> Option<u8> {
168+
let mut first = read_usize(api, node + DIFF_VEC_FIRST)?;
169+
let last = read_usize(api, node + DIFF_VEC_LAST)?;
170+
171+
let mut mask = 0u8;
172+
let mut guard = 0usize;
173+
while first != last && guard < 64 {
174+
guard += 1;
175+
let type_id = read_u8(api, first + DIFF_TYPE_ID)?;
176+
let enable = read_u8(api, first + DIFF_ENABLE)?;
177+
mask |= enable.wrapping_shl(u32::from(type_id) & 0x1f);
178+
first = first.wrapping_add(DIFF_STRIDE);
179+
}
180+
Some(mask)
181+
}
182+
183+
/// MSVC std::map 中序后继。
184+
fn inorder_successor(api: &Api, node: usize, head: usize) -> Option<usize> {
185+
let right = read_usize(api, node + NODE_RIGHT)?;
186+
if read_u8(api, right + NODE_ISNIL)? == 0 {
187+
// 有右子树:取右子树最左节点。
188+
let mut cur = right;
189+
let mut guard = 0usize;
190+
loop {
191+
guard += 1;
192+
if guard > MAX_NODES {
193+
return Some(head);
194+
}
195+
let left = read_usize(api, cur + NODE_LEFT)?;
196+
if read_u8(api, left + NODE_ISNIL)? != 0 {
197+
return Some(cur);
198+
}
199+
cur = left;
200+
}
201+
}
202+
203+
// 无右子树:上溯到第一个「自己是左子」的祖先。
204+
let mut cur = node;
205+
let mut parent = read_usize(api, node + NODE_PARENT)?;
206+
let mut guard = 0usize;
207+
while read_u8(api, parent + NODE_ISNIL)? == 0 && cur == read_usize(api, parent + NODE_RIGHT)? {
208+
guard += 1;
209+
if guard > MAX_NODES {
210+
return Some(head);
211+
}
212+
cur = parent;
213+
parent = read_usize(api, parent + NODE_PARENT)?;
214+
}
215+
Some(parent)
216+
}
217+
218+
/// 编码为 reflector Music 帧 payload:count(2B LE) + 每曲[id(2B LE) + mask(1B)]。
219+
fn encode(list: &[(u16, u8)]) -> Vec<u8> {
220+
let count = u16::try_from(list.len()).unwrap_or(u16::MAX);
221+
let mut buf = Vec::with_capacity(2 + list.len() * 3);
222+
buf.extend_from_slice(&count.to_le_bytes());
223+
for &(id, mask) in list.iter().take(count as usize) {
224+
buf.extend_from_slice(&id.to_le_bytes());
225+
buf.push(mask);
226+
}
227+
buf
228+
}
229+
230+
/// 若地址处是 E9 相对 jmp(编译器 thunk),解析其目标;否则原样返回。
231+
fn resolve_thunk(api: &Api, addr: usize) -> usize {
232+
let mut op = [0u8; 1];
233+
if api.mem_read(addr, &mut op) && op[0] == 0xE9 {
234+
if let Some(rel) = read_i32(api, addr + 1) {
235+
return (addr + 5).wrapping_add(rel as usize);
236+
}
237+
}
238+
addr
239+
}
240+
241+
fn read_i32(api: &Api, addr: usize) -> Option<i32> {
242+
let mut buf = [0u8; 4];
243+
api.mem_read(addr, &mut buf).then(|| i32::from_le_bytes(buf))
244+
}
245+
246+
fn read_usize(api: &Api, addr: usize) -> Option<usize> {
247+
let mut buf = [0u8; 4];
248+
api.mem_read(addr, &mut buf)
249+
.then(|| u32::from_le_bytes(buf) as usize)
250+
}
251+
252+
fn read_u16(api: &Api, addr: usize) -> Option<u16> {
253+
let mut buf = [0u8; 2];
254+
api.mem_read(addr, &mut buf).then(|| u16::from_le_bytes(buf))
255+
}
256+
257+
fn read_u8(api: &Api, addr: usize) -> Option<u8> {
258+
let mut buf = [0u8; 1];
259+
api.mem_read(addr, &mut buf).then(|| buf[0])
260+
}

0 commit comments

Comments
 (0)