Hacking Gaming Reverse Engineering and Hacking the Fishing Game in Phantom Hourglass

  • Thread starter Thread starter MopSec
  • Start date Start date
  • Views Views 2,495
  • Replies Replies 10
  • Likes Likes 8

MopSec

"Leftist snowflake milennial"
Member
Joined
Jun 14, 2011
Messages
1,887
Solutions
1
Reaction score
1,360
Trophies
3
Age
28
Location
Munich
XP
4,500
Country
Germany
First off, I wanna preface this by saying that I have no idea if this is even remotely interesting for anybody at all, but I feel pretty proud of my work and some people might get a kick out of it, too, so there you go. Maybe I can even teach some people stuff about game hacking.

Tl;dr: The fishing minigame / sidequest in Legend of Zelda: Phantom Hourglass annoyed me. I wanted some cheats for it for years. There weren't any. Now I made some. Check them out at the bottom of this post.

1. Introduction
Legend of Zelda: Phantom Hourglass is an underrated game in my opinion and I like it a lot. However, there is one thing that has always annoyed me, especially because I typically play every Zelda game to 100% completion and I find myself replaying them A LOT. Cheats have always been very much fair game for me, though (In single player games only!). There is a Heart Container in the game which is rewarded to the player after they complete a fishing sidequest which involves catching some specific species of fish which spawn as random encounters. For years, I have been looking for a way to tip the odds in my favour when playing this part of the game, meaning manipulating the game in a way which still allows me to play that sidequest normally but without having to grind for hours. (Realistically, I would say no more than two or three hours. But still.)

In this post, I want to show you how I have now finally accomplished this goal, sharing my findings with all of you. Maybe someone will find some use in the cheat codes that I have made or will find this as a useful reading material for game hacking and reverse engineering in general. As there is basically no technical background information about this minigame that I have found online, I feel comfortable that this is basically the only source of low-level implementation details for this part of the game.

2. How does the minigame work?
Early on in Phantom Hourglass, the player can receive a fishing rod from an NPC after doing a side quest. After the fishing rod is obtained and the player sets sail and enters the open sea area of the game, a fish icon will appear on the map. If the player sails to the close vicinity of that icon, they can press a button on the touch screen to trigger the fishing minigame. This basic fish icon (the only type available at this point) moves rather slowly around the map and can generally be caught up with pretty easily by the player.
At first, there are three types of fish that can spawn: The very small Skippyjack, the medium-sized Toona and the - at that point - large Loovar. The species of the fish is determined by RNG. Actually catching the fish involves completing a short minigame which, depending on the type of fish that has spawned, can range from very easy to still pretty easy once you get the hang of it. The game explains the steps very clearly as well.
Once the player has caught the elusive Loovar species, they are rewarded the "Big Catch Lure". At that point, there is a small chance that a new type of fish spawns when interacting with the regular fishing spot: The Rusty Swordfish. After that has been caught, a new type of fish icon very rarely appears on the map which depicts a swordfish. This icon moves very quickly, so the player needs to catch up with it by intercepting it from the front instead of catching up to it. This can take a few minutes on every attempt.
When fishing at a swordfish spot, there is again a very small chance that the "Legendary Neptoona" spawns instead of a regular Swordfish. This fish is the biggest and most difficult one to catch, but if the player manages to reel it in, they are rewarded with an extra Heart Container.

3. The recommend way to do it
Looking online, it seems like it is generally agreed upon to not try and grind for the Heart Container by waiting for a swordfish icon to spawn again and again until Neptoona finally appears. Basically, you are supposed to play through the game and every time you see the swordfish icon, sail to it and try your luck. Should you decide to grind for the Heart Container anyway, it is recommended to set sail from any island, draw a very short sailing route (or just exit out from the route drawing dialogue completely) and wait two or three minutes. [1][2] If no swordfish icon appears, sail back to the nearest island and repeat. In my experience this can take ages.

4. The Problem and Solution (For Me)
Generally, I am not a big fan of RNG based minigames and / or side quests. However, when you can quickly try them again and again, I usually do not mind them too much. My problem with this particular side quest comes from the fact that it basically combines two RNG based "dice rolls" together. The first is getting the swordfish icon to spawn, the second is getting Neptoona to spawn. Of course, if you fail the second roll, you will have to wait for a new swordfish icon to spawn once again, starting from square one.
In previous playthroughs, I have abused save states to cheese my way through this. Basically, I would wait for a swordfish icon to spawn, sail up to it and just before reaching it, I would create a save state on my R4 cartridge and I would just re-load that save state until I had caught the elusive fish. The problem with that is:
a) it feels clunky
b) Save states on R4 carts don't work very well
c) save states don't work at all on TwilightMenu++ which is usually my preferred way to play NDS games
Because of the above reasons, I have wanted to look at a different solution for a while now.
As stated previously I want to manipulate the game in a way that makes the side quest still completely playable (i.e. not simply forcing every fish to be Neptoona or patching the logic when the NPC is checking if you have caught Neptoona to just always act like you have done so). Basically, I wanted everything to still feel pretty much original, only with higher odds of finding the rarest fish types and a higher chance of finding the swordfish icon fishing spots in the first place. In the following sections, I want to show you how I have managed to accomplish just that.

Tools used:
- DesMuME: Good emulator with very good RAM search support => Use this to find interesting addresses in RAM
- No$GBA Debug version: DS emulator with support for execution and memory access breakpoints => Use this to identify the instructions in the game's code which access the RAM addresses identified earlier
- Ghidra: Open Source Decompiler for loads of architectures => Use this to gain more understanding of the code with all its overlays

5. First piece of the puzzle: Swordfish Icons
My first goal was to understand the logic behind the swordfish icons a little better, specifically how they are generated and when.
First off, I made the following high-level assumption:
Somewhere in the code, there is variable, let's call it "swordfish_available" which is normally set to 0 / False and then at some point gets set to 1 / True.
With this assumption, I set off to find a variable like that.

