From 491382fe261ee814d2427609a49be96203858441 Mon Sep 17 00:00:00 2001 From: vincentadamnemessis Date: Wed, 29 Jul 2026 21:29:58 +0800 Subject: [PATCH] fix(mh3g): preserve Shakalaka mask state bytes --- crates/mh3g-save-convert/src/converter.rs | 37 ++++++++++++------- crates/mh3g-save-convert/src/transforms.rs | 21 +++++------ tools/compatibility-wrapper/README.md | 2 +- tools/compatibility-wrapper/README.zh-CN.md | 2 +- tools/compatibility-wrapper/gen_wrapper.py | 4 +- tools/compatibility-wrapper/validate_patch.py | 18 ++++----- tools/compatibility-wrapper/wrapper.c | 2 +- 7 files changed, 47 insertions(+), 39 deletions(-) diff --git a/crates/mh3g-save-convert/src/converter.rs b/crates/mh3g-save-convert/src/converter.rs index c7c44cd..d67ff5f 100644 --- a/crates/mh3g-save-convert/src/converter.rs +++ b/crates/mh3g-save-convert/src/converter.rs @@ -327,20 +327,21 @@ mod tests { } #[test] - fn remaps_unobserved_shakalaka_scalar_fields_in_both_companion_records() { + fn remaps_shakalaka_scalars_without_swapping_mask_state_bytes() { let mut source = vec![0_u8; THREE_DS_SIZE]; source[..JP_3DS_HEADER.len()].copy_from_slice(&JP_3DS_HEADER); - // Each Shakalaka record has three u32 header fields followed by a - // 0xDA-byte u16 scalar table. The compatibility operation list covered - // only a subset of this record, so a valid mask-mastery scalar deeper - // in either table could remain little-endian. + // Each Shakalaka record starts with endian-sensitive numeric fields, + // but its mask/mastery state at relative 0xDE is byte-packed and has + // the same byte order on 3DS and Wii U. Treating that tail as u16 + // changes Cha-Cha's observed `01 09` state to `09 01`, which makes the + // Wii U dialogue path consume an invalid mastery value. let cha_cha_scalar = 0x6F74; let kayamba_scalar = 0x70C8; - let cha_cha_last_scalar = 0x7028; - let kayamba_last_scalar = 0x7170; - let cha_cha_opaque_byte = 0x702A; - let kayamba_opaque_byte = 0x7172; + let cha_cha_last_scalar = 0x7020; + let kayamba_last_scalar = 0x7168; + let cha_cha_mask_state = 0x7022; + let kayamba_mask_state = 0x716A; source[JP_3DS_HEADER.len() + cha_cha_scalar..JP_3DS_HEADER.len() + cha_cha_scalar + 2] .copy_from_slice(&[0x12, 0x34]); source[JP_3DS_HEADER.len() + kayamba_scalar..JP_3DS_HEADER.len() + kayamba_scalar + 2] @@ -351,8 +352,12 @@ mod tests { source[JP_3DS_HEADER.len() + kayamba_last_scalar ..JP_3DS_HEADER.len() + kayamba_last_scalar + 2] .copy_from_slice(&[0xEF, 0x01]); - source[JP_3DS_HEADER.len() + cha_cha_opaque_byte] = 0xA5; - source[JP_3DS_HEADER.len() + kayamba_opaque_byte] = 0x5A; + source[JP_3DS_HEADER.len() + cha_cha_mask_state + ..JP_3DS_HEADER.len() + cha_cha_mask_state + 2] + .copy_from_slice(&[0x01, 0x09]); + source[JP_3DS_HEADER.len() + kayamba_mask_state + ..JP_3DS_HEADER.len() + kayamba_mask_state + 2] + .copy_from_slice(&[0x02, 0x05]); let output = convert_3ds_to_cemu(&source).unwrap(); let payload = &output[JP_CEMU_HEADER.len()..]; @@ -366,8 +371,14 @@ mod tests { &payload[kayamba_last_scalar..kayamba_last_scalar + 2], &[0x01, 0xEF] ); - assert_eq!(payload[cha_cha_opaque_byte], 0xA5); - assert_eq!(payload[kayamba_opaque_byte], 0x5A); + assert_eq!( + &payload[cha_cha_mask_state..cha_cha_mask_state + 2], + &[0x01, 0x09] + ); + assert_eq!( + &payload[kayamba_mask_state..kayamba_mask_state + 2], + &[0x02, 0x05] + ); } #[test] diff --git a/crates/mh3g-save-convert/src/transforms.rs b/crates/mh3g-save-convert/src/transforms.rs index 97f10bb..3fa0650 100644 --- a/crates/mh3g-save-convert/src/transforms.rs +++ b/crates/mh3g-save-convert/src/transforms.rs @@ -43,15 +43,15 @@ const FARM_FELYNE_SLOTS_START: usize = 0x6144; const HUNTING_FLEET_SHIP_COUNT_START: usize = 0x5BC6; const HUNTING_FLEET_SHIP_COUNT_END: usize = HUNTING_FLEET_SHIP_COUNT_START + 2; const HUNTING_FLEET_DISPATCH_RECORD_START: usize = 0x5D18; -// Cha-Cha and Kayamba have two adjacent, fixed-width companion records. Each -// has three u32 header fields followed by a contiguous 109-entry u16 scalar -// table. The trailing bytes are packed/opaque state and must remain -// byte-preserved. +// Cha-Cha and Kayamba have two adjacent, fixed-width companion records. Each +// starts with three u32 header fields and endian-sensitive u16 scalars. The +// tail beginning at relative 0xDE is byte-packed mask/mastery state: it has +// the same byte order on 3DS and Wii U and must remain byte-preserved. const SHAKALAKA_RECORD_START: usize = 0x6F44; const SHAKALAKA_RECORD_COUNT: usize = 2; const SHAKALAKA_RECORD_STRIDE: usize = 0x148; const SHAKALAKA_U32_HEADER_SIZE: usize = 0x0C; -const SHAKALAKA_U16_TABLE_END: usize = 0xE6; +const SHAKALAKA_MASK_STATE_START: usize = 0xDE; const OFFLINE_HUNTER_EQUIPMENT_CACHE_START: usize = 0x75B0; const OFFLINE_HUNTER_HEADER_START: usize = 0x75E0; const OFFLINE_HUNTER_COUNT: usize = 6; @@ -756,7 +756,7 @@ fn apply_shakalaka_companion_corrections( for relative in (0..SHAKALAKA_U32_HEADER_SIZE).step_by(4) { copy_reversed(source, target, record_start + relative, 4)?; } - for relative in (SHAKALAKA_U32_HEADER_SIZE..SHAKALAKA_U16_TABLE_END).step_by(2) { + for relative in (SHAKALAKA_U32_HEADER_SIZE..SHAKALAKA_MASK_STATE_START).step_by(2) { copy_reversed(source, target, record_start + relative, 2)?; } } @@ -846,12 +846,9 @@ pub fn apply_japanese_wiiu_corrections( target[offset] = source[offset]; } - // The compatibility operation list covers only a subset of the - // Cha-Cha/Kayamba scalar fields. The fixed companion schema is broader: - // all 109 u16 fields in each record need endian conversion, including a - // player's independently progressed mask-mastery values. Reassert the - // full bounded schema so valid values cannot remain in 3DS little-endian - // form. + // The compatibility operation list covers only a subset of the numeric + // Cha-Cha/Kayamba fields. Reassert the bounded numeric prefix, stopping + // before the platform-invariant byte-packed mask/mastery state. apply_shakalaka_companion_corrections(source, target)?; // These fields are read as big-endian values by the Wii U title. MEOW v5 diff --git a/tools/compatibility-wrapper/README.md b/tools/compatibility-wrapper/README.md index dcb1d43..ce457e7 100644 --- a/tools/compatibility-wrapper/README.md +++ b/tools/compatibility-wrapper/README.md @@ -2,7 +2,7 @@ > **Only for the legacy `mh3g-save-convert-v0.0.3` converter core.** The core after PR #17 already performs these conversions natively. Do not layer this wrapper on a newer core, or the affected fields will be converted twice. -This directory preserves the tester-provided Windows hotfix build path. The v0.0.3 WinUI package calls `tools/mh3g-save-convert.exe`; this wrapper patches a temporary copy of the input for the missing companion-mask proficiency, guild-card arena, and CEC arena conversions, then delegates all remaining work to `mh3g-save-convert-core.exe`. +This directory preserves the tester-provided Windows hotfix build path. The v0.0.3 WinUI package calls `tools/mh3g-save-convert.exe`; this wrapper patches a temporary copy of the input for the missing companion numeric-prefix, guild-card arena, and CEC arena conversions while preserving byte-packed mask mastery state, then delegates all remaining work to `mh3g-save-convert-core.exe`. ## Build on Windows diff --git a/tools/compatibility-wrapper/README.zh-CN.md b/tools/compatibility-wrapper/README.zh-CN.md index 2ff5911..e3fdd26 100644 --- a/tools/compatibility-wrapper/README.zh-CN.md +++ b/tools/compatibility-wrapper/README.zh-CN.md @@ -8,7 +8,7 @@ `mh3g-save-convert-v0.0.3` 发布包中的 WinUI 程序会调用 `tools/mh3g-save-convert.exe`。由于新版代码仓库当时没有可直接下载的 Windows 构建产物,本包装层会: -1. 在临时副本中补齐随从面具熟练度、名片竞技场记录和 CEC 竞技场记录的遗漏转换; +1. 在临时副本中补齐随从数值前缀、名片竞技场记录和 CEC 竞技场记录的遗漏转换,同时保留无需换序的面具熟练度字节状态; 2. 调用原版转换器核心 `mh3g-save-convert-core.exe` 完成其余转换; 3. 保持原界面与调用参数兼容。 diff --git a/tools/compatibility-wrapper/gen_wrapper.py b/tools/compatibility-wrapper/gen_wrapper.py index d5f9d57..86c32df 100644 --- a/tools/compatibility-wrapper/gen_wrapper.py +++ b/tools/compatibility-wrapper/gen_wrapper.py @@ -4,7 +4,7 @@ native_transforms = (ROOT / 'crates/mh3g-save-convert/src/transforms.rs').read_text(encoding='utf-8') native_markers = ( - 'const SHAKALAKA_U16_TABLE_END: usize = 0xE6;', + 'const SHAKALAKA_MASK_STATE_START: usize = 0xDE;', 'const GUILD_CARD_ARENA_RECORD_COUNT: usize = 110;', 'fn apply_shakalaka_companion_corrections(', 'fn apply_guild_card_arena_corrections(', @@ -176,7 +176,7 @@ def arr(name): const WORD *a=companion?c1:c0; int n=companion?7:6,i; for(i=0;i 1: diff --git a/tools/compatibility-wrapper/wrapper.c b/tools/compatibility-wrapper/wrapper.c index d743b3e..25bd351 100644 --- a/tools/compatibility-wrapper/wrapper.c +++ b/tools/compatibility-wrapper/wrapper.c @@ -135,7 +135,7 @@ static BOOL old_shaka_u16(int companion,int rel){ const WORD *a=companion?c1:c0; int n=companion?7:6,i; for(i=0;i