Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
/coilsnake/assets/ccc/lib/*.ccs
.DS_store
git_commit.py
/venv/
37 changes: 33 additions & 4 deletions coilsnake/model/eb/fonts.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,17 @@
FONT_IMAGE_ARRANGEMENT_WIDTH = 16
_FONT_IMAGE_ARRANGEMENT_96 = EbTileArrangement(width=FONT_IMAGE_ARRANGEMENT_WIDTH, height=6)
_FONT_IMAGE_ARRANGEMENT_128 = EbTileArrangement(width=FONT_IMAGE_ARRANGEMENT_WIDTH, height=8)
_FONT_IMAGE_ARRANGEMENT_224 = EbTileArrangement(width=FONT_IMAGE_ARRANGEMENT_WIDTH, height=14)

for y in range(_FONT_IMAGE_ARRANGEMENT_96.height):
for x in range(_FONT_IMAGE_ARRANGEMENT_96.width):
_FONT_IMAGE_ARRANGEMENT_96[x, y].tile = y * _FONT_IMAGE_ARRANGEMENT_96.width + x
for y in range(_FONT_IMAGE_ARRANGEMENT_128.height):
for x in range(_FONT_IMAGE_ARRANGEMENT_128.width):
_FONT_IMAGE_ARRANGEMENT_128[x, y].tile = y * _FONT_IMAGE_ARRANGEMENT_128.width + x
for y in range(_FONT_IMAGE_ARRANGEMENT_224.height):
for x in range(_FONT_IMAGE_ARRANGEMENT_224.width):
_FONT_IMAGE_ARRANGEMENT_224[x, y].tile = y * _FONT_IMAGE_ARRANGEMENT_224.width + x


class EbFont(object):
Expand All @@ -29,9 +33,28 @@ def __init__(self, num_characters=96, tile_width=16, tile_height=8):

def from_block(self, block, tileset_offset, character_widths_offset):
self.tileset.from_block(block=block, offset=tileset_offset, bpp=1)
for i in range(96, self.num_characters):
self.tileset.clear_tile(i, color=1)
self.character_widths = block[character_widths_offset:character_widths_offset + self.num_characters].to_list()
if self.num_characters == 224:
# to allow 224 characters in the font, we modify how the game access the font tileset.
# by default, the game access a character by doing : (char_id - 0x50) & 0x7f
# we change that in coilsnake/modules/eb/FontModule.py to : char_id - 0x20
# this mean we have 0x30 new characters to add BEFORE the current tileset
for i in range(223, -1, -1):
if i < 0x30 or i >= 0x90:
# whiten tiles that represents used control codes to indicate that they shouldn't be used
if i == 0 or i == 2 or i == 15:
self.tileset.clear_tile(i, color=0)
else:
self.tileset.clear_tile(i, color=1)
else:
self.tileset.tiles[i] = self.tileset.tiles[i - 0x30]
self.character_widths = block[
character_widths_offset - 0x30 :
character_widths_offset - 0x30 + self.num_characters
].to_list()
else:
for i in range(96, self.num_characters):
self.tileset.clear_tile(i, color=1)
self.character_widths = block[character_widths_offset:character_widths_offset + self.num_characters].to_list()

def to_block(self, block):
tileset_offset = block.allocate(size=self.tileset.block_size(bpp=1))
Expand All @@ -47,6 +70,8 @@ def to_files(self, image_file, widths_file, image_format="png", widths_format="y
image = _FONT_IMAGE_ARRANGEMENT_96.image(self.tileset, FONT_IMAGE_PALETTE)
elif self.num_characters == 128:
image = _FONT_IMAGE_ARRANGEMENT_128.image(self.tileset, FONT_IMAGE_PALETTE)
elif self.num_characters == 224:
image = _FONT_IMAGE_ARRANGEMENT_224.image(self.tileset, FONT_IMAGE_PALETTE)
image.save(image_file, image_format)
del image

Expand All @@ -61,6 +86,8 @@ def from_files(self, image_file, widths_file, image_format="png", widths_format=
self.tileset.from_image(image, _FONT_IMAGE_ARRANGEMENT_96, FONT_IMAGE_PALETTE)
elif self.num_characters == 128:
self.tileset.from_image(image, _FONT_IMAGE_ARRANGEMENT_128, FONT_IMAGE_PALETTE)
elif self.num_characters == 224:
self.tileset.from_image(image, _FONT_IMAGE_ARRANGEMENT_224, FONT_IMAGE_PALETTE)
del image

if widths_format == "yml":
Expand All @@ -72,6 +99,8 @@ def image_size(self):
arr = _FONT_IMAGE_ARRANGEMENT_96
elif self.num_characters == 128:
arr = _FONT_IMAGE_ARRANGEMENT_128
elif self.num_characters == 224:
arr = _FONT_IMAGE_ARRANGEMENT_224

return arr.width * self.tileset.tile_width, arr.height * self.tileset.tile_height

Expand Down Expand Up @@ -126,4 +155,4 @@ def from_files(self, image_file, image_format="png"):
image = open_indexed_image(image_file)
self.palette.from_image(image)
self.tileset.from_image(image, _CREDITS_PREVIEW_ARRANGEMENT, self.palette)
del image
del image
112 changes: 112 additions & 0 deletions coilsnake/modules/eb/FontModule.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from coilsnake.util.common.image import open_indexed_image
from coilsnake.util.common.yml import yml_load, yml_dump
from coilsnake.util.eb.pointer import from_snes_address, to_snes_address
from coilsnake.util.eb.helper import patch


log = logging.getLogger(__name__)
Expand All @@ -18,6 +19,28 @@
CREDITS_GRAPHICS_ASM_POINTER = 0x4f1a7
CREDITS_PALETTES_ADDRESS = 0x21e914

# Constants for the ROM patching
SBC = 0xE9
NOP = 0xEA
LDA = 0xA9
LDX = 0xA2
FUNCTION_OFFSETS = {
"printStat" : 0xC19282, #Original_Address: 0xC19249
"printNewlineIfNeeded" : (0xEF01F5, 0xEF01F8), #Original_Address: 0xEF01D2
"getStringRenderWidth" : (0xC43E6C, 0xC43E6F), #Original_Address: 0xC43E31
"prefillKeyboardInput" : (0xC440E0, 0xC440E3, 0xC44151, 0xC4418D),#Original_Address: 0xC440B5
"emptyKeyboardInput" : (0xC441D5, 0xC441F9), #Original_Address: 0xC441B7
"writeCharacterToKeyboardInputBuffer" : (0xC44272, 0xC44275), #Original_Address: 0xC4424A
"renderSmallTextToVRAM" : (0xC4454A, 0xC4454D), #Original_Address: 0xC444FB
"printAutoNewline" : (0xC44752, 0xC44755), #Original_Address: 0xC445E1
"renderVWFCharToWindow" : (0xC44EEC, 0xC44EEF), #Original_Address: 0xC44E61
"getTextWidth" : (0xC4501D, 0xC45020), #Original_Address: 0xC44FF3
"printPrice" : 0xC450E1, #Original_Address: 0xC4507A
"prepareWindowGraphics" : (0xC47D3B, 0xC47D3E), #Original_Address: 0xC47C3F
"renderWholeCharacter" : (0xC48289, 0xC4828C), #Original_Address: 0xC4827B
"renderLargeCharacterInternal" : (0xC499A6, 0xC499A9), #Original_Address: 0xC4999B
"renderCastNameText" : (0xC4E5E6, 0xC4E5E9), #Original_Address: 0xC4E583
}

class FontModule(EbModule):
NAME = "Fonts"
Expand Down Expand Up @@ -62,6 +85,95 @@ def write_to_rom(self, rom):
self.font_pointer_table.to_block(block=rom,
offset=from_snes_address(FONT_POINTER_TABLE_OFFSET))

# we're patching the ROM if the user uses 224 character per font
# as of right now, to acces a certain character, the game does this : (char_id - 0x50) & 0x7f
# we're changing it to that : char_id - 0x20
# so all SBC #$50 becomes SBC #$20
# and all AND #$007F becomes NOPs (we're skipping them)
# we're also changing the internal ID of some hardcoded character IDs
if all(font.num_characters == 224 for font in self.fonts):
log.debug("Patching the ROM so it can support 224 character per font")

# AND #$007F -> NOP NOP NOP
patch(rom, 3, FUNCTION_OFFSETS["printStat"], [NOP, NOP, NOP])

# SBC #$50 -> SBC #$20
# AND #$007F -> NOP NOP NOP
patch(rom, 3, FUNCTION_OFFSETS["printNewlineIfNeeded"][0], [SBC, 0x20, 0x00])
patch(rom, 3, FUNCTION_OFFSETS["printNewlineIfNeeded"][1], [NOP, NOP, NOP])

# SBC #$50 -> SBC #$20
# AND #$007F -> NOP NOP NOP
patch(rom, 3, FUNCTION_OFFSETS["getStringRenderWidth"][0], [SBC, 0x20, 0x00])
patch(rom, 3, FUNCTION_OFFSETS["getStringRenderWidth"][1], [NOP, NOP, NOP])

# SBC #$50 -> SBC #$20
# AND #$007F -> NOP NOP NOP
patch(rom, 3, FUNCTION_OFFSETS["prefillKeyboardInput"][0], [SBC, 0x20, 0x00])
patch(rom, 3, FUNCTION_OFFSETS["prefillKeyboardInput"][1], [NOP, NOP, NOP])
# LDA #32 -> LDA #$50
# this is an hardcoded bullet point char ID
patch(rom, 2, FUNCTION_OFFSETS["prefillKeyboardInput"][2], [LDA, 0x50])
# LDA #3 -> LDA #$33
# this is an hardcoded middle dot char ID
patch(rom, 2, FUNCTION_OFFSETS["prefillKeyboardInput"][3], [LDA, 0x33])

# LDA #3 -> LDA #$33
# this is an hardcoded middle dot char ID
patch(rom, 3, FUNCTION_OFFSETS["emptyKeyboardInput"][0], [LDA, 0x33, 0x00])
# LDA #32 -> LDA #$50
# this is an hardcoded bullet point char ID
patch(rom, 2, FUNCTION_OFFSETS["emptyKeyboardInput"][1], [LDA, 0x50])

# SBC #$50 -> SBC #$20
# AND #$007F -> NOP NOP NOP
patch(rom, 3, FUNCTION_OFFSETS["writeCharacterToKeyboardInputBuffer"][0], [SBC, 0x20, 0x00])
patch(rom, 3, FUNCTION_OFFSETS["writeCharacterToKeyboardInputBuffer"][1], [NOP, NOP, NOP])

# SBC #CHAR::SPACE -> SBC #$20
# AND #$007F -> NOP NOP NOP
patch(rom, 3, FUNCTION_OFFSETS["renderSmallTextToVRAM"][0], [SBC, 0x20, 0x00])
patch(rom, 3, FUNCTION_OFFSETS["renderSmallTextToVRAM"][1], [NOP, NOP, NOP])

# SBC #$50 -> SBC #$20
# AND #$007F -> NOP NOP NOP
patch(rom, 3, FUNCTION_OFFSETS["printAutoNewline"][0], [SBC, 0x20, 0x00])
patch(rom, 3, FUNCTION_OFFSETS["printAutoNewline"][1], [NOP, NOP, NOP])

# SBC #$50 -> SBC #$20
# AND #$007F -> NOP NOP NOP
patch(rom, 3, FUNCTION_OFFSETS["renderVWFCharToWindow"][0], [SBC, 0x20, 0x00])
patch(rom, 3, FUNCTION_OFFSETS["renderVWFCharToWindow"][1], [NOP, NOP, NOP])

# SBC #$50 -> SBC #$20
# AND #$007F -> NOP NOP NOP
patch(rom, 3, FUNCTION_OFFSETS["getTextWidth"][0], [SBC, 0x20, 0x00])
patch(rom, 3, FUNCTION_OFFSETS["getTextWidth"][1], [NOP, NOP, NOP])

# LDX #4 -> LDX #$34
# this is an hardcoded dollar sign char ID
patch(rom, 3, FUNCTION_OFFSETS["printPrice"], [LDX, 0x34, 0x00])

# SBC #80 -> SBC #$20
# AND #$007F -> NOP NOP NOP
patch(rom, 3, FUNCTION_OFFSETS["prepareWindowGraphics"][0], [SBC, 0x20, 0x00])
patch(rom, 3, FUNCTION_OFFSETS["prepareWindowGraphics"][1], [NOP, NOP, NOP])

# SBC #$50 -> SBC #$20
# AND #$007F -> NOP NOP NOP
patch(rom, 3, FUNCTION_OFFSETS["renderWholeCharacter"][0], [SBC, 0x20, 0x00])
patch(rom, 3, FUNCTION_OFFSETS["renderWholeCharacter"][1], [NOP, NOP, NOP])

# SBC #$50 -> SBC #$20
# AND #$007F -> NOP NOP NOP
patch(rom, 3, FUNCTION_OFFSETS["renderLargeCharacterInternal"][0], [SBC, 0x20, 0x00])
patch(rom, 3, FUNCTION_OFFSETS["renderLargeCharacterInternal"][1], [NOP, NOP, NOP])

# SBC #80 -> SBC #$20
# AND #$007F -> NOP NOP NOP
patch(rom, 3, FUNCTION_OFFSETS["renderCastNameText"][0], [SBC, 0x20, 0x00])
patch(rom, 3, FUNCTION_OFFSETS["renderCastNameText"][1], [NOP, NOP, NOP])

self.write_credits_font_to_rom(rom)

def read_from_project(self, resource_open):
Expand Down
8 changes: 7 additions & 1 deletion coilsnake/util/eb/helper.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
from coilsnake.util.eb.pointer import from_snes_address

def is_in_bank(bank, address):
return (address >> 16) == bank


def not_in_bank(bank, address):
return not is_in_bank(bank, address)
return not is_in_bank(bank, address)


def patch(rom, size, offset, instructions):
rom[from_snes_address(offset) : from_snes_address(offset + size)] = instructions