A few months ago, I had the urge to start disassembling Yoshi's Island. I couldn't take on such a big project alone, so I enlisted Raidenthequick (who comes from the YI speedrunning community) and we started to work.
We're nowhere near done yet, but I've been tossing around the idea of making a thread to document our progress for a while now; I figured now would be as good of a time as any!
Mostly sprite-related. I have fully disassembled bank 03, which is kind of the "main sprite bank" and I'm pretty sure it's good/correct though I'd like a review if anyone would be so kind. (Disassembly/bank03.txt) Currently duking out all the sprites in these tables, which spans multiple banks and will take a while. Me and Alex decided to go half and half on them, though of course I got the half with all the bosses :3
Speaking of, I figured out Hookbill's AI and I documented all of my findings for anyone curious (in bank 01 pretty much at the beginning).
Beyond that, helped figure out the level header layout / copying routine (at $108B05 for the curious) and lots of other random junk.
The stuff I've done thus far is pretty scattered, but:
I've almost completely finished disassembling bank $00. There's some tables in there that I'm unsure of, but I'll iron those out soon. The stuff in there is mostly init routines and such. I started working on plowing through the posts in the offset thread to see what's already been documented, and disassembling them. As a result, I've documented a lot of things across a lot of banks (banks $17, $3F, $0F, $0A and $08 come to mind).
As raiden said, we've been churning out sprites in tandem. I don't know how many he's done exactly, but between us we have in the ballpark of 50 sprites fully disassembled. The sprite I'm working on at this very second is the shy-guys on stilts.
Additionally, I recently started disassembling some of the GSU (super fx) routines in banks $08, $09, $0A and $0B. I've only done a couple thus far, but we've been logging every GSU call that we come across (see the list here).
Over the past few weeks I've also been trying to figure out level data. I haven't moved onto sprites quite yet, but the objects in a level are laid out thusly:
Code
OOOOOOOO YYYYXXXX yyyyxxxx LLLLLLLL HHHHHHHH
OOOOOOOO = object number
YYYYyyyy = y-position
XXXXxxxx = x-position
LLLLLLLL = length
HHHHHHHH = height
objects that cannot be extended in a certain direction omit the last byte
Some minor stuff I've done here and there: some palette and tilemap documentation, a few level modes disassembled, and some graphics.
e: also you can view how the level header is compressed here
In fact, the header as you noted it down in the document is not quite correct anymore; at least some bits of there aren't as unknown anymore as they were back in the EggVine-Days; I'll try and post an updated table of the header bits later on.
On a side note, do any of you happen to know, how to know whether a GFX File is compressed or not?
A bit of background: There is a table at headered PC 0x37B1F, SNES $06:F71F with 0x1D9 entries, 3 byte/entry; ending at headered PC 0x380A9, SNES $06:FEA9.
Each entry is a pointer to one of the GFX files. Some of them are compressed, some of them are not.
The address is calculated as follows: Take an entry, for example entry 0x159, found at headered PC 0x37B1F + 3*0x159 = 0x37F2A, SNES $06:FD2A; there are three bytes there: [4AF85C].
Now these are little-endian, so you revert them: [5CF84A].
Getting their headered PC addresses is easy now, you subtract 0x400000 and add 0x200 => 0x5CF84A - 0x400000 + 0x200 = 0x1CFA4A
So, by retrieving the addresses on their own, I don't see how it's possible to tell whether the data is compressed or not.
On another note, what kind of compression is used? I have found something about the LC_LZ2 compression, which is used for SMW, but it could be, that some other stuff is used as well. Also, the GFX Files are either 4BBP, 2BBP or Tilemap-Format; is there a way to find out, which?
Yeah, that's a lot of questions, but I hope at least the most important one can be answered (finding out whether a GFX File is compressed or uncompressed)
In fact, the header as you noted it down in the document is not quite correct anymore; at least some bits of there aren't as unknown anymore as they were back in the EggVine-Days; I'll try and post an updated table of the header bits later on.
That would be fantastic.
e: to figure out if a GFX file is compressed, try compressing it again; if it shrinks by any noticeable amount, it's not compressed. This works even for unknown compression methods.
I'll figure out which compression method is used later
Sadly, that isn't exactly helpful; there is only one tool, which is able to decompress/compress graphics from/to a ROM, and that is YCompress. The problem is, that I want to write an application, which is able to decompress the graphics and I was hoping for a table or something like that, which tells the game, what method (if any) of decompression it has to use for each graphics file.
Anyways, as for the Level Header, this is what I've come up with:
Looking around for such a table right now. In the meantime, YI uses LC_LZ2 and LC_LZ16. The latter is a gsu routine located at $0A8000. I'll disassemble that routine in a minute.
Sadly LC_LZ16 is not as well-documented as LC_LZ2 and I have no idea how the decompression works by reading this post... :/
The LC_LZ2 was easy to understand and to implement, because we have an Article about it here in the Wiki. Maybe you disassembling that routine can shed some light?
Edit:
I have used DotPeek for disassembling Golden Egg at least partially, it's a mess without comments...
Anyway, I think I have found the Class, which is responsible for decompressing the GFX and by the looks of it he's just translated the SNES Assembly into C# code ^^ Decompress.cs
I'll see if I can get any new insights from this.
Romi must return and release the source of GE, it's better than EggVine, but there are still many things to improve.
Maybe you disassembling that routine can shed some light?
Probably will. Figuring it out right now.
e: Raidenthequick and I have discussed the possibility of making an editor. It's still up in the air, and certainly won't begin until we finish the disassembly, but it's a possibility.
IIIIIIII YYYYYYYY XXXXXXXX
IIIIIIII = sprite ID
YYYYYYYY = y-position
XXXXXXXX = x-position
y-position is shifted right one bit in golden egg
e.g. 01101010 -> 00110101
Sprites are organized by room. The end of the room is marked with two $FF's.
--------------------
Warning: Opinions expressed by Lexie or others in this post do not necessarily reflect the views, opinions, or position of Lexie himself on the matter(s) being discussed therein.
There's no need to post that, it is already exactly known how to read Level Data, you can just ask me =P
Code
Level Header is 10 Bytes
5-Byte Objects:
IIIIIIII ID (can't be 00/FF)
XXXXYYYY High X and High Y
xxxxyyyy Low X and Low Y
WWWWWWWW Width - 1, signed
HHHHHHHH Height - 1, signed
01 76 45 03 04 => Object 01 at (74|65) with Width 4, Height 5
4-Byte Objects:
IIIIIIII ID (can't be 00/FF)
XXXXYYYY High X and High Y
xxxxyyyy Low X and Low Y
LLLLLLLL Length - 1, signed
3C 73 48 FE => Object 3C at (74|38) with length -3
4-Byte Xtended Objects
00000000 This Byte is always zero
XXXXYYYY High X and High Y
xxxxyyyy Low X and Low Y
IIIIIIII ID
00 70 F0 FF => Xtended Object FF at (7F|00)
Sprites (3 Bytes each)
iiiiiiii Low ID
YYYYYYYI High ID and Y-Coordinate
XXXXXXXX X-Coordinate
3D A9 78 => Sprite 0x13D at (78|54)
Screen Exits (Level-Warp, 5 Bytes each)
TTTTTTTT Target page (between 00 and 7F)
LLLLLLLL Destination Level (between 00 and DD)
XXXXXXXX Destination X-Coordinate
YYYYYYYY Destination Y-Coordinate
EEEEEEEE Destination Entrance Type (between 00 and 0A)
70 00 0A 77 05 => At Screen Position (0|7), leads to Level 0, X-Coordinate 0A, Y-Coordinate 77, exits Pipe
Screen Exits (Minibattle, Entrance Type = 0, 5 Bytes each)
TTTTTTTT Target page (between 00 and 7F)
MMMMMMMM Destination Minibattle (between DE and E9)
XXXXXXXX Return X-Coordinate
YYYYYYYY Return Y-Coordinate
LLLLLLLL Return Level (between 00 and E9)
70 DF 0A 77 05 => At Screen Position (0|7), leads to Minibattle 2, returns at X-Coordinate 0A, Y-Coordinate 77, Level 05, exits Door
How to read Level Data:
Code
currentOffset = objectDataOffset
levelObjects = new List<Object>
screenExits = new List<Screenexit>
levelHeader = GenerateLevelHeader(rom, currentOffset)
currentOffset += 10
//An FF is no Object, it's the end of Object Data
while(rom[currentOffset] != FF):
levelObjects.append(GenerateObject(rom, currentOffset))
currentOffset += levelObjects.Last().GetLength() //Either 4 or 5
//Must be incremented or you're stuck at the first FF
currentOffset++
//An FF is no Target Page, it's the end of Screen Exit Data
while(rom[currentOffset] != FF):
screenExits.append(GenerateScreenExit(rom, currentOffset))
currentOffset += 5
currentOffset = spriteDataOffset
sprites = new List<Sprite>
//Two consecutive FF-bytes mark the end of Sprite Data
while(rom[currentOffset] != FF && rom[currentOffset+1] != FF):
sprites.append(GenerateSprite(rom, currentOffset)
currentOffset += 3
Warning: Opinions expressed by Lexie or others in this post do not necessarily reflect the views, opinions, or position of Lexie himself on the matter(s) being discussed therein.
<p4plus2> Lexie: I've been busy
<p4plus2> I'll get to it when I can
--------------------
Warning: Opinions expressed by Lexie or others in this post do not necessarily reflect the views, opinions, or position of Lexie himself on the matter(s) being discussed therein.
Figured out the local and sram high score saving routines
Code
; save high score loop
$01/BE9D 20 E4 BE JSR $BEE4 [$00:BEE4] ;
$01/BEA0 A5 36 LDA $36 [$00:0036] ;
$01/BEA2 05 35 ORA $35 [$00:0035] ;
$01/BEA4 29 F0 AND #$F0 ;
$01/BEA6 F0 3B BEQ $3B [$BEE3] ;
$01/BEA8 22 B7 B2 01 JSL $01B2B7[$01:B2B7] ;
$01/BEAC A2 1F LDX #$1F ;
$01/BEAE AD 85 03 LDA $0385 [$00:0385] ;\
$01/BEB1 10 05 BPL $05 [$BEB8] ; |
$01/BEB3 20 38 BF JSR $BF38 [$00:BF38] ; | if bonus, go to bonus game
$01/BEB6 A2 29 LDX #$29 ; |
$01/BEB8 8E 18 01 STX $0118 [$00:0118] ;/
$01/BEBB A9 F1 LDA #$F1 ;
$01/BEBD 85 4D STA $4D [$00:004D] ;
$01/BEBF EE 20 02 INC $0220 [$00:0220] ;
$01/BEC2 AE 1A 02 LDX $021A [$00:021A] ; load level ID
$01/BEC5 AD 0C 03 LDA $030C [$00:030C] ; load current or high score (whichever is higher)
$01/BEC8 DD B8 02 CMP $02B8,x[$00:02B8] ;\ compare to high score
$01/BECB F0 13 BEQ $13 [$BEE0] ; | branch if you didn't get a high score
$01/BECD 90 11 BCC $11 [$BEE0] ;/
$01/BECF 48 PHA ; push high score
$01/BED0 BD 22 02 LDA $0222,x[$00:0222] ;\
$01/BED3 29 7F AND #$7F ; | branch if you have beat the level before
$01/BED5 F0 08 BEQ $08 [$BEDF] ;/
$01/BED7 BD B8 02 LDA $02B8,x[$00:02B8] ;\
$01/BEDA 09 80 ORA #$80 ; | store old score for the overworld score change (when you get a new high score)
$01/BEDC 8D 20 02 STA $0220 [$00:0220] ;/
$01/BEDF 68 PLA ; pull high score
$01/BEE0 9D B8 02 STA $02B8,x[$00:02B8] ; store new high score
$01/BEE3 60 RTS ; return
Code
; high score sram save loop
$10/82B3 A0 00 LDY #$00 ;
$10/82B5 B9 22 02 LDA $0222,y[$10:0222] ;\
$10/82B8 29 01 AND #$01 ; | branch if you've beaten the level
$10/82BA F0 07 BEQ $07 [$82C3] ;/
$10/82BC B9 B8 02 LDA $02B8,y[$10:02B8] ;\
$10/82BF 09 80 ORA #$80 ; | sets the high bit of the high score address to indicate the level has been beaten
$10/82C1 87 00 STA [$00] [$50:0342] ;/ store high score for the level in RAM
$10/82C3 C2 20 REP #$20 ;
$10/82C5 E6 00 INC $00 [$00:0000] ;
$10/82C7 E2 20 SEP #$20 ;
$10/82C9 C8 INY ;
$10/82CA C0 48 CPY #$48 ;
$10/82CC 90 E7 BCC $E7 [$82B5] ;
relevant addresses:
$7E021A level ID
$7E0220 old score for the overworld score change (when you get a new high score)
$7E0222,x log of your history in the level, indexed by level ID
format: X------Y (X = unlocked but not beaten; Y = beaten level)
if none of the bits are set, you haven't unlocked the level yet
$7E02B8,x table of high scores, indexed by level ID
$7E030C current or high score (whichever is higher)
Additionally, I found the bonus item sram save routine:
Incidentally, when the game saves your high score to sram, it makes a duplicate table and gets the checksum of it before and after a new high score is added. The process is done thusly:
Code
$10/82F2 DA PHX ;\
$10/82F3 DA PHX ; |
$10/82F4 BD 12 80 LDA $8012,x[$10:8012] ; |
$10/82F7 8D 14 30 STA $3014 [$10:3014] ; | get high score checksum before a new high score is added
$10/82FA A2 08 LDX #$08 ; |
$10/82FC A9 83 DE LDA #$DE83 ; |
$10/82FF 22 44 DE 7E JSL $7EDE44[$7E:DE44] ;/ GSU init
$10/8303 FA PLX ;
$10/8304 AD 00 30 LDA $3000 [$10:3000] ;\
$10/8307 9F 70 7E 70 STA $707E70,x[$70:7E70] ;/ store high score checksum
$10/830B BD 12 80 LDA $8012,x[$10:8012] ;\
$10/830E 8D 02 30 STA $3002 [$10:3002] ; |
$10/8311 BD 3A 80 LDA $803A,x[$10:803A] ; |
$10/8314 8D 14 30 STA $3014 [$10:3014] ; | store high scores and get new checksum
$10/8317 A2 08 LDX #$08 ; |
$10/8319 A9 73 DE LDA #$DE73 ; |
$10/831C 22 44 DE 7E JSL $7EDE44[$7E:DE44] ;/ GSU init
$10/8320 FA PLX ;
$10/8321 AD 00 30 LDA $3000 [$10:3000] ;\
$10/8324 9F 76 7E 70 STA $707E76,x[$70:7E76] ;/ store new high score checksum
$10/8328 E2 20 SEP #$20 ;
$10/832A AB PLB ;
$10/832B 6B RTL ;
I've documented the GSU routine as well:
Code
; r1 = source table (dw $7C00, $7C68, $7CD0)
; r10 = desination table (dw $7D38, $7DA0, $7E08)
; indexed by save file
0008:DE73 2A 12 move r2,r10 ; move desination into r2
0008:DE75 02 cache ;\ table copy loop
0008:DE76 FC 34 00 iwt r12,#0034 ; | load number of high scores to save (number of levels total)
0008:DE79 FD 7C DE iwt r13,#DE7C ; | set loop address
0008:DE7C 41 ldw (r1) ; |\ copy high score from source table to desination table
0008:DE7D 32 stw (r2) ; |/
0008:DE7E D1 inc r1 ; |\
0008:DE7F D1 inc r1 ; | | loop until every high score is saved
0008:DE80 D2 inc r2 ; | |
0008:DE81 3C loop ; |/
0008:DE82 D2 inc r2 ;/
; r10 = destination
0008:DE83 02 cache ;\ checksum loop
0008:DE84 FC 34 00 iwt r12,#0034 ; | load number of high scores
0008:DE87 FD 8C DE iwt r13,#DE8C ; | set loop address
0008:DE8A A1 00 ibt r1,#00 ; |
0008:DE8C 4A ldw (r10) ; | load destination table
0008:DE8D 11 51 add r1 ; | add score to r1
0008:DE8F DA inc r10 ; |\
0008:DE90 3C loop ; | | loop through every index ($34 times)
0008:DE91 DA inc r10 ;/ /
0008:DE92 F0 77 77 iwt r0,#7777 ;\ compute checksum
0008:DE95 61 sub r1 ;/ r0 = final checksum
0008:DE96 00 stop ;\ halt gsu processing
0008:DE97 01 nop ;/
The checksum of the duplicate table before the copy is stored to $707E70,x; the checksum of the table after the copy is stored to $707E76,x. Both are indexed with the save file.
--------------------
Warning: Opinions expressed by Lexie or others in this post do not necessarily reflect the views, opinions, or position of Lexie himself on the matter(s) being discussed therein.
Figured out how the game checks the high score checksum:
Code
CODE_108000: 8B PHB ;
CODE_108001: 4B PHK ;
CODE_108002: AB PLB ;
CODE_108003: C2 20 REP #$20 ;
CODE_108005: A2 04 LDX #$04 ;\
CODE_108007: 20 18 80 JSR CODE_108018 ; |
CODE_10800A: CA DEX ; | check checksums loop
CODE_10800B: CA DEX ; |
CODE_10800C: 10 F9 BPL CODE_108007 ;/
CODE_10800E: E2 20 SEP #$20 ;
CODE_108010: AB PLB ;
CODE_108011: 6B RTL ;
DATA_108012: dw $7C00, $7C68, $7CD0
CODE_108018: 86 0E STX $0E ; store save file number
CODE_10801A: BD 12 80 LDA $8012,x ;\
CODE_10801D: 8D 14 30 STA $3014 ;/ load high score table index into r10
CODE_108020: A2 08 LDX #$08 ;\
CODE_108022: A9 83 DE LDA #$DE83 ; | generate checksum
CODE_108025: 22 44 DE 7E JSL CODE_7EDE44 ;/ GSU init
CODE_108029: A6 0E LDX $0E ; load save file
CODE_10802B: AD 00 30 LDA $3000 ;\
CODE_10802E: DF 70 7E 70 CMP $707E70,x ; | check if checksum is correct
CODE_108032: F0 05 BEQ CODE_108039 ;/ return if it is
CODE_108034: 20 A8 80 JSR CODE_1080A8 ; if not, double-check checksum with the table copy
CODE_108037: 80 DF BRA CODE_108018 ; generate new checksum
CODE_108039: 60 RTS ; return
DATA_10803A: dw $7D38, $7DA0, $7E08
; initialization state for high scores saved in sram
DATA_108040: dw $0003, $0080, $0000, $0000
DATA_108048: dw $0000, $0000, $8000, $0080
DATA_108050: dw $0000, $0000, $0000, $0000
DATA_108058: dw $8000, $0080, $0000, $0000
DATA_108060: dw $0000, $0000, $8000, $0080
DATA_108068: dw $0000, $0000, $0000, $0000
DATA_108070: dw $8000, $0080, $0000, $0000
DATA_108078: dw $0000, $0000, $8000, $0080
DATA_108080: dw $0000, $0000, $0000, $0000
DATA_108088: dw $8000, $0080, $0000, $0000
DATA_108090: dw $0000, $0000, $0000, $0000
DATA_108098: dw $0000, $0000, $0000, $0000
DATA_1080A0: dw $0000, $0000, $0000, $0000
CODE_1080A8: BD 3A 80 LDA $803A,x ;\ load high score table copy into r10
CODE_1080AB: 8D 14 30 STA $3014 ;/
CODE_1080AE: A2 08 LDX #$08 ;\
CODE_1080B0: A9 83 DE LDA #$DE83 ; | generate checksum
CODE_1080B3: 22 44 DE 7E JSL CODE_7EDE44 ;/ GSU init
CODE_1080B7: A6 0E LDX $0E ; load save file
CODE_1080B9: AD 00 30 LDA $3000 ;\
CODE_1080BC: DF 76 7E 70 CMP $707E76,x ; | check if checksum is correct
CODE_1080C0: F0 29 BEQ CODE_1080EB ;/ branch if it is
CODE_1080C2: A9 40 80 LDA #$8040 ;\
CODE_1080C5: 8D 02 30 STA $3002 ; |
CODE_1080C8: A9 10 00 LDA #$0010 ; |
CODE_1080CB: 29 FF 00 AND #$00FF ; |
CODE_1080CE: 8D 04 30 STA $3004 ; | clears high scores and generates a new checksum
CODE_1080D1: BD 3A 80 LDA $803A,x ; |
CODE_1080D4: 8D 14 30 STA $3014 ; |
CODE_1080D7: A2 08 LDX #$08 ; |
CODE_1080D9: A9 59 DE LDA #$DE59 ; |
CODE_1080DC: 22 44 DE 7E JSL CODE_7EDE44 ;/ GSU init
CODE_1080E0: A6 0E LDX $0E ; load save file
CODE_1080E2: AD 00 30 LDA $3000 ;\
CODE_1080E5: 9F 76 7E 70 STA $707E76,x ;/ store new checksum
CODE_1080E9: 80 BD BRA CODE_1080A8 ; check checksum again
CODE_1080EB: BD 3A 80 LDA $803A,x ;\
CODE_1080EE: 8D 02 30 STA $3002 ; |
CODE_1080F1: BD 12 80 LDA $8012,x ; | copy the high score table and generate a new checksum
CODE_1080F4: 8D 14 30 STA $3014 ; |
CODE_1080F7: A2 08 LDX #$08 ; |
CODE_1080F9: A9 73 DE LDA #$DE73 ; |
CODE_1080FC: 22 44 DE 7E JSL CODE_7EDE44 ;/ GSU init
CODE_108100: A6 0E LDX $0E ; load save file
CODE_108102: AD 00 30 LDA $3000 ;\
CODE_108105: 9F 70 7E 70 STA $707E70,x ;/ store new checksum
CODE_108109: 60 RTS ;
If a checksum gets corrupted, this routine re-initializes that save file:
Code
; r1 = table of init high scores ($108040)
; r2 = ROM data bank ($10)
; r10 = table of all high scores
0008:DE59 02 cache ;\ clear high scores from SRAM
0008:DE5A FC 34 00 iwt r12,#0034 ; |\ load number of high scores
0008:DE5D FD 67 DE iwt r13,#DE67 ; | | set loop address
0008:DE60 B2 3F DF romb ; | | set the ROM data bank to $10
0008:DE63 2A 12 move r2,r10 ; | | move the address of r10 into r2
0008:DE65 21 1E move r14,r1 ; |/ set ROM buffer address register
0008:DE67 EF getb ; |\
0008:DE68 DE inc r14 ; | | load data from $108040
0008:DE69 3D EF getbh ; |/
0008:DE6B DE inc r14 ; |\
0008:DE6C 32 stw (r2) ; | | store re-initialized high score
0008:DE6D D2 inc r2 ; | | loop until every high score is re-initialized
0008:DE6E 3C loop ; | |
0008:DE6F D2 inc r2 ;/ /
0008:DE70 05 11 bra DE83 ;\ get new checksum
0008:DE72 01 nop ;/
on an related note: the word checksum is forever etched into my eyeballs now
e: this is all done shortly after reset
--------------------
Warning: Opinions expressed by Lexie or others in this post do not necessarily reflect the views, opinions, or position of Lexie himself on the matter(s) being discussed therein.
I'm curious! What will be the use of this disassembly?
Basically if you want to get anything nice done ASM-wise with a game, you need a disassembly so you can see exactly how the game handles doing a given thing.
--------------------
Warning: Opinions expressed by Lexie or others in this post do not necessarily reflect the views, opinions, or position of Lexie himself on the matter(s) being discussed therein.
Follow Us On