To do this, I set sail with my ship and while no swordfish icon was visible, continued to scan the RAM for memory addresses which stay the same (swordfish_available = 0).
As soon as the icon appeared, I made a new search, scanning the previous addresses (which haven't changed all this time) for changed values instead (swordfish_available = 1).
As it turns out, I found the address 0x027E1090 which holds 0x02 when only a regular fish icon is there, and gets increased to 0x03 when the swordfish icon appears.
Now, the idea was to look at which instruction is writing to this address and basically follow the flow of the code to determine which code paths decide if the icon should be spawned or not.
Looking further down the line, I found two addresses which looked like a timer (random looking values that decrease with every frame) and as soon as they hit zero, the swordfish icon appears.
However, it seemed like the timer often just does not start. If that is the case, the player needs to sail back to an island and then go back to sea, then check the timer again and see if it is now running.
Sound familiar?
After a bit of back and forth, I found an interesting function at 02183d0c.
Analysing this function, the following snippet caught my attention:
1763544575851.png


Basically, what happens is this: uVar9 is a randomly generated 32 bit integer. In the if-condition in line 6, uVar9 is multiplied by 100, creating a 64-bit integer.
The 32 most significant bits of this new value are then compared to 0x14 (20 in decimal). If the value is higher than 20, the function returns zero, if the value is lower or equal 20, the function continues.
At this point, I decided to make another assumption: This looks like our first dice roll. If the calculated value is lower than 20, the "swordfish timer" starts, otherwise it doesn't.
In Python-esque code it would look something like this:
Code:
rand_value = random(0, 0xFFFF_FFFF)
timer_check = (rand_value * 100) >> 0x20

if (timer_check > 20):
    return 0
else:
    start swordfish timer

To verify my assumption I basically got rid of the if-condition, changing the b.le (branch if lower or equal) instruction at 0x02183e34 to a b (branch always) instruction. That means, whatever the comparison between the calculated value and 0x14 is, the function will carry on instead of just returning 0.

The result was: Yes! It worked! First try!
This gives us the following cheat, compatible with the US version of Phantom Hourglass

Force Swordfish Spawn [MopSec]
52183E34 DA000004
02183E34 EA000004

What does that mean now when looking back at the previous hints mentioned in section 3 and my personal experience?
Basically, this tells us that the advice to wait on the open sea for two minutes and then going back to an island if no swordfish icon has spawned is absolutely correct! Confirming a GameFAQs thread from 2008, nice! :D
After two minutes, we can rightfully assume that the timer for the icon has never been started and we could wait forever for the icon to spawn. Going back to an island and then back to the open sea gives us another dice roll so we can try again.

Looking at the odds, let's look at the calculation of our values again:
Code:
rand_value = random(0, 0xFFFF_FFFF)
timer_check = (rand_value * 100) >> 0x20

timer_check is now actually a 64-bit integer, which can range from 0 to 0xFFFF_FFFF_FFFF_FFFF. The value is then shifted by 0x20 though, meaning that only the most-signifcant eight bytes are actually stored (for this particular part of the code). Mathematically this gives us a range of 0x00 to 0x63 (0 to 99) for timer_check (because 0xFFFF_FFFF * 100 = 0x0000_0063_FFFF_FF9C and 0x0000_0063_FFFF_FF9C >> 0x20 = 0x63). So we have 100 possible values for timer_check and the timer is only started when the value is below 20. That means the game gives us a chance of about 20% for a swordfish icon to spawn every time the player sets sail from an island. Not too bad, actually :D

6. Second piece of the puzzle: Increase the chance for Neptoona to spawn
Alright, now comes the most difficult part. Surprisingly so, to be honest. I thought figuring out the timer would be much more complicated than messing with the actual fish encounter.
I started off by trying to identify an address in RAM that stores the type of fish that is currently being encountered as soon as the player actually starts fishing.
What I thought would be quick and easy turned out to be really difficult, though.
In the end, I found a few addresses which always held the same value when you have found a specific type of fish but changing these values to completely wrong ones did not change anything in the game, meaning I had no idea if I was looking at the right addresses.

02216979 and 0221697D seemed to always hold the following values:
0256 and 032E: Loovar
01AE and 028C: Toona
0147 and 01EB: Skippyjack
02F8 and 03D7: Swordfish

Further down the line, I have found the address 0x0221692C which depending on the fish species that was encountered, holds the following values:
0x2218654 => Toona
0x22185F4 => Swordfish
0x2218614 => Skippyjack
0x2218674 => Loovar

These values would only be set right when the fish bites the lure, though. In the cutscene leading up to that moment, the address would hold a default value. This is interesting, because I have done some experiments where I made a save state in DesMuME right before starting the fishing sequence and checked to see if I could get the fish type to change and in all of those cases, the fish type stayed the same after reloading the save state and trying again. This does imply that the fish type is calculated right at the beginning of the whole process and not just when the fish bites. Well, either that or the RNG state does not change often enough so the fish type might be generated later, but is kind of set in stone earlier.

Like previously mentioned, manipulating these values did not change anything at all, but I had still hoped to find something interesting when looking which instructions write the values to those addresses.
Following this further and further I noticed that there always seemed to be one specific register which hold a value from 0 to 3 which more or less decided which values would be written to the above mentioned addresses. Let's call this value the "type decider" However, at some point that register would be loaded from a stack address (0x027E37BC) where loads of functions write to, so finding which function writes our "type decider" using memory breakpoints seemed very unfeasible.
Basically, there is one function at 0x020c3fe8 which writes to that address, but it is executed from many places, making tracing the origin of the "type decider" very difficult.
1763547705601.png

param_4 here does hold our type decider value, but without knowing where this function is called from, we cannot tell where it is calculated.
Simply setting a breakpoint on the line marked in green here was not really an option, because the function is called from many places with different purposes.

The next steps honestly cost me an entire weekend without any results, so I won't be sharing much of that here. My idea was to try to find other values in RAM (the size of the fish which is displayed after catching it, the distance between the fish and the player as they're reeling it in which is always displayed) and to try to find interesting addresses and / or functions in their vicinity. This did not yield any significant results.

Running out of options and patience, I went back to the "type decider" and tried to determine where it is actually coming from.
For this, I decided to use Ghidra's cross-referencing functionality to try to find out which functions call 0x020c3fe8 and try to find one that looked interesting to me.
There are quite a lot of candidates, though:
1763548322894.png
Z
Basically, my idea was to set an execution breakpoint on all of these addresses one by one (at least those, where the function itself looked interesting enough) and checking when they are hit. Ideally, I would find a point where the function is entered right as the player enters the fishing screen, because that's when the fish type seems to be persisted in memory.

After a few dozen tries, here is what I have found:
1763553709260.png

Here we go, a call of this interesting function which is sometimes used to set the "type decider" variable that is triggered right when we enter the fishing screen. Exactly what I was looking for.

Scrolling a bit further up in this function we find this:
1763553886105.png

Hmmmm an if-condition where a value is multiplied by 100 shifted right by 0x20 and then compared with something else. Have we not seen this before?
Also, for some reason this is repeated a maximum amount of five times, because of that while-loop around the if-condition. Counting Neptoona, there are five types of fish.
Hmmmmmmmm.
To make a long story short, iVar5 in this screenshot is actually stored in register r5 of the processor. If we set a breakpoint at 0x2152888 and then change r5 to any value between 0 and 4, we can successfully force the game to spawn any fish of our choosing, including the Legendary Neptoona!
These are the required values:
r5 = 0 => Skippyjack
r5 = 1 => Toona
r5 = 2 => Loovar
r5 = 3 => Swordfish
r5 = 4 => Neptoona (actually it is not quite that simple, I will get to that later)

How do we write a cheat code that fulfills my goals set in section four, though? For that, we need to reverse this whole process a little more.

Here is what is happening in pseudocode:
Code:
r8 = rand() +r1 (0 in this case)
r3 = (r8 * 100) & 0xFFFFFFFF
r1 = (r8 * 100) >> 0x20
r0=0
r1 = (r8*r0)+r1 ??? => r1 = r1
r14 = 0x21863F0
r3 = 0x14
r14 = (r4*r3)+r14
r1 = (r7*r2)+r1
store r8 at 0x027e0768
r2 = *r14 (0x0A in this case)
while r3 < 5:
    r2 = *(r14 + (r3 >> 2))
    r0 = r0 + r2
    if r0 >= r1:
        r5 = r3 & 0xFF
        break;
    else:
        r3 += 1

