Submission #9360: TaoTao's NES Wizardry: Proving Grounds of the Mad Overlord "save glitch" in 00:25.88

Nintendo Entertainment System
save glitch
(Submitted: save glitch)
(Submitted: Wizardry - Proving Grounds of the Mad Overlord (Japan).nes JPN)
BizHawk 2.9.1
3012 (cycle count 138984786)
116.36083940609412
1807
PowerOn
Synced on BizHawk 2.9.1 with SubNESHawk core.
			
Submitted by TaoTao on 10/26/2024 5:29 AM
Submission Comments
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:
addrdecription
$6000hero struct 0
$6080hero struct 0 backup
$6100hero struct 1
$6180hero struct 1 backup
......
Here is the details of hero struct:
offsettypedescription
0x00u8scenario ID (0:not exists, 1:exists)
0x01u8name length
0x02u8[8]name
0x0Au8race
0x0Bu8class
0x0Cu8alignment
0x0Du8strength
0x0Eu8iq
0x0Fu8piety
0x10u8vitality
0x11u8agility
0x12u8luck
0x13u8[6]gold (12-digits packed BCD)
0x19u8[6]experience point (12-digits packed BCD)
0x1Fu16beHP
0x21u16bemax HP
0x23u16beexperience level
0x25u8status (0:OK)
0x26u8age (year)
0x27u8age (week)
0x28i8AC
0x29u8[7]mage MP
0x30u8[7]priest MP
0x37u8[7]mage max MP
0x3Eu8[7]priest max MP
0x45u8[7]learned spells mask (bitset)
0x4Cu8[8]inventory item flags
0x54u8[8]inventory item IDs
0x5Cu8inventory item count
0x5Du8out of tavern (bit7)
0x5Eu8maze position x
0x5Fu8maze position y
0x60u8maze position depth
0x61u8poison value
0x62u8badges
0x63u8[13]unused (usually zero cleared)
0x70u8checksum of chunk 0 (offset 0x00..=0x07)
0x71u8checksum of chunk 1 (offset 0x08..=0x0F)
0x72u8checksum of chunk 2 (offset 0x10..=0x17)
0x73u8checksum of chunk 3 (offset 0x18..=0x1F)
0x74u8checksum of chunk 4 (offset 0x20..=0x27)
0x75u8checksum of chunk 5 (offset 0x28..=0x2F)
0x76u8checksum of chunk 6 (offset 0x30..=0x37)
0x77u8checksum of chunk 7 (offset 0x38..=0x3F)
0x78u8checksum of chunk 8 (offset 0x40..=0x47)
0x79u8checksum of chunk 9 (offset 0x48..=0x4F)
0x7Au8checksum of chunk 10 (offset 0x50..=0x57)
0x7Bu8checksum of chunk 11 (offset 0x58..=0x5F)
0x7Cu8checksum of chunk 12 (offset 0x60..=0x67)
0x7Du8checksum of chunk 13 (offset 0x68..=0x6F)
0x7Eu16beCRC (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:
leftright
~~~ chunk 0 ~~~~~~ chunk 15 ~~~
0x00: scenario ID0x7F: CRC lo
0x01: name length0x7E: 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: race0x75: checksums[5]
0x0B: class0x74: checksums[4]
0x0C: alignment0x73: checksums[3]
0x0D: strength0x72: checksums[2]
0x0E: iq0x71: checksums[1]
0x0F: piety0x70: checksums[0]
~~~ chunk 2 ~~~~~~ chunk 13 ~~~
0x10: vitality0x6F: unused[12]
0x11: agility0x6E: unused[11]
0x12: luck0x6D: 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 hi0x60: maze depth
~~~ chunk 4 ~~~~~~ chunk 11 ~~~
0x20: HP lo0x5F: maze y
0x21: MaxHP hi0x5E: maze x
0x22: MaxHP lo0x5D: out of tavern
0x23: XL hi0x5C: ItemCount
0x24: XL lo0x5B: ItemIDs[7]
0x25: status0x5A: ItemIDs[6]
0x26: year0x59: ItemIDs[5]
0x27: week0x58: ItemIDs[4]
~~~ chunk 5 ~~~~~~ chunk 10 ~~~
0x28: AC0x57: 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.

nymx: Claiming for judging.
nymx: It always blows my mind, when these kind of TASes are created. I love how creative the method is that corrupts memory to the point that it is tricked into the ownership of the Amulet. Technically, I understand the process here...but I have no idea how to go about the discovery of it. Great job on this!
Accepting as "Fastest Completion" for a branch of "save glitch".

despoa: Processing...
Last Edited by despoa on 11/9/2024 3:18 PM
Page History Latest diff List referrers