Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 24 additions & 13 deletions crates/mh3g-save-convert/src/converter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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()..];
Expand All @@ -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]
Expand Down
21 changes: 9 additions & 12 deletions crates/mh3g-save-convert/src/transforms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)?;
}
}
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tools/compatibility-wrapper/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion tools/compatibility-wrapper/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 保持原界面与调用参数兼容。

Expand Down
4 changes: 2 additions & 2 deletions tools/compatibility-wrapper/gen_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(',
Expand Down Expand Up @@ -176,7 +176,7 @@ def arr(name):
const WORD *a=companion?c1:c0; int n=companion?7:6,i; for(i=0;i<n;i++)if(a[i]==rel)return TRUE; return FALSE;
}
static BOOL patch_user_file(LPCWSTR path){ BYTE *b; SIZE_T sz; int c,rel; if(!read_all(path,&b,&sz))return FALSE; if(sz<4+0x8A00||b[0]!=0x2B||b[1]||b[2]||b[3]){HeapFree(GetProcessHeap(),0,b);return FALSE;}
for(c=0;c<2;c++)for(rel=0x0C;rel<0xE6;rel+=2)if(!old_shaka_u16(c,rel))swap2(b+4+0x6F44+c*0x148+rel);
for(c=0;c<2;c++)for(rel=0x0C;rel<0xDE;rel+=2)if(!old_shaka_u16(c,rel))swap2(b+4+0x6F44+c*0x148+rel);
if(!write_all(path,b,sz)){HeapFree(GetProcessHeap(),0,b);return FALSE;} HeapFree(GetProcessHeap(),0,b); return TRUE; }

static const WORD CARD_STATIC_KEYS[382]={
Expand Down
18 changes: 9 additions & 9 deletions tools/compatibility-wrapper/validate_patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(',
Expand All @@ -25,15 +25,15 @@ def arr(name):
expected_old=[]
for c in range(2):
st=0x6F44+c*0x148
vals=sorted(o-st for o in user_s2 if st+0x0C<=o<st+0xE6)
vals=sorted(o-st for o in user_s2 if st+0x0C<=o<st+0xDE)
expected_old.append(vals)
assert expected_old==[[0x0C,0x0E,0x10,0x14,0x18,0x1C],[0x0C,0x0E,0x12,0x18,0x1C,0x24,0x2C]], expected_old
missing_shaka=[]
for c in range(2):
st=0x6F44+c*0x148
for rel in range(0x0C,0xE6,2):
for rel in range(0x0C,0xDE,2):
if st+rel not in user_s2: missing_shaka.append(st+rel)
assert len(missing_shaka)==205
assert len(missing_shaka)==197
# no old 4-byte transform overlaps any missing 2-byte field
for o in missing_shaka:
assert not any(x < o+2 and o < x+4 for x in user_s4)
Expand Down Expand Up @@ -65,14 +65,14 @@ def arena4(b,o):
des=base[:]; staged=base[:]
for c in range(2):
st=0x6F44+c*0x148
for rel in range(0x0C,0xE6,2): swap2(des,st+rel)
for rel in range(0x0C,0xE6,2):
for rel in range(0x0C,0xDE,2): swap2(des,st+rel)
for rel in range(0x0C,0xDE,2):
if st+rel not in user_s2: swap2(staged,st+rel)
for o in user_s2:
if any(0x6F44+c*0x148+0x0C<=o<0x6F44+c*0x148+0xE6 for c in range(2)): swap2(staged,o)
if any(0x6F44+c*0x148+0x0C<=o<0x6F44+c*0x148+0xDE for c in range(2)): swap2(staged,o)
for c in range(2):
st=0x6F44+c*0x148
assert staged[st+0x0C:st+0xE6]==des[st+0x0C:st+0xE6]
assert staged[st+0x0C:st+0xDE]==des[st+0x0C:st+0xDE]
# card equivalence only arena table
base=bytearray(rnd.randbytes(98*0xE00))
des=base[:]; staged=base[:]
Expand All @@ -92,7 +92,7 @@ def arena4(b,o):
for row in cec_old: arena4(staged,s*0xE00+0x9B4+row*4)
assert staged==des
print('PASS')
print('shakalaka_missing=205')
print('shakalaka_missing=197')
print('guild_card_static=382 guild_card_missing=10398')
print('cec_static_per_slot=73 cec_missing_per_slot=37')
if len(sys.argv) > 1:
Expand Down
2 changes: 1 addition & 1 deletion tools/compatibility-wrapper/wrapper.c
Original file line number Diff line number Diff line change
Expand Up @@ -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<n;i++)if(a[i]==rel)return TRUE; return FALSE;
}
static BOOL patch_user_file(LPCWSTR path){ BYTE *b; SIZE_T sz; int c,rel; if(!read_all(path,&b,&sz))return FALSE; if(sz<4+0x8A00||b[0]!=0x2B||b[1]||b[2]||b[3]){HeapFree(GetProcessHeap(),0,b);return FALSE;}
for(c=0;c<2;c++)for(rel=0x0C;rel<0xE6;rel+=2)if(!old_shaka_u16(c,rel))swap2(b+4+0x6F44+c*0x148+rel);
for(c=0;c<2;c++)for(rel=0x0C;rel<0xDE;rel+=2)if(!old_shaka_u16(c,rel))swap2(b+4+0x6F44+c*0x148+rel);
if(!write_all(path,b,sz)){HeapFree(GetProcessHeap(),0,b);return FALSE;} HeapFree(GetProcessHeap(),0,b); return TRUE; }

static const WORD CARD_STATIC_KEYS[382]={
Expand Down
Loading