Variables like r1, r2 and r3 refer to the actual CPU registers in this case.
So let us talk this through:
In the beginning, a random value is calculated and multiplied by 100. In this case, the most significant 32 bits are stored in r1. This will once again be a random value between 0 and 99 which will be used to determine the fish type. After some more definitions and calculations, the important thing comes in the while loop:
First, a value from memory is being read. The base address for this read is r14 (set before the while loop) but r3, shifted right by two, is added to that address. The value that was read is then added to r0.
Then we compare our random number in r1 with the memory value in r0. If the random value is smaller than r0, r5 is set to r3 and that sets our fish type.
In other words, the fish type is directly determined by how often the code passes through the while loop while not fulfulling the if-condition.

For the final piece of the puzzle, we need to look at r14, this seemingly random memory address where a number is being read from which is then compared with our random number.
In my attemps (fishing at a regular, non-swordfish icon) I had seen that when the instruction r14 = (r4*r3)+r14 is executed, r3 is 2. r14 and r3 are always constant in that case, so r14 becomes 0x2186418.
The first value stored there is 0x0A. This is read by the instruction r2 = *(r14 + (r3 >> 2))) in the first round through the while loop (r3 = 0). In the second round (r3 = 1), the address we read is now 0x218641C (1 << 2 = 4) and the value read from there is 0x1E, meaning r0 becomes 0x28.
Like I mentioned earlier, r5 (and with it the fish type) is set, when r1 is smaller than r0. So the bigger r0 gets, the more likely it is that this condition is fulfilled.

Putting all of this together, let's go through two examples for a regular, non-swordfish, fishing spot when the player has already unlocked the Big Catch Lure (more on that later).
a) r1 = 31 (the RNG has rolled a 31)
1. r0 is set to 0 and r14 is set to 0x2186418 before the while loop​
2. First round of the while loop (r3 = 0): r2 = 10 (read from r14), r0 = r2 + r0 = 10 => r0 is smaller than r1, r3 is incremented, while loop continues​
3. Second round (r3 = 1): r2 = 30 (read from r14 + 1 >> 2), r0 = r2 + r0 = 40 => r0 is greater than r1 => while loop breaks, r5 is set to 1​
4. The player encounters a Toona​
b) r1 = 97
1. r0 is set to 0 and r14 is set to 0x2186418 before the while loop​
2. First round of the while loop (r3 = 0): r2 = 10 (read from r14), r0 = r2 + r0 = 10 => r0 is smaller than r1, r3 is incremented, while loop continues​
3. Second round (r3 = 1): r2 = 30 (read from r14 + 1 >> 2), r0 = r2 + r0 = 40 => r0 is smaller than r1, r3 is incremented, while loop continues​
4. Third round (r3 = 2): r2 = 40 (read from r14 + 2 >> 2), r0 = r2 + r0 = 80 => r0 is smaller than r1, r3 is incremented, while loop continues​
5. Fourth round (r3 = 3): r2 = 20(read from r14 + 3 >> 2), r0 = r2 + r0 = 100=> r0 is greater than r1 => while loop breaks, r5 is set to 3​
6. The player encounters a swordfish​
At this point, two things are very clear: The values stored to wherever r14 points directly give you the probability of encountering that specific fish type or to be precise, they are modifiers for the probability which define a range of possible values for r1 to generate a specific fish type encounter. In this particular example (regular, non-swordfish, fishing spot, Big Catch Lure unlocked), the probability is as follows:
Skippyjack: 10%
Toona: 30%
Loovar: 40%
Swordfish: 20%

The other thing that is clear (which we already knew but hey, it's nice to see how the game handles that) is that this type of fishing spot will never legitimately give you a Neptoona encounter. r0 reaches 100 when r3 = 3 and because r1 can never be greater than 100, r3 will never be set to 4.

At this point, my best guess was that the instruction r14 = (r4*r3)+r14 will change r14 according to the type of fishing spot the player is located on. As r3 is set to 20 right before this instruction, r4 is the only real variable here. In the previous case r4 was set to 2, but when fishing from a swordfish icon, its value becomes 3. This makes our base address r14 0x218642C.
The probability modifiers found at this address are 0x00, 0x00, 0x00, 0x46, 0x1E
Going back to our previous examples, this time for the swordfish icon fishing spots:
a) r1 = 31 (the RNG has rolled a 31)
1. r0 is set to 0 and r14 is set to 0x218642C before the while loop​
2. First round of the while loop (r3 = 0): r2 = 0(read from r14), r0 = r2 + r0 = 0=> r0 is smaller than r1, r3 is incremented, while loop continues​
3. Second round (r3 = 1): r2 = 0 (read from r14 + 1 >> 2), r0 = r2 + r0 = 0 => r0 is smaller than r1, r3 is incremented, while loop continues​
4. Third round (r3 = 2): r2 = 0 (read from r14 + 2 >> 2), r0 = r2 + r0 = 0 => r0 is smaller than r1, r3 is incremented, while loop continues​
5. Fourth round (r3 = 3): r2 = 70 (read from r14 + 3 >> 2), r0 = r2 + r0 = 70 => r0 is greater than r1 => while loop breaks, r5 is set to 3​
6. The player encounters a swordfish​
b) r1 = 97
1. r0 is set to 0 and r14 is set to 0x218642C before the while loop​
2. First round of the while loop (r3 = 0): r2 = 0(read from r14), r0 = r2 + r0 = 0=> r0 is smaller than r1, r3 is incremented, while loop continues​
3. Second round (r3 = 1): r2 = 0 (read from r14 + 1 >> 2), r0 = r2 + r0 = 0 => r0 is smaller than r1, r3 is incremented, while loop continues​
4. Third round (r3 = 2): r2 = 0 (read from r14 + 2 >> 2), r0 = r2 + r0 = 0 => r0 is smaller than r1, r3 is incremented, while loop continues​
5. Fourth round (r3 = 3): r2 = 70 (read from r14 + 3 >> 2), r0 = r2 + r0 = 70 => r0 is smaller than r1, r3 is incremented, while loop continues​
6. Fifth round (r3 = 4): r2 = 30 (read from r14 + 4 >> 2), r0 = r2 + r0 = 100 => r0 is greater than r1 => while loop breaks, r5 is set to 4​
6. The player encounters a swordfish..... Wait what????​
So in that second example, we definitely should have found Neptoona, right?
The reason we do not is right here:
1763557221317.png

(The variable and functions names were named by myself)
Basically, if the game rolls a number which would cause a Neptoona encounter (r5 = 4) there is an additional check to determine if the player has already caught Neptoona, as there is only one of it.
If the fish type was previously set to 4 but Neptoona has already been caught, the fish type is "downgraded" to a regular swordfish.

