Submission Text Full Submission Page
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.


TASVideoAgent
They/Them
Moderator
Joined: 8/3/2004
Posts: 15527
Location: 127.0.0.1
Patashu
He/Him
Joined: 10/2/2005
Posts: 4042
A hero who can't take a single step but can save the world - classic. Nice technical expertise. Things I wonder: * Why the hero dies instantly, what's wrong with them? * If this can be console verified * Why there's encryption in a NES game. Is it just a copy-paste of PC code (where it DOES make sense to discourage save file peeking) or if they were trying to thwart gameshark users or something.
My Chiptune music, made in Famitracker: http://soundcloud.com/patashu My twitch. I stream mostly shmups & rhythm games http://twitch.tv/patashu My youtube, again shmups and rhythm games and misc stuff: http://youtube.com/user/patashu
Experienced player (918)
Joined: 9/18/2008
Posts: 153
Location: Japan
Patashu wrote:
* Why the hero dies instantly, what's wrong with them?
He has 255 (legal or illegal) items, and receives various effects from them. In this case, he has a negative healing value 0x98 (-104) on address $7B36, so a single step will instantly kill him.
* If this can be console verified
Possibly. I believe this run doesn't depend on uninitialized values on Save RAM, but the RNG of this game is very sensitive to CPU cycles, so accurate emulation will be hard.
* Why there's encryption in a NES game. Is it just a copy-paste of PC code (where it DOES make sense to discourage save file peeking) or if they were trying to thwart gameshark users or something.
I don't know (I haven't read the PC version code). But, this game seems to make an significant effort to protect user's savedata. The scramble function swaps the nibbles of the original byte, and the developers might thought that it increases the possibility of successful savedata repairing? EDIT: This game always enables Save RAM (probably it doesn't have functionality to disable it). So, it's possible that hero structs and their backups occasionally corrupted by some programming errors. The nibble swapping might be a measure to increase survivability of backups? (just a guess)
Bigbass
He/Him
Moderator
Joined: 2/2/2021
Posts: 189
Location: Midwest
TaoTao wrote:
Patashu wrote:
* If this can be console verified
Possibly. I believe this run doesn't depend on uninitialized values on Save RAM, but the RNG of this game is very sensitive to CPU cycles, so accurate emulation will be hard.
I've just done some attempts, unsuccessfully. Performing sub-frame replays is a bit, uhh clumsy, on my hardware right now. This'll require some more in-depth investigation to see what precisely is wrong. Easily could be something on my end, not necessarily the TAS or the emulation itself. I can't read Japanese, but I can tell that whatever is going wrong, happens at least as early as inputting the player's name (it takes a whole lot longer than it should, and the wrong name is typed.)
TAS Verifications | Mastodon | Github | Discord: @bigbass
Experienced player (918)
Joined: 9/18/2008
Posts: 153
Location: Japan
Bigbass wrote:
I've just done some attempts, unsuccessfully. Performing sub-frame replays is a bit, uhh clumsy, on my hardware right now. This'll require some more in-depth investigation to see what precisely is wrong. Easily could be something on my end, not necessarily the TAS or the emulation itself. I can't read Japanese, but I can tell that whatever is going wrong, happens at least as early as inputting the player's name (it takes a whole lot longer than it should, and the wrong name is typed.)
Thanks for your attempts! I said "this run doesn't depend on uninitialized values on Save RAM", but strictly speaking, that's somewhat incorrect. This game essentially depends on an initial Save RAM state, so ideally, a verification should start with the exact same state of the Save RAM. Alternatively, the Save RAM should be cleared with in-game initialization functionality. (If you "delete" all heroes and execute "delete" once more, the game prompts to re-initialize the Save RAM. (steps: "edge of town" ("まちはずれ") -> "training grounds" ("くんれんじょう へ いく") -> "delete" (キャラクタをけす)) If your verification started with the same state of Save RAM, I don't have an idea for now. For reference: Save RAM initialization routine is located at $06:86F8. The game checks a "magic string" at $7FF0-$7FFF, and if it differs from the expected magic string by 7 bytes or more, the game initializes Save RAM.

1730432664