dm2-saves: Mac in-bitstream multi-byte fields decoded as little-endian — game_tick, GlobalWords, timer fields scrambled #168
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
Multi-byte fields inside the suppression bitstream are interpreted as little-endian on every platform. On Mac saves they are big-endian: the 68k engine memcpys its BE structs through the suppression writer, with per-field byte-swapped mask tables (the same model already implemented correctly for champions via
MAC_CHAMPION_MASKand for the timer count inSaveFile::decode_full). Decoding them LE yields scrambled values.Byte-exact round-trips cannot catch this: encode is the exact inverse of decode, so the wrong interpretation is invisible until a value is displayed, edited, or converted.
Evidence (Mac fixtures)
Probing
fixtures/mac-us/saves/*with the current decoder:early_gamegame_tickearly_gamelast_creature_tick0x38ffffff0xffffff38endgamegame_tickendgamelast_action_tick0xffffff38is exactly the engine's documented new-game init constant forlast_creature_tick(seesavegamebuffer.rsfield docs), and 351 is a plausible tick for a just-started game;endgame's reversedgame_tick/last_action_tickare 7 ticks apart (a save moment). The LE readings are incoherent. DOS fixtures behave correctly under LE (control: sksave0game_tick= 284,800 withlast_creature_tick=0x00045742nearby).Affected sites
savegamebuffer.rsfrom_decoded_bytes/as_bytes(LE always):game_tick,random_seed,last_creature_tick,last_action_tick,rain_next_special,timer_count_raw(the count itself is special-cased indecode_full, but the struct field remains LE-scrambled and is printed bydm2 info).globals.rsdecode_globals/GlobalWords::encode_to(u16::from_le_bytes/to_le_bytesunconditionally). Latent — all fixture global words are zero — butdm2 save globalon a Mac save reads/writes values ×256 off.timer.rsfrom_decoded_bytes/as_bytes:w_00(LE always);val_a/val_bare exposed as byte pairs so interpretation is caller-side, but the same applies.records.rstype-7val_b(u16::from_le_bytes([data[2], data[3]]), no byte-order branch) around line 855.checkcode.rscloud_indexdecode/encode (~673/1007) andrecords.rsdecode/encode_cloud_postfix(~933/957) useMASK_10BIT_CLOUD_INDEX = [0xff, 0x03]+ LE on both platforms. On Mac the mask layout is[0x03, 0xff], so the decoded value is bit-permuted. Note the crate already implements the correct swapped model for the 9-bit actuator extra (records.rs~775-782ByteOrder::Bigbranch) — the 10-bit fields are inconsistent with it.cloud_indexis a timer-array index in the engine.Fields whose payload fits a single byte (player x/y/dir/map, party count, weather low bytes, etc.) are unaffected, which is why the existing Mac plausibility tests pass.
Fix direction
One byte-order-aware interpretation layer for in-bitstream multi-byte fields, mirrored exactly in encode (the champion code is the template: swapped mask + BE accessors). Doc claims that these fields are "always little-endian" need updating (tracked separately).
Reproduction
Decode any Mac fixture with
SaveFile::parse(bytes, ByteOrder::Big)+decode_savegamebuffer()and comparegame_tickagainst the byte-reversed reading;early_game'slast_creature_tickreversal reproducing the0xffffff38new-game constant is the decisive check.Fixed by #209 (merged into
main).