Before we finish this up, let's look at the different types of fishing spots once more.
Here are the types that I have located in memory and determined the corridors for r1 for:
Code:
r4 = 0 (Fishing just unlocked):
modifiers: 0x37, 0x1E, 0x0F, 0x00, 0x00
0 < r1 < 55: Skippyjack
55 < r1 < 85: Toona
85 < r1 < 100: Loovar

r4 = 1 (??):
modifiers: 0x00, 0x00, 0x00, 0x46, 0x1E
0 < r1 < 70: Swordfish
70 < r1 < 100: Neptoona


r4 = 2 (normal icon, Loovar caught, Big Catch Lure received):
modifiers: 0xA, 0x1E, 0x28, 0x14, 0x00
0 < r1 < 10: Skippyjack
10 < r1 < 40: Toona
40 < r1 < 80: Loovar
80 < r1 < 100: Swordfish
Neptoona never possible

r4 = 3 => Swordfish icon:
modifiers: 0, 0, 0, 0x46, 0x1E
0 < r1 < 70: Swordfish
70 < r1 < 100: Neptoona

As you can see, the first type of spot is the one that gets unlocked as soon as the player receives the fishing rod, with only the three basic species of fish available.
I have explained r4 = 2 and r4 = 3 above. The one question mark that remains is the r4 = 1 type. These are identical to the r4 = 3 type but I have no idea when they appear. May just be an additional type for when the player has caught a Swordfish but not Neptoona, yet, I really don't know. That would also make the additional check that I explained above redundant, as type 3 could just set the probability for Swordfish to 100 in that case. Anyway, I suppose it doesn't really matter.

So what do we learn from this?
The game basically asks you to catch three types of rare fish, first the Loovar, then the regular Rusty Swordfish, then Neptoona. The probability for the first two is 15% and 20% in the regular fishing spots. Not too bad since these spots are guaranteed to always appear (as soon as you have the fishing rod)
For Neptoona, there is a 30% chance but coupled with the 20% chance of a swordfish icon appearing, we end up at a 6% chance that the player gets each time they leave a port.

After all this work, I feel like just setting the chance for a swordfish icon to 100% with the above cheat would already be a huge improvement. But I have not invested that much time to just leave it at that.
May I propose the following probabilities?
Code:
r4 = 0 (Fishing just unlocked):
modifiers: 0x37, 0x1E, 0x0F, 0x00, 0x00
0 < r1 < 35: Skippyjack
35 < r1 < 75: Toona
75 < r1 < 100: Loovar

r4 = 1 (??):
modifiers: 0x00, 0x00, 0x00, 0x46, 0x1E
0 < r1 < 60: Swordfish
60 < r1 < 100: Neptoona


r4 = 2 (normal icon, Loovar caught):
modifiers: 0xA, 0x1E, 0x28, 0x14, 0x00
0 < r1 < 10: Skippyjack
10 < r1 < 40: Toona
40 < r1 < 60: Loovar
60 < r1 < 100: Swordfish
Neptoona never possible

r4 = 3 => Swordfish icon:
modifiers: 0, 0, 0, 0x46, 0x1E
0 < r1 < 60: Swordfish
60 < r1 < 100: Neptoona

If that sounds cool to you, you can use these cheat codes:
Code:
Easy Loovar [MopSec]
921863F0 00000037
221863F0 00000023
921863F4 0000001E
221863F4 00000028
921863F8 0000000F
221863F8 00000019

Easy Neptoona [MopSec]
92186410 00000046
22186410 0000003C
92186414 0000001E
22186414 00000028
92186438 00000046
22186438 0000003C
9218643C 0000001E
2218643C 00000028

Easy Swordfish [MopSec]
92186420 00000028
22186420 00000014
92186424 00000014
22186424 00000028



7. Conclusion
Alright. So. All of this took me about two full weekends to work on. Definitely more time than it would have taken me to play any future Phantom Hourglass run completely legitimately but god damn did it feel rewarding. I hope this was interesting to at least some of you. Feel free to reach out for questions, more details, tell me I've got too much time on my hands, whatever you fancy.

Here are the codes again:
Code:
Force Swordfish Spawn [MopSec]
52183E34 DA000004
02183E34 EA000004

Easy Loovar [MopSec]
921863F0 00000037
221863F0 00000023
921863F4 0000001E
221863F4 00000028
921863F8 0000000F
221863F8 00000019

Easy Neptoona [MopSec]
92186410 00000046
22186410 0000003C
92186414 0000001E
22186414 00000028
92186438 00000046
22186438 0000003C
9218643C 0000001E
2218643C 00000028

Easy Swordfish [MopSec]
92186420 00000028
22186420 00000014
92186424 00000014
22186424 00000028


[1] https://gamefaqs.gamespot.com/boards/932377-the-legend-of-zelda-phantom-hourglass/43467922
[2] ZeldaDungeon PH Walkthrough
 
I'm not interested in the hack itself but I want to say good job! This hack brigs me way back to my romhacking times! Keep this spirit!

As an experiment you might try and create a tool that patches the ROM for you allowing to choose which hack to add and the percentage values? might be intereseting
 
Very impressive. With those skills, maybe think about helping out a decompilation project? Even for Phantom Hourglass?

Thanks for the fun read/ interesting write-up.
 
Very impressive. With those skills, maybe think about helping out a decompilation project? Even for Phantom Hourglass?

Thanks for the fun read/ interesting write-up.
Hi there :)
I've definitely thought about it. Of course my free time is very limited but it's definitely on the back of my mind
 
  • Love
Reactions: Chellarina
Hi there :)
I've definitely thought about it. Of course my free time is very limited but it's definitely on the back of my mind
You should just do whatever is fun of course.

Seemed like you had fun with this though and as far as I understand it, the process you did was similar to a decompilation.

And a less liked game like Phantom Hourglass always needs people more.

But no pressure, I'm not one to talk.
 
  • Like
Reactions: MopSec
Code:
r4 = 0 (Fishing just unlocked):
modifiers: 0x37, 0x1E, 0x0F, 0x00, 0x00
0 < r1 < 35: Skippyjack
35 < r1 < 75: Toona
75 < r1 < 100: Loovar

r4 = 1 (??):
modifiers: 0x00, 0x00, 0x00, 0x46, 0x1E
0 < r1 < 60: Swordfish
60 < r1 < 100: Neptoona


r4 = 2 (normal icon, Loovar caught):
modifiers: 0xA, 0x1E, 0x28, 0x14, 0x00
0 < r1 < 10: Skippyjack
10 < r1 < 40: Toona
40 < r1 < 60: Loovar
60 < r1 < 100: Swordfish
Neptoona never possible

r4 = 3 => Swordfish icon:
modifiers: 0, 0, 0, 0x46, 0x1E
0 < r1 < 60: Swordfish
60 < r1 < 100: Neptoona

