Funny thing, those offsets in Gex3 are correct. It's compressed data (headerless zlib) ;*)
Good question though. You have to analyze the code.
First, figure out the address they stuck the table at in rdram. In the case of Gex3 (USA), that's 80080CC0.
There's a couple ways to go about this. The lazy way is to run the game in a debugger and set a read breakpoint on some of the values.
Another way is to search for usage. It's terribly rare you wind up with just a pointer to the table. The most likely scenario is they hardcoded the pointer in ASM, then add offsets to the base value to get entries and elements. In this case, you need to know a little about ASM.
The typical way to make a pointer is to load two halfwords into the same register, like:
ADDIU V0,V0,0CC0 ;V0=80080CC0
LW V1,0CC8 (V0) ;V1=@80080CC8: 0x159C4C0
The tricky part is that both of these treat the lower HW as a signed value. If hte bottom part is 0x8000 through 0xFFFF, you need to increase the upper part by 1. An example:
goal: load a word from 800FDCD0
LW V1,DCD0 ;V1=@800EDCD0: 800F0000+FFFFDCD0
LW V1,DCD0 ;V1=@800FDCD0: 80100000+FFFFDCD0
Technically the LUI also treats the upper value as a signed value. Point of interest:
The point here is that the values are split. You'll want to search for one half, then eyeball the surrounding code for the other half. It's easiest done looking at disassembly, unless you're good at working it out without one. You're looking for a 3C**8008, ****0CC0 for pulling filenames, then 3C**8008, ****0CC8/C for the values. They aren't necessarily going to be near each other either.
One somewhat helpful tool is the search mask feature in HexEdit. You can mask away the registers from the opcodes, and search at 16 and 32bit aligned addresses. It's not a super-user-friendly hex editor, but there's some nice special features hard to find in another freebie.
(Caveat: never looked at this one before and was working rather fast and distracted, so could be wrong about some of this.)
So, searching for filename hits Gex3 you'll get only two hits, both in the same function:
80031128(string): find filename A0 in filetable and load file
Within that, at 800312A8, is where the values are loaded.
A0 = p->buffer on the stack for the decompression object
S1 = start offset
@800FDCD0 = p->target, [spoiler]aligned to a QW boundry so the cache doesn't scream[/spoiler]
S2 = end - start, or compressed size of file
801DA920(p->object, p->target, available size of target)
Should initialize a decompression object saved to the buffer A0, setting target to A1 of size A2.
801DA800(p->object, start, size)
Basically a wrapper for the decompressor itself. It works on blocks 0x4000 at a time and manages all that on the side.
80031E70() loads data, 801DAAE0() decompresses
80031E70(hardware address, p->target, size aligned to DW)
Posts a hardware read request for A2 bytes from hardware A0 to rdram A1
80069B80(p->target, size) invalidates the cache before any magic happens
8006C3D0(aegh...lazy...) posts a message to the PI request thread to pull the data
80069F50(p->queue, p->target, mode) waits(1) until message queue A0 stuffs a message from the PI request thread in A1
Decompresses data. Uses a pointer table at 801DC688 based on "modes".
Long story short, follow that around and you find the zlib tables. You can also tell by the routine: first bit is a "final block" bit, next two the type of block (store, lz+huff, lz IIRC), plus the annoying bitwise nature of it all. They're all headerless, which should be type "Z".