|
| 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