Okay, so for a non-cheatcode user the way to do this seems to be this:
  1. Get fishing rod on Bannan Island.
  2. Fish at normal fish shadows until you catch a Loovar, 25% chance every attempt.
  3. Go to Bannan Island to get the Big Catch Lure.
  4. Fish at normal fish shadows until you catch a Rusty Swordfish, 40% chance every attempt.
  5. Go to Bannan Island to get ship part and unlock swordfish shadows.
  6. Pick any of the four map quadrants. See note.
  7. Go in and out of any port on the map.
    • A) Draw a route straight back to the port, then immediately press STOP. If map elements are moving (meaning: in-game timer is running) you did it right.
    • B) Wait for up to 2 minutes. The 120 second mark is the last opportunity for the swordfish shadow to appear, IF it's going to appear. This has a 20% chance of happening.
    • C-1) Did a swordfish shadow appear? Go fish it. There is 40% chance of it being a Neptoona.
    • C-2) No swordfish shadow? Go do step 7 again.
Note on swordfish shadow: After getting ship part/unlocking swordfish shadows I stayed at Bannan and just went in & out of its port. No swordfish after 8-10 tries. I did one teleport to the Omega sign (Ω, bottom right quadrant) and then did step 7 from Dee Ess Island. Got the shadow after two tries.

Note on fishing technique: When you need to pull, one might think that you need to put the stylus down at the top of the screen, drag down, lift quickly and drag down again. This is actually wrong. A more efficient technique is to put the stylus at the top of the screen, slowly drag it down (probably in line with the current leaning of the fishing line) and then hold the stylus at the bottom of the screen for 1-2 seconds. Then you drag from the top again.

If I've misunderstood any of it, please correct me and I will edit my post.
 
Last edited by sasja-san,
Okay, so for a non-cheatcode user the way to do this seems to be this:
  1. Get fishing rod on Bannan Island.
  2. Fish at normal fish shadows until you catch a Loovar, 25% chance every attempt.
  3. Go to Bannan Island to get the Big Catch Lure.
  4. Fish at normal fish shadows until you catch a Rusty Swordfish, 40% chance every attempt.
  5. Go to Bannan Island to get ship part and unlock swordfish shadows.
  6. Pick any of the four map quadrants
  7. Go in and out of any port on the map.
    • A) Draw a route straight back to the port, then immediately press STOP. If map elements are moving (meaning: in-game timer is running) you did it right.
    • B) Wait for 2 minutes. At the 2 minute mark there is a 20% chance that a swordfish shadow appears on the map.
    • C-1) Did a swordfish shadow appear? Go fish it. There is 40% chance of it being a Neptoona.
    • C-2) No swordfish shadow? Go do step 5 again.

If I've misunderstood any of it, please correct me and I will edit my post.
You're right!
One minor nitpick: At 7b), it's not like the swordfish shadow will appear at the two minute mark, it can also appear earlier. It's just that when you reach the two minute mark and the shadow still isn't there, it's best to try again because it probably is never going to spawn in this "cycle"
 
First off, I wanna preface this by saying that I have no idea if this is even remotely interesting for anybody at all, but I feel pretty proud of my work and some people might get a kick out of it, too, so there you go. Maybe I can even teach some people stuff about game hacking.

Tl;dr: The fishing minigame / sidequest in Legend of Zelda: Phantom Hourglass annoyed me. I wanted some cheats for it for years. There weren't any. Now I made some. Check them out at the bottom of this post.

1. Introduction
Legend of Zelda: Phantom Hourglass is an underrated game in my opinion and I like it a lot. However, there is one thing that has always annoyed me, especially because I typically play every Zelda game to 100% completion and I find myself replaying them A LOT. Cheats have always been very much fair game for me, though (In single player games only!). There is a Heart Container in the game which is rewarded to the player after they complete a fishing sidequest which involves catching some specific species of fish which spawn as random encounters. For years, I have been looking for a way to tip the odds in my favour when playing this part of the game, meaning manipulating the game in a way which still allows me to play that sidequest normally but without having to grind for hours. (Realistically, I would say no more than two or three hours. But still.)

In this post, I want to show you how I have now finally accomplished this goal, sharing my findings with all of you. Maybe someone will find some use in the cheat codes that I have made or will find this as a useful reading material for game hacking and reverse engineering in general. As there is basically no technical background information about this minigame that I have found online, I feel comfortable that this is basically the only source of low-level implementation details for this part of the game.

2. How does the minigame work?
Early on in Phantom Hourglass, the player can receive a fishing rod from an NPC after doing a side quest. After the fishing rod is obtained and the player sets sail and enters the open sea area of the game, a fish icon will appear on the map. If the player sails to the close vicinity of that icon, they can press a button on the touch screen to trigger the fishing minigame. This basic fish icon (the only type available at this point) moves rather slowly around the map and can generally be caught up with pretty easily by the player.
At first, there are three types of fish that can spawn: The very small Skippyjack, the medium-sized Toona and the - at that point - large Loovar. The species of the fish is determined by RNG. Actually catching the fish involves completing a short minigame which, depending on the type of fish that has spawned, can range from very easy to still pretty easy once you get the hang of it. The game explains the steps very clearly as well.
Once the player has caught the elusive Loovar species, they are rewarded the "Big Catch Lure". At that point, there is a small chance that a new type of fish spawns when interacting with the regular fishing spot: The Rusty Swordfish. After that has been caught, a new type of fish icon very rarely appears on the map which depicts a swordfish. This icon moves very quickly, so the player needs to catch up with it by intercepting it from the front instead of catching up to it. This can take a few minutes on every attempt.
When fishing at a swordfish spot, there is again a very small chance that the "Legendary Neptoona" spawns instead of a regular Swordfish. This fish is the biggest and most difficult one to catch, but if the player manages to reel it in, they are rewarded with an extra Heart Container.

3. The recommend way to do it
Looking online, it seems like it is generally agreed upon to not try and grind for the Heart Container by waiting for a swordfish icon to spawn again and again until Neptoona finally appears. Basically, you are supposed to play through the game and every time you see the swordfish icon, sail to it and try your luck. Should you decide to grind for the Heart Container anyway, it is recommended to set sail from any island, draw a very short sailing route (or just exit out from the route drawing dialogue completely) and wait two or three minutes. [1][2] If no swordfish icon appears, sail back to the nearest island and repeat. In my experience this can take ages.

4. The Problem and Solution (For Me)
Generally, I am not a big fan of RNG based minigames and / or side quests. However, when you can quickly try them again and again, I usually do not mind them too much. My problem with this particular side quest comes from the fact that it basically combines two RNG based "dice rolls" together. The first is getting the swordfish icon to spawn, the second is getting Neptoona to spawn. Of course, if you fail the second roll, you will have to wait for a new swordfish icon to spawn once again, starting from square one.
In previous playthroughs, I have abused save states to cheese my way through this. Basically, I would wait for a swordfish icon to spawn, sail up to it and just before reaching it, I would create a save state on my R4 cartridge and I would just re-load that save state until I had caught the elusive fish. The problem with that is:
a) it feels clunky
b) Save states on R4 carts don't work very well
c) save states don't work at all on TwilightMenu++ which is usually my preferred way to play NDS games
Because of the above reasons, I have wanted to look at a different solution for a while now.
As stated previously I want to manipulate the game in a way that makes the side quest still completely playable (i.e. not simply forcing every fish to be Neptoona or patching the logic when the NPC is checking if you have caught Neptoona to just always act like you have done so). Basically, I wanted everything to still feel pretty much original, only with higher odds of finding the rarest fish types and a higher chance of finding the swordfish icon fishing spots in the first place. In the following sections, I want to show you how I have managed to accomplish just that.

