Ackin with Zoinkity
Scientific method: set up a situation where only one or two things will change, search for a change, then keep filtering until you get what you want. Likewise, set up a situation where only one thing is static, keep filtering, etc. With said value, place a breakpoint to see what reads or writes it, then hack. It has its place, but it matters what you're looking for.
For instance, say you want to find how a game is printing text. Animal Forest, for example, which floats directly on Zelda's engine (complete with notes!). How I did it was like so:
1) Look for the first line of actual text to appear. That should be typed font; images are processed an entirely different way.
2) Do a text search in memory to see where this line appears. You'll get more than one hit, which is annoying.
Digression: At this point I did something annoyingly complicated to figure out which one was the original copy. You could, like any sane individual, just set a 'read' breakpoint at the start of each copy. I set breaks to see when ROM was accessed to find the original load routine to know where that was at. Anyway...
3) Set a 'read' breakpoint on the text to see when it is read. That will breakpoint on the first command to access it.
4) Know the pattern.
See, mainline N64 games follow certain patterns, even if they didn't use the usual devkits. To print a string they'll steal code from a C fprintf() function. That means you'll have one routine accepts a string, and that calls a routine that loads each character, then it will call a subroutine that loads the font's image into memory, splattering it someplace in memory with a happy microcode snippet around it. If you're not lucky, you'll wind up with a routine that is loading the character to find out the width of it, but that's obvious because it will be much shorter and return a value V0 that's probably somewhere between 4 and 0x20. Rare to see fonts over 0x20 wide, though it happens. That may seem esoteric, but it is a surprisingly useful trick. V0 will almost always be a return value. If it isn't, they must have stuffed the value they want to return into something else, or it doesn't return anything at all. V0 and occasionally V1 are almost always return values. If it computed a floating point number, that will likely be F0 unless they're doing something funny. The only exception is post-development stuff, but their code is terrible anyway.
The end of a subroutine is always JR RA. JR as in "Jump Register" and RA as in "Return Address". That begins executing code at whatever address RA is, and RA is automatically set to the line following any JAL (jump and link).
So, when you get your hit you can scroll down to the JR RA, set a breakpoint, and look at what the thing is returning. In this case, if you just scrolled past a lot of float math and subroutine you must be in a displayer. Make a note of what you think this is, then advance into the parent routine.
. 5) Scan the code to guess what its use is, label it, then advance to the parent just after this was called.
6) Repeat, scanning each one for something pertinent.
When you think you found a few of these, you can set a breakpoint at the beginning of each routine. When you do so, look at the variables each one gets fed. That leads to the big next rule:
7) You are what you eat.
. A subroutine that uses text will do something to text. A subroutine that uses an image does something with the image. Look at the data that's passed to it.
If they follow the usual rules, A0-A3 will be used first, and after that look at values in SP+10, SP+14, etc. You can tell how many are getting passed either by what's used or by what's set before a JAL. These may either contain values themselves or pointers to values. Pointers, thankfully, are little more than memory addresses. Another way to tell is looking to see which of the A0-A3 variables are stored to the stack or to S0-S7 registers.
In Animal Forest, setting a break on the first line of text to appear will stop someplace in 80090140 (returns width of txt char A0). If you check what called it by advancing one command from its JAL, you'll be within 8009028C. There you'll see that it passed the char as A0 to 80090140, so you do sort of know what was going on. Plus, the return value was a width saved to F0. 8009028C itself, after a bit of poking, computes the render width for single character A0 using a mode set in A1. It converts your float F0 into an integer V0. That's within 800902CC, so you see what called that. If you do this you'll eventually find 80090E98. From the values it reads in you can tell it writes text. A1 is the string, A2 the string length, A3 and SP+10 are both floats meaning they're most likely screen positions. The next four are a color value broken apart into four registers and reassembled, a very foolish thing done in most games.
Now, you found a print routine. You even know how to use it. The only issue is that you can't just "use" it. You need to have some kind of hook or trigger to execute it. It could be a periodic event, a controller key, or heck a GS code.(Sorry hooking isn't any clearer, but it really is a case-by-case trick. Theory is a more practical example than non-portable code.) That's where some of the registers mentioned above are helpful. If you look at the address in the SI rdram register that's the target controller buffer. If you set a read breakpoint on that you can trace it back to whatever copies or reads it next. GS hackers usually call it a day right there and list them as 'activators'. At any rate, someplace in the code will be a routine or two that reads controller button input. If you write out your new script someplace unused you can inject a JAL to the code, print to the screen, and return right in the midst of a controller input read.
Ram and ROM
Instead of printf (I'd have to find some crazy hook for it) here's a little something I did on request for somebody: hooking the debug menu to the title screen for OoT 1.0 and MM 1.0 ;*) Finding debug is a bit inconsequential. I sort of cheated; a while back I wrote a program that takes games formatted like this with a filetable (StarFox64, Zeldas, Animal Forest, etc.) and unbundles them to make a complete uncompressed ROM. From there, searched for the all-too-obvious "debug.c" string. The file with a hit was B9E400 (OoT), so searched for uses of the ROM address. I should note here you can write a ROM address in a couple ways. Sometimes it would be 00B9E400 which would be fed to the PI, in hardware 10B9E400, but B0B9E400 would be the most common.
Anyway, you get two hits in the filetable and one in the file A62940. It's in a table of entries with ROM address ranges and pointers. If you poke around in those files you'd see they are used for the different menus. Typically just browsing for snippets of debug strings will suffice, though sometimes you'll need to disassemble code.
So, you do the next obvious thing: peek at each of the different menus to find one to replace with debug. Again I sort of cheated. By booting in Nemu and setting breaks on each address you can tell when each is triggered. It doesn't take long to realize that BA12C0 is the file select menu (loaded when you press Start on the title screen). So, you replace its entry with the one for debug. Guess sizes based on patterns.
00BA12C0 00BB11E0 80803880 808137C0 00000000 80812394 80812388 becomes 00B9E400 00BA1160 808009C0 80803720 00000000 80801C14 80801C08
Ah, but how do you replace it? Well, you need to decompress the A62840.yaz0 file first. Then, find the values in the file, which would be at +0xE0394. Then, recompress and hope it fits right. If it doesn't, well, things will need to move and that's a per-game issue. Well, it isn't that simple...
Oh, about those values. If you were interested you could trace what each was and how it works. The first two values are not truly a ROM address unless your ROM was entirely decompressed. These kinds of games use a filelist that has two sets of entries. First is the actual ROM start and end addresses, then the start and end addresses if the entire ROM was decompressed, followed with a flag to tell if it was compressed, filesizes, etc. They lookup and access files using the virtual addresses of a decompressed ROM, but the data is at the ROM address in the table.
The 808xxxxx addresses are also not real Ram addresses. They're for a special pointer correction system used with compressed files. After all, you can't tell where the file might be at runtime, but you still need to code pointers to data. If you don't use the TLB to automate it you'll have to correct the pointers with a table as they did here. If you had added, replaced, or moved a pointer with anything other than another pointer you'd have to update the table. Again, most games aren't like that. They have their own nonsense schemes.
Supposing you didn't have a decompressed copy of the ROM, you could peek at memory as the game is running. Find one menu, see what loads it, spot that it comes from a table, then try out different table entries. Find the one unused. Then, if you don't know what file the menu table is in, set a breakpoint on the PI to see when it loads the memory range with the table in. Actually, in this case the file is compressed, so you can set a write breakpoint on the data. Either way, even if you don't have the start address you can assume it by searching backward for Yaz0.
Long story short: search for data. When you find it, find the source. Decompress, edit, recompress, then test.
Also, keep in mind this sounds simpler than it normally would be. I'm familiar with how this kind of game was coded.
Funny thing is that printing a line of text on command poses a somewhat conditional problem. I didn't mention it here, but the screen is only drawn to at certain times. Sure, you can do a direct draw to the video buffer (Bad! Refer to GE's debug menu which flickers horribly due to this.) In fact, because 3D data is processed and returned the way it is you usually have to apply text after the result is returned or attach the script to something that will be processed. Otherwise, you wind up playing around with the Z buffer until everything is layered right.
Also, you need a hook to display your text on-command. Zelda's debug menu is usually hooked to a main menu since this is a simple, solid event that can be replaced without danger. It could not (easily) be hooked to a button sequence because there are multiple controller interpreters. It would only work when, say, you're in-game on foot versus in a menu, etc.
However, you could add, alter, or remove a line of the debug menu. Heck, I reorganized the debug menu in Animal Forest and redrew all the menus in GE. That, by the way, is the last little point:
8) The spectacular is just that. Do something useful with your time.
Anyone can swap images. Anyone can swap models. Anyone can swap text. Only hackers can look at a block of data, work out what it does, index everything that does that to it, then rewrite it all.
Case and point: the multiplayer OoT project. Amazing idea, and apparently he never got multi-screen multiplayer working right. However, during the project he indexed a bunch of data dictating special attribute flags. Look for the YouTube videos of bomb arrows and dark arrows and such. He worked out why certain debug rooms were all buggered up, he worked out how to hook enemies so you could 'activate' their attack patterns, set up a global buffer for more Link colors. In actual fact simple mods; the end result is something new.
Hopefully something in there was useful. To be honest it really is an acquired skill and takes practice. The Zelda anti-community may be unhelpful, but it is a remarkably simple game to tamper with all things considered. Regular information, not a huge number of special cases, simple compression, existing tools, etc.