This run beats Famicom version of Wizardry scenario #1 in ~26 seconds, crafting a fake hero struct by abusing the savedata scrambling process.
For now, I haven't found a method to execute a save glitch on NES version, so I used Famicom version.
Game objectives
- Emulator used: BizHawk 2.9.1 (SubNesHawk core)
- Aims for fastest time
- Corrupts save data
- Uses a game restart sequence
Run overview
- I create a new hero for the save glitch, manipulating his properties (name, stats, etc.) appropriately.
- I order to delete the new hero, and reset the game immediately after his CRC is corrupted (his backup is preserved).
- I craft a fake hero with subframe resets.
- I change the item name language setting to English.
- I add the fake hero to my party. (If the item name is Japanese, the game loops infinitely here.)
- I go to the maze, and immediately return to the castle. (Trivia: this fake hero instantly dies if he walks.)
- The game thinks that the fake hero has the amulet of Werdna, and executes the ending scene.
The fake hero has 255 (0xFF) items, and he has an alignment value 0x5E (item ID of the amulet). That's why the game thinks he has the amulet.
Run details
Memory map
To understand the save glitch, some knowledge of memory map is needed.
The size of a hero struct is 0x80, and its backup also has the same size. The hero structs and their backups sit on $6000-$73FF:
addr | decription |
---|---|
$6000 | hero struct 0 |
$6080 | hero struct 0 backup |
$6100 | hero struct 1 |
$6180 | hero struct 1 backup |
... | ... |
Here is the details of hero struct:
offset | type | description |
---|---|---|
0x00 | u8 | scenario ID (0:not exists, 1:exists) |
0x01 | u8 | name length |
0x02 | u8[8] | name |
0x0A | u8 | race |
0x0B | u8 | class |
0x0C | u8 | alignment |
0x0D | u8 | strength |
0x0E | u8 | iq |
0x0F | u8 | piety |
0x10 | u8 | vitality |
0x11 | u8 | agility |
0x12 | u8 | luck |
0x13 | u8[6] | gold (12-digits packed BCD) |
0x19 | u8[6] | experience point (12-digits packed BCD) |
0x1F | u16be | HP |
0x21 | u16be | max HP |
0x23 | u16be | experience level |
0x25 | u8 | status (0:OK) |
0x26 | u8 | age (year) |
0x27 | u8 | age (week) |
0x28 | i8 | AC |
0x29 | u8[7] | mage MP |
0x30 | u8[7] | priest MP |
0x37 | u8[7] | mage max MP |
0x3E | u8[7] | priest max MP |
0x45 | u8[7] | learned spells mask (bitset) |
0x4C | u8[8] | inventory item flags |
0x54 | u8[8] | inventory item IDs |
0x5C | u8 | inventory item count |
0x5D | u8 | out of tavern (bit7) |
0x5E | u8 | maze position x |
0x5F | u8 | maze position y |
0x60 | u8 | maze position depth |
0x61 | u8 | poison value |
0x62 | u8 | badges |
0x63 | u8[13] | unused (usually zero cleared) |
0x70 | u8 | checksum of chunk 0 (offset 0x00..=0x07) |
0x71 | u8 | checksum of chunk 1 (offset 0x08..=0x0F) |
0x72 | u8 | checksum of chunk 2 (offset 0x10..=0x17) |
0x73 | u8 | checksum of chunk 3 (offset 0x18..=0x1F) |
0x74 | u8 | checksum of chunk 4 (offset 0x20..=0x27) |
0x75 | u8 | checksum of chunk 5 (offset 0x28..=0x2F) |
0x76 | u8 | checksum of chunk 6 (offset 0x30..=0x37) |
0x77 | u8 | checksum of chunk 7 (offset 0x38..=0x3F) |
0x78 | u8 | checksum of chunk 8 (offset 0x40..=0x47) |
0x79 | u8 | checksum of chunk 9 (offset 0x48..=0x4F) |
0x7A | u8 | checksum of chunk 10 (offset 0x50..=0x57) |
0x7B | u8 | checksum of chunk 11 (offset 0x58..=0x5F) |
0x7C | u8 | checksum of chunk 12 (offset 0x60..=0x67) |
0x7D | u8 | checksum of chunk 13 (offset 0x68..=0x6F) |
0x7E | u16be | CRC (CRC-16-CCITT) |
Hero struct backup
This game often saves hero structs to their backup. Backup is scrambled like below:
for i in 0..=0x7F { backup[i] = scramble(hero[0x7F - i]); }
scramble
function swaps nibbles of a byte, and inverts it:
fn scramble(b: u8) -> u8 { return !b.rotate_left(4); }
Note that the
scramble
function and the entire scrambling process is symmetric (in other words, you will get the original data if you execute them twice). So, scrambling and de-scrambling are equivalent.
On power/reset, the game first verifies checksums/CRC of each hero struct. If a verification failed, the game tries to restore the hero struct from its backup. If the checksums/CRC of the backup is also wrong, the game tries to repair the hero struct with some heuristics (this repair functionality is not considered in this run).
When the game tries to restore a hero struct from its backup, it first (de-)scrambles the backup. Its result is written to the same backup memory, from the end to the beginning.
Save glitch with subframe reset
As said above, the game (de-)scrambles the backup from the end to the beginning. So, if you reset during this process, you will have a backup consisting of plaintext and ciphertext.
Usually, any backup is a complete ciphertext. And, if you reset immediately after it is (de-)scrambled, it becomes a complete plaintext. From the symmetry, they are essentially equivalent. From here, I will describe things from plaintext perspective to avoid confusion. (And, consider that plaintext is overwritten from the beginning to the end.)
For example, let's assume that a backup is a complete plaintext first, and you reset the game after it is partly scrambled. The backup will be like below ('P' means plaintext, and 'C' means ciphertext):
0x00 0x7F +==============================+ | C | P | P | +==============================+ =======>* RESET!
And, note that the left/right part will be unchanged anymore even if you reset the game. So, this glitch can be thought as determining (P,C) / (C,P) chunk pairs from both edges. (The center area is (P,P) or (C,C), and has 0 or more bytes.)
In a backup, offset
i
and 0x7F-i
becomes a plain/cipher byte pair. Here is the table:
left | right |
---|---|
~~~ chunk 0 ~~~ | ~~~ chunk 15 ~~~ |
0x00: scenario ID | 0x7F: CRC lo |
0x01: name length | 0x7E: CRC hi |
0x02: name[0] | 0x7D: checksums[13] |
0x03: name[1] | 0x7C: checksums[12] |
0x04: name[2] | 0x7B: checksums[11] |
0x05: name[3] | 0x7A: checksums[10] |
0x06: name[4] | 0x79: checksums[9] |
0x07: name[5] | 0x78: checksums[8] |
~~~ chunk 1 ~~~ | ~~~ chunk 14 ~~~ |
0x08: name[6] | 0x77: checksums[7] |
0x09: name[7] | 0x76: checksums[6] |
0x0A: race | 0x75: checksums[5] |
0x0B: class | 0x74: checksums[4] |
0x0C: alignment | 0x73: checksums[3] |
0x0D: strength | 0x72: checksums[2] |
0x0E: iq | 0x71: checksums[1] |
0x0F: piety | 0x70: checksums[0] |
~~~ chunk 2 ~~~ | ~~~ chunk 13 ~~~ |
0x10: vitality | 0x6F: unused[12] |
0x11: agility | 0x6E: unused[11] |
0x12: luck | 0x6D: unused[10] |
0x13: gold[0] | 0x6C: unused[9] |
0x14: gold[1] | 0x6B: unused[8] |
0x15: gold[2] | 0x6A: unused[7] |
0x16: gold[3] | 0x69: unused[6] |
0x17: gold[4] | 0x68: unused[5] |
~~~ chunk 3 ~~~ | ~~~ chunk 12 ~~~ |
0x18: gold[5] | 0x67: unused[4] |
0x19: xp[0] | 0x66: unused[3] |
0x1A: xp[1] | 0x65: unused[2] |
0x1B: xp[2] | 0x64: unused[1] |
0x1C: xp[3] | 0x63: unused[0] |
0x1D: xp[4] | 0x62: badges |
0x1E: xp[5] | 0x61: poison |
0x1F: HP hi | 0x60: maze depth |
~~~ chunk 4 ~~~ | ~~~ chunk 11 ~~~ |
0x20: HP lo | 0x5F: maze y |
0x21: MaxHP hi | 0x5E: maze x |
0x22: MaxHP lo | 0x5D: out of tavern |
0x23: XL hi | 0x5C: ItemCount |
0x24: XL lo | 0x5B: ItemIDs[7] |
0x25: status | 0x5A: ItemIDs[6] |
0x26: year | 0x59: ItemIDs[5] |
0x27: week | 0x58: ItemIDs[4] |
~~~ chunk 5 ~~~ | ~~~ chunk 10 ~~~ |
0x28: AC | 0x57: ItemIDs[3] |
0x29: MagMP[0] | 0x56: ItemIDs[2] |
0x2A: MagMP[1] | 0x55: ItemIDs[1] |
0x2B: MagMP[2] | 0x54: ItemIDs[0] |
0x2C: MagMP[3] | 0x53: ItemFlags[7] |
0x2D: MagMP[4] | 0x52: ItemFlags[6] |
0x2E: MagMP[5] | 0x51: ItemFlags[5] |
0x2F: MagMP[6] | 0x50: ItemFlags[4] |
~~~ chunk 6 ~~~ | ~~~ chunk 9 ~~~ |
0x30: PriMP[0] | 0x4F: ItemFlags[3] |
0x31: PriMP[1] | 0x4E: ItemFlags[2] |
0x32: PriMP[2] | 0x4D: ItemFlags[1] |
0x33: PriMP[3] | 0x4C: ItemFlags[0] |
0x34: PriMP[4] | 0x4B: Spells[6] |
0x35: PriMP[5] | 0x4A: Spells[5] |
0x36: PriMP[6] | 0x49: Spells[4] |
0x37: MagMaxMP[0] | 0x48: spells[3] |
~~~ chunk 7 ~~~ | ~~~ chunk 8 ~~~ |
0x38: MagMaxMP[1] | 0x47: Spells[2] |
0x39: MagMaxMP[2] | 0x46: Spells[1] |
0x3A: MagMaxMP[3] | 0x45: Spells[0] |
0x3B: MagMaxMP[4] | 0x44: PriMaxMp[6] |
0x3C: MagMaxMP[5] | 0x43: PriMaxMp[5] |
0x3D: MagMaxMP[6] | 0x42: PriMaxMp[4] |
0x3E: PriMaxMp[0] | 0x41: PriMaxMp[3] |
0x3F: PriMaxMp[1] | 0x40: PriMaxMp[2] |
But, offset pair (0x00, 0x7F) is a special case. The game overwrites the scenario ID in a backup before (de-)scrambling it. So, a scenario ID is always plaintext, and a CRC lo-byte can be chosen as you like.
Considering all of above, you can craft a "valid" fake hero who has 0xFF items. I used Z3 solver to find a solution (my solver code).
This sub-frame reset operation can be done also on NES version. But, I couldn't find any solution for NES version, because you can only fewer character kinds for a hero name than Famicom version. (Especially, it's problematic that you cannot use 0x7F as a name byte, because a checksum value 0x08 (zero-filled chunk) is scrambled to 0x7F.)
Possible improvements
In Famicom version, the games displays a logo every time you reset. If you can execute a save glitch in NES version, it will be quite fast.
I think my method is not applicable to NES version (not strictly proven though). But, this game overwrites the savedata in various ways (e.g. SRAM clearing, savedata repairing functionality, etc.). So, you might be able to execute a save glitch on NES version with some detours.