from ..assembler import ASM from ..utils import formatText, setReplacementName from ..roomEditor import RoomEditor from .. import entityData import os import bsdiff4 def imageTo2bpp(filename): import PIL.Image baseimg = PIL.Image.new('P', (1,1)) baseimg.putpalette(( 128, 0, 128, 0, 0, 0, 128, 128, 128, 255, 255, 255, )) img = PIL.Image.open(filename) img = img.quantize(colors=4, palette=baseimg) print (f"Palette: {img.getpalette()}") assert (img.size[0] % 8) == 0 tileheight = 8 if img.size[1] == 8 else 16 assert (img.size[1] % tileheight) == 0 cols = img.size[0] // 8 rows = img.size[1] // tileheight result = bytearray(rows * cols * tileheight * 2) index = 0 for ty in range(rows): for tx in range(cols): for y in range(tileheight): a = 0 b = 0 for x in range(8): c = img.getpixel((tx * 8 + x, ty * 16 + y)) if c & 1: a |= 0x80 >> x if c & 2: b |= 0x80 >> x result[index] = a result[index+1] = b index += 2 return result def updateGraphics(rom, bank, offset, data): if offset + len(data) > 0x4000: updateGraphics(rom, bank, offset, data[:0x4000-offset]) updateGraphics(rom, bank + 1, 0, data[0x4000 - offset:]) else: rom.banks[bank][offset:offset+len(data)] = data if bank < 0x34: rom.banks[bank-0x20][offset:offset + len(data)] = data def gfxMod(rom, filename): if os.path.exists(filename + ".names"): for line in open(filename + ".names", "rt"): if ":" in line: k, v = line.strip().split(":", 1) setReplacementName(k, v) ext = os.path.splitext(filename)[1].lower() if ext == ".bin": updateGraphics(rom, 0x2C, 0, open(filename, "rb").read()) elif ext in (".png", ".bmp"): updateGraphics(rom, 0x2C, 0, imageTo2bpp(filename)) elif ext == ".bdiff": updateGraphics(rom, 0x2C, 0, prepatch(rom, 0x2C, 0, filename)) elif ext == ".json": import json data = json.load(open(filename, "rt")) for patch in data: if "gfx" in patch: updateGraphics(rom, int(patch["bank"], 16), int(patch["offset"], 16), imageTo2bpp(os.path.join(os.path.dirname(filename), patch["gfx"]))) if "name" in patch: setReplacementName(patch["item"], patch["name"]) else: updateGraphics(rom, 0x2C, 0, imageTo2bpp(filename)) def createGfxImage(rom, filename): import PIL.Image bank_count = 8 img = PIL.Image.new("P", (32 * 8, 32 * 8 * bank_count)) img.putpalette(( 128, 0, 128, 0, 0, 0, 128, 128, 128, 255, 255, 255, )) for bank_nr in range(bank_count): bank = rom.banks[0x2C + bank_nr] for tx in range(32): for ty in range(16): for y in range(16): a = bank[tx * 32 + ty * 32 * 32 + y * 2] b = bank[tx * 32 + ty * 32 * 32 + y * 2 + 1] for x in range(8): c = 0 if a & (0x80 >> x): c |= 1 if b & (0x80 >> x): c |= 2 img.putpixel((tx*8+x, bank_nr * 32 * 8 + ty*16+y), c) img.save(filename) def prepatch(rom, bank, offset, filename): bank_count = 8 base_sheet = [] result = [] for bank_nr in range(bank_count): base_sheet[0x4000 * bank_nr:0x4000 * (bank_nr + 1) - 1] = rom.banks[0x2C + bank_nr] with open(filename, "rb") as patch: file = patch.read() result = bsdiff4.patch(src_bytes=bytes(base_sheet), patch_bytes=file) return result def noSwordMusic(rom): # Skip no-sword music override # Instead of loading the sword level, we put the value 1 in the A register, indicating we have a sword. rom.patch(2, 0x0151, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) rom.patch(2, 0x3AEF, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) rom.patch(3, 0x0996, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) rom.patch(3, 0x0B35, ASM("ld a, [$DB44]"), ASM("ld a, $01"), fill_nop=True) def removeNagMessages(rom): # Remove "this object is heavy, bla bla", and other nag messages when touching an object rom.patch(0x02, 0x32BB, ASM("ld a, [$C14A]"), ASM("ld a, $01"), fill_nop=True) # crystal blocks rom.patch(0x02, 0x32EC, ASM("ld a, [$C5A6]"), ASM("ld a, $01"), fill_nop=True) # cracked blocks rom.patch(0x02, 0x32D3, ASM("jr nz, $25"), ASM("jr $25"), fill_nop=True) # stones/pots rom.patch(0x02, 0x2B88, ASM("jr nz, $0F"), ASM("jr $0F"), fill_nop=True) # ice blocks def removeLowHPBeep(rom): rom.patch(2, 0x233A, ASM("ld hl, $FFF3\nld [hl], $04"), b"", fill_nop=True) # Remove health beep def slowLowHPBeep(rom): rom.patch(2, 0x2338, ASM("ld a, $30"), ASM("ld a, $60")) # slow slow hp beep def removeFlashingLights(rom): # Remove the switching between two backgrounds at mamu, always show the spotlights. rom.patch(0x00, 0x01EB, ASM("ldh a, [$E7]\nrrca\nand $80"), ASM("ld a, $80"), fill_nop=True) # Remove flashing colors from shopkeeper killing you after stealing and the mad batter giving items. rom.patch(0x24, 0x3B77, ASM("push bc"), ASM("ret")) def forceLinksPalette(rom, index): # This forces the link sprite into a specific palette index ignoring the tunic options. rom.patch(0, 0x1D8C, ASM("ld a, [$DC0F]\nand a\njr z, $03\ninc a"), ASM("ld a, $%02X" % (index)), fill_nop=True) rom.patch(0, 0x1DD2, ASM("ld a, [$DC0F]\nand a\njr z, $03\ninc a"), ASM("ld a, $%02X" % (index)), fill_nop=True) # Fix the waking up from bed palette if index == 1: rom.patch(0x21, 0x33FC, "A222", "FF05") elif index == 2: rom.patch(0x21, 0x33FC, "A222", "3F14") elif index == 3: rom.patch(0x21, 0x33FC, "A222", "037E") for n in range(6): rom.patch(0x05, 0x1261 + n * 2, "00", f"{index:02x}") def fastText(rom): rom.patch(0x00, 0x24CA, ASM("jp $2485"), ASM("call $2485")) def noText(rom): for idx in range(len(rom.texts)): if not isinstance(rom.texts[idx], int) and (idx < 0x217 or idx > 0x21A): rom.texts[idx] = rom.texts[idx][-1:] def reduceMessageLengths(rom, rnd): # Into text from Marin. Got to go fast, so less text. (This intro text is very long) rom.texts[0x01] = formatText(rnd.choice([ "Let's a go!", "Remember, sword goes on A!", "Avoid the heart piece of shame!", "Marin? No, this is Zelda. Welcome to Hyrule", "Why are you in my bed?", "This is not a Mario game!", "MuffinJets was here...", "Remember, there are no bugs in LADX", "#####, #####, you got to wake up!\nDinner is ready.", "Go find the stepladder", "Pizza power!", "Eastmost penninsula is the secret", "There is no cow level", "You cannot lift rocks with your bear hands", "Thank you, daid!", "There, there now. Just relax. You've been asleep for almost nine hours now." ])) # Reduce length of a bunch of common texts rom.texts[0xEA] = formatText("You've got a Guardian Acorn!") rom.texts[0xEB] = rom.texts[0xEA] rom.texts[0xEC] = rom.texts[0xEA] rom.texts[0x08] = formatText("You got a Piece of Power!") rom.texts[0xEF] = formatText("You found a {SEASHELL}!") rom.texts[0xA7] = formatText("You've got the {COMPASS}!") rom.texts[0x07] = formatText("You need the {NIGHTMARE_KEY}!") rom.texts[0x8C] = formatText("You need a {KEY}!") # keyhole block rom.texts[0x09] = formatText("Ahhh... It has the Sleepy {TOADSTOOL}, it does! We'll mix it up something in a jiffy, we will!") rom.texts[0x0A] = formatText("The last thing I kin remember was bitin' into a big juicy {TOADSTOOL}... Then, I had the darndest dream... I was a raccoon! Yeah, sounds strange, but it sure was fun!") rom.texts[0x0F] = formatText("You pick the {TOADSTOOL}... As you hold it over your head, a mellow aroma flows into your nostrils.") rom.texts[0x13] = formatText("You've learned the ^{SONG1}!^ This song will always remain in your heart!") rom.texts[0x18] = formatText("Will you give me 28 {RUPEES} for my secret?", ask="Give Don't") rom.texts[0x19] = formatText("How about it? 42 {RUPEES} for my little secret...", ask="Give Don't") rom.texts[0x1e] = formatText("...You're so cute! I'll give you a 7 {RUPEE} discount!") rom.texts[0x2d] = formatText("{ARROWS_10}\n10 {RUPEES}!", ask="Buy Don't") rom.texts[0x32] = formatText("{SHIELD}\n20 {RUPEES}!", ask="Buy Don't") rom.texts[0x33] = formatText("Ten {BOMB}\n10 {RUPEES}", ask="Buy Don't") rom.texts[0x3d] = formatText("It's a {SHIELD}! There is space for your name!") rom.texts[0x42] = formatText("It's 30 {RUPEES}! You can play the game three more times with this!") rom.texts[0x45] = formatText("How about some fishing, little buddy? I'll only charge you 10 {RUPEES}...", ask="Fish Not Now") rom.texts[0x4b] = formatText("Wow! Nice Fish! It's a lunker!! I'll give you a 20 {RUPEE} prize! Try again?", ask="Cast Not Now") rom.texts[0x4e] = formatText("You're short of {RUPEES}? Don't worry about it. You just come back when you have more money, little buddy.") rom.texts[0x4f] = formatText("You've got a {HEART_PIECE}! Press SELECT on the Subscreen to see.") rom.texts[0x8e] = formatText("Well, it's an {OCARINA}, but you don't know how to play it...") rom.texts[0x90] = formatText("You found the {POWER_BRACELET}! At last, you can pick up pots and stones!") rom.texts[0x91] = formatText("You got your {SHIELD} back! Press the button and repel enemies with it!") rom.texts[0x93] = formatText("You've got the {HOOKSHOT}! Its chain stretches long when you use it!") rom.texts[0x94] = formatText("You've got the {MAGIC_ROD}! Now you can burn things! Burn it! Burn, baby burn!") rom.texts[0x95] = formatText("You've got the {PEGASUS_BOOTS}! If you hold down the Button, you can dash!") rom.texts[0x96] = formatText("You've got the {OCARINA}! You should learn to play many songs!") rom.texts[0x97] = formatText("You've got the {FEATHER}! It feels like your body is a lot lighter!") rom.texts[0x98] = formatText("You've got a {SHOVEL}! Now you can feel the joy of digging!") rom.texts[0x99] = formatText("You've got some {MAGIC_POWDER}! Try sprinkling it on a variety of things!") rom.texts[0x9b] = formatText("You found your {SWORD}! It must be yours because it has your name engraved on it!") rom.texts[0x9c] = formatText("You've got the {FLIPPERS}! If you press the B Button while you swim, you can dive underwater!") rom.texts[0x9e] = formatText("You've got a new {SWORD}! You should put your name on it right away!") rom.texts[0x9f] = formatText("You've got a new {SWORD}! You should put your name on it right away!") rom.texts[0xa0] = formatText("You found the {MEDICINE}! You should apply this and see what happens!") rom.texts[0xa1] = formatText("You've got the {TAIL_KEY}! Now you can open the Tail Cave gate!") rom.texts[0xa2] = formatText("You've got the {SLIME_KEY}! Now you can open the gate in Ukuku Prairie!") rom.texts[0xa3] = formatText("You've got the {ANGLER_KEY}!") rom.texts[0xa4] = formatText("You've got the {FACE_KEY}!") rom.texts[0xa5] = formatText("You've got the {BIRD_KEY}!") rom.texts[0xa6] = formatText("At last, you got a {MAP}! Press the START Button to look at it!") rom.texts[0xa8] = formatText("You found a {STONE_BEAK}! Let's find the owl statue that belongs to it.") rom.texts[0xa9] = formatText("You've got the {NIGHTMARE_KEY}! Now you can open the door to the Nightmare's Lair!") rom.texts[0xaa] = formatText("You got a {KEY}! You can open a locked door.") rom.texts[0xab] = formatText("You got 20 {RUPEES}! JOY!", center=True) rom.texts[0xac] = formatText("You got 50 {RUPEES}! Very Nice!", center=True) rom.texts[0xad] = formatText("You got 100 {RUPEES}! You're Happy!", center=True) rom.texts[0xae] = formatText("You got 200 {RUPEES}! You're Ecstatic!", center=True) rom.texts[0xdc] = formatText("Ribbit! Ribbit! I'm Mamu, on vocals! But I don't need to tell you that, do I? Everybody knows me! Want to hang out and listen to us jam? For 300 Rupees, we'll let you listen to a previously unreleased cut! What do you do?", ask="Pay Leave") rom.texts[0xe8] = formatText("You've found a {GOLD_LEAF}! Press START to see how many you've collected!") rom.texts[0xed] = formatText("You've got the Mirror Shield! You can now turnback the beams you couldn't block before!") rom.texts[0xee] = formatText("You've got a more Powerful {POWER_BRACELET}! Now you can almost lift a whale!") rom.texts[0xf0] = formatText("Want to go on a raft ride for a hundred {RUPEES}?", ask="Yes No Way") def allowColorDungeonSpritesEverywhere(rom): # Set sprite set numbers $01-$40 to map to the color dungeon sprites rom.patch(0x00, 0x2E6F, "00", "15") # Patch the spriteset loading code to load the 4 entries from the normal table instead of skipping this for color dungeon specific exception weirdness rom.patch(0x00, 0x0DA4, ASM("jr nc, $05"), ASM("jr nc, $41")) rom.patch(0x00, 0x0DE5, ASM(""" ldh a, [$F7] cp $FF jr nz, $06 ld a, $01 ldh [$91], a jr $40 """), ASM(""" jr $0A ; skip over the rest of the code cp $FF ; check if color dungeon jp nz, $0DAB inc d jp $0DAA """), fill_nop=True) # Disable color dungeon specific tile load hacks rom.patch(0x00, 0x06A7, ASM("jr nz, $22"), ASM("jr $22")) rom.patch(0x00, 0x2E77, ASM("jr nz, $0B"), ASM("jr $0B")) # Finally fill in the sprite data for the color dungeon for n in range(22): data = bytearray() for m in range(4): idx = rom.banks[0x20][0x06AA + 44 * m + n * 2] bank = rom.banks[0x20][0x06AA + 44 * m + n * 2 + 1] if idx == 0 and bank == 0: v = 0xFF elif bank == 0x35: v = idx - 0x40 elif bank == 0x31: v = idx elif bank == 0x2E: v = idx + 0x40 else: assert False, "%02x %02x" % (idx, bank) data += bytes([v]) rom.room_sprite_data_indoor[0x200 + n] = data # Patch the graphics loading code to use DMA and load all sets that need to be reloaded, not just the first and last rom.patch(0x00, 0x06FA, 0x07AF, ASM(""" ;We enter this code with the right bank selected for tile data copy, ;d = tile row (source addr = (d*$100+$4000)) ;e = $00 ;$C197 = index of sprite set to update (target addr = ($8400 + $100 * [$C197])) ld a, d add a, $40 ldh [$51], a xor a ldh [$52], a ldh [$54], a ld a, [$C197] add a, $84 ldh [$53], a ld a, $0F ldh [$55], a ; See if we need to do anything next ld a, [$C10E] ; check the 2nd update flag and a jr nz, getNext ldh [$91], a ; no 2nd update flag, so clear primary update flag ret getNext: ld hl, $C197 inc [hl] res 2, [hl] ld a, [$C10D] cp [hl] ret nz xor a ; clear the 2nd update flag when we prepare to update the last spriteset ld [$C10E], a ret """), fill_nop=True) rom.patch(0x00, 0x0738, "00" * (0x073E - 0x0738), ASM(""" ; we get here by some color dungeon specific code jumping to this position ; We still need that color dungeon specific code as it loads background tiles xor a ldh [$91], a ldh [$93], a ret """)) rom.patch(0x00, 0x073E, "00" * (0x07AF - 0x073E), ASM(""" ;If we get here, only the 2nd flag is filled and the primary is not. So swap those around. ld a, [$C10D] ;copy the index number ld [$C197], a xor a ld [$C10E], a ; clear the 2nd update flag inc a ldh [$91], a ; set the primary update flag ret """), fill_nop=True) def updateSpriteData(rom): # Change the special sprite change exceptions rom.patch(0x00, 0x0DAD, 0x0DDB, ASM(""" ; Check for indoor ld a, d and a jr nz, noChange ldh a, [$F6] ; hMapRoom cp $C9 jr nz, sirenRoomEnd ld a, [$D8C9] ; wOverworldRoomStatus + ROOM_OW_SIREN and $20 jr z, noChange ld hl, $7837 jp $0DFE sirenRoomEnd: ldh a, [$F6] ; hMapRoom cp $D8 jr nz, noChange ld a, [$D8FD] ; wOverworldRoomStatus + ROOM_OW_WALRUS and $20 jr z, noChange ld hl, $783B jp $0DFE noChange: """), fill_nop=True) rom.patch(0x20, 0x3837, "A4FF8BFF", "A461FF72") rom.patch(0x20, 0x383B, "A44DFFFF", "A4C5FF70") # For each room update the sprite load data based on which entities are in there. for room_nr in range(0x316): if room_nr == 0x2FF: continue values = [None, None, None, None] if room_nr == 0x00E: # D7 entrance opening values[2] = 0xD6 values[3] = 0xD7 if 0x211 <= room_nr <= 0x21E: # D7 throwing ball thing. values[0] = 0x66 r = RoomEditor(rom, room_nr) for obj in r.objects: if obj.type_id == 0xC5 and room_nr < 0x100: # Pushable Gravestone values[3] = 0x82 for x, y, entity in r.entities: sprite_data = entityData.SPRITE_DATA[entity] if callable(sprite_data): sprite_data = sprite_data(r) if sprite_data is None: continue for m in range(0, len(sprite_data), 2): idx, value = sprite_data[m:m+2] if values[idx] is None: values[idx] = value elif isinstance(values[idx], set) and isinstance(value, set): values[idx] = values[idx].intersection(value) assert len(values[idx]) > 0 elif isinstance(values[idx], set) and value in values[idx]: values[idx] = value elif isinstance(value, set) and values[idx] in value: pass elif values[idx] == value: pass else: assert False, "Room: %03x cannot load graphics for entity: %02x (Index: %d Failed: %s, Active: %s)" % (room_nr, entity, idx, value, values[idx]) data = bytearray() for v in values: if isinstance(v, set): v = next(iter(v)) elif v is None: v = 0xff data.append(v) if room_nr < 0x100: rom.room_sprite_data_overworld[room_nr] = data else: rom.room_sprite_data_indoor[room_nr - 0x100] = data def bin_to_rgb(word): red = word & 0b11111 word >>= 5 green = word & 0b11111 word >>= 5 blue = word & 0b11111 return (red, green, blue) def rgb_to_bin(r, g, b): return (b << 10) | (g << 5) | r