Tools used:
- DesMuME: Good emulator with very good RAM search support => Use this to find interesting addresses in RAM
- No$GBA Debug version: DS emulator with support for execution and memory access breakpoints => Use this to identify the instructions in the game's code which access the RAM addresses identified earlier
- Ghidra: Open Source Decompiler for loads of architectures => Use this to gain more understanding of the code with all its overlays

5. First piece of the puzzle: Swordfish Icons
My first goal was to understand the logic behind the swordfish icons a little better, specifically how they are generated and when.
First off, I made the following high-level assumption:
Somewhere in the code, there is variable, let's call it "swordfish_available" which is normally set to 0 / False and then at some point gets set to 1 / True.
With this assumption, I set off to find a variable like that.

To do this, I set sail with my ship and while no swordfish icon was visible, continued to scan the RAM for memory addresses which stay the same (swordfish_available = 0).
As soon as the icon appeared, I made a new search, scanning the previous addresses (which haven't changed all this time) for changed values instead (swordfish_available = 1).
As it turns out, I found the address 0x027E1090 which holds 0x02 when only a regular fish icon is there, and gets increased to 0x03 when the swordfish icon appears.
Now, the idea was to look at which instruction is writing to this address and basically follow the flow of the code to determine which code paths decide if the icon should be spawned or not.
Looking further down the line, I found two addresses which looked like a timer (random looking values that decrease with every frame) and as soon as they hit zero, the swordfish icon appears.
However, it seemed like the timer often just does not start. If that is the case, the player needs to sail back to an island and then go back to sea, then check the timer again and see if it is now running.
Sound familiar?
After a bit of back and forth, I found an interesting function at 02183d0c.
Analysing this function, the following snippet caught my attention:
View attachment 540265

Basically, what happens is this: uVar9 is a randomly generated 32 bit integer. In the if-condition in line 6, uVar9 is multiplied by 100, creating a 64-bit integer.
The 32 most significant bits of this new value are then compared to 0x14 (20 in decimal). If the value is higher than 20, the function returns zero, if the value is lower or equal 20, the function continues.
At this point, I decided to make another assumption: This looks like our first dice roll. If the calculated value is lower than 20, the "swordfish timer" starts, otherwise it doesn't.
In Python-esque code it would look something like this:
Code:
rand_value = random(0, 0xFFFF_FFFF)
timer_check = (rand_value * 100) >> 0x20

if (timer_check > 20):
    return 0
else:
    start swordfish timer

To verify my assumption I basically got rid of the if-condition, changing the b.le (branch if lower or equal) instruction at 0x02183e34 to a b (branch always) instruction. That means, whatever the comparison between the calculated value and 0x14 is, the function will carry on instead of just returning 0.

The result was: Yes! It worked! First try!
This gives us the following cheat, compatible with the US version of Phantom Hourglass

Force Swordfish Spawn [MopSec]
52183E34 DA000004
02183E34 EA000004

What does that mean now when looking back at the previous hints mentioned in section 3 and my personal experience?
Basically, this tells us that the advice to wait on the open sea for two minutes and then going back to an island if no swordfish icon has spawned is absolutely correct! Confirming a GameFAQs thread from 2008, nice! :D
After two minutes, we can rightfully assume that the timer for the icon has never been started and we could wait forever for the icon to spawn. Going back to an island and then back to the open sea gives us another dice roll so we can try again.

Looking at the odds, let's look at the calculation of our values again:
Code:
rand_value = random(0, 0xFFFF_FFFF)
timer_check = (rand_value * 100) >> 0x20

timer_check is now actually a 64-bit integer, which can range from 0 to 0xFFFF_FFFF_FFFF_FFFF. The value is then shifted by 0x20 though, meaning that only the most-signifcant eight bytes are actually stored (for this particular part of the code). Mathematically this gives us a range of 0x00 to 0x63 (0 to 99) for timer_check (because 0xFFFF_FFFF * 100 = 0x0000_0063_FFFF_FF9C and 0x0000_0063_FFFF_FF9C >> 0x20 = 0x63). So we have 100 possible values for timer_check and the timer is only started when the value is below 20. That means the game gives us a chance of about 20% for a swordfish icon to spawn every time the player sets sail from an island. Not too bad, actually :D

6. Second piece of the puzzle: Increase the chance for Neptoona to spawn
Alright, now comes the most difficult part. Surprisingly so, to be honest. I thought figuring out the timer would be much more complicated than messing with the actual fish encounter.
I started off by trying to identify an address in RAM that stores the type of fish that is currently being encountered as soon as the player actually starts fishing.
What I thought would be quick and easy turned out to be really difficult, though.
In the end, I found a few addresses which always held the same value when you have found a specific type of fish but changing these values to completely wrong ones did not change anything in the game, meaning I had no idea if I was looking at the right addresses.

02216979 and 0221697D seemed to always hold the following values:
0256 and 032E: Loovar
01AE and 028C: Toona
0147 and 01EB: Skippyjack
02F8 and 03D7: Swordfish

Further down the line, I have found the address 0x0221692C which depending on the fish species that was encountered, holds the following values:
0x2218654 => Toona
0x22185F4 => Swordfish
0x2218614 => Skippyjack
0x2218674 => Loovar

These values would only be set right when the fish bites the lure, though. In the cutscene leading up to that moment, the address would hold a default value. This is interesting, because I have done some experiments where I made a save state in DesMuME right before starting the fishing sequence and checked to see if I could get the fish type to change and in all of those cases, the fish type stayed the same after reloading the save state and trying again. This does imply that the fish type is calculated right at the beginning of the whole process and not just when the fish bites. Well, either that or the RNG state does not change often enough so the fish type might be generated later, but is kind of set in stone earlier.

Like previously mentioned, manipulating these values did not change anything at all, but I had still hoped to find something interesting when looking which instructions write the values to those addresses.
Following this further and further I noticed that there always seemed to be one specific register which hold a value from 0 to 3 which more or less decided which values would be written to the above mentioned addresses. Let's call this value the "type decider" However, at some point that register would be loaded from a stack address (0x027E37BC) where loads of functions write to, so finding which function writes our "type decider" using memory breakpoints seemed very unfeasible.
Basically, there is one function at 0x020c3fe8 which writes to that address, but it is executed from many places, making tracing the origin of the "type decider" very difficult.
View attachment 540274
param_4 here does hold our type decider value, but without knowing where this function is called from, we cannot tell where it is calculated.
Simply setting a breakpoint on the line marked in green here was not really an option, because the function is called from many places with different purposes.

The next steps honestly cost me an entire weekend without any results, so I won't be sharing much of that here. My idea was to try to find other values in RAM (the size of the fish which is displayed after catching it, the distance between the fish and the player as they're reeling it in which is always displayed) and to try to find interesting addresses and / or functions in their vicinity. This did not yield any significant results.

