Jump to content
NHL'94 Forums

Recommended Posts

Posted (edited)

(Through this remember that SNES is little endian, in many cases you need to byte swap the data to get something readable)

94 SNES has 976 frames. Seems like they put the frame data ahead of each frame's tile data in SNES.

Frame data header is 0x10000-0x10F40 (4 bytes per frame, 976 frames)

image.png

These seem to be 32-bit offsets that are relative from the current code segment, which in this case starts at 0x10000.

The first offset is F40, so the first frame's data should be at 0x10F40.


Frame data for first frame is 0x10F40-0x10F6B -- which points to tile Data from 0x10F6B-0x1108B
Frame 1 uses 9 tiles. 0x120 bytes of tile data. Each tile is 0x20 bytes or 32 bytes of data.

All Tile data is approximately 0x10F4C-0x75880 (4bpp planar composite).

Here is the Tile data for frame 1 in TM:

image.png

 

frame 1 assembled by hand in Photoshop:

image.png

I don't fully understand how the tiles are being placed on the canvas yet. Here is a look at the Frame Data for a few frames:

Frame 1 frame data starts 0x10F40-0x10F6A -- 9 tiles, 2B in length (43) -- assuming tile data = 9*4bytes=36bytes -- 7 bytes extra
Frame 1 tile data starts 0x10F6B
03 80 09 00 FA FF 07 00 E2 FF 04 00 A0 00 80 00 2B 00 02 E4 00 00 FA FF E2 FF 00 08 FF FA FF F2 FF 02 08 FF FE FF 02 00 04 08 00

Frame 2 frame data starts 0x1108B-0x110C3 -- 11 tiles, 39 in length (57) - assuming tile data = 11*4bytes=44bytes -- 13 bytes extra
Frame 2 tile data starts 0x110C4
05 80 0B 00 F7 FF 09 00 E3 FF 05 00 C0 00 A0 00 39 00 01 E5 00 00 FD FF E3 FF 00 08 FF F7 FF F3 FF 02 08 FF 07 00 FB FF 04 08 00 F8 FF 03 00 05 08 00 00 00 03 00 14 08 00

So a couple of things above threw me off. One-- the frame data is an uneven length. I've since found out that for SNES it doesn't matter as much to align the data to even lengths as it is on Genesis since the CPU can handle unaligned data accesses. And when they can shorten 32-bit data to 16-bit, they do, since the SNES CPU doesn't natively support 32-bit data reads (which can lead to the misalignment). Secondly, there is not outright pattern to why the second frame's data is so much bigger than the first frame-- its only two tiles bigger. It doesn't account for the extra space in the header.

Now what I think I do know is that byte 3 = Number of tiles

Frame 1 Byte 3 - 0x09 = 9 tiles

Frame 2 Byte 3 - 0x0B = 11 tiles

I also believe the 17th byte indicates the length of the frame data

Frame 1 Byte 17  - 0x2B = 43 bytes

Frame 2 Byte 17 - 0x39 = 57 bytes

I wanted to see how Frame 2 was assembled:

image.png

image.png

(the above is an approximation-- might be off a pixel on one of the shadow tiles or pants tiles but I believe this mostly right)

So this shows me that the tile laying system is sufficiently complex, its similar to how sprites are laid in the frame in the Genesis version-- seems like tiles on SNES can be laid on a per pixel basis.

There's a couple of options to figure this out-- one is to find the addframe function in the code. Another is to just mess around with changing values in the frame data for frame 1 and see what happens in the game.

I also wanted to find the animation data. The easiest thing to do was to search for SPApflip as it has a bunch of -4096 values which will stand out (below is frame data from NHLPA93 Gen):

image.png

In NHLPA 93's rom for Genesis (I've been referencing this since this is what I've been recently working with disassembling) you can find the animation data for PFLIP direction 4-7 at 0x52AA-52B9:

image.png

 

I asked grok to write me a node script to find `00 F0 ?? ?? 00 F0 ?? 00 F0` and voila, the script found me this at 0xEEA0A-0xEEA19:

image.png

the puck frames are slightly offset between Genesis and SNES here

Genesis references frame 0x18B (395) and 0x18F (399)

SNES references frame 0x18D (397) and 0x191 (401)

I actually had found the animation frames much easier on 94/95PC because the animation header format was the same between Genesis and PC. Here there is a difference:

image.png

Genesis animation header is on the left, SNES animation header is on the right. On both, you've got eight 16-bit offsets to direction 0-7's animation data. However, on Genesis its relative from the start of the Animation Header. On SNES I believe its relative from the start of the code segment, which starts at 0xE0000.

I traced the data back to the start and end and it looks like the animation data for SNES 94 lives at 0xEE0FC-EFF6E.

OK, that's all that's known for now!

Edited by bcrt2000
  • Love 1
  • bcrt2000 changed the title to NHL 94 SNES Sprite/Frame & Animation format
Posted

I found I believe the equivalent of GetHot-- this is one place in the code that I've found accesses the Frame Data Header. I ran this through Grok and this is what it tells me it does:

.9B:C002 read_frame_header_and_adjust_hotspot: ; CODE XREF: sub_9E8C35+43↓P
.9B:C002                               ; sub_9EEB2E+66↓P
.9B:C002                               ; Reads frame data pointer from table, extracts signed byte hotspot/offset values at +$12/+$13,
.9B:C002                               ; applies flip negations based on orientation flags, and optional 90-degree rotation transform.
.9B:C002                               ; Input: A = index (transferred to Y for RAM lookup)
.9B:C002                               ; Output: Adjusted X/Y hotspots in word_7E00A5 (X) and word_7E00A9 (Y)
.9B:C002                               ; Uses: X (preserved), Y (temp), word_7E000C/000E (temp long pointer)
.9B:C002
.9B:C002               PHX             ; Preserve original X
.9B:C003               TAY             ; Y = input index
.9B:C004               LDA word_7E0D97,Y ; Load frame number or state from RAM table
.9B:C007               DEC             ; frame_index - 1
.9B:C008               ASL             ; \
.9B:C009               ASL             ; / *4 for table offset (assuming 4-byte entries)
.9B:C00A               TAX             ; X = table index
.9B:C00B               LDA frame_data_header,X ; Load low 16-bit offset to frame data
.9B:C00F               CLC
.9B:C010               ADC #$8000      ; Add base address low
.9B:C013               STA D,word_7E000C ; Store to temp pointer low
.9B:C015               LDA word_828002,X ; Load high/bank offset
.9B:C019               ADC #$82        ; Add base bank
.9B:C01C               STA D,word_7E000E ; Store to temp pointer bank (forms 24-bit pointer)
.9B:C01E               TYX             ; Y = original X (unused here?)
.9B:C01F               LDY #$12        ; Y = $12 (offset 18 decimal into frame data)
.9B:C022               LDA [D,word_7E000C],Y ; Load byte at frame_data + $12 (likely X hotspot/offset)
.9B:C024               AND #$FF        ; Mask to byte
.9B:C027               CMP #$80        ; Check if signed negative (>= $80)
.9B:C02A               BCC sign_extend_x_done ; If < $80, positive - skip sign extend
.9B:C02C               ORA #$FF00      ; Else, sign extend to negative word
.9B:C02F
.9B:C02F sign_extend_x_done:             ; CODE XREF: read_frame_header_and_adjust_hotspot+28↑j
.9B:C02F               STA D,word_7E00A5 ; Store adjusted X hotspot/offset
.9B:C031               INY             ; Y = $13 (offset 19 decimal)
.9B:C032               LDA [D,word_7E000C],Y ; Load byte at frame_data + $13 (likely Y hotspot/offset)
.9B:C034               AND #$FF        ; Mask to byte
.9B:C037               CMP #$80        ; Check if signed negative
.9B:C03A               BCC sign_extend_y_done ; If < $80, positive - skip
.9B:C03C               ORA #$FF00      ; Else, sign extend
.9B:C03F
.9B:C03F sign_extend_y_done:             ; CODE XREF: read_frame_header_and_adjust_hotspot+38↑j
.9B:C03F               STA D,word_7E00A9 ; Store adjusted Y hotspot/offset
.9B:C041               LDA word_7E0E03,X ; Load orientation flags for this frame
.9B:C044               BIT #$800       ; Test bit 11 (horizontal flip/mirror flag?)
.9B:C047               BEQ no_hflip    ; If clear, skip negate
.9B:C049               LDA #0
.9B:C04C               SEC
.9B:C04D               SBC D,word_7E00A5 ; Negate X
.9B:C04F               STA D,word_7E00A5
.9B:C051
.9B:C051 no_hflip:                       ; CODE XREF: read_frame_header_and_adjust_hotspot+45↑j
.9B:C051               LDA word_7E0E03,X ; Reload flags (redundant, but original)
.9B:C054               BIT #$1000      ; Test bit 12 (vertical flip/mirror flag?)
.9B:C057               BNE no_vflip    ; If set, skip negate (inverted logic: clear = apply flip)
.9B:C059               LDA #0
.9B:C05C               SEC
.9B:C05D               SBC D,word_7E00A9 ; Negate Y
.9B:C05F               STA D,word_7E00A9
.9B:C061
.9B:C061 no_vflip:                       ; CODE XREF: read_frame_header_and_adjust_hotspot+55↑j
.9B:C061               LDA word_7E15DA ; Load potential rotation/mode flag
.9B:C064               BEQ no_rotation ; If zero, skip transform
.9B:C066               LDA D,word_7E00A5 ; Load current X
.9B:C068               LDY D,word_7E00A9 ; Load current Y to Y reg
.9B:C06A               EOR #$FFFF
.9B:C06D               INC             ; Negate X (+1 for two's complement)
.9B:C06E               STA D,word_7E00A9 ; Store -old_X as new Y
.9B:C070               STY D,word_7E00A5 ; Store old_Y as new X (90° clockwise rotation)
.9B:C072
.9B:C072 no_rotation:                    ; CODE XREF: read_frame_header_and_adjust_hotspot+62↑j
.9B:C072               PLX             ; Restore original X
.9B:C073               RTL             ; Return long
.9B:C073 ; End of function read_frame_header_and_adjust_hotspot

The function (read_frame_data_header, which prepares frame hotspot/offset values) only reads two specific bytes from the provided frame data: the signed byte at offset $12 (decimal 18, likely a horizontal/X hotspot or attachment offset) and the signed byte at offset $13 (decimal 19, likely a vertical/Y hotspot or attachment offset). These are sign-extended to 16-bit words before further processing (conditional negations for flips and an optional 90° rotation transform, based on external flags not provided here).

Frame 1

  • Byte at offset $12 (18): $02 (sign-extended to $0002 or +2 decimal).
  • Byte at offset $13 (19): $E4 (sign-extended to $FFE4 or -28 decimal).

Frame 2

  • Byte at offset $12 (18): $01 (sign-extended to $0001 or +1 decimal).
  • Byte at offset $13 (19): $E5 (sign-extended to $FFE5 or -27 decimal).

 

---

So at least that's potentially 2 more values from the header that we know. 

  • Love 1
Posted (edited)

A couple of things-- apparently that wasn't the full GetHot function. It was originally detected as data but when analyzing it it seems like it reads bytes 0x12-0x15. 0x14-15 is another X,Y pair-- potentially a secondary hotspot or some sort of bounding box. It doesn't matter in frame1/2 case as its zero'd out on both.

Secondly, I think I was rash to dismiss the idea that SNES didn't use a sprite system like Genesis. I think that explains why the frame data is different lengths. Frame 2 uses more sprites than Frame 1.

I think I might have figured out how the data is laid out-- I think the first byte in the Frame Data indicates how many sprites are used, and then after that its 7 bytes per Sprite. 

Frame 1 Data (Sprite Header): 03 80 09 00 FA FF 07 00 E2 FF 04 00 A0 00 80 00 2B 00 02 E4 00 00 
Frame 1 Sprite 1: FA FF E2 FF 00 08 FF 
Frame 1 Sprite 2: FA FF F2 FF 02 08 FF 
Frame 1 Sprite 3: FE FF 02 00 04 08 00

Frame 2 Data (Sprite Header): 05 80 0B 00 F7 FF 09 00 E3 FF 05 00 C0 00 A0 00 39 00 01 E5 00 00 
Frame 2 Sprite 1: FD FF E3 FF 00 08 FF 
Frame 2 Sprite 2: F7 FF F3 FF 02 08 FF 
Frame 2 Sprite 3: 07 00 FB FF 04 08 00 
Frame 2 Sprite 4: F8 FF 03 00 05 08 00 
Frame 2 Sprite 5: 00 00 03 00 14 08 00

I'll have to look at more frames to see if this pattern holds up.

I think byte 1-2 are the X coordinate of the sprite, and byte 3-4 are the Y coordinate of the sprite. And then the remaining bytes must have to do with tile selection, and potentially flip bytes and/or palette selection

Frame 1 Data (Sprite Header): 03 80 09 00 FA FF 07 00 E2 FF 04 00 A0 00 80 00 2B 00 02 E4 00 00 
Frame 1 Sprite 1: FA FF E2 FF 00 08 FF <- -6, -30
Frame 1 Sprite 2: FA FF F2 FF 02 08 FF <- -6, -14
Frame 1 Sprite 3: FE FF 02 00 04 08 00 <- -2, 2

Frame 2 Data (Sprite Header): 05 80 0B 00 F7 FF 09 00 E3 FF 05 00 C0 00 A0 00 39 00 01 E5 00 00 
Frame 2 Sprite 1: FD FF E3 FF 00 08 FF <- -3, -29
Frame 2 Sprite 2: F7 FF F3 FF 02 08 FF <- -9, -13
Frame 2 Sprite 3: 07 00 FB FF 04 08 00 <-  7, -5
Frame 2 Sprite 4: F8 FF 03 00 05 08 00 <- -8, 3
Frame 2 Sprite 5: 00 00 03 00 14 08 00 <-  0, 3
Edited by bcrt2000
  • Love 1
Posted (edited)

I want to detail the rest of what I know about the sprite format before I put a pause on this for now. At some point I will write a sprite extractor, but I want to focus on 93/94 disassembly to get something compilable out ASAP.

Here is the file format for the Frame Data/Sprite Header for NHL 94 SNES that is known so far:

I think the one thing that can be confusing is how the tiles are picked--

First is Size-- FF seems to indicate a 2x2 tiled Sprite, and 00 seems to indicate a 1x1 tiled Sprite

If its a 1x1 tiled sprite, its simple, the First Tile gets selected.

If its a 2x2 tiled sprite though, it selects First Tile, First Tile + 1, (First Tile +1) + Stride, (First Tile + 1) + Stride + 1

So for Frame 1:

Frame X Data (Sprite Header): [Number of Sprites] 80 [Number of Tiles] 00 E2 FF 07 00 E2 FF [Stride] 00 A0 00 80 00 [Length of Frame Data] 00 02 E4 [Hostpot 2 X] [Hotspot 2 Y]
Frame 1 Data (Sprite Header): 03                  80 09                00 FA FF 07 00 E2 FF 04       00 A0 00 80 00 2B 					   00 02 E4 00         00 
Frame X Sprite X: [X offset] [Y offset] [First Tile] [Palette?] [Size]
Frame 1 Sprite 1: FA FF      E2 FF      00           08         FF <- -6, -30
Frame 1 Sprite 2: FA F2      FF FF      02           08         FF <- -6, -14
Frame 1 Sprite 3: FE FF      02 00      04           08         00 <- -2, 2

Frame 1 has a Stride of 4.

Sprite 1: Tile 0, 1, 5, 6

Sprite 2: Tile 2, 3, 7, 8

Sprite 3: Tile 4

image.png

image.png

 

Frame 2:

Frame X Data (Sprite Header): [Number of Sprites] 80 [Number of Tiles] 00 E2 FF 07 00 E2 FF [Stride] 00 A0 00 80 00 [Length of Frame Data] 00 02 E4 [Hostpot 2 X] [Hotspot 2 Y]
Frame 2 Data (Sprite Header): 05                  80 0B                00 F7 FF 09 00 E3 FF 05       00 C0 00 A0 00 39                     00 01 E5 00            00 
Frame X Sprite X: [X offset] [Y offset] [First Tile] [Palette?] [Size]
Frame 2 Sprite 1: FD FF      E3 FF      00           08         FF <- -3, -29
Frame 2 Sprite 2: F7 FF      F3 FF      02           08         FF <- -9, -13
Frame 2 Sprite 3: 07 00      FB FF      04           08         00 <-  7, -5
Frame 2 Sprite 4: F8 FF      03 00      05           08         00 <- -8, 3
Frame 2 Sprite 5: 00 00      03 00      14           08         00 <-  0, 3

Frame 2 has a Stride of 5.

Sprite 1: Tile 0,1,6,7

Sprite 2: Tile 2,3,8,9

Sprite 3: Tile 4

Sprite 4: Tile 5

Sprite 5: This is where it gets weird and I don't fully understand-- the value is 20 which is double the value of 10. Could it have something to do with flipping the tile perhaps? I haven't investigated enough. I have to look at more frames to figure this out, and also closely compare how the frame is rendered in the game to how I would compose the frame.

image.png

image.png

Frame X Sprite X: [X offset] [Y offset] [First Tile] [Palette?] [Size]
Frame 2 Sprite 1: FD FF      E3 FF      00           08         FF <- -3, -29
Frame 2 Sprite 2: F7 FF      F3 FF      02           08         FF <- -9, -13
Frame 2 Sprite 3: 07 00      FB FF      04           08         00 <-  7, -5
Frame 2 Sprite 4: F8 FF      03 00      05           08         00 <- -8, 3
Frame 2 Sprite 5: 00 00      03 00      14           08         00 <-  0, 3

 

So 95%+ of the NHL94 frames format is known at this point IMO. There may be some weird things that pop up that should be easy to figure out given by the limited number of bytes used per sprite.

Edited by bcrt2000
  • Love 1
Posted (edited)

I'm not sure if this helps, but I have been using Claude Opus 4.5 to parse through interesting regions of code and had it basically read this post, outlining some information about the animations. I think a lot of it summarizes your stuff, but there might be some interesting tidbits here. As always, cross-reference this before taking it at face value...these assembly banks are massive in terms of tokens, and it could have hallucinated on some of this.:

 

1. Animation System Overview

The NHL '94 SNES animation system consists of three distinct layers:

Layer Purpose ROM Location
Animation Scripts Sequence of frame IDs + timing $0EE0FC - $0EFF6E (Bank 9E)
Frame Header Table 4-byte pointers to Frame Data $010000 - $010F40 (976 × 4 bytes)
Frame + Tile Data Sprite composition + 4bpp tiles $010F4C - $075880

1.1 ROM Layout Details

 
$010000 - $010F3F : Frame Header Table (976 entries × 4 bytes = 3904 bytes)
Each entry: [Low Word] [High Word] → Relative offset from $010000
 
$010F40 : First Frame Data begins (Frame 1)
$010F6B : First Frame Tile Data begins
...
$075880 : End of Tile Data (approximate)
 
$0EE0FC - $0EFF6E : Animation Script Data (relative to code segment $0E0000)

1.2 Key Differences: Genesis vs SNES

Aspect Genesis (NHLPA 93) SNES (NHL 94)
Animation Header Offsets Relative to header start Relative to code segment ($0E0000)
Puck Frame IDs 0x18B (395), 0x18F (399) 0x18D (397), 0x191 (401)
Data Alignment Must be even Unaligned OK (CPU handles)
Tile Format 4bpp linear 4bpp planar composite

2. RAM Address Map (Per-Player, Indexed by X)

Address Name Purpose
$11A3,X Script Base Pointer to animation script start within Bank 9D
$11C3,X Script Offset Current position within script (advances by 4)
$11E3,X Animation Timer Counts down; triggers frame advance when negative
$0D97,X Current Sprite ID Frame/Sprite index (1-976) for rendering
$14E3,X Action State High-level action (0=None, 1=KO/Pass, 8=Slapshot, etc.)
$0ADB Global Speed Timer decrement rate (affects all players)
$1163,X Player Speed Factor Per-player speed modifier

3. Core Animation Routine: CODE_9EA425

Location: 

 

bank_9E.asm Lines 4305-4408 (Address $9EA425)

 

4. Animation Script Format

4.1 Script Entry Structure (4 Bytes)

Based on the ASM analysis of CODE_9EA425 lines 4356-4400:

Byte Offset Size Description
0-1 [$89],Y Word Sprite/Frame ID (1-976). Stored to $0D97,X
2-3 [$89],Y+2 Word Duration/Timer Value. Loaded to $11E3,X

4.2 Special Opcodes

Value Meaning Action
Negative Duration End of Script Reset $11C3,X to 0, clear $14E3,X
$FDFE - $FEC2 Special Actions Triggers alternate behavior (e.g., falling animation)

4.3 Evidence from ASM

 
; Line 4356: Load SpriteID (Bytes 0-1)
LDA.B [$89],Y ; 9EA480 | Load word at script+offset
STA.B $AD ; Store to temp
 
; Line 4393: Load Duration (Bytes 2-3)
LDY.B $A5 ; 9EA4C6
INY ; Y = offset + 2
INY
LDA.B [$89],Y ; Load Duration word
...
STA.W $11E3,X ; Reload timer

5. Frame Data Structure (Per bcrt2000 Research)

5.1 Frame Header Table

  • Location: $010000 (Bank 82 relative: $828000)
  • Size: 976 frames × 4 bytes = 3904 bytes
  • Format: [Low Word] [High Word] → 24-bit pointer to Frame Data

5.2 Sprite Header Layout (22 Bytes)

Offset Size Name Description
$00 1 NumSprites Number of sprites in this frame
$01 1 Flag Always $80
$02 1 NumTiles Total tile count for DMA
$03 1 Padding Always $00
$04-$05 2 Unknown (Often $FFxx)
$06-$07 2 Unknown  
$08-$09 2 Unknown  
$0A 1 Stride Row stride for 2×2 tile calculation
$0B 1 Padding Always $00
$0C-$0D 2 Unknown  
$0E-$0F 2 Unknown  
$10-$11 2 DataLength Total length of Frame Data in bytes
$12 1 HotspotX Signed X offset (sign-extended)
$13 1 HotspotY Signed Y offset (sign-extended)
$14 1 Hotspot2X Secondary X (often zero)
$15 1 Hotspot2Y Secondary Y (often zero)

5.3 Sprite Entry Format (7 Bytes Each)

After the 22-byte header, sprite entries follow:

Bytes Name Description
0-1 X Offset Signed 16-bit, Little Endian
2-3 Y Offset Signed 16-bit, Little Endian
4 First Tile Starting tile index
5 Palette/Flags Usually $08
6 Size $FF = 2×2 tiles, $00 = 1×1 tile

5.4 Tile Selection Algorithm

For 1×1 sprites (Size = $00😞

  • Use First Tile directly

For 2×2 sprites (Size = $FF😞

 
Tile[0] = First Tile
Tile[1] = First Tile + 1
Tile[2] = First Tile + 1 + Stride
Tile[3] = First Tile + 1 + Stride + 1

5.5 Concrete Examples

Frame 1 (Stride = 4, 3 Sprites, 9 Tiles)

 
Header: 03 80 09 00 FA FF 07 00 E2 FF 04 00 A0 00 80 00 2B 00 02 E4 00 00
^NumSpr ^NumTile ^Stride ^Hot
 
Sprite 1: FA FF E2 FF 00 08 FF → X=-6, Y=-30, Tile 0, 2×2 → Tiles 0,1,5,6
Sprite 2: FA FF F2 FF 02 08 FF → X=-6, Y=-14, Tile 2, 2×2 → Tiles 2,3,7,8
Sprite 3: FE FF 02 00 04 08 00 → X=-2, Y=+2, Tile 4, 1×1 → Tile 4

Frame 2 (Stride = 5, 5 Sprites, 11 Tiles)

 
Header: 05 80 0B 00 F7 FF 09 00 E3 FF 05 00 C0 00 A0 00 39 00 01 E5 00 00
^NumSpr ^NumTile ^Stride ^Hot
 
Sprite 1: FD FF E3 FF 00 08 FF → X=-3, Y=-29, Tile 0, 2×2 → Tiles 0,1,6,7
Sprite 2: F7 FF F3 FF 02 08 FF → X=-9, Y=-13, Tile 2, 2×2 → Tiles 2,3,8,9
Sprite 3: 07 00 FB FF 04 08 00 → X=+7, Y=-5, Tile 4, 1×1 → Tile 4
Sprite 4: F8 FF 03 00 05 08 00 → X=-8, Y=+3, Tile 5, 1×1 → Tile 5
Sprite 5: 00 00 03 00 14 08 00 → X=0, Y=+3, Tile 20,1×1 → Tile 20 (flipped?)

Note: Sprite 5's First Tile = $14 (20) is unusual. May indicate tile flipping via high bits.


5.5 Hotspot Extraction Routine: CODE_9BC002

Location: 

 

bank_9B.asm Lines 5494-5557 (Address $9BC002)

This function reads the X/Y hotspot values from Frame Data and applies flip/rotation transforms.

Pseudocode

 
CODE_9BC002 (read_frame_header_and_adjust_hotspot):
PHX ; Save X
TAY ; Y = player index
 
A = RAM[$0D97 + Y] ; Load current SpriteID
A = (A - 1) * 4 ; Table index (4 bytes/entry)
X = A
 
// Build 24-bit pointer to Frame Data
$0C = DATA8_828000[X] + $8000
$0E = DATA8_828002[X] + $82
 
X = Y ; Restore player index
Y = $12 ; Offset to Hotspot X
 
A = [$0C + Y] & 0xFF ; Read byte at offset $12
IF A >= $80 THEN A |= $FF00 ; Sign extend if negative
$A5 = A ; Store Hotspot X
 
Y++ ; Y = $13
A = [$0C + Y] & 0xFF ; Read byte at offset $13
IF A >= $80 THEN A |= $FF00 ; Sign extend if negative
$A9 = A ; Store Hotspot Y
 
// Apply H-Flip
IF (RAM[$0E03 + X] & $0800) THEN
$A5 = 0 - $A5 ; Negate X
 
// Apply V-Flip (note: inverted logic)
IF !(RAM[$0E03 + X] & $1000) THEN
$A9 = 0 - $A9 ; Negate Y
 
// Apply 90° Rotation
IF RAM[$15DA] != 0 THEN
temp = $A5
$A5 = $A9 ; X = old Y
$A9 = -temp ; Y = -old X
 
PLX ; Restore X
RTL

Key RAM Addresses

Address Purpose
$0D97,Y Current Sprite ID (used to index Frame Header Table)
DATA8_828000 Frame Header Table (4 bytes × 976 entries)
$0E03,X Orientation Flags (bit 11 = H-flip, bit 12 = V-flip)
$15DA Rotation Mode (non-zero = 90° clockwise rotation)
$A5 Output: Adjusted Hotspot X
$A9 Output: Adjusted Hotspot Y

Callers (from bank_9E.asm)

This function is called 8 times during rendering:

  • Line 1624 ($9E8C78)
  • Line 3047 ($9E982C)
  • Line 3227 ($9E99B8)
  • Line 5769 ($9EAFD8)
  • Line 11095 ($9EDCF7)
  • Line 12489 ($9EE805)
  • Line 12935 ($9EEB94)
  • Line 13403 ($9EEFCD)

6. Animation Header Table (SPAflip / SPAlist)

6.1 Location

  • SNES: $EE0FC (Bank 9E, Address 0xEE0FC relative to code segment)
  • Genesis (NHLPA 93): $0052AA (for comparison)

6.2 Format

 
[Direction 0 Offset] [Direction 1 Offset] ... [Direction 7 Offset]

Each is a 16-bit word pointing to the animation sequence for that direction.

6.3 Example (from bcrt2000's screenshot)

 
000EEA00: 04 00 96 01 04 00 91 01 FC FF 8D 01 ...
^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^ ^^^^^
Dur0 ID0 Dur1 ID1 Loop Target

Interpretation:

  • 04 00 = Duration 4
  • 96 01 = Frame ID 0x0196 (406)
  • FC FF = Loop Opcode (0xFC) with target 0xFF

7. Key Differences from My Previous Implementation

Aspect Previous Code Authentic Behavior
SpriteID Size 8-bit (
 
byte)
16-bit (ushort) - Supports 976 frames
Byte Order [SpriteID] [Duration] [VelX] [VelY] [SpriteID Low][SpriteID High] [Duration Low][Duration High]
Bank $9D0000 + offset Correct, but must use long pointer via $89/$8B
Timer Init +1 on negative Actually ~A + 1 (two's complement) for sign handling
Loop Opcode 0xFC byte Negative Duration value triggers reset

 
Edited by vancecookcobxin
  • Like 1

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

  • Who's Online   0 Members, 0 Anonymous, 211 Guests (See full list)

    • There are no registered users currently online
×
×
  • Create New...