diff --git a/.gitignore b/.gitignore index d55dafa..c777e4e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ /coilsnake/assets/ccc/lib/*.ccs .DS_store git_commit.py +/venv/ diff --git a/coilsnake/model/eb/fonts.py b/coilsnake/model/eb/fonts.py index 22072bf..cdd5c3b 100644 --- a/coilsnake/model/eb/fonts.py +++ b/coilsnake/model/eb/fonts.py @@ -12,6 +12,7 @@ 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): @@ -19,6 +20,9 @@ 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): @@ -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)) @@ -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 @@ -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": @@ -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 @@ -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 \ No newline at end of file + del image diff --git a/coilsnake/modules/eb/FontModule.py b/coilsnake/modules/eb/FontModule.py index 80c845a..5ad3d09 100644 --- a/coilsnake/modules/eb/FontModule.py +++ b/coilsnake/modules/eb/FontModule.py @@ -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__) @@ -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" @@ -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): diff --git a/coilsnake/util/eb/helper.py b/coilsnake/util/eb/helper.py index 42e1f6e..85cb1d7 100644 --- a/coilsnake/util/eb/helper.py +++ b/coilsnake/util/eb/helper.py @@ -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) \ No newline at end of file + return not is_in_bank(bank, address) + + +def patch(rom, size, offset, instructions): + rom[from_snes_address(offset) : from_snes_address(offset + size)] = instructions