Running out of options and patience, I went back to the "type decider" and tried to determine where it is actually coming from.
For this, I decided to use Ghidra's cross-referencing functionality to try to find out which functions call 0x020c3fe8 and try to find one that looked interesting to me.
There are quite a lot of candidates, though:
View attachment 540280Z
Basically, my idea was to set an execution breakpoint on all of these addresses one by one (at least those, where the function itself looked interesting enough) and checking when they are hit. Ideally, I would find a point where the function is entered right as the player enters the fishing screen, because that's when the fish type seems to be persisted in memory.

After a few dozen tries, here is what I have found:
View attachment 540290
Here we go, a call of this interesting function which is sometimes used to set the "type decider" variable that is triggered right when we enter the fishing screen. Exactly what I was looking for.

Scrolling a bit further up in this function we find this:
View attachment 540291
Hmmmm an if-condition where a value is multiplied by 100 shifted right by 0x20 and then compared with something else. Have we not seen this before?
Also, for some reason this is repeated a maximum amount of five times, because of that while-loop around the if-condition. Counting Neptoona, there are five types of fish.
Hmmmmmmmm.
To make a long story short, iVar5 in this screenshot is actually stored in register r5 of the processor. If we set a breakpoint at 0x2152888 and then change r5 to any value between 0 and 4, we can successfully force the game to spawn any fish of our choosing, including the Legendary Neptoona!
These are the required values:
r5 = 0 => Skippyjack
r5 = 1 => Toona
r5 = 2 => Loovar
r5 = 3 => Swordfish
r5 = 4 => Neptoona (actually it is not quite that simple, I will get to that later)

How do we write a cheat code that fulfills my goals set in section four, though? For that, we need to reverse this whole process a little more.

Here is what is happening in pseudocode:
Code:
r8 = rand() +r1 (0 in this case)
r3 = (r8 * 100) & 0xFFFFFFFF
r1 = (r8 * 100) >> 0x20
r0=0
r1 = (r8*r0)+r1 ??? => r1 = r1
r14 = 0x21863F0
r3 = 0x14
r14 = (r4*r3)+r14
r1 = (r7*r2)+r1
store r8 at 0x027e0768
r2 = *r14 (0x0A in this case)
while r3 < 5:
    r2 = *(r14 + (r3 >> 2))
    r0 = r0 + r2
    if r0 >= r1:
        r5 = r3 & 0xFF
        break;
    else:
        r3 += 1

Variables like r1, r2 and r3 refer to the actual CPU registers in this case.
So let us talk this through:
In the beginning, a random value is calculated and multiplied by 100. In this case, the most significant 32 bits are stored in r1. This will once again be a random value between 0 and 99 which will be used to determine the fish type. After some more definitions and calculations, the important thing comes in the while loop:
First, a value from memory is being read. The base address for this read is r14 (set before the while loop) but r3, shifted right by two, is added to that address. The value that was read is then added to r0.
Then we compare our random number in r1 with the memory value in r0. If the random value is smaller than r0, r5 is set to r3 and that sets our fish type.
In other words, the fish type is directly determined by how often the code passes through the while loop while not fulfulling the if-condition.

For the final piece of the puzzle, we need to look at r14, this seemingly random memory address where a number is being read from which is then compared with our random number.
In my attemps (fishing at a regular, non-swordfish icon) I had seen that when the instruction r14 = (r4*r3)+r14 is executed, r3 is 2. r14 and r3 are always constant in that case, so r14 becomes 0x2186418.
The first value stored there is 0x0A. This is read by the instruction r2 = *(r14 + (r3 >> 2))) in the first round through the while loop (r3 = 0). In the second round (r3 = 1), the address we read is now 0x218641C (1 << 2 = 4) and the value read from there is 0x1E, meaning r0 becomes 0x28.
Like I mentioned earlier, r5 (and with it the fish type) is set, when r1 is smaller than r0. So the bigger r0 gets, the more likely it is that this condition is fulfilled.

Putting all of this together, let's go through two examples for a regular, non-swordfish, fishing spot when the player has already unlocked the Big Catch Lure (more on that later).
a) r1 = 31 (the RNG has rolled a 31)
1. r0 is set to 0 and r14 is set to 0x2186418 before the while loop​
2. First round of the while loop (r3 = 0): r2 = 10 (read from r14), r0 = r2 + r0 = 10 => r0 is smaller than r1, r3 is incremented, while loop continues​
3. Second round (r3 = 1): r2 = 30 (read from r14 + 1 >> 2), r0 = r2 + r0 = 40 => r0 is greater than r1 => while loop breaks, r5 is set to 1​
4. The player encounters a Toona​
b) r1 = 97
1. r0 is set to 0 and r14 is set to 0x2186418 before the while loop​
2. First round of the while loop (r3 = 0): r2 = 10 (read from r14), r0 = r2 + r0 = 10 => r0 is smaller than r1, r3 is incremented, while loop continues​
3. Second round (r3 = 1): r2 = 30 (read from r14 + 1 >> 2), r0 = r2 + r0 = 40 => r0 is smaller than r1, r3 is incremented, while loop continues​
4. Third round (r3 = 2): r2 = 40 (read from r14 + 2 >> 2), r0 = r2 + r0 = 80 => r0 is smaller than r1, r3 is incremented, while loop continues​
5. Fourth round (r3 = 3): r2 = 20(read from r14 + 3 >> 2), r0 = r2 + r0 = 100=> r0 is greater than r1 => while loop breaks, r5 is set to 3​
6. The player encounters a swordfish​
At this point, two things are very clear: The values stored to wherever r14 points directly give you the probability of encountering that specific fish type or to be precise, they are modifiers for the probability which define a range of possible values for r1 to generate a specific fish type encounter. In this particular example (regular, non-swordfish, fishing spot, Big Catch Lure unlocked), the probability is as follows:
Skippyjack: 10%
Toona: 30%
Loovar: 40%
Swordfish: 20%

