J. R. R. Tolkien's Lord of the Rings for Super Nintendo shipped with a bug as you get toward the later parts of the game.
Short version: use these Pro Action Replay codes to un-glitch the late-game passwords.
81CBF10C
81A3900C
81A35C0C
Longer explanation below.
If you're in Moria past the entrance and the first part and request a password, the game will give you one. However, if you write down and try to use that same password later, the game won't accept it.
This is especially troublesome because
a) it's in the most tedious part of the game, a part you wouldn't ever want to re-do, and
b) even if you back-track to the Moria entrance, it won't go back to giving you valid passwords. The game's password system is basically cursed.
I investigated to understand this more. First, the password validation converts this character into a number.
The exact way it does this is described in an earlier post. So for here, L is 9, M would be 10, and so on.
Starting from there, I found out more by
- typing a numerical garbage password like "023741" into the first six characters
- taking memory dump of RAM
- running a relative searcher on the memory dump, looking for a pattern like the one above
- found one result. This was lucky in a bunch of ways. It was lucky how the number was indeed stored contiguously in RAM, not immediately over-written, the password characters' values are sequential (e.g., the value of password character '1' numerically comes right before '2') and that I had picked a unique enough number
- Used that to get the RAM address of the character boxed in red above
- Set a break-on-read of that RAM address in the debugger
- Breakpoint hit when you press the button for password confirm. This also, was a bit lucky. The game only read the password back for initial graphics or when you hit 'confirm'. No reading it back continuously, no noisy breakpoints.
- Stepped through in debugger to see what it did with the password character. When it copied the password character, set break-on-read of that too. This led to un-tangling the password characters from what I called the AreaNumbers below. I saved the code and marked it up with comments and labels.
Password validation calls this function
// Function: ReadPasswordLocationCode()
// Precondition: location code is stored at $81:039C
// Postcondition: result stored in 801CCB, 801CCD, 801CC9.
// The result is what I'm calling an "AreaNumber"
// plus positional information about
// where in the world to load the player.
//
// Early-game AreaNumbers are high-numbered.
// Late-game ones are low.
// Examples:
// Crossroads is 0x12A.
// Rivendell is 0x138.
// Moria Entrance is 0xEF.
// Moria 1 is 0x51.
// Moria 2 is 0x0C.
$81/CBEA B9 84 03 LDA $0384,y[$81:039C] ; Load location code. E.g., the password
; character 'M', which is 0xA
$81/CBED 29 1F 00 AND #$001F ; Ignore upper bits
$81/CBF0 C9 0A 00 CMP #$000A
$81/CBF3 90 02 BCC $02 [$CBF7] ; If the password character is equal or greater than
;'M' (0xA), fall through
; to LocationCodeTooHigh.
; Otherwise, goto LocationCodeOk.
LocationCodeTooHigh:
$81/CBF5 38 SEC
$81/CBF6 6B RTL ; Bail
LocationCodeOk:
$81/CBF7 85 90 STA $90 [$00:0090]
$81/CBF9 A5 90 LDA $90 [$00:0090]
$81/CBFB 0A ASL A
$81/CBFC AA TAX
// Write the output
$81/CBFD BF 18 CC 81 LDA $81CC18,x[$81:CC2A]
$81/CC01 8F CB 1C 80 STA $801CCB[$80:1CCB]
$81/CC05 BF 30 CC 81 LDA $81CC30,x[$81:CC42]
$81/CC09 8F CD 1C 80 STA $801CCD[$80:1CCD]
$81/CC0D BF 48 CC 81 LDA $81CC48,x[$81:CC5A]
$81/CC11 8F C9 1C 80 STA $801CC9[$80:1CC9]
$81/CC15 C8 INY
$81/CC16 18 CLC
$81/CC17 6B RTL
It's pretty easy to see the problem. It validates your location code is too high if you specify 'M' or 'N', but those are codes the game gives you. It was clearly a mistake. Changing the line
$81/CBF0 C9 0A 00 CMP #$000A
to
$81/CBF0 C9 0C 00 CMP #$000C
will fix it, allowing through AreaNumbers up to N (since a password character N = 11 = 0xB), the maximum the game will give you.
This unblocks the password validation code. But, if you were to patch the above change and try it, you'd see the screen fade but hang there forever spinning in a long loop and hard locked not loading the level. So we're not out of the woods yet.
Terminology
Crashed, halted- the game's computer stopped executing instructions
Hard lock- the game's computer is executing instructions, but it appears unresponsive to inputs e.g., the screen is black
Soft lock- the game displays graphics and appears responsive but can not be won
The hang happens because we get past the password-validation and into level-loading yet there's parts of the level loading code that block out AreaNumbers belonging to Moria.
We get here
// Function: AreaLoadStaging()
// Preconditions: Location codes have been written to 801CCB, 801CCD 801CC9
// Expected behavior: Sanitize out bad location codes (e.g., bad
// AreaNumbers) and call a common function AreaLoadHelper().
// AreaLoadHelper() is a common channel used during both password-based
// loading and normal level loading as you move from one place to another
// in the game.
$81/A377 E2 30 SEP #$30
$81/A379 AF 0E 1D 80 LDA $801D0E
$81/A37D CF 72 03 80 CMP $800372
$81/A381 F0 76 BEQ $76
$81/A383 AF 72 03 80 LDA $800372
$81/A387 30 70 BMI $70
$81/A389 C2 20 REP #$20
$81/A38B AF C5 1C 80 LDA $801CC5 ; Load the area number.
$81/A38F C9 54 00 CMP #$0054
$81/A392 B0 52 BCS $52 [$A3E6] ; If area number < 54, fall through to
; InvalidAreaNumber_TooLow.
; Otherwise, goto ValidAreaNumber.
InvalidAreaNumber_TooLow:
$81/A394 E2 20 SEP #$20
$81/A396 AF 0E 1D 80 LDA $801D0E
$81/A39A C9 04 CMP #$04
$81/A39C D0 0E BNE $0E
$81/A3AC E2 20 SEP #$20
$81/A3AE A9 00 LDA #$00
$81/A3B0 48 PHA
$81/A3B1 AF 72 03 80 LDA $800372
$81/A3B5 48 PHA
$81/A3B6 F4 06 00 PEA $0006
$81/A3B9 22 02 80 81 JSL $818002
$81/A3BD 85 34 STA $34 ; Fall through into BadLoop
BadLoop:
$81/A3BF A9 00 LDA #$00
$81/A3C1 48 PHA
$81/A3C2 AF 72 03 80 LDA $800372
$81/A3C6 48 PHA
$81/A3C7 F4 06 00 PEA $0006
$81/A3CA 22 02 80 81 JSL $818002
$81/A3CE C5 34 CMP $34
$81/A3D0 F0 ED BEQ $ED
// This hangs forever :(
ValidAreaIndex:
$81/A3E6 E2 20 SEP #$20
$81/A3E8 A9 00 LDA #$00
//... clipped for brevity
This code snippet makes reference to another function I'm calling AreaLoadHelper. I'm not posting the code to AreaLoadHelper because it's veering a bit off topic. If you want to see the code for that, it's here. Look for 'Function: AreaLoadHelper() - 801D0D'.
Looking through, it's possible they originally intended for bad location codes to do something more elegant than a hang in this function (early out?). Or perhaps not, if they were sure this code wasn't reachable.
Anyway, changing
$81/A38F C9 54 00 CMP #$0054
to
$81/A38F C9 0C 00 CMP #$000C
fixes it here. The AreaNumbers for the last two levels are 0051 and 000C, so 0C covers it. The fact that it's the same number as patched above is really a coincidence.
If you were to apply this change and run the game, you would still see the same symptom as before where the screen would fade to black yet no level would get loaded. This is because it gets further in the level-loading code before getting stuck again. It was a very loose spin. I had an unpleasant debugging experience. That's because if you break in the debugger while it's hanging, you're too late. The root cause of the hang was here
// Function: CallAreaLoadHelperArg4 ($81:A33A)
// Preconditions: An AreaNumber is passed in through address $80:1CC5.
// Expected result: Validate the AreaNumber.
// If valid, push the argument '4' onto the stack and then call AreaLoadHelper(),
// a common code path shared between password-loading usual traversal
// through the game world.
$81/A33A E2 30 SEP #$30
$81/A33C AF 0E 1D 80 LDA $801D0E
$81/A340 CF 72 03 80 CMP $800372
$81/A344 F0 2E BEQ $2E
$81/A346 AF 72 03 80 LDA $800372
$81/A34A 30 28 BMI $28
$81/A34C C2 20 REP #$20
$81/A34E AF C3 1C 80 LDA $801CC3
$81/A352 C9 EF 00 CMP #$00EF
$81/A355 F0 09 BEQ $09
$81/A357 AF C5 1C 80 LDA $801CC5
$81/A35B C9 54 00 CMP #$0054 ; Load AreaNumber
$81/A35E 90 14 BCC $14 [$A374] ; If too low, goto InvalidLocation.
; If okay, fall through to ValidAreaNumber
ValidAreaNumber:
$81/A360 E2 20 SEP #$20
$81/A362 A9 00 LDA #$00
$81/A364 48 PHA
$81/A365 AF 72 03 80 LDA $800372
$81/A369 48 PHA
$81/A36A F4 00 01 PEA $0100
$81/A36D F4 04 00 PEA $0004 ; Push args
$81/A370 22 02 80 81 JSL $818002[$81:8002] ; Call AreaLoadHelper()
InvalidLocation:
$81/A374 C2 30 REP #$30
$81/A376 6B RTL
Another place where the validation is on the wrong bounds.
So, change
$81/A35B C9 54 00 CMP #$0054
to
$81/A35B C9 0C 00 CMP #$000C
allowing through both within-Moria area numbers, it works.
It is kind of good password-resuming the last two areas was something superficial like this, and not a deeper problem like a huge swath of level loading being actually not implemented. It turns out, the odds a "not implemented" is low anyway, since that part was written in a reasonable way- i.e., if you can visit an area, you can password-resume to it. They share the same code.
As for the fix since the total amount of changes is small, you could use a ROM patch, but it's even easier as a cheat code like a Pro Action Replay code. Expressing the above changes as SNES Pro Action Replay the codes are
81CBF10C
81A3900C
81A35C0C
These are value changes to ROM code (it is not self modified), so if you're applying cheats in an emulator you don't need to re-start the game to apply them.
Here is a demo of the codes in action
I was gonna post this to GameFaqs but they have this
oh well.
Anyway, you can use the cheat codes in a Pro Action Replay device for Super Nintendo, or any mainstream emulator (tested ZSNES, Snes9x) to unblock the game. Enjoy
This is all great info! This is the first time I’ve seen anyone other than myself take more than a surface-level interest in this game. I have big plans for this game, but I haven’t come back to it in a while, so finding this now was a welcome surprise.
I have a background in programming, but no experience working with disassembling retro games like this. I’m finding it difficult to track down anything more than some text strings when looking at either the ROM or at active memory. Do you have any resources that you could point to that could help me learn how to approach this?
Thank you for all the effort you’ve already put into understanding this game, it’s definitely appreciated.
December 31, 2021 @ 6:10 amThanks for your comment, happy to help. Same, for this game I’ve seen some interest in understanding the password format although that’s all that comes to mind.
I don’t have any general-purpose debugging guides since it depends a lot on what you’re looking to do. What kind of change are you thinking of doing?
In general it helps to start small and use a debugger.
December 31, 2021 @ 6:28 amOriginally, I had just wanted to just see if there were any interesting secrets in the game that no one had found before (there’s an unopenable gate in the back of Bree that I’ve been very curious about since I was a kid), but recently I’ve been thinking about whether it would be feasible to make a randomizer for this game. I can’t imagine many people would play it, but I’m more than happy just doing it for myself.
December 31, 2021 @ 6:52 amI can imagine that as a start I’m looking for where the item data is actually stored in the ROM, but in my couple hours of cursory debugging, I haven’t made much progress yet.
It should be doable to find the RAM address of inventory items same way I found the location code, in the post above (with “get the RAM address of the character boxed in red above”). Or, you can take a memory dump of RAM, get an item in the game, dump RAM again and diff what changed. The diff will be noisy. But the item data will be in there somewhere.
A randomizer sounds really cool! it would breathe some life into the game.
I remember that gate in Bree. I didn’t see unused areas when testing. But I didn’t exhaustively try to spawn at all spawnable locations. I wonder if anyone has tried disabling collisions and walking through.
December 31, 2021 @ 7:19 am