OK, I'm going to turn this into a little bit of a class, unless somebody tells me to shut up.
My preferred method of hacking/reverse-engineering is to treat it like any other investigation (somebody once called it the "No Method Method"). I know a lot of people who get caught up in a "I want to do X. Where's the step-by-step method for doing that?" mentality, and it seems so limiting. When you're investigating, rather than following instructions, you take what you already know, and apply it to find out what you don't. Using the "No Random Battles" code mentioned previously as an example, here's the approach I took.
What is known:
- You randomly encounter enemies when walking in dungeons or the world map.
- There is an item in the game that prevents or lessens random encounters, the Champion Rune.
- The Champion Rune only blocks encounters if the enemy party can be let go, rather than run from.
- The ID of the Champion Rune is 0x40 when equipped.
- The game breaks its functionality down into files, and only loads them when necessary. However, random encounters can occur almost everywhere, and on every type of map. The code for it will need to be loaded most of the time, if not all the time.
Given that, I started by searching the main executable for Immediate values of 0x40. An "immediate value" is a concept in low-level programming. It means that the operations data values are either contained in the operation itself, or inline with the code. "Immediate" is a type of "addressing mode". The MIPS RISC processor that the PSX uses can handle immediate values of up to 16-bits, and good high-level compilers will tend to optimize such values so that they're immediate rather than stored in memory as variables. So, since the ID is a small amount of data, immediate values seems a good place to start.
This search returns about 600 results. That's a lot, but I can pare it down further. I'm only interested in operations that place the value 0x40 on a register, so I can exclude places where 0x40 is the index in a load operation, where the zero register is not the first operand, AND operations, Set Less Than operations, etc. Note that the "zero" register is special-purpose. It's hardwired to be zero at all times, so it fills in when you need to compare to zero, or when you just need one of the operands to be zero. It cannot be modified, and any attempt to do so is an illegal operation.
After narrowing it down, I'm left with less than 50 operations to look at. Most of them can be dismissed. How this is done is a matter of experience. I know that places where the value is loaded and immediately dumped into memory aren't likely, I have an idea what graphical subroutines look like, and so on. I can't really say with 100% certainty that they're not what I'm looking for, but the chances seem slight enough that I can ignore them for now.
After a minute or so, the following pops up.
Code: Select all
TEXT:80093BA0 jal rand
TEXT:80093BA4 addiu $s1, $v0, 1
TEXT:80093BA8 sll $v1, $v0, 9
TEXT:80093BAC addu $v0, $v1, $v0
TEXT:80093BB0 bgez $v0, loc_80093BBC
TEXT:80093BB4 nop
TEXT:80093BB8 addiu $v0, 0x7FFF
TEXT:80093BBC
TEXT:80093BBC loc_80093BBC: # CODE XREF: sub_80093ADC+D4j
TEXT:80093BBC sra $v0, 15
TEXT:80093BC0 slt $v0, $s1
TEXT:80093BC4 beqz $v0, loc_80093C78
.......
TEXT:80093C0C li $a2, 0x40
TEXT:80093C10 jal sub_80074CFC
TEXT:80093C14 sw $v0, 0x1260($v1)
TEXT:80093C18
TEXT:80093C18 Has_ChampionRune:
TEXT:80093C18 beqz $v0, loc_80093C30
TEXT:80093C1C nop
TEXT:80093C20 jal sub_800BAA00
TEXT:80093C24 nop
TEXT:80093C28 bnez $v0, loc_80093C78
TEXT:80093C2C nop
TEXT:80093C30
TEXT:80093C30 loc_80093C30: # CODE XREF: sub_80093ADC:Has_ChampionRunej
TEXT:80093C30 lw $v0, 0x2C4($s0)
TEXT:80093C34 nop
I added the Has_ChampionRune label, so I could find this place later. I also cut out a fair bit of code, because it's not necessary to this explanation.
Here we have a call to the library rand() function, followed shortly thereafter by 0x40 being loaded on $a2 and then a call (JAL) to some subroutine. Most of the registers on the PSX's main processor are general-purpose, including $a0 - $a3, but some of them have special uses by convention. In the case of the $a registers, they're
argument registers, as well as being available for general use. An argument is a value passed into a function to be operated with or on. Looking at the routine at 0x80074CFC, you can see it's multipurpose. Konami didn't do object-oriented or anything close to it at this point in time. If they needed functions to look at ten different arrays, they wrote a multipurpose one that takes a "type" as one of the arguments and then does a different search based on the type. Looking through the individual functions performed by the routine, there's one that loops through up to 6 "things", checking to see if part of their data is equal to what was passed on $a2. This looks like it could search the active party for a particular Rune.
Another register with a special purpose is $v0. It often holds a "return" value from a function. In the case of sub_80074CFC, it puts 1 on $v0 if it finds what it's looking for. That's pretty typical. 1 is "found"; 0 is "not found".
So glancing at the code, this looks like a pretty solid bet for being where the Champion Rune's effect is applied. We have a hypothesis, so let's try to falsify it. You can see immediately after the call, it skips over a section of code if $v0 (the return value) is zero. We can assume that if this is where the Champion Rune does its work, then if it's "found", the skipped code should be executed. Let's make it so that the Champion Rune is always found.
Test Code
80093C18 0001
What I've done here is modify the BEQZ operation at 0x80093C18. This address actually holds the value 0x10400005, which is a machine op-code. My disassembler shorthands it to "BEQZ $v0" and a label, but it's actually, "beq $v0, zero, +5", or, "jump down five operations if $v0 is equal to register zero (0)". The code above will make it jump down 1 operation instead, which essentially makes it like the branch isn't there. The theory is that that means the game will always check to see if the enemy can be "let go" and prevent the encounter, even if you're not wearing a Champion Rune.
To test it, wander around in an area where your party outclasses the enemies for a while, and see if you get into any encounters. I tested it in two spots for about five minutes each, and didn't run into anything except one party that I could only run from. The hypothesis seems sound. We can call this a cheat for producing the "Champion Rune Effect".
This success makes it pretty likely that we've found where random encounters are checked and set, so let's try to expand on this and make a "No Encounters" code.
Looking back at the game code, there are any number of ways this might be accomplished, but let's work from the cheat code we have. Right below that BEQZ that was modified, there's a call to a function. We know that the Champion Rune only works on enemies that can be "let go", so it's reasonable to guess that this function checks that (a brief look at its code makes that seem likely). Right below it is a BNEZ op that looks at $v0 again, and jumps to the exit of the subroutine if it's not zero. Seems like that function will return 1 (or at least non-zero) if the encounter can be avoided. BNEZ is another shorthand of my assembler. It's actually "bne $v0, zero, %whatever jump gets to the exit%". So let's change that so that it
always jumps to the routines exit, instead of spawning an encounter.
Test Code 2
80093C18 0001
80093C2A 1000
We include the previous cheat, to trick it into thinking there's a Champion Rune on the party. Then we just have to make it look like the enemy can always be let go. The easy way to do that is to make it so that the branch operation's condition will always be true. So we change it to "beq zero, zero, %whatever%". The zero register is always zero and any register is always equal to itself. So that means the branch will always be taken, and the code between it and its target will never be executed. Note that in this case, the jump value is untouched by the cheat, so we don't even need to care what it is. We've just modified the requested operation, and its operands.
Testing this code, in an area where the enemies are more than twice my party's current level, I don't get into any encounters for about 5 minutes. It seems like we have a working code.
Lessened Encounters (Champion Rune Effect)
80093C18 0001
No Random Encounters
80093C18 0001
80093C2A 1000
The "No Random Encounters" code can actually be shortened to one line, or expanded with type D conditional cheat codes to make it activate and deactivate when different buttons are pressed. You can also make it so that the Champion Rune is a legitimate "No Encounters" item.