From 2cab70c3b7f89202bf7760dacf9cec0eda85bf0b Mon Sep 17 00:00:00 2001 From: vincentadamnemessis Date: Tue, 18 Aug 2026 13:23:07 +0800 Subject: [PATCH] fix(mh3g): preserve small monster hunt counts --- README.md | 9 ++ README.zh-CN.md | 2 + crates/mh3g-save-convert/src/compatibility.rs | 135 ++++++++++++++++++ crates/mh3g-save-convert/src/transforms.rs | 119 ++++++++++++++- .../adr/0017-mh3g-official-transfer-parity.md | 29 +++- 5 files changed, 289 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bb4119d..106db97 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,15 @@ same state-only bit mapping to the personal record, received guild cards, and CEC/offline-hall partners. This prevents historical conversions from turning hidden rows with non-zero counters into extra monster-guide pages. +Slay and capture totals use a separate schema: 86 valid monster IDs +(`0x00..0x55`), each stored as a `u16` that changes from 3DS little-endian to +Wii U big-endian. Earlier releases omitted the three slay counters for Giggi, +Aptonoth, and Popo, causing Wii U to misread ordinary values as tens of +thousands and clamp the UI to `9999`. Current conversion reasserts all 86 +entries in both count tables. Compatibility repair only updates count fields +that still match historical converter output, preserving later Wii U/Cemu +progress. + ### Before you write: paths, inspection, and dry-run The examples below run from this repository after Rust is installed. Define a diff --git a/README.zh-CN.md b/README.zh-CN.md index 0536ebf..574b3b1 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -152,6 +152,8 @@ Windows 前置条件、行为和可选参数见 怪物是否出现在图鉴中只由 3DS 源存档的发现/大小冠状态字节决定,不能根据讨伐数或捕获数推断。当前转换对个人记录、收到的公会名片以及 CEC/离线集会所伙伴统一使用同一份“仅状态字节”映射,避免旧逻辑把“计数非零但尚未发现”的记录错误扩展成额外图鉴页。 +怪物的讨伐数和捕获数是另一组独立数据:`0x00..0x55` 共 86 个有效怪物 ID,讨伐和捕获各有一张 86 项的 `u16` 表,从 3DS 小端转为 Wii U 大端。旧版遗漏了 Giggi、Aptonoth 和 Popo 的三项讨伐数,Wii U 会把普通数值误读为数万并在界面钳制为 `9999`;当前转换会重申两张完整计数表。修复已转换存档时,仅更新仍等于旧转换结果的计数字段,玩家在 Wii U/Cemu 后续产生的新计数会保留。 + 如果解压后的目录中直接包含 `user2`,使用 CLI 转换核心槽位时必须传入这个**文件**。在原生工作台中,选择该目录并选中 `user2` 时,只会解析其直接子文件 `user2`。如果目录中直接包含 `card1` 到 `quest4`,该目录只能作为 `convert-extras` 输入。如果解压结果外面还有一层包装目录,需要先进入这一层;上述预期文件必须是 CLI 路径或上述受限 GUI 选择的直接子项。 ### 写入前:设置路径、检查和 dry-run diff --git a/crates/mh3g-save-convert/src/compatibility.rs b/crates/mh3g-save-convert/src/compatibility.rs index 6cd3987..e9797e1 100644 --- a/crates/mh3g-save-convert/src/compatibility.rs +++ b/crates/mh3g-save-convert/src/compatibility.rs @@ -36,6 +36,17 @@ const GUILD_CARD_MONSTER_LOG_COUNT: usize = 50; const GUILD_CARD_MONSTER_LOG_STRIDE: usize = 10; const USER_MONSTER_GUIDE_PACKED_STATE_START: usize = 0x5760; const USER_MONSTER_GUIDE_PACKED_STATE_LEN: usize = 0x1C; +const USER_MONSTER_SLAY_COUNT_START: usize = 0x5784; +const USER_MONSTER_CAPTURE_COUNT_START: usize = 0x5884; +const USER_MONSTER_COUNT_STRIDE: usize = 2; +// Only lanes whose historical 0.0.3-0.0.6 output differs from the current +// official-transfer correction belong in revision detection/repair. Adding the +// already-identical lanes would dilute revision scores without changing any +// bytes. Fresh conversion still normalizes all 0x56 valid monster IDs. +const USER_HISTORICAL_DIFFERENT_SLAY_COUNT_IDS: [usize; 4] = [26, 27, 28, 84]; +const USER_HISTORICAL_DIFFERENT_CAPTURE_COUNT_IDS: [usize; 20] = [ + 21, 22, 23, 26, 27, 28, 29, 30, 31, 32, 33, 76, 77, 78, 79, 80, 81, 82, 83, 84, +]; const USER_ITEM_ACQUIRED_BITSET_START: usize = 0x65C4; const USER_ITEM_ACQUIRED_BITSET_WORD_COUNT: usize = 48; const USER_ITEM_ACQUIRED_BITSET_WORD_SIZE: usize = 4; @@ -447,6 +458,24 @@ fn repair_fields(filename: &str) -> Result, ConversionError> { width: 1, }); } + for monster_id in USER_HISTORICAL_DIFFERENT_SLAY_COUNT_IDS { + fields.push(FieldSpec { + name: format!("monster-slay-count-{monster_id}"), + offset: header + + USER_MONSTER_SLAY_COUNT_START + + monster_id * USER_MONSTER_COUNT_STRIDE, + width: USER_MONSTER_COUNT_STRIDE, + }); + } + for monster_id in USER_HISTORICAL_DIFFERENT_CAPTURE_COUNT_IDS { + fields.push(FieldSpec { + name: format!("monster-capture-count-{monster_id}"), + offset: header + + USER_MONSTER_CAPTURE_COUNT_START + + monster_id * USER_MONSTER_COUNT_STRIDE, + width: USER_MONSTER_COUNT_STRIDE, + }); + } for word in 0..USER_ITEM_ACQUIRED_BITSET_WORD_COUNT { fields.push(FieldSpec { name: format!("item-acquired-word-{word}"), @@ -705,6 +734,16 @@ mod tests { source[source_guide_state..source_guide_state + USER_MONSTER_GUIDE_PACKED_STATE_LEN] .fill(0xFF); source[source_guide_state + USER_MONSTER_GUIDE_PACKED_STATE_LEN - 1] = 0x00; + let source_giggi = JP_3DS_HEADER.len() + USER_MONSTER_SLAY_COUNT_START + 0x1A * 2; + let source_aptonoth = JP_3DS_HEADER.len() + USER_MONSTER_SLAY_COUNT_START + 0x1B * 2; + let source_popo = JP_3DS_HEADER.len() + USER_MONSTER_SLAY_COUNT_START + 0x1C * 2; + source[source_giggi..source_giggi + 2].copy_from_slice(&0x0584_u16.to_le_bytes()); + source[source_aptonoth..source_aptonoth + 2].copy_from_slice(&0x00EC_u16.to_le_bytes()); + source[source_popo..source_popo + 2].copy_from_slice(&0x015C_u16.to_le_bytes()); + let source_capture_21 = JP_3DS_HEADER.len() + USER_MONSTER_CAPTURE_COUNT_START + 21 * 2; + let source_capture_22 = JP_3DS_HEADER.len() + USER_MONSTER_CAPTURE_COUNT_START + 22 * 2; + source[source_capture_21..source_capture_21 + 2].copy_from_slice(&0x1234_u16.to_le_bytes()); + source[source_capture_22..source_capture_22 + 2].copy_from_slice(&0x2345_u16.to_le_bytes()); let source_item_word = JP_3DS_HEADER.len() + USER_ITEM_ACQUIRED_BITSET_START; source[source_item_word..source_item_word + 4] .copy_from_slice(&0x1234_5678_u32.to_le_bytes()); @@ -715,12 +754,19 @@ mod tests { .unwrap(); let unrelated = JP_CEMU_HEADER.len() + 0x240; current[unrelated] ^= 0x5A; + let popo = JP_CEMU_HEADER.len() + USER_MONSTER_SLAY_COUNT_START + 0x1C * 2; + current[popo..popo + 2].copy_from_slice(&0x015D_u16.to_be_bytes()); + let capture_22 = JP_CEMU_HEADER.len() + USER_MONSTER_CAPTURE_COUNT_START + 22 * 2; + current[capture_22..capture_22 + 2].copy_from_slice(&0x2346_u16.to_be_bytes()); let merged = merge_component(&source, ¤t, "user2", ConverterRevision::V0_0_6).unwrap(); let guide_state = JP_CEMU_HEADER.len() + USER_MONSTER_GUIDE_PACKED_STATE_START; let item_word = JP_CEMU_HEADER.len() + USER_ITEM_ACQUIRED_BITSET_START; let appearance = JP_CEMU_HEADER.len() + USER_APPEARANCE_RGBA_OFFSET; + let giggi = JP_CEMU_HEADER.len() + USER_MONSTER_SLAY_COUNT_START + 0x1A * 2; + let aptonoth = JP_CEMU_HEADER.len() + USER_MONSTER_SLAY_COUNT_START + 0x1B * 2; + let capture_21 = JP_CEMU_HEADER.len() + USER_MONSTER_CAPTURE_COUNT_START + 21 * 2; assert_eq!( &merged.bytes[guide_state..guide_state + USER_MONSTER_GUIDE_PACKED_STATE_LEN], @@ -734,6 +780,25 @@ mod tests { &merged.bytes[appearance..appearance + 4], &[0xFA, 0xEF, 0xE6, 0xFF] ); + assert_eq!(&merged.bytes[giggi..giggi + 2], &0x0584_u16.to_be_bytes()); + assert_eq!( + &merged.bytes[aptonoth..aptonoth + 2], + &0x00EC_u16.to_be_bytes() + ); + assert_eq!( + &merged.bytes[popo..popo + 2], + &0x015D_u16.to_be_bytes(), + "later Wii U progress must win at the whole-field boundary" + ); + assert_eq!( + &merged.bytes[capture_21..capture_21 + 2], + &0x1234_u16.to_be_bytes() + ); + assert_eq!( + &merged.bytes[capture_22..capture_22 + 2], + &0x2346_u16.to_be_bytes(), + "later Wii U capture progress must win at the whole-field boundary" + ); assert_eq!(merged.bytes[unrelated], current[unrelated]); assert!(merged.fields.iter().any(|field| { field.name == "monster-guide-packed-state-byte-26" @@ -745,6 +810,76 @@ mod tests { assert!(merged.fields.iter().any(|field| { field.name == "player-appearance-rgba" && field.status == MergeFieldStatus::Repaired })); + assert!(merged.fields.iter().any(|field| { + field.name == "monster-slay-count-26" && field.status == MergeFieldStatus::Repaired + })); + assert!(merged.fields.iter().any(|field| { + field.name == "monster-slay-count-27" && field.status == MergeFieldStatus::Repaired + })); + assert!(merged.fields.iter().any(|field| { + field.name == "monster-slay-count-28" + && field.status == MergeFieldStatus::PreservedConflict + })); + assert!(merged.fields.iter().any(|field| { + field.name == "monster-capture-count-21" && field.status == MergeFieldStatus::Repaired + })); + assert!(merged.fields.iter().any(|field| { + field.name == "monster-capture-count-22" + && field.status == MergeFieldStatus::PreservedConflict + })); + } + + #[test] + fn compatibility_count_fields_cover_exact_historical_delta_lanes() { + let mut source = source(); + for monster_id in 0..0x56 { + let slay = JP_3DS_HEADER.len() + + USER_MONSTER_SLAY_COUNT_START + + monster_id * USER_MONSTER_COUNT_STRIDE; + let capture = JP_3DS_HEADER.len() + + USER_MONSTER_CAPTURE_COUNT_START + + monster_id * USER_MONSTER_COUNT_STRIDE; + source[slay..slay + 2].copy_from_slice(&(0xA500_u16 | monster_id as u16).to_le_bytes()); + source[capture..capture + 2] + .copy_from_slice(&(0x5A00_u16 | monster_id as u16).to_le_bytes()); + } + + let historical = + convert_3ds_to_cemu_named_for_revision(&source, "user2", ConverterRevision::V0_0_6) + .unwrap(); + let current = convert_3ds_to_cemu_named(&source, "user2").unwrap(); + let differing_ids = |table_start: usize| { + (0..0x56) + .filter(|monster_id| { + let offset = + JP_CEMU_HEADER.len() + table_start + monster_id * USER_MONSTER_COUNT_STRIDE; + historical[offset..offset + 2] != current[offset..offset + 2] + }) + .collect::>() + }; + + assert_eq!( + differing_ids(USER_MONSTER_SLAY_COUNT_START), + USER_HISTORICAL_DIFFERENT_SLAY_COUNT_IDS + ); + assert_eq!( + differing_ids(USER_MONSTER_CAPTURE_COUNT_START), + USER_HISTORICAL_DIFFERENT_CAPTURE_COUNT_IDS + ); + + let fields = repair_fields("user2").unwrap(); + let count_fields = fields + .iter() + .filter(|field| { + field.name.starts_with("monster-slay-count-") + || field.name.starts_with("monster-capture-count-") + }) + .count(); + assert_eq!( + count_fields, + USER_HISTORICAL_DIFFERENT_SLAY_COUNT_IDS.len() + + USER_HISTORICAL_DIFFERENT_CAPTURE_COUNT_IDS.len() + ); } #[test] diff --git a/crates/mh3g-save-convert/src/transforms.rs b/crates/mh3g-save-convert/src/transforms.rs index 3c8edae..d3981ad 100644 --- a/crates/mh3g-save-convert/src/transforms.rs +++ b/crates/mh3g-save-convert/src/transforms.rs @@ -32,6 +32,16 @@ const SECOND_RGBA_OFFSET: usize = 0x73E4; // though both the acquired-book bit and the personal monster record are set. const MONSTER_GUIDE_PACKED_STATE_START: usize = 0x5760; const MONSTER_GUIDE_PACKED_STATE_LEN: usize = 0x1C; +// Both games expose one u16 slay counter and one u16 capture counter for each +// known monster ID 0x00..=0x55. The tables keep the same numeric values, but +// 3DS stores each lane little-endian and Wii U stores it big-endian. The +// historical MEOW table omitted the slay lanes for IDs 0x1A..=0x1C; Wii U then +// interpreted ordinary counts as values above 9999 and the UI saturated them +// to 9999. +const MONSTER_SLAY_COUNT_START: usize = 0x5784; +const MONSTER_CAPTURE_COUNT_START: usize = 0x5884; +const MONSTER_COUNT_ENTRY_COUNT: usize = 0x56; +const MONSTER_COUNT_STRIDE: usize = 2; // The Wii U executable indexes this region as a 1536-bit item-acquisition // bitset: item_id >> 5 selects one of 48 u32 words and item_id & 31 selects the // bit inside that word. Five independently paired official transfers agree @@ -867,8 +877,8 @@ fn apply_confirmed_numeric_and_record_corrections( } for (index, monster_id) in MONSTER_IDS.into_iter().enumerate() { - let slay_offset = 0x5784 + monster_id * 2; - let capture_offset = 0x5884 + monster_id * 2; + let slay_offset = MONSTER_SLAY_COUNT_START + monster_id * MONSTER_COUNT_STRIDE; + let capture_offset = MONSTER_CAPTURE_COUNT_START + monster_id * MONSTER_COUNT_STRIDE; let size_offset = 0x5984 + monster_id * 4; for offset in [slay_offset, capture_offset, size_offset, size_offset + 2] { copy_reversed(source, target, offset, 2)?; @@ -1028,6 +1038,22 @@ fn apply_current_official_transfer_corrections( ..MONSTER_GUIDE_PACKED_STATE_START + MONSTER_GUIDE_PACKED_STATE_LEN], ); + // Reassert the complete counter schema from the original source rather + // than extending the historical MEOW table. This keeps 0.0.3-0.0.6 replay + // byte-reproducible for compatibility detection while fixing every current + // slay/capture lane, including the three historically omitted small + // monsters (Giggi, Aptonoth, and Popo). + for monster_id in 0..MONSTER_COUNT_ENTRY_COUNT { + for table_start in [MONSTER_SLAY_COUNT_START, MONSTER_CAPTURE_COUNT_START] { + copy_reversed( + source, + target, + table_start + monster_id * MONSTER_COUNT_STRIDE, + MONSTER_COUNT_STRIDE, + )?; + } + } + for index in 0..MONSTER_IDS.len() { apply_current_hunter_notes_display_state(source, target, 0x81B4 + index * 10 + 8)?; } @@ -1186,8 +1212,9 @@ mod tests { use super::{ DEVILJHO_BOOK_ITEM_ID, EQUIPMENT_BOX_START, EVENT_FLAG_START, GUILD_CARD_SLOT_SIZE, ITEM_ACQUIRED_BITSET_START, ITEM_ACQUIRED_BITSET_WORD_COUNT, - ITEM_ACQUIRED_BITSET_WORD_SIZE, MONSTER_GUIDE_PACKED_STATE_LEN, - MONSTER_GUIDE_PACKED_STATE_START, MONSTER_IDS, PLAYER_APPEARANCE_PACKED_STYLE_OFFSET, + ITEM_ACQUIRED_BITSET_WORD_SIZE, MONSTER_CAPTURE_COUNT_START, MONSTER_COUNT_ENTRY_COUNT, + MONSTER_COUNT_STRIDE, MONSTER_GUIDE_PACKED_STATE_LEN, MONSTER_GUIDE_PACKED_STATE_START, + MONSTER_IDS, MONSTER_SLAY_COUNT_START, PLAYER_APPEARANCE_PACKED_STYLE_OFFSET, PLAYER_APPEARANCE_RGBA_OFFSET, PLAYER_APPEARANCE_SCALAR_OFFSETS, QUEST_COMPLETION_START, SECOND_RGBA_OFFSET, apply_arena_records, apply_endian_swaps, apply_japanese_wiiu_corrections, apply_japanese_wiiu_corrections_for_revision, @@ -1457,6 +1484,90 @@ mod tests { ); } + #[test] + fn current_corrections_convert_every_monster_count_lane_to_big_endian() { + let mut source = vec![0_u8; PAYLOAD_SIZE]; + source[MONSTER_GUIDE_PACKED_STATE_START + ..MONSTER_GUIDE_PACKED_STATE_START + MONSTER_GUIDE_PACKED_STATE_LEN] + .copy_from_slice(&[ + 0x10, 0x32, 0x54, 0x76, 0x98, 0xBA, 0xDC, 0xFE, 0x5A, 0xA5, 0x11, 0x22, 0x33, 0x44, + 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xF0, 0x0F, 0xC3, 0x3C, + ]); + for monster_id in 0..MONSTER_COUNT_ENTRY_COUNT { + let slays = 0x1100_u16 + monster_id as u16; + let captures = 0x2200_u16 + monster_id as u16; + let slay_offset = MONSTER_SLAY_COUNT_START + monster_id * MONSTER_COUNT_STRIDE; + let capture_offset = MONSTER_CAPTURE_COUNT_START + monster_id * MONSTER_COUNT_STRIDE; + source[slay_offset..slay_offset + 2].copy_from_slice(&slays.to_le_bytes()); + source[capture_offset..capture_offset + 2].copy_from_slice(&captures.to_le_bytes()); + } + let mut target = source.clone(); + + apply_japanese_wiiu_corrections(&source, &mut target).unwrap(); + + for monster_id in 0..MONSTER_COUNT_ENTRY_COUNT { + let slay_offset = MONSTER_SLAY_COUNT_START + monster_id * MONSTER_COUNT_STRIDE; + let capture_offset = MONSTER_CAPTURE_COUNT_START + monster_id * MONSTER_COUNT_STRIDE; + assert_eq!( + u16::from_be_bytes(target[slay_offset..slay_offset + 2].try_into().unwrap()), + 0x1100_u16 + monster_id as u16, + "slay count for monster ID {monster_id:#04x}" + ); + assert_eq!( + u16::from_be_bytes( + target[capture_offset..capture_offset + 2] + .try_into() + .unwrap() + ), + 0x2200_u16 + monster_id as u16, + "capture count for monster ID {monster_id:#04x}" + ); + } + assert_eq!( + &target[MONSTER_GUIDE_PACKED_STATE_START + ..MONSTER_GUIDE_PACKED_STATE_START + MONSTER_GUIDE_PACKED_STATE_LEN], + &source[MONSTER_GUIDE_PACKED_STATE_START + ..MONSTER_GUIDE_PACKED_STATE_START + MONSTER_GUIDE_PACKED_STATE_LEN] + ); + } + + #[test] + fn current_corrections_fix_small_monster_counts_that_historically_saturated_to_9999() { + const CASES: [(usize, u16); 3] = [(0x1A, 0x0584), (0x1B, 0x00EC), (0x1C, 0x015C)]; + let mut source = vec![0_u8; PAYLOAD_SIZE]; + for (monster_id, value) in CASES { + let offset = MONSTER_SLAY_COUNT_START + monster_id * MONSTER_COUNT_STRIDE; + source[offset..offset + 2].copy_from_slice(&value.to_le_bytes()); + } + let mut historical = source.clone(); + apply_japanese_wiiu_corrections_for_revision( + &source, + &mut historical, + ConverterRevision::V0_0_6, + ) + .unwrap(); + let mut current = source.clone(); + apply_japanese_wiiu_corrections(&source, &mut current).unwrap(); + + for (monster_id, value) in CASES { + let offset = MONSTER_SLAY_COUNT_START + monster_id * MONSTER_COUNT_STRIDE; + assert_eq!( + &historical[offset..offset + 2], + &value.to_le_bytes(), + "historical replay for monster ID {monster_id:#04x}" + ); + assert!( + u16::from_be_bytes(historical[offset..offset + 2].try_into().unwrap()) > 9999, + "historical Wii U interpretation should reproduce the saturated UI value" + ); + assert_eq!( + ¤t[offset..offset + 2], + &value.to_be_bytes(), + "current conversion for monster ID {monster_id:#04x}" + ); + } + } + #[test] fn current_corrections_match_official_appearance_field_boundaries() { let mut source = vec![0_u8; PAYLOAD_SIZE]; diff --git a/docs/adr/0017-mh3g-official-transfer-parity.md b/docs/adr/0017-mh3g-official-transfer-parity.md index 1415c0c..d508e94 100644 --- a/docs/adr/0017-mh3g-official-transfer-parity.md +++ b/docs/adr/0017-mh3g-official-transfer-parity.md @@ -57,6 +57,26 @@ still omits Deviljho. Current conversion restores the complete byte-packed array; compatibility repair handles it one byte at a time so later Wii U progress in another byte is not reverted. +The displayed hunt totals are a third, separate schema. There are 86 valid +monster IDs (`0x00..0x55`), with one `u16` slay counter per ID at payload +`0x5784 + id * 2` and one `u16` capture counter per ID at +`0x5884 + id * 2`. All five official-transfer pairs preserve each numeric +value while changing its encoding from 3DS little-endian to Wii U big-endian. +Static inspection of both executables independently confirms the `0x56` loop +limit and the `0x270F` (9999) saturation used by the counter update paths. +Historical conversion missed the slay lanes for IDs `0x1A`, `0x1B`, and +`0x1C` (Giggi, Aptonoth, and Popo). Wii U consequently interpreted ordinary +values such as `0x0584` as `0x8405` and clamped the displayed total to 9999. +This count schema does not overlap the packed monster-list state. + +Deterministic replay shows that the historical and current algorithms differ +at slay IDs `26, 27, 28, 84` and capture IDs +`21..23, 26..33, 76..84`. Only the first three slay lanes are non-zero in all +five paired samples and directly reproduce the reported symptom. Compatibility +repair nevertheless covers the exact 24 differing lanes so future non-zero +values are corrected, while excluding already-identical lanes from revision +detection scores. + ## Decision Current conversion layers the official-transfer corrections after the closed @@ -73,7 +93,12 @@ Current conversion layers the official-transfer corrections after the closed 6. convert all 48 item-acquisition words independently and include them in compatibility repair under their actual item-bitset semantics; 7. preserve the 28-byte packed monster-list state verbatim and repair its - historical output at byte granularity. + historical output at byte granularity; +8. reassert all 86 valid slay and capture counters from the original source as + independent LE-to-BE `u16` fields. Historical replay remains unchanged; + compatibility repair includes only lanes whose historical output differs + from the current rule and preserves any later Wii U value at whole-field + granularity. ## Consequences @@ -82,6 +107,8 @@ Current conversion layers the official-transfer corrections after the closed - Hidden source monsters no longer create extra guide pages after conversion. - The Deviljho book and other acquired-item unlocks no longer disappear merely because their 32-bit word was interpreted in the wrong byte order. +- Giggi, Aptonoth, and Popo hunt totals no longer become saturated 9999 values + because their two bytes were left in 3DS order. - Received cards and offline-hall partners cannot drift from the personal Hunter's Notes mapping. - Existing 0.0.5/0.0.6 saves remain detectable and can be repaired without