The other thing that is clear (which we already knew but hey, it's nice to see how the game handles that) is that this type of fishing spot will never legitimately give you a Neptoona encounter. r0 reaches 100 when r3 = 3 and because r1 can never be greater than 100, r3 will never be set to 4.

At this point, my best guess was that the instruction r14 = (r4*r3)+r14 will change r14 according to the type of fishing spot the player is located on. As r3 is set to 20 right before this instruction, r4 is the only real variable here. In the previous case r4 was set to 2, but when fishing from a swordfish icon, its value becomes 3. This makes our base address r14 0x218642C.
The probability modifiers found at this address are 0x00, 0x00, 0x00, 0x46, 0x1E
Going back to our previous examples, this time for the swordfish icon fishing spots:
a) r1 = 31 (the RNG has rolled a 31)
1. r0 is set to 0 and r14 is set to 0x218642C before the while loop​
2. First round of the while loop (r3 = 0): r2 = 0(read from r14), r0 = r2 + r0 = 0=> r0 is smaller than r1, r3 is incremented, while loop continues​
3. Second round (r3 = 1): r2 = 0 (read from r14 + 1 >> 2), r0 = r2 + r0 = 0 => r0 is smaller than r1, r3 is incremented, while loop continues​
4. Third round (r3 = 2): r2 = 0 (read from r14 + 2 >> 2), r0 = r2 + r0 = 0 => r0 is smaller than r1, r3 is incremented, while loop continues​
5. Fourth round (r3 = 3): r2 = 70 (read from r14 + 3 >> 2), r0 = r2 + r0 = 70 => r0 is greater than r1 => while loop breaks, r5 is set to 3​
6. The player encounters a swordfish​
b) r1 = 97
1. r0 is set to 0 and r14 is set to 0x218642C before the while loop​
2. First round of the while loop (r3 = 0): r2 = 0(read from r14), r0 = r2 + r0 = 0=> r0 is smaller than r1, r3 is incremented, while loop continues​
3. Second round (r3 = 1): r2 = 0 (read from r14 + 1 >> 2), r0 = r2 + r0 = 0 => r0 is smaller than r1, r3 is incremented, while loop continues​
4. Third round (r3 = 2): r2 = 0 (read from r14 + 2 >> 2), r0 = r2 + r0 = 0 => r0 is smaller than r1, r3 is incremented, while loop continues​
5. Fourth round (r3 = 3): r2 = 70 (read from r14 + 3 >> 2), r0 = r2 + r0 = 70 => r0 is smaller than r1, r3 is incremented, while loop continues​
6. Fifth round (r3 = 4): r2 = 30 (read from r14 + 4 >> 2), r0 = r2 + r0 = 100 => r0 is greater than r1 => while loop breaks, r5 is set to 4​
6. The player encounters a swordfish..... Wait what????​
So in that second example, we definitely should have found Neptoona, right?
The reason we do not is right here:
View attachment 540303
(The variable and functions names were named by myself)
Basically, if the game rolls a number which would cause a Neptoona encounter (r5 = 4) there is an additional check to determine if the player has already caught Neptoona, as there is only one of it.
If the fish type was previously set to 4 but Neptoona has already been caught, the fish type is "downgraded" to a regular swordfish.

Before we finish this up, let's look at the different types of fishing spots once more.
Here are the types that I have located in memory and determined the corridors for r1 for:
Code:
r4 = 0 (Fishing just unlocked):
modifiers: 0x37, 0x1E, 0x0F, 0x00, 0x00
0 < r1 < 55: Skippyjack
55 < r1 < 85: Toona
85 < r1 < 100: Loovar

r4 = 1 (??):
modifiers: 0x00, 0x00, 0x00, 0x46, 0x1E
0 < r1 < 70: Swordfish
70 < r1 < 100: Neptoona


r4 = 2 (normal icon, Loovar caught, Big Catch Lure received):
modifiers: 0xA, 0x1E, 0x28, 0x14, 0x00
0 < r1 < 10: Skippyjack
10 < r1 < 40: Toona
40 < r1 < 80: Loovar
80 < r1 < 100: Swordfish
Neptoona never possible

r4 = 3 => Swordfish icon:
modifiers: 0, 0, 0, 0x46, 0x1E
0 < r1 < 70: Swordfish
70 < r1 < 100: Neptoona

As you can see, the first type of spot is the one that gets unlocked as soon as the player receives the fishing rod, with only the three basic species of fish available.
I have explained r4 = 2 and r4 = 3 above. The one question mark that remains is the r4 = 1 type. These are identical to the r4 = 3 type but I have no idea when they appear. May just be an additional type for when the player has caught a Swordfish but not Neptoona, yet, I really don't know. That would also make the additional check that I explained above redundant, as type 3 could just set the probability for Swordfish to 100 in that case. Anyway, I suppose it doesn't really matter.

So what do we learn from this?
The game basically asks you to catch three types of rare fish, first the Loovar, then the regular Rusty Swordfish, then Neptoona. The probability for the first two is 15% and 20% in the regular fishing spots. Not too bad since these spots are guaranteed to always appear (as soon as you have the fishing rod)
For Neptoona, there is a 30% chance but coupled with the 20% chance of a swordfish icon appearing, we end up at a 6% chance that the player gets each time they leave a port.

After all this work, I feel like just setting the chance for a swordfish icon to 100% with the above cheat would already be a huge improvement. But I have not invested that much time to just leave it at that.
May I propose the following probabilities?
Code:
r4 = 0 (Fishing just unlocked):
modifiers: 0x37, 0x1E, 0x0F, 0x00, 0x00
0 < r1 < 35: Skippyjack
35 < r1 < 75: Toona
75 < r1 < 100: Loovar

r4 = 1 (??):
modifiers: 0x00, 0x00, 0x00, 0x46, 0x1E
0 < r1 < 60: Swordfish
60 < r1 < 100: Neptoona


r4 = 2 (normal icon, Loovar caught):
modifiers: 0xA, 0x1E, 0x28, 0x14, 0x00
0 < r1 < 10: Skippyjack
10 < r1 < 40: Toona
40 < r1 < 60: Loovar
60 < r1 < 100: Swordfish
Neptoona never possible

r4 = 3 => Swordfish icon:
modifiers: 0, 0, 0, 0x46, 0x1E
0 < r1 < 60: Swordfish
60 < r1 < 100: Neptoona

If that sounds cool to you, you can use these cheat codes:
Code:
Easy Loovar [MopSec]
921863F0 00000037
221863F0 00000023
921863F4 0000001E
221863F4 00000028
921863F8 0000000F
221863F8 00000019

Easy Neptoona [MopSec]
92186410 00000046
22186410 0000003C
92186414 0000001E
22186414 00000028
92186438 00000046
22186438 0000003C
9218643C 0000001E
2218643C 00000028

Easy Swordfish [MopSec]
92186420 00000028
22186420 00000014
92186424 00000014
22186424 00000028



7. Conclusion
Alright. So. All of this took me about two full weekends to work on. Definitely more time than it would have taken me to play any future Phantom Hourglass run completely legitimately but god damn did it feel rewarding. I hope this was interesting to at least some of you. Feel free to reach out for questions, more details, tell me I've got too much time on my hands, whatever you fancy.

Here are the codes again:
Code:
Force Swordfish Spawn [MopSec]
52183E34 DA000004
02183E34 EA000004

Easy Loovar [MopSec]
921863F0 00000037
221863F0 00000023
921863F4 0000001E
221863F4 00000028
921863F8 0000000F
221863F8 00000019

Easy Neptoona [MopSec]
92186410 00000046
22186410 0000003C
92186414 0000001E
22186414 00000028
92186438 00000046
22186438 0000003C
9218643C 0000001E
2218643C 00000028

Easy Swordfish [MopSec]
92186420 00000028
22186420 00000014
92186424 00000014
22186424 00000028


[1] https://gamefaqs.gamespot.com/boards/932377-the-legend-of-zelda-phantom-hourglass/43467922
[2] ZeldaDungeon PH Walkthrough
The cheat codes at the end should be usable via TwilightMenu, correct?
 

Site & Scene News

Popular threads in this forum