This post describes how to change players’ names arbitrarily (longer or shorter) in the game NHL ’94 for Super Nintendo by changing the ROM.
Players’ names are shown as text in the menus and in-game.
In the team selection menu, it’s their first initial and last name. In other places, it’s the full first and last name.
Goal is to change how names appear in all these places. Have new names’ length be allowed to be different from the old names.
My own intended usage for this is to only change a few names, so optimize for that. And optimize for game stability because I plan to play the game, not just do a quick demo. I don’t care if the result is not elegant or wasteful in terms of memory. The result can operate on an expanded ROM.
The Very Easy, Very Limited Way
Easiest way to change names is a pure data hack. Doesn’t require any knowledge about SNES at all, only basic computer literacy. Open the ROM in a hex editor such as HxD. Scroll down until you see players’ names on the right, and type over them. Hit Ctrl+S to save. Start your game and go.
Why does this work? Because all player names are literally stored as ASCII, no need for character translation tables or whatever. Also, each name is only stored in 1 place.
But of course, the catch is whatever new name you pick has to be the same length or shorter. Ideally, the same length. Shorter only kind of works because you can pad names with spaces, but no guarantees that’ll look right when you see it laid out on the screen. And if the new name is longer it definitely won’t work. Since the goal is to let you change names without length restrictions we have to do something more.
The reasons why you can’t change the name length as a pure data hack take some context to explain. So they are explained further down. The first thing you need to know is the data you just tried to hack- what is it, really.
Player-data Format
This describes where and how players’ names are stored in the ROM, as well as the neighboring data.
There is what I’m calling a “main pointer table”. The table has 28 values. This table is stored at ROM address 0x9CA5E7.
If you dump the raw data for the table it’s
4F EB 9C 00 AC AB 9C 00 32 AE 9C 00 C1 B0 9C 00
40 B3 9C 00 4F C0 9C 00 D7 B5 9C 00 9D B8 9C 00
4D E9 9C 00 1D BB 9C 00 B2 BD 9C 00 DB C2 9C 00
7E C5 9C 00 2B C8 9C 00 9C CA 9C 00 06 CD 9C 00
AC CF 9C 00 37 D2 9C 00 D2 D4 9C 00 60 D7 9C 00
D4 D9 9C 00 4A DC 9C 00 DC DE 9C 00 7E E1 9C 00
AD E6 9C 00 05 E4 9C 00 57 A6 9C 00 04 A9 9C 00
Formatted nicely, the table is
int mainTable[] = {
9CEB4F, // Anaheim
9CABAC, // Boston
9CAE32, // Buffalo
9CB0C1, // Calgary
9CB340, // Chicago
9CC04F, // Dallas
9CB5D7, // Detroit
9CB89D, // Edmonton
9CE94D, // Florida
9CBB1D, // Hartford
9CBDB2, // LA Kings
9CC2DB, // Montreal
9CC57E, // New Jersey
9CC82B, // NY Islanders
9CCA9C, // NY Rangers
9CCD06, // Ottawa
9CCFAC, // Philly
9CD237, // Pittsburgh
9CD4D2, // Quebec
9CD760, // San Jose
9CD9D4, // St Louis
9CDC4A, // Tampa Bay
9CDEDC, // Toronto
9CE17E, // Vancouver
9CE6AD, // Washington
9CE405, // Winnepeg
9CA657, // All Stars East
9CA904 // All Stars West
};
There is 1 value per hockey team.
The first element in the array belongs to Anaheim, and the value is 0x9CEB4F.
The second element in the array belongs to Boston, and the value is 0x9CABAC, and so on.
The teams are ordered alphabetically, except for the all-stars teams at the end. The values themselves aren’t in any particular order.
Although the all-stars teams are comprised of players which exist on other teams, they have their own entries in the table. That’s the game opting for perf and simplicity of code in the perf-memory tradeoff.
Looking at the values of elements in this array, you might think they also look like ROM addresses and you would be right.
As for what’s stored at each address- here’s a description
[H0] [H1] [some number of header bytes] // A two-byte low-endian length H, and a variable-length
// header stream of length H-2
For each player:
[L0] [L1] [player's name] // A two-byte low-endian length L, and then a variable-length string
// of length L-2
[PlayerNumber] // A byte for the player number. It's in a decimal format.
// Leftmost half-byte is the tens place value. Rightmost half-byte is
// the ones place value.
[WeightClass, Agility] // A byte. Leftmost half-byte is the player's weight class.
// Rightmost half-byte is their agility rating.
// Weight class is displayed as a measurement in pounds when
// displayed on the team roster page.
// To convert from weight class to pounds, it's
// pounds = 140 + (weightClass * 8)
// Weight classes range from 0 to 14 in practice. Higher numbers may
// be hacked.
// Agility rating is from 0 to 6.
[Speed, OffenseAware] // A byte. Leftmost half-byte is player's speed. Rightmost half-byte
// is their offense awareness rating.
// Ratings are from 0 to 6.
[DefenseAware, ShotPower] // A byte. Leftmost half-byte is player's defense awareness rating.
// Rightmost half-byte is their shot power.
// Ratings are from 0 to 6.
[Checking, Handedness] // A byte. Leftmost half-byte is player's checking rating. Rightmost
// half-byte is their handedness.
// Checking rating is from 0 to 6.
// For handedness, if the value is even (divisible by 2) they shoot
// left. If it's odd they shoot right.
[StickHandling, ShotAccuracy] // A byte. Leftmost half-byte is player's stick handling rating.
// Rightmost half-byte is their shot accuracy.
// Ratings are from 0 to 6.
[Endurance, Roughness] // A byte. Leftmost half-byte is player's endurance rating. Rightmost
// half-byte is their 'roughness' rating.
// Ratings are from 0 to 6.
// The 'roughnesss' stat is a hidden stat. It exists but is not
// displayed in the game or the manual.
[PassAccuracy, Aggression] // A byte. Leftmost half-byte is player's pass accuracy rating.
// Rightmost half-byte is their aggression rating.
// Ratings are from 0 to 6.
Then, at the end:
[00] [00] // Two zero bytes mean that's the end of the player data for this
// team.
For example, if we dereference and dump what’s stored at 9CC2DB (Montreal), we get
55 00 0E 00 79 02 1D 00 0E 00 13 00 15 00 52 11
21 E8 49 C5 00 01 12 11 07 03 0C 04 00 01 17 12
07 03 0C 04 00 01 16 11 08 04 0E 03 00 01 13 14
09 05 0D 03 00 01 12 11 07 03 0C 05 00 01 13 14
08 04 05 03 00 01 11 17 07 06 03 0D 00 01 16 13
03 0D 07 06 00 0D 00 50 61 74 72 69 63 6B 20 52
6F 79 33 66 44 46 00 00 55 66 ...
Cleaned up, this is
Cleaned up, this is
headerSize = 0x0055;
byte header[] = {0E 00 79 02 1D 00 0E 00 13 00 15 00 52 11
21 E8 49 C5 00 01 12 11 07 03 0C 04 00 01 17 12
07 03 0C 04 00 01 16 11 08 04 0E 03 00 01 13 14
09 05 0D 03 00 01 12 11 07 03 0C 05 00 01 13 14
08 04 05 03 00 01 11 17 07 06 03 0D 00 01 16 13
03 0D 07 06 00 };
player0 = {
// L0 L1 P a t r i c k __ R o y
0D 00 50 61 74 72 69 63 6B 20 52 6F 79 // "Patrick Roy"
// 11 character length +
// 2 byte string size = 13 = 0xD, converted to
// two-byte low endian that's 0D 00
33 // Number 33
66 // Weight class 6, agility 6
44 // speed 4, OffAware 4
46 // DefAware 4, shot power 6
00 // checking 0, handedness L
00 // stick handling 0, shot accuracy 0
55 // endurance 5, roughness 5
66 // pass accuracy 6, aggression 6
}
Etc, for the rest of the players. Then there are some team-related strings at the end.
Heads up that viable (e.g., a subsequent team’s) game data can follow immediately after the data for each team. So no, you’re not free to grow things off the end.
Side thing: stats stored affect the stats in-game although you’ll notice they don’t correlate to those stats exactly. That’s because each game applies RNG. Try it yourself if you want. Boot the game, pick two teams and look at a player’s stats. Reset the game, pick the exact same matchup and player to look at. The stats will be slightly different. If you play a lot of sports games maybe this is not surprising. There is this real-world idea that any team could potentially beat any other team.
Strings are stored with a length rather than being delimited, so that’s nice. However, there’s a bunch of player data parsing code that needs to skip over player’s names to get to their stats. So if you make a player’s name shorter, you’d need to either pad with spaces (could cause things to display weirdly) or update the length value and move that person’s stats plus everyone else on the team’s data backwards. I’ve listed all the stats out because you need to know how much to move if you do it that way, and it’s also helpful to know what it is you’re moving.
And of course for making player’s names longer, there isn’t any extra space off the end, so you need something else entirely. This was the case I was more interested in.
Why not move the whole team data, and update the “main pointer table” entry?
This seemed like a swell idea at first. To get names as long as you want, just make a totally new copy of the player data someplace else with your new names, then adjust the “main pointer table” value to point to it.
Unfortunately that won’t work. It is true the “main table” above is a table of long (four-byte) pointers. But, the upper two bytes of each entry are either
- dead data, sometimes
- dead data, always
By “dead data” I mean the data isn’t read and doesn’t do anything.
Instead of reading data from the upper two bytes, the game will use a hardcoded number which is $9C.
Here’s an example of what I mean. This is from the code to figure out a player’s first initial and last name for the team selection screen.
; Precondition: 9F1CDC and $9f1CDE have been
; initialized with
; main table elements for home and away teams
; respectively.
; We're in 16-bit mode.
;
$9F/C732 A9 9C 00 LDA #$009C ; Hardcode $9C in the upper part
$9F/C735 85 8B STA $8B
$9F/C737 A4 91 LDY $91
$9F/C739 B9 DC 1C LDA $1CDC,y[$9F:1CDC] ; Load the short pointer from the main table
$9F/C73C 85 89 STA $89 ; Store the short pointer in the lower part
;
; ... do stuff that uses direct addressing on
; 24-bit pointer. For example, ADC [$89].
There are multiple instances of this pattern. I found several without trying super hard and I believe there are more. If you want to change a “main pointer table” entry, you don’t just change the entry- you need to change some undetermined number of places in game code.
Why store long pointers in the table if they were gonna do it this way? They could have just stored short pointers. Seems like a waste of space. It might have been, they started out planning for long pointers but wanted to optimize reading/dereferencing of the table values, then they never tried to go back and shrink the table.
Anyway, it’s hard to safely detect all the places that pull values out of this table so it’s difficult to “fix” the table and reclaim the space. I didn’t try. If you want to try it to be absolutely safe you might want to set a breakpoint in the debugger and play the game extensively. We don’t have source code and you can’t statically disassemble this platform.
So suppose you begrugingly accept the fact that all table entries need to live in bank $9C. Can you change lower bytes only and make that work? Unfortunately that’s not such a good idea either. You can adjust these table values to point someplace else in bank $9C, but there isn’t all kinds of free space in that bank to put anything.
If we want to move player data (including player names’ string data) to a different location in memory, a different bank- I prefer a safer option that is more targeted. Completely bypass the use of the “main pointer table” entry JUST when we are loading player names. This lets us be really confident in things working. We make a code change in a specific, testable place that we understand really well.
Detouring the loading of player names
NHL ’94 has a function I’m calling LookupPlayerName().
The code for LookupPlayerName() is
; Function: LookupPlayerName()
; Gets the address of a player's name string,
; based on the player's index on the team and some
; previously-set table data.
;
; Preconditions:
; $91 contains HomeOrAway. 0 == home, 2 == away
; $9F1CDC and $9F1CDE have been initialized with
; main table elements for home and away teams
; respectively.
; $A5 contains PlayerIndexOnTeam
;
; Postconditions:
; $89-$8B = Address of the player's name string
; data. (Includes the length field that comes first)
;
; $A5 gets scrambled.
$9F/C732 A9 9C 00 LDA #$009C ; Hardcode 9C in the upper bytes. Easy enough.
$9F/C735 85 8B STA $8B [$00:008B] ;
;
$9F/C737 A4 91 LDY $91 [$00:0091] ; Load the choice of HomeOrAway. 0 == home, 2 == away
$9F/C739 B9 DC 1C LDA $1CDC,y[$9F:1CDC] ; Load PlayerNamesStartAddress for the corresponding
; team.
; As mentioned in the preconditions this has been set
; up for us. As it happens, it's somewhere far away in
; the chain of function calls.
$9F/C73C 85 89 STA $89 [$00:0089] ; And then store the lower bytes.
LookupPlayerName_GetOffsetOfPerPlayerData:
$9F/C73E A0 00 00 LDY #$0000
$9F/C741 18 CLC
$9F/C742 67 89 ADC [$89] [$9C:C2DB] ; Use the fact that the first two bytes of the
; PlayerNamesStartAddress
; data will give us the offset to the per-player data.
$9F/C744 85 89 STA $89 [$00:0089] ; For example, for Montreal we add 0x55 to get to the
; start of the per-player data.
$9F/C746 80 0A BRA $0A [$C752] ; Goto LookupPlayerName_CheckDone
LookupPlayerName_ForEachPlayerIndexOnTeam:
$9F/C748 A5 89 LDA $89 [$00:0089] ; Load the length of the player's name.
$9F/C74A 18 CLC
$9F/C74B 67 89 ADC [$89] [$9C:C330] ; Increment the current $89-$8B pointer by the length
$9F/C74D 69 08 00 ADC #$0008 ; Plus 8, it's padding (Not really but let's pretend
; it is)
$9F/C750 85 89 STA $89 [$00:0089] ; Update the current $89-$8B pointer
LookupPlayerName_CheckDone:
$9F/C752 C6 A5 DEC $A5 [$00:00A5]
$9F/C754 10 F2 BPL $F2 [$C748] ; branch-if-positive
; LookupPlayerName_ForEachPlayerIndexOnTeam
$9F/C756 6B RTL
The idea is to detour this function. Use an “alternate main table” which actually does honor the long pointer, and shim LookupPlayerName() to use the “alternate main table” instead. From testing I found that LookupPlayerName() is a centralized place and changing it is sufficient.
How the detouring goes is we chuck a payload someplace there’s space (say, $A08100, in expanded ROM space). Then replace the code for LookupPlayerName() with
$9F/C732 5C 00 81 A0 JMP $A08100 ; Jump into expanded ROM space where we put the
; detour payload
NOP
NOP
...
NOPs are not strictly needed but added for hygiene. (Could use a BRK instead when you are getting things running)
As for the payload itself, it’s
$A0/8100 DA PHX ; Caller doesn't like it if X is scrambled
A4 91 LDY $91 ; Load the team index, which has been stored at
; 9F1C98/9F1C9A for home/away.
B9 98 1C LDA $1C98, y[$9F:1C98]
0A ASL ; Multiply by 4 to turn index into an offset
0A ASL
AA TAX ; Use the team index to look up into the
; "alternate main table".
BF 00 D0 A8 LDA 0xA8D000,x ; Load the array element from 0xA8D000, store it in
; $89-$8C
85 89 STA $89
E8 INX
E8 INX
BF 00 D0 A8 LDA 0xA8D000,x
85 8B STA $8B
; The "alternate main table" is formatted a bit
; differently from the "main table".
; Each element is itself an array, one four-byte
; element per player.
; Use $A5 as a counter to get to the right player.
PlayerIndexIncrement:
A5 A5 LDA $A5 ; Sets Z
F0 0C BEQ $0C ; goto DonePlayerIndex
E6 89 INC $89
E6 89 INC $89
E6 89 INC $89
E6 89 INC $89
C6 A5 DEC A5
80 F0 BRA $F0 ; goto PlayerIndexIncrement
DonePlayerIndex:
; We have the element for the right player stored
; at $89-$8C.
; The element is a pointer.
; It'll be either $9Cxxxx if we're keeping the
; original names, or $A8xxxx/whatever if
; we're using new names.
; Dereference it, and store the dereferenced result
; at $89-$8C.
A7 89 LDA [$89]
48 PHA
E6 89 INC $89
E6 89 INC $89
A7 89 LDA [$89]
85 8B STA $8B
68 PLA
85 89 STA $89
FA PLX ; Restore X and return.
6B RTL
One thing that allows the detour to work is there’s nothing in the detour that requires execution out of bank $9F (bank $9F is where the original function is). It’s okay if the code executes in bank $A8. And by a stroke of good fortune, it happens that the original code is also okay running from other banks. No absolute short addressing (local bank). This is hugely helpful when getting things up and running.
The “alternate main table” lives at 0xA8D000, chosen arbitrarily.
If this code were a bit smaller it could actually be copied overtop the implementation of LookupPlayerName() with no jumping out. Original LookupPlayerName is 37 bytes, this routine is 55 bytes. Alas, it won’t fit, so I put it at $A08100. Not a big deal since we are putting stuff in expanded ROM space anyway.
Putting it all together, the full list of things to patch are
- the JMP at the beginning of LookupPlayerName
- code snippet above it’s supposed to JMP to
- the “alternate main table” at 0xA8D000 and set of tables each of its entries points to
- the strings that the “alternate main table” points to
Not too bad.
Could do all this manually. I suggest making a program to do it so that you don’t make mistakes. And plus you can easily configure whatever new player names you want. I made an editor that does the above.
Example in the editor:

Patched game result


Enjoy
Download the editor here:
https://github.com/clandrew/nhl94e/releases/tag/v1.0
Or, find the editor source code here
https://github.com/clandrew/nhl94e
Find this post, in text form here:
https://raw.githubusercontent.com/clandrew/nhl94e/main/docs/PlayerNames.txt
This post explains Lagoon_hitbox.ips, a proof-of-concept patch created to enlarge the hitboxes in the game Lagoon for SNES. The patch was really quick+and+dirty. Nonetheless it’s posted here:
http://secretplace.cml-a.com/edits.php
What follows is a description of the patch and how it works.
First, here are some useful memory locations
$01:0502-0503 - NASIR's X position in the map
$01:0504-0505 - NASIR's Y position in the map
$01:050A- The direction NASIR is facing.
- 00 means right
- 01 means down
- 02 means left
- 03 means up
$01:B710-B717 - The offsets of NASIR's hit box from his position, if he is facing right
$01:B718-B71F - The offsets of NASIR's hit box from his position, if he is facing down
$01:B720-B727 - The offsets of NASIR's hit box from his position, if he is facing left
$01:B728-B730 - The offsets of NASIR's hit box from his position, if he is facing up
In Lagoon, the following code is invoked whenever an action button is pressed, even if you’re not near anything.
$01/9BBD AD 0A 05 LDA $050A ; A = the direction NASIR is facing.
;
$01/9BC0 0A ASL A ; A *= 8
$01/9BC1 0A ASL A ;
$01/9BC2 0A ASL A ;
$01/9BC3 18 CLC ;
$01/9BC4 69 20 ADC #$20 ; A += 0x20
;
; Now A effectively stores an array index with
; which to load hitbox offsets.
; If facing right: A = 0x20
; If facing down: A = 0x28
; If facing left: A = 0x30
; If facing up: A = 0x38
;
$01/9BC6 20 C3 B6 JSR $B6C3 ; Call CalculatePlayerHitboxDimensions()
$01/9BC9 60 RTS
The function CalculatePlayerHitboxDimensions() looks like the following
CalculatePlayerHitboxDimensions:
; Preconditions: A is set to one of
; {0x20, 0x28, 0x30, 0x38} depending on
; the direction NASIR is facing, as described
; above.
; Postconditions: $40, $42, $44, and $46 contain
; the dimensions of NASIR's hit box; left,
; right, top and bottom respectively.
$01/B6C3 C2 20 REP #$20
$01/B6C5 A8 TAY
$01/B6C6 AD 02 05 LDA $0502 ; Load NASIR's X position
$01/B6C9 18 CLC
$01/B6CA 79 F0 B6 ADC $B6F0,y ; Add left edge hitbox offset to NASIR's X position
$01/B6CD 85 40 STA $40 ; Store it as an output
;
$01/B6CF AD 02 05 LDA $0502 ; Load NASIR's X position
$01/B6D2 18 CLC
$01/B6D3 79 F2 B6 ADC $B6F2,y ; Add right edge hitbox offset to NASIR's X position
$01/B6D6 85 42 STA $42 ; Store it as an output
;
$01/B6D8 AD 04 05 LDA $0504 ; Load NASIR's Y position
$01/B6DB 18 CLC
$01/B6DC 79 F4 B6 ADC $B6F4,y ; Add top edge hitbox offset to NASIR's Y position
$01/B6DF 85 44 STA $44 ; Store it as an output
;
$01/B6E1 AD 04 05 LDA $0504 ; Load NASIR's Y position
$01/B6E4 18 CLC
$01/B6E5 79 F6 B6 ADC $B6F6,y ; Add bottom edge hitbox offset to NASIR's Y position
$01/B6E8 85 46 STA $46 ; Store it as an output
;
$01/B6EA 29 FF 00 AND #$00FF ; Clean up and return
$01/B6ED E2 20 SEP #$20
$01/B6EF 60 RTS
If you were to skim the code quickly you’d see it loads hitbox dimensions from memory. From that, you might get the impression they are something dynamic. But, they come from a table hard-coded in ROM data. (you can see this based on the particular address they’re loaded from).
Just so you know, the hitbox table data contains this (Semantics are left, right, top, bottom)
Facing Raw data Plain hex offsets Signed dec offsets
------ -------- ----------------- ------------------
Right 00 00 19 00 F8 FF 08 00 0h, 19h, FFF8h, 8h 0, 25, -8, 8
Down F0 FF 10 00 00 00 0F 00 FFF0h, 10h, 0h, 0Fh -16, 16, 0, 15
Left E7 FF 00 00 F8 FF 08 00 FFE7h, 0h, FFF8h, 8h -25, 0, -8, 8
Up F0 FF 10 00 F1 FF 00 00 FFF0h, 10h, FFF1h, 0h -16, 16, -15, 0
Yes, the game uses slightly differently-sized hitboxes depending on the direction you’re facing.
Now, the patch. What this patch does is instead of offsetting NASIR’s position by values from this table, it hacks it to offset the position simply by a hardcoded number. The hardcoded numbers yield bigger hitboxes than the offsets from the table.
It always applies the hitbox of offsets {-0x30, 0x32, -0x38, 0x30 } = {-48, 50, -56, 48 }. The hitbox size is 98×104 which is about 5 times bigger than the default.
The patch modifies just four operations in CalculatePlayerHitboxDimensions:
CalculatePlayerHitboxDimensions:
; Preconditions: A is set to one of {0x20, 0x28, 0x30, 0x38} depending on
; the direction NASIR is facing, as described above.
; Postconditions: $40, $42, $44, and $46 contain the dimensions of
; NASIR's hit box; left, right, top and bottom respectively.
$01/B6C3 C2 20 REP #$20
$01/B6C5 A8 TAY
$01/B6C6 AD 02 05 LDA $0502 ; Load NASIR's X position
$01/B6C9 18 CLC
$01/B6CA E9 30 00 SBC #$0030 ; Apply left edge hitbox offset -30h
$01/B6CD 85 40 STA $40 ; Store it as an output
;
$01/B6CF AD 02 05 LDA $0502 ; Load NASIR's X position
$01/B6D2 18 CLC
$01/B6D3 69 32 00 ADC #$0032 ; Apply left edge hitbox offset 32h
$01/B6D6 85 42 STA $42 ; Store it as an output
;
$01/B6D8 AD 04 05 LDA $0504 ; Load NASIR's Y position
$01/B6DB 18 CLC
$01/B6DC E9 38 00 SBC #$0038 ; Apply left edge hitbox offset -38h
$01/B6DF 85 44 STA $44 ; Store it as an output
;
$01/B6E1 AD 04 05 LDA $0504 ; Load NASIR's Y position
$01/B6E4 18 CLC
$01/B6E5 69 30 00 ADC #$0030 ; Apply left edge hitbox offset 30h
$01/B6E8 85 46 STA $46 ; Store it as an output
;
$01/B6EA 29 FF 00 AND #$00FF ; Clean up and return
$01/B6ED E2 20 SEP #$20
$01/B6EF 60 RTS
And there you have it, the code for the proof-of-concept posted at the link above.
Here is a small improvement that can be made to the above hack. First, it’d be cleaner to modify the hitbox region offsets in the ROM directly. So let’s do that instead.
To re-iterate, the default values are (with semantics left, right, top, bottom)-
Facing Raw data Plain hex offsets Signed dec offsets
------ -------- ----------------- ------------------
Right 00 00 19 00 F8 FF 08 00 0h, 19h, FFF8h, 8h 0, 25, -8, 8
Down F0 FF 10 00 00 00 0F 00 FFF0h, 10h, 0h, 0Fh -16, 16, 0, 15
Left E7 FF 00 00 F8 FF 08 00 FFE7h, 0h, FFF8h, 8h -25, 0, -8, 8
Up F0 FF 10 00 F1 FF 00 00 FFF0h, 10h, FFF1h, 0h -16, 16, -15, 0
The ROM file offsets for each direction are
Facing Headerless ROM file offset
------ --------------------------
Right B710
Down B718
Left B720
Up B728
While we can patch the table manually, it makes for easier testing of changes if you use a patching program.
Here’s some C++ code for one:
enum Direction
{
FacingRight = 0,
FacingDown = 1,
FacingLeft = 2,
FacingUp = 3
};
struct HitboxDir
{
int Left;
int Right;
int Top;
int Bottom;
};
void PushValue(int b, std::vector<unsigned char>* out)
{
if (b >= 0)
{
assert(b < 256);
out->push_back(b); // little endian
out->push_back(0);
}
else
{
int u = 0x10000 + b;
int low = u & 0xFF;
out->push_back(low);
u >>= 8;
assert(u < 256);
int high = u & 0xFF;
out->push_back(high);
}
}
int main()
{
FILE* pB = nullptr;
fopen_s(&pB, "Lagoon.hitbox.v2.smc", "rb");
// Check size
fseek(pB, 0, SEEK_END);
long sizeB = ftell(pB);
fseek(pB, 0, SEEK_SET);
std::vector<unsigned char> dataB;
dataB.resize(sizeB);
fread(dataB.data(), 1, sizeB, pB);
fclose(pB);
HitboxDir allDirs[] =
{
{0, 25, -8, 8},
{-16, 16, 0, 15},
{-25, 0, -8, 8},
{-16, 16, -15, 0}
};
// Enlarge hitboxes
int ff = 3;
int hf = 2;
allDirs[FacingRight].Right *= ff;
allDirs[FacingRight].Top *= hf;
allDirs[FacingRight].Bottom *= hf;
allDirs[FacingDown].Bottom *= ff;
allDirs[FacingDown].Left *= hf;
allDirs[FacingDown].Right *= hf;
allDirs[FacingLeft].Left *= ff;
allDirs[FacingLeft].Top *= hf;
allDirs[FacingLeft].Bottom *= hf;
allDirs[FacingUp].Top *= ff;
allDirs[FacingUp].Left *= hf;
allDirs[FacingUp].Right *= hf;
// Transfer hitbox info into byte data
std::vector<unsigned char> hitboxBytes;
for (int i = 0; i < 4; ++i)
{
PushValue(allDirs[i].Left, &hitboxBytes);
PushValue(allDirs[i].Right, &hitboxBytes);
PushValue(allDirs[i].Top, &hitboxBytes);
PushValue(allDirs[i].Bottom, &hitboxBytes);
}
// Patch the new tables in
int destOffset = 0xB710;
for (size_t i = 0; i < hitboxBytes.size(); ++i)
{
dataB[destOffset + i] = hitboxBytes[i];
}
fopen_s(&pB, "Lagoon.hitbox.v3.smc", "wb");
fwrite(dataB.data(), 1, dataB.size(), pB);
fclose(pB);
}
Running this patching program yields the table
Facing Raw data Plain hex offsets Signed dec offsets
------ -------- ----------------- ------------------
Right 00 00 4B 00 F0 FF 10 00 0h, 4Bh, FFF0h, 10h 0, 75, -16, 16
Down E0 FF 20 00 00 00 2D 00 FFE0h, 20h, 0, 2Dh -32, 32, 0, 45
Left B5 FF 00 00 F0 FF 10 00 FFB5h, 0h, FFF0h, 10h -75, 0, -16, 16
Up E0 FF 20 00 D3 FF 00 00 FFE0h, 20h, FFD3, 0h -32, 32, -45, 0
Find a convenient, buildable version of patcher here: https://github.com/clandrew/lagoonhitbox/
You can use the patcher to change the hitboxes as you want. If this concept seems useful then it’d be a good idea to fuss with the values until they yield something desirable.
Note:
- All ROM file offsets are on headerless ROMs.
- The hitboxes calculated from the routine described here is used for both talking to NPCs, and combat. While there might be a motive to affect only combat, there’ve also been complaints that the hitboxes when talking to NPCs are too fussy, so YMMV.
- If you make the hitboxes obscenely large it can make the game hard to play. For example, if NPCs A and B are standing close to each other, attempting to talk to A might acidentally cause conversation with B.
This post is also available in text form here:
https://raw.githubusercontent.com/clandrew/lagoonhitbox/master/lagoon_patch_info.txt
One of my favorite retro sports games is Ice Hockey for the NES. This game gets overlooked because it’s on the simplistic side and not tied to a real-life franchise, but it’s still a good time.
I started getting into watching hockey streaming so I’ve been playing this game as something to do during intermission or when waiting for the game to start. One thing led to another and I ended up making a romhack to change the sprites over to be female looking.
Before:

After:

Some more gameplay



Here’s a demo of using the tool for simple task of crossing-out the puck sprite

Instead of crossing out the puck I made other changes to the sprites of course, and used this tool to import them back into the ROM.
Ultimately it may have been possible to use someone’s already-made tool in lieu of making a new one. I tried one, YY-CHR since it’s popular. But, I had problems getting it to understand externally-pasted or externally-imported images, and YY-CHR’s built-in image editor was not sufficient for my workflow. If that one didn’t pan out, it may not bode well for the others. Since I was familiar with the image formats it was not too much to simply make a new thing.
About this game’s mechanic, if you aren’t familiar- this is 4-on-4 hockey. Skater players come in three varieties: light, medium and heavy.
- The light skater is fast but has a weak shot and can be knocked over easily.
- The heavy skater is slow but has a strong shot and tends not to get knocked over.
- And, the medium skater is in the middle.
You pick what type your 4 players should be. A typical game involves a balance of skaters, although ultimately it’s up to you.
The players, ref, goalie and zamboni driver are edited. 👍
To play it you don’t need to use the tool, of course. I’m posting a patch so you can just patch your ROM.
Click here to download the patch (IPS). Patch was created using LunarIPS. LunarIPS is also recommended for applying patches. Apply the patch to an unzipped, NA release ROM, size 40976 bytes. Don’t hesitate to contact me if you want to play but don’t know how any of this works, I’ll set you up.
And click here to download the source spritesheets if you want them for some reason.
If you want to change the sprites to fit your own creative inspiration, you too can use the tool I made, posted to GitHub here.
Update (1/15/2020): Fixed missing bun in one frame of heavy player animation, fixed back-of-ref’s-head-during-penalty animation