def tweakMap(rom): # 5 holes at the castle, reduces to 3 re = RoomEditor(rom, 0x078) re.objects[-1].count = 3 re.overlay[7 + 6 * 10] = re.overlay[9 + 6 * 10] re.overlay[8 + 6 * 10] = re.overlay[9 + 6 * 10] re.store(rom)
def readEntrances(rom): result = [] entrance_rooms = [ 0x0D3, 0x024, 0x0B5, (0x09, 0x109A), 0x0D9, (0x1A, 0x034E), (0x09, 0x07EC), 0x010, 0x077 ] for idx, room in enumerate(entrance_rooms): if isinstance(room, tuple): re = RoomEditor(rom, bank_nr=room[0], address=room[1]) else: re = RoomEditor(rom, room) warp = None for obj in re.objects: if isinstance(obj, ObjectWarp) and (obj.map_nr < 9 or obj.map_nr == 0xff): warp = obj if warp.map_nr == 0xFF: result.append(8) else: result.append(warp.map_nr) return result
def readBossMapping(rom): mapping = [] for dungeon_nr in range(9): r = RoomEditor(rom, BOSS_ROOMS[dungeon_nr][0]) if r.entities: mapping.append(BOSS_ENTITIES.index(r.entities[0])) elif isinstance(r.objects[-1], ObjectWarp) and r.objects[-1].room == 0x1ef: mapping.append(3) elif isinstance(r.objects[-1], ObjectWarp) and r.objects[-1].room == 0x2f8: mapping.append(6) else: mapping.append(dungeon_nr) return mapping
def readEntrances(rom): result = [] entrance_rooms = [0x0D3, 0x024, 0x0B5, "Alt2B", 0x0D9, "Alt8C", "Alt0E", 0x010, 0x077] for idx, room in enumerate(entrance_rooms): re = RoomEditor(rom, room) warp = None for obj in re.objects: if isinstance(obj, ObjectWarp) and (obj.map_nr < 9 or obj.map_nr == 0xff): warp = obj if not warp: # TODO: This indicates a different map setup... result.append(idx) elif warp.map_nr == 0xFF: result.append(8) else: result.append(warp.map_nr) return result
def removeOwlEvents(rom): # Remove all the owl events from the entity tables. for room in range(0x100): re = RoomEditor(rom, room) if re.hasEntity(0x41): re.removeEntities(0x41) re.store(rom) # Clear texts used by the owl. Potentially reused somewhere else. rom.texts[0x0D9] = b'\xff' # used by boomerang # 1 Used by empty chest (master stalfos message) # 9 used by keysanity items # 1 used by bowwow in chest # 1 used by item for other player message # 2 used by arrow chest messages # 2 used by tunics for idx in range(0x0BE, 0x0CE): rom.texts[idx] = b'\xff'
def patch(self, rom, option, *, multiworld=None): if option != SWORD or multiworld is not None: # Set the heart piece data super().patch(rom, option, multiworld=multiworld) # Patch the room to contain a heart piece instead of the sword on the beach re = RoomEditor(rom, 0x0F2) re.removeEntities(0x31) # remove sword re.addEntity(5, 5, 0x35) # add heart piece re.store(rom) # Prevent shield drops from the like-like from turning into swords. rom.patch(0x03, 0x1B9C, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True) rom.patch(0x03, 0x244D, ASM("ld a, [$DB4E]"), ASM("ld a, $01"), fill_nop=True)
def removeBirdKeyHoleDrop(rom): # Prevent the cave with the bird key from dropping you in the water # (if you do not have flippers this would softlock you) rom.patch( 0x02, 0x1176, ASM(""" ldh a, [$F7] cp $0A jr nz, $30 """), ASM(""" nop nop nop nop jr $30 """)) # Remove the hole that drops you all the way from dungeon7 entrance to the water in the cave re = RoomEditor(rom, 0x01E) re.removeObject(5, 4) re.store(rom)
def doubleTrouble(rom): for n in range(0x316): if n == 0x2FF: continue re = RoomEditor(rom, n) # Bosses if re.hasEntity(0x59): # Moldorm (TODO; double heart container drop) re.removeEntities(0x59) re.entities += [(3, 2, 0x59), (4, 2, 0x59)] re.store(rom) if re.hasEntity(0x5C): # Ghini re.removeEntities(0x5C) re.entities += [(3, 2, 0x5C), (4, 2, 0x5C)] re.store(rom) if re.hasEntity(0x5B): # slime eye re.removeEntities(0x5B) re.entities += [(3, 2, 0x5B), (6, 2, 0x5B)] re.store(rom) if re.hasEntity(0x65): # angler fish re.removeEntities(0x65) re.entities += [(6, 2, 0x65), (6, 5, 0x65)] re.store(rom) # Slime eel bugs out on death if duplicated. # if re.hasEntity(0x5D): # slime eel # re.removeEntities(0x5D) # re.entities += [(6, 2, 0x5D), (6, 5, 0x5D)] # re.store(rom) if re.hasEntity( 0x5A): # facade (TODO: Drops two hearts, shared health?) re.removeEntities(0x5A) re.entities += [(2, 3, 0x5A), (6, 3, 0x5A)] re.store(rom) # Evil eagle causes a crash, and messes up the intro sequence and generally is just a mess if I spawn multiple # if re.hasEntity(0x63): # evil eagle # re.removeEntities(0x63) # re.entities += [(3, 4, 0x63), (2, 4, 0x63)] # re.store(rom) # # Remove that links movement is blocked # rom.patch(0x05, 0x2258, ASM("ldh [$A1], a"), "0000") # rom.patch(0x05, 0x1AE3, ASM("ldh [$A1], a"), "0000") # rom.patch(0x05, 0x1C5D, ASM("ldh [$A1], a"), "0000") # rom.patch(0x05, 0x1C8D, ASM("ldh [$A1], a"), "0000") # rom.patch(0x05, 0x1CAF, ASM("ldh [$A1], a"), "0000") if re.hasEntity(0x62): # hot head (TODO: Drops thwo hearts) re.removeEntities(0x62) re.entities += [(2, 2, 0x62), (4, 4, 0x62)] re.store(rom) if re.hasEntity(0xF9): # hardhit beetle re.removeEntities(0xF9) re.entities += [(2, 2, 0xF9), (5, 4, 0xF9)] re.store(rom) # Minibosses if re.hasEntity(0x89): re.removeEntities(0x89) re.entities += [(2, 3, 0x89), (6, 3, 0x89)] re.store(rom) if re.hasEntity(0x81): re.removeEntities(0x81) re.entities += [(2, 3, 0x81), (6, 3, 0x81)] re.store(rom) if re.hasEntity(0x60): dodongo = [e for e in re.entities if e[2] == 0x60] x = (dodongo[0][0] + dodongo[1][0]) // 2 y = (dodongo[0][1] + dodongo[1][1]) // 2 re.entities += [(x, y, 0x60)] re.store(rom) if re.hasEntity(0x8e): re.removeEntities(0x8e) re.entities += [(1, 1, 0x8e), (7, 1, 0x8e)] re.store(rom) if re.hasEntity(0x92): re.removeEntities(0x92) re.entities += [(2, 3, 0x92), (4, 3, 0x92)] re.store(rom) if re.hasEntity(0xf4): re.removeEntities(0xf4) re.entities += [(2, 1, 0xf4), (6, 1, 0xf4)] re.store(rom) if re.hasEntity(0xf8): re.removeEntities(0xf8) re.entities += [(2, 2, 0xf8), (6, 2, 0xf8)] re.store(rom) if re.hasEntity(0xe4): re.removeEntities(0xe4) re.entities += [(5, 2, 0xe4), (5, 5, 0xe4)] re.store(rom) if re.hasEntity(0x88): # Armos knight (TODO: double item drop) re.removeEntities(0x88) re.entities += [(3, 3, 0x88), (6, 3, 0x88)] re.store(rom) if re.hasEntity( 0x87 ): # Lanmola (TODO: killing one drops the item, and marks as done) re.removeEntities(0x87) re.entities += [(2, 2, 0x87), (1, 1, 0x87)] re.store(rom)
def changeMiniBosses(rom, mapping): # Fix avalaunch not working when entering a room from the left or right rom.patch(0x03, 0x0BE0, ASM(""" ld [hl], $50 ld hl, $C2D0 add hl, bc ld [hl], $00 jp $4B56 """), ASM(""" ld a, [hl] sub $08 ld [hl], a ld hl, $C2D0 add hl, bc ld [hl], b ; b is always zero here ret """), fill_nop=True) # Remove the powder fairy from giant buzz blob rom.patch(0x36, 0x14F7, ASM("jr nz, $05"), ASM("jr $05")) for target, name in mapping.items(): re = RoomEditor(rom, MINIBOSS_ROOMS[target]) re.entities = [e for e in re.entities if e[2] == 0x61] # Only keep warp, if available re.entities += MINIBOSS_ENTITIES[name] if re.room == 0x228 and name != "GRIM_CREEPER": for x in range(3, 7): for y in range(0, 3): re.removeObject(x, y) if name == "CUE_BALL": re.objects += [ Object(3, 3, 0x2c), ObjectHorizontal(4, 3, 0x22, 2), Object(6, 3, 0x2b), Object(3, 4, 0x2a), ObjectHorizontal(4, 4, 0x21, 2), Object(6, 4, 0x29), ] if name == "BLAINO": # BLAINO needs a warp object to hit you to the entrance of the dungeon. if len(re.getWarps()) < 1: # Default to start house. target = (0x10, 0x2A3, 0x50, 0x7c, 0x2A3) if 0x100 <= re.room < 0x11D: #D1 target = (0, 0x117, 80, 80) elif 0x11D <= re.room < 0x140: #D2 target = (1, 0x136, 80, 80) elif 0x140 <= re.room < 0x15D: #D3 target = (2, 0x152, 80, 80) elif 0x15D <= re.room < 0x180: #D4 target = (2, 0x174, 80, 80) elif 0x180 <= re.room < 0x1AC: #D5 target = (2, 0x1A1, 80, 80) elif 0x1B0 <= re.room < 0x1DE: #D6 target = (2, 0x1D4, 80, 80) elif 0x200 <= re.room < 0x22D: #D7 target = (6, 0x20E, 80, 80) elif 0x22D <= re.room < 0x26C: #D8 target = (7, 0x25D, 80, 80) elif re.room >= 0x300: #D0 target = (0xFF, 0x312, 80, 80) elif re.room == 0x2E1: #Moblin cave target = (0x15, 0x2F0, 0x50, 0x7C) re.objects.append(ObjectWarp(1, *target)) if name == "DODONGO": # Remove breaking floor tiles from the room. re.objects = [obj for obj in re.objects if obj.type_id != 0xDF] if name == "ROLLING_BONES" and target == 2: # Make rolling bones pass trough walls so it does not get stuck here. rom.patch(0x03, 0x02F1 + 0x81, "84", "95") re.store(rom)
def changeBosses(rom, mapping): for dungeon_nr in range(9): target = mapping[dungeon_nr] if target == dungeon_nr: continue if target == 3: # D4 fish boss # If dungeon_nr == 6: use normal eagle door towards fish. if dungeon_nr == 6: # Add the staircase to the boss, and fix the warp back. re = RoomEditor(rom, 0x22E) for obj in re.objects: if isinstance(obj, ObjectWarp): obj.type_id = 2 obj.map_nr = 3 obj.room = 0x1EF obj.target_x = 24 obj.target_y = 16 re.store(rom) re = RoomEditor(rom, 0x1EF) re.objects[-1] = ObjectWarp( 1, dungeon_nr if dungeon_nr < 8 else 0xff, 0x22E, 72, 80) re.store(rom) else: # Set the proper room event flags rom.banks[0x14][BOSS_ROOMS[dungeon_nr][0] - 0x100] = 0x2A # Patch the fish heart container to open up the right room. rom.patch( 0x03, 0x1A0F, ASM("ld hl, $D966"), ASM("ld hl, $%04x" % (0xD800 + BOSS_ROOMS[dungeon_nr][0]))) # Add the staircase to the boss, and fix the warp back. re = getCleanBossRoom(rom, dungeon_nr) re.objects += [ Object(4, 4, 0xBE), ObjectWarp(2, 3, 0x1EF, 24, 16) ] re.store(rom) re = RoomEditor(rom, 0x1EF) re.objects[-1] = ObjectWarp( 1, dungeon_nr if dungeon_nr < 8 else 0xff, BOSS_ROOMS[dungeon_nr][0], 72, 80) re.store(rom) # Patch the proper item towards the D4 boss rom.banks[0x3E][0x3800 + 0x01ff] = rom.banks[0x3E][ 0x3800 + BOSS_ROOMS[dungeon_nr][0]] rom.banks[0x3E][0x3300 + 0x01ff] = rom.banks[0x3E][ 0x3300 + BOSS_ROOMS[dungeon_nr][0]] elif target == 6: # Evil eagle rom.banks[0x14][BOSS_ROOMS[dungeon_nr][0] - 0x100] = 0x2A # Patch the eagle heart container to open up the right room. rom.patch( 0x03, 0x1A04, ASM("ld hl, $DA2E"), ASM("ld hl, $%04x" % (0xD800 + BOSS_ROOMS[dungeon_nr][0]))) rom.patch( 0x02, 0x1FC8, ASM("cp $06"), ASM("cp $%02x" % (dungeon_nr if dungeon_nr < 8 else 0xff))) # Add the staircase to the boss, and fix the warp back. re = getCleanBossRoom(rom, dungeon_nr) re.objects += [Object(4, 4, 0xBE), ObjectWarp(2, 6, 0x2F8, 72, 80)] re.store(rom) re = RoomEditor(rom, 0x2F8) re.objects[-1] = ObjectWarp(1, dungeon_nr if dungeon_nr < 8 else 0xff, BOSS_ROOMS[dungeon_nr][0], 72, 80) re.store(rom) # Patch the proper item towards the D7 boss rom.banks[0x3E][0x3800 + 0x0223] = rom.banks[0x3E][ 0x3800 + BOSS_ROOMS[dungeon_nr][0]] rom.banks[0x3E][0x3300 + 0x0223] = rom.banks[0x3E][ 0x3300 + BOSS_ROOMS[dungeon_nr][0]] else: rom.banks[0x14][BOSS_ROOMS[dungeon_nr][0] - 0x100] = 0x21 rom.room_sprite_data_indoor[BOSS_ROOMS[dungeon_nr][0] - 0x100] = SPRITE_DATA[target] for room in BOSS_ROOMS[dungeon_nr][1:]: rom.room_sprite_data_indoor[room - 0x100] = b'\xff\xff\xff\xff' re = getCleanBossRoom(rom, dungeon_nr) re.entities = [BOSS_ENTITIES[target]] if target == 4: # For slime eel, we need to setup the right wall tiles. rom.banks[0x20][0x2EB3 + BOSS_ROOMS[dungeon_nr][0] - 0x100] = 0x06 if target == 5: # Patch facade so he doesn't use the spinning tiles, which is a problem for the sprites. rom.patch(0x04, 0x121D, ASM("cp $14"), ASM("cp $00")) rom.patch(0x04, 0x1226, ASM("cp $04"), ASM("cp $00")) rom.patch(0x04, 0x127F, ASM("cp $14"), ASM("cp $00")) if target == 7: pass # For hot head, add some lava (causes graphical glitches) # re.animation_id = 0x06 # re.objects += [ # ObjectHorizontal(3, 2, 0x06, 4), # ObjectHorizontal(2, 3, 0x06, 6), # ObjectHorizontal(2, 4, 0x06, 6), # ObjectHorizontal(3, 5, 0x06, 4), # ] re.store(rom)
def changeBosses(rom, mapping): # Fix the color dungeon not properly warping to room 0 with the boss. rom.patch( 0x14, 0x04E0, "0000000000000000" + "0000000000000000" + "0000000000000000" + "0000010000020300" + "0004050607080900" + "00000A0B0C0D0000" + "00000E0F10110000" + "0000121314150000", "FFFFFFFFFFFFFFFF" + "FFFFFFFFFFFFFFFF" + "FFFFFFFFFFFFFFFF" + "FF0001FFFF0203FF" + "FF040506070809FF" + "FFFF0A0B0C0DFFFF" + "FFFF0E0F1011FFFF" + "FFFF12131415FFFF") # Fix the genie death not really liking pits/water. rom.patch(0x04, 0x0521, ASM("ld [hl], $81"), ASM("ld [hl], $91")) for dungeon_nr in range(9): target = mapping[dungeon_nr] if target == dungeon_nr: continue if target == 3: # D4 fish boss # If dungeon_nr == 6: use normal eagle door towards fish. if dungeon_nr == 6: # Add the staircase to the boss, and fix the warp back. re = RoomEditor(rom, 0x22E) for obj in re.objects: if isinstance(obj, ObjectWarp): obj.type_id = 2 obj.map_nr = 3 obj.room = 0x1EF obj.target_x = 24 obj.target_y = 16 re.store(rom) re = RoomEditor(rom, 0x1EF) re.objects[-1] = ObjectWarp( 1, dungeon_nr if dungeon_nr < 8 else 0xff, 0x22E, 24, 16) re.store(rom) else: # Set the proper room event flags rom.banks[0x14][BOSS_ROOMS[dungeon_nr][0] - 0x100] = 0x2A # Add the staircase to the boss, and fix the warp back. re = getCleanBossRoom(rom, dungeon_nr) re.objects += [ Object(4, 4, 0xBE), ObjectWarp(2, 3, 0x1EF, 24, 16) ] re.store(rom) re = RoomEditor(rom, 0x1EF) re.objects[-1] = ObjectWarp( 1, dungeon_nr if dungeon_nr < 8 else 0xff, BOSS_ROOMS[dungeon_nr][0], 72, 80) re.store(rom) # Patch the fish heart container to open up the right room. rom.patch( 0x03, 0x1A0F, ASM("ld hl, $D966"), ASM("ld hl, $%04x" % (getBossRoomStatusFlagLocation(dungeon_nr)))) # Patch the proper item towards the D4 boss rom.banks[0x3E][0x3800 + 0x01ff] = rom.banks[0x3E][ 0x3800 + BOSS_ROOMS[dungeon_nr][0]] rom.banks[0x3E][0x3300 + 0x01ff] = rom.banks[0x3E][ 0x3300 + BOSS_ROOMS[dungeon_nr][0]] elif target == 6: # Evil eagle rom.banks[0x14][BOSS_ROOMS[dungeon_nr][0] - 0x100] = 0x2A # Patch the eagle heart container to open up the right room. rom.patch( 0x03, 0x1A04, ASM("ld hl, $DA2E"), ASM("ld hl, $%04x" % (getBossRoomStatusFlagLocation(dungeon_nr)))) rom.patch( 0x02, 0x1FC8, ASM("cp $06"), ASM("cp $%02x" % (dungeon_nr if dungeon_nr < 8 else 0xff))) # Add the staircase to the boss, and fix the warp back. re = getCleanBossRoom(rom, dungeon_nr) re.objects += [Object(4, 4, 0xBE), ObjectWarp(2, 6, 0x2F8, 72, 80)] re.store(rom) re = RoomEditor(rom, 0x2F8) re.objects[-1] = ObjectWarp(1, dungeon_nr if dungeon_nr < 8 else 0xff, BOSS_ROOMS[dungeon_nr][0], 72, 80) re.store(rom) # Patch the proper item towards the D7 boss rom.banks[0x3E][0x3800 + 0x02E8] = rom.banks[0x3E][ 0x3800 + BOSS_ROOMS[dungeon_nr][0]] rom.banks[0x3E][0x3300 + 0x02E8] = rom.banks[0x3E][ 0x3300 + BOSS_ROOMS[dungeon_nr][0]] else: rom.banks[0x14][BOSS_ROOMS[dungeon_nr][0] - 0x100] = 0x21 re = getCleanBossRoom(rom, dungeon_nr) re.entities = [BOSS_ENTITIES[target]] if target == 4: # For slime eel, we need to setup the right wall tiles. rom.banks[0x20][0x2EB3 + BOSS_ROOMS[dungeon_nr][0] - 0x100] = 0x06 if target == 5: # Patch facade so he doesn't use the spinning tiles, which is a problem for the sprites. rom.patch(0x04, 0x121D, ASM("cp $14"), ASM("cp $00")) rom.patch(0x04, 0x1226, ASM("cp $04"), ASM("cp $00")) rom.patch(0x04, 0x127F, ASM("cp $14"), ASM("cp $00")) if target == 7: pass # For hot head, add some lava (causes graphical glitches) # re.animation_id = 0x06 # re.objects += [ # ObjectHorizontal(3, 2, 0x06, 4), # ObjectHorizontal(2, 3, 0x06, 6), # ObjectHorizontal(2, 4, 0x06, 6), # ObjectHorizontal(3, 5, 0x06, 4), # ] re.store(rom)
def changeEntrances(rom, mapping): warp_to_indoor = {} warp_to_outdoor = {} for key in mapping.keys(): info = ENTRANCE_INFO[key] re = RoomEditor( rom, info.alt_room if info.alt_room is not None else info.room) warp = re.getWarps()[info.index if info.index not in (None, "all") else 0] warp_to_indoor[key] = warp assert info.target == warp.room, "%s != %03x" % (key, warp.room) re = RoomEditor(rom, warp.room) for warp in re.getWarps(): if warp.room == info.room: warp_to_outdoor[key] = warp assert key in warp_to_outdoor, "Missing warp to outdoor on %s" % (key) for key, target in mapping.items(): if key == target: continue info = ENTRANCE_INFO[key] # Change the entrance to point to the new indoor room re = RoomEditor(rom, info.room) re.changeWarp(warp_to_indoor[key].room, warp_to_indoor[target]) re.store(rom) if info.alt_room: re = RoomEditor(rom, info.alt_room) re.changeWarp(warp_to_indoor[key].room, warp_to_indoor[target]) re.store(rom) # Change the exit to point to the right outside re = RoomEditor(rom, warp_to_indoor[target].room) re.changeWarp(ENTRANCE_INFO[target].room, warp_to_outdoor[key]) re.store(rom) if ENTRANCE_INFO[target].instrument_room is not None: re = RoomEditor(rom, ENTRANCE_INFO[target].instrument_room) re.changeWarp(ENTRANCE_INFO[target].room, warp_to_outdoor[key]) re.store(rom)
def addBetaRoom(rom): re = RoomEditor(rom, 0x1FC) re.objects[-1].target_y -= 0x10 re.store(rom) re = RoomEditor(rom, 0x038) re.changeObject(5, 1, 0xE1) re.removeObject(0, 0) re.removeObject(0, 1) re.removeObject(0, 2) re.removeObject(6, 1) re.objects.append(ObjectVertical(0, 0, 0x38, 3)) re.objects.append(ObjectWarp(1, 0x1F, 0x1FC, 0x50, 0x7C)) re.store(rom) rom.room_sprite_data_indoor[0x0FC] = rom.room_sprite_data_indoor[0x1A1]
def patch(self, rom, option, *, multiworld=None): super().patch(rom, option, multiworld=multiworld) re = RoomEditor(rom, self.room) # Make the bird key accessible without the rooster re.removeObject(1, 6) re.removeObject(2, 6) re.removeObject(3, 5) re.removeObject(3, 6) re.moveObject(1, 5, 2, 6) re.moveObject(2, 5, 3, 6) re.addEntity(3, 5, 0x9D) re.store(rom) # Do not give the rooster rom.patch(0x19, 0x0E9D, ASM("ld [$DB7B], a"), "", fill_nop=True)
def exportOverworld(rom): import PIL.Image path = os.path.dirname(__file__) for room_index in list(range(0x100)) + [ "Alt06", "Alt0E", "Alt1B", "Alt2B", "Alt79", "Alt8C" ]: room = RoomEditor(rom, room_index) if isinstance(room_index, int): room_nr = room_index else: room_nr = int(room_index[3:], 16) tileset_index = rom.banks[0x3F][0x2f00 + room_nr] attributedata_bank = rom.banks[0x1A][0x2476 + room_nr] attributedata_addr = rom.banks[0x1A][0x1E76 + room_nr * 2] attributedata_addr |= rom.banks[0x1A][0x1E76 + room_nr * 2 + 1] << 8 attributedata_addr -= 0x4000 metatile_info = rom.banks[0x1A][0x2B1D:0x2B1D + 0x400] attrtile_info = rom.banks[attributedata_bank][ attributedata_addr:attributedata_addr + 0x400] palette_index = rom.banks[0x21][0x02EF + room_nr] palette_addr = rom.banks[0x21][0x02B1 + palette_index * 2] palette_addr |= rom.banks[0x21][0x02B1 + palette_index * 2 + 1] << 8 palette_addr -= 0x4000 hidden_warp_tiles = [] for obj in room.objects: if obj.type_id in WARP_TYPE_IDS and room.overlay[ obj.x + obj.y * 10] != obj.type_id: if obj.type_id != 0xE1 or room.overlay[ obj.x + obj.y * 10] != 0x53: # Ignore the waterfall 'caves' hidden_warp_tiles.append(obj) if obj.type_id == 0xC5 and room_nr < 0x100 and room.overlay[ obj.x + obj.y * 10] == 0xC4: # Pushable gravestones have the wrong overlay by default room.overlay[obj.x + obj.y * 10] = 0xC5 if obj.type_id == 0xDC and room_nr < 0x100: # Flowers above the rooster windmill need a different tile hidden_warp_tiles.append(obj) image_filename = "tiles_%02x_%02x_%02x_%02x_%04x.png" % ( tileset_index, room.animation_id, palette_index, attributedata_bank, attributedata_addr) data = { "width": 10, "height": 8, "type": "map", "renderorder": "right-down", "tiledversion": "1.4.3", "version": 1.4, "tilewidth": 16, "tileheight": 16, "orientation": "orthogonal", "tilesets": [{ "columns": 16, "firstgid": 1, "image": image_filename, "imageheight": 256, "imagewidth": 256, "margin": 0, "name": "main", "spacing": 0, "tilecount": 256, "tileheight": 16, "tilewidth": 16 }], "layers": [{ "data": [n + 1 for n in room.overlay], "width": 10, "height": 8, "id": 1, "name": "Tiles", "type": "tilelayer", "visible": True, "opacity": 1, "x": 0, "y": 0, }, { "id": 2, "name": "EntityLayer", "type": "objectgroup", "visible": True, "opacity": 1, "x": 0, "y": 0, "objects": [{ "width": 16, "height": 16, "x": entity[0] * 16, "y": entity[1] * 16, "name": entityData.NAME[entity[2]], "type": "entity" } for entity in room.entities] + [{ "width": 8, "height": 8, "x": 0, "y": idx * 8, "name": "%x:%02x:%03x:%02x:%02x" % (obj.warp_type, obj.map_nr, obj.room, obj.target_x, obj.target_y), "type": "warp" } for idx, obj in enumerate(room.getWarps()) if isinstance( obj, ObjectWarp)] + [{ "width": 16, "height": 16, "x": obj.x * 16, "y": obj.y * 16, "name": "%02X" % (obj.type_id), "type": "hidden_tile" } for obj in hidden_warp_tiles], }], "properties": [ { "name": "tileset", "type": "string", "value": "%02X" % (tileset_index) }, { "name": "animationset", "type": "string", "value": "%02X" % (room.animation_id) }, { "name": "attribset", "type": "string", "value": "%02X:%04X" % (attributedata_bank, attributedata_addr) }, { "name": "palette", "type": "string", "value": "%02X" % (palette_index) }, ] } if isinstance(room_index, str): json.dump( data, open("%s/overworld/export/%s.json" % (path, room_index), "wt")) else: json.dump( data, open("%s/overworld/export/%02X.json" % (path, room_index), "wt")) if not os.path.exists("%s/overworld/export/%s" % (path, image_filename)): tilemap = rom.banks[0x2F][tileset_index * 0x100:tileset_index * 0x100 + 0x200] tilemap += rom.banks[0x2C][0x1200:0x1800] tilemap += rom.banks[0x2C][0x0800:0x1000] anim_addr = { 2: 0x2B00, 3: 0x2C00, 4: 0x2D00, 5: 0x2E00, 6: 0x2F00, 7: 0x2D00, 8: 0x3000, 9: 0x3100, 10: 0x3200, 11: 0x2A00, 12: 0x3300, 13: 0x3500, 14: 0x3600, 15: 0x3400, 16: 0x3700 }.get(room.animation_id, 0x0000) tilemap[0x6C0:0x700] = rom.banks[0x2C][anim_addr:anim_addr + 0x40] palette = [] for n in range(8 * 4): p0 = rom.banks[0x21][palette_addr] p1 = rom.banks[0x21][palette_addr + 1] pal = p0 | p1 << 8 palette_addr += 2 r = (pal & 0x1F) << 3 g = ((pal >> 5) & 0x1F) << 3 b = ((pal >> 10) & 0x1F) << 3 palette += [r, g, b] img = PIL.Image.new("P", (16 * 16, 16 * 16)) img.putpalette(palette) def drawTile(x, y, index, attr): for py in range(8): a = tilemap[index * 16 + py * 2] b = tilemap[index * 16 + py * 2 + 1] if attr & 0x40: a = tilemap[index * 16 + 14 - py * 2] b = tilemap[index * 16 + 15 - py * 2] for px in range(8): bit = 0x80 >> px if attr & 0x20: bit = 0x01 << px c = (attr & 7) << 2 if a & bit: c |= 1 if b & bit: c |= 2 img.putpixel((x + px, y + py), c) for x in range(16): for y in range(16): idx = x + y * 16 metatiles = metatile_info[idx * 4:idx * 4 + 4] attrtiles = attrtile_info[idx * 4:idx * 4 + 4] drawTile(x * 16 + 0, y * 16 + 0, metatiles[0], attrtiles[0]) drawTile(x * 16 + 8, y * 16 + 0, metatiles[1], attrtiles[1]) drawTile(x * 16 + 0, y * 16 + 8, metatiles[2], attrtiles[2]) drawTile(x * 16 + 8, y * 16 + 8, metatiles[3], attrtiles[3]) img.save("%s/overworld/export/%s" % (path, image_filename)) world = { "maps": [{ "fileName": "%02X.json" % (n), "height": 128, "width": 160, "x": (n & 0x0F) * 160, "y": (n >> 4) * 128 } for n in range(0x100)], "onlyShowAdjacentMaps": False, "type": "world" } json.dump(world, open("%s/overworld/export/world.world" % (path), "wt"))
def createDungeonOnlyOverworld(rom): # Skip the whole egg maze. rom.patch(0x14, 0x0453, "75", "73") instrument_rooms = [ 0x102, 0x12A, 0x159, 0x162, 0x182, 0x1B5, 0x22C, 0x230, 0x301 ] path = os.path.dirname(__file__) # Start with clearing all the maps, because this just generates a bunch of room in the rom. for n in range(0x100): re = RoomEditor(rom, n) re.entities = [] re.objects = [] if os.path.exists("%s/overworld/dive/%02X.json" % (path, n)): re.loadFromJson("%s/overworld/dive/%02X.json" % (path, n)) entrances = list( filter(lambda obj: obj.type_id in WARP_TYPE_IDS, re.objects)) for obj in re.objects: if isinstance(obj, ObjectWarp) and entrances: e = entrances.pop(0) other = RoomEditor(rom, obj.room) for o in other.objects: if isinstance(o, ObjectWarp) and o.warp_type == 0: o.room = n o.target_x = e.x * 16 + 8 o.target_y = e.y * 16 + 16 other.store(rom) if obj.room == 0x1F5: # Patch the boomang guy exit other = RoomEditor(rom, "Alt1F5") other.getWarps()[0].room = n other.getWarps()[0].target_x = e.x * 16 + 8 other.getWarps()[0].target_y = e.y * 16 + 16 other.store(rom) if obj.warp_type == 1 and (obj.map_nr < 8 or obj.map_nr == 0xFF ) and obj.room not in (0x1B0, 0x23A, 0x23D): other = RoomEditor(rom, instrument_rooms[min(8, obj.map_nr)]) for o in other.objects: if isinstance(o, ObjectWarp) and o.warp_type == 0: o.room = n o.target_x = e.x * 16 + 8 o.target_y = e.y * 16 + 16 other.store(rom) re.store(rom)
def isNormalOverworld(rom): return len(RoomEditor(rom, 0x010).getWarps()) > 0
def changeMiniBosses(rom, mapping): # Fix avalaunch not working when entering a room from the left or right rom.patch(0x03, 0x0BE0, ASM(""" ld [hl], $50 ld hl, $C2D0 add hl, bc ld [hl], $00 jp $4B56 """), ASM(""" ld a, [hl] sub $08 ld [hl], a ld hl, $C2D0 add hl, bc ld [hl], b ; b is always zero here ret """), fill_nop=True) for target, name in mapping.items(): re = RoomEditor(rom, MINIBOSS_ROOMS[target]) re.entities = [e for e in re.entities if e[2] == 0x61] # Only keep warp, if available re.entities += MINIBOSS_ENTITIES[name] if re.room == 0x228 and name != "GRIM_CREEPER": for x in range(3, 7): for y in range(0, 3): re.removeObject(x, y) if name == "CUE_BALL": re.objects += [ Object(3, 3, 0x2c), ObjectHorizontal(4, 3, 0x22, 2), Object(6, 3, 0x2b), Object(3, 4, 0x2a), ObjectHorizontal(4, 4, 0x21, 2), Object(6, 4, 0x29), ] if name == "BLAINO": # BLAINO needs a warp object to hit you to the entrance of the dungeon. if len(re.getWarps()) < 1: # Default to start house. target = (0x10, 0x2A3, 0x50, 0x7c, 0x2A3) if 0x100 <= re.room < 0x11D: #D1 target = (0, 0x117, 80, 80) elif 0x11D <= re.room < 0x140: #D2 target = (1, 0x136, 80, 80) elif 0x140 <= re.room < 0x15D: #D3 target = (2, 0x152, 80, 80) elif 0x15D <= re.room < 0x180: #D4 target = (2, 0x174, 80, 80) elif 0x180 <= re.room < 0x1AC: #D5 target = (2, 0x1A1, 80, 80) elif 0x1B0 <= re.room < 0x1DE: #D6 target = (2, 0x1D4, 80, 80) elif 0x200 <= re.room < 0x22D: #D7 target = (6, 0x20E, 80, 80) elif 0x22D <= re.room < 0x26C: #D8 target = (7, 0x25D, 80, 80) elif re.room >= 0x300: #D0 target = (0xFF, 0x312, 80, 80) elif re.room == 0x2E1: #Moblin cave target = (0x15, 0x2F0, 0x50, 0x7C) re.objects.append(ObjectWarp(1, *target)) re.store(rom) sprite_data = entityData.SPRITE_DATA[MINIBOSS_ENTITIES[name][0][2]] for n in range(0, len(sprite_data), 2): rom.room_sprite_data_indoor[re.room - 0x100][sprite_data[n]] = sprite_data[n + 1]
def changeEntrances(rom, mapping): dungeon_entrance_rooms = [0x117, 0x136, 0x152, 0x17a, 0x1a1, 0x1d4, 0x20e, 0x25d, 0x312] instrument_rooms = [0x102, 0x12A, 0x159, 0x162, 0x182, 0x1B5, 0x22C, 0x230, 0x301] alt_rooms = {0x02B: "Alt2B", 0x08C: "Alt8C", 0x00E: "Alt0E"} world_entrance_rooms = [] enter_warps = [] exit_warps = [] for room in dungeon_entrance_rooms: world_entrance_rooms.append(RoomEditor(rom, room).getWarps()[0].room) for idx, room in enumerate(world_entrance_rooms): if alt_rooms.get(room, None) is not None: re = RoomEditor(rom, alt_rooms[room]) else: re = RoomEditor(rom, room) warp = None for obj in re.objects: if isinstance(obj, ObjectWarp) and (obj.map_nr < 9 or obj.map_nr == 0xff): warp = obj enter_warps.append(warp) if warp: re = RoomEditor(rom, warp.room) warp = None for obj in re.objects: if isinstance(obj, ObjectWarp) and obj.room == room: warp = obj exit_warps.append(warp) re = RoomEditor(rom, instrument_rooms[idx]) for obj in re.objects: if isinstance(obj, ObjectWarp): assert obj.room == world_entrance_rooms[idx], "D%d instrument room not warping to entrance %03x != %03x" % (idx + 1, obj.room, world_entrance_rooms[idx]) for a, b in enumerate(mapping): re = RoomEditor(rom, world_entrance_rooms[a]) re.changeWarpTarget(enter_warps[a].room, enter_warps[b].room, enter_warps[b].map_nr, enter_warps[b].target_x, enter_warps[b].target_y) re.store(rom) if alt_rooms.get(world_entrance_rooms[a], None) is not None: re = RoomEditor(rom, alt_rooms[world_entrance_rooms[a]]) re.changeWarpTarget(enter_warps[a].room, enter_warps[b].room, enter_warps[b].map_nr, enter_warps[b].target_x, enter_warps[b].target_y) re.store(rom) re = RoomEditor(rom, enter_warps[b].room) re.changeWarpTarget(world_entrance_rooms[b], exit_warps[a].room, exit_warps[a].map_nr, exit_warps[a].target_x, exit_warps[a].target_y) re.store(rom) re = RoomEditor(rom, instrument_rooms[b]) re.changeWarpTarget(world_entrance_rooms[b], exit_warps[a].room, exit_warps[a].map_nr, exit_warps[a].target_x, exit_warps[a].target_y) re.store(rom)
def getCleanBossRoom(rom, dungeon_nr): re = RoomEditor(rom, BOSS_ROOMS[dungeon_nr][0]) new_objects = [] for obj in re.objects: if isinstance(obj, ObjectWarp): continue if obj.type_id == 0xBE: # Remove staircases continue if obj.type_id == 0x06: # Remove lava continue if obj.type_id == 0x1c: # Change D1 pits into normal pits obj.type_id = 0x01 if obj.type_id == 0x1e: # Change D1 pits into normal pits obj.type_id = 0xaf if obj.type_id == 0x1f: # Change D1 pits into normal pits obj.type_id = 0xb0 if obj.type_id == 0xF5: # Change open doors into closing doors. obj.type_id = 0xF1 new_objects.append(obj) # Make D4 room a valid fighting room by removing most content. if dungeon_nr == 3: new_objects = new_objects[:2] + [ Object(1, 1, 0xAC), Object(8, 1, 0xAC), Object(1, 6, 0xAC), Object(8, 6, 0xAC) ] # D7 has an empty room we use for most bosses, but it needs some adjustments. if dungeon_nr == 6: # Move around the unused and instrument room. rom.banks[0x14][0x03a0 + 6 + 1 * 8] = 0x00 rom.banks[0x14][0x03a0 + 7 + 2 * 8] = 0x2C rom.banks[0x14][0x03a0 + 7 + 3 * 8] = 0x23 rom.banks[0x14][0x03a0 + 6 + 5 * 8] = 0x00 rom.banks[0x14][0x0520 + 7 + 2 * 8] = 0x2C rom.banks[0x14][0x0520 + 7 + 3 * 8] = 0x23 rom.banks[0x14][0x0520 + 6 + 5 * 8] = 0x00 re.floor_object &= 0x0F new_objects += [ Object(4, 0, 0xF0), Object(1, 6, 0xBE), ObjectWarp(1, dungeon_nr, 0x22E, 24, 16) ] # Set the stairs towards the eagle tower top to our new room. r = RoomEditor(rom, 0x22E) r.objects[-1] = ObjectWarp(1, dungeon_nr, re.room, 24, 112) r.store(rom) # Remove the normal door to the instrument room r = RoomEditor(rom, 0x22e) r.removeObject(4, 0) r.store(rom) rom.banks[0x14][0x22e - 0x100] = 0x00 r = RoomEditor(rom, 0x22c) r.changeObject(0, 7, 0x03) r.changeObject(2, 7, 0x03) r.store(rom) re.objects = new_objects re.entities = [] return re
def changeEntrances(rom, mapping): entrance_rooms = [ 0x0D3, 0x024, 0x0B5, 0x02B, 0x0D9, 0x08C, 0x00E, 0x010, 0x077 ] instrument_rooms = [ 0x102, 0x12A, 0x159, 0x162, 0x182, 0x1B5, 0x22C, 0x230, 0x301 ] alt_rooms = [ None, None, None, (0x09, 0x109A), None, (0x1A, 0x034E), (0x09, 0x07EC), None, None ] enter_warps = [] exit_warps = [] for idx, room in enumerate(entrance_rooms): if alt_rooms[idx] is not None: re = RoomEditor(rom, bank_nr=alt_rooms[idx][0], address=alt_rooms[idx][1]) else: re = RoomEditor(rom, room) warp = None for obj in re.objects: if isinstance(obj, ObjectWarp) and (obj.map_nr < 9 or obj.map_nr == 0xff): warp = obj enter_warps.append(warp) if warp: re = RoomEditor(rom, warp.room) warp = None for obj in re.objects: if isinstance(obj, ObjectWarp) and obj.room == room: warp = obj exit_warps.append(warp) re = RoomEditor(rom, instrument_rooms[idx]) for obj in re.objects: if isinstance(obj, ObjectWarp): assert obj.room == entrance_rooms[idx] for a, b in enumerate(mapping): re = RoomEditor(rom, entrance_rooms[a]) re.changeWarpTarget(enter_warps[a].room, enter_warps[b].room, enter_warps[b].map_nr, enter_warps[b].target_x, enter_warps[b].target_y) re.store(rom) if alt_rooms[a] is not None: re = RoomEditor(rom, bank_nr=alt_rooms[a][0], address=alt_rooms[a][1]) re.changeWarpTarget(enter_warps[a].room, enter_warps[b].room, enter_warps[b].map_nr, enter_warps[b].target_x, enter_warps[b].target_y) re.store(rom) re = RoomEditor(rom, enter_warps[b].room) re.changeWarpTarget(entrance_rooms[b], exit_warps[a].room, exit_warps[a].map_nr, exit_warps[a].target_x, exit_warps[a].target_y) re.store(rom) re = RoomEditor(rom, instrument_rooms[b]) re.changeWarpTarget(entrance_rooms[b], exit_warps[a].room, exit_warps[a].map_nr, exit_warps[a].target_x, exit_warps[a].target_y) re.store(rom)
def setRaftGoal(rom): rom.texts[0x1A3] = formatText(b"Just sail away.") # Remove the egg and egg event handler. re = RoomEditor(rom, 0x006) for x in range(4, 7): for y in range(0, 4): re.removeObject(x, y) re.objects.append(ObjectHorizontal(4, 1, 0x4d, 3)) re.objects.append(ObjectHorizontal(4, 2, 0x03, 3)) re.objects.append(ObjectHorizontal(4, 3, 0x03, 3)) re.entities = [] re.updateOverlay() re.store(rom) re = RoomEditor(rom, 0x08D) re.objects[6].count = 4 re.objects[7].x += 2 re.objects[7].type_id = 0x2B re.objects[8].x += 2 re.objects[8].count = 2 re.objects[9].x += 1 re.objects[11] = ObjectVertical(7, 5, 0x37, 2) re.objects[12].x -= 1 re.objects[13].x -= 1 re.objects[14].x -= 1 re.objects[14].type_id = 0x34 re.objects[17].x += 3 re.objects[17].count -= 3 re.updateOverlay() re.overlay[7 + 60] = 0x33 re.store(rom) re = RoomEditor(rom, 0x0E9) re.objects[30].count = 1 re.objects[30].x += 2 re.overlay[7 + 70] = 0x0E re.overlay[8 + 70] = 0x0E re.store(rom) re = RoomEditor(rom, 0x0F9) re.objects = [ ObjectHorizontal(4, 0, 0x0E, 6), ObjectVertical(9, 0, 0xCA, 8), ObjectVertical(8, 0, 0x0E, 8), Object(3, 0, 0x38), Object(3, 1, 0x32), ObjectHorizontal(4, 1, 0x2C, 3), Object(7, 1, 0x2D), ObjectVertical(7, 2, 0x38, 5), Object(7, 7, 0x34), ObjectHorizontal(0, 7, 0x2F, 7), ObjectVertical(2, 3, 0xE8, 4), ObjectVertical(3, 2, 0xE8, 5), ObjectVertical(4, 2, 0xE8, 2), ObjectVertical(4, 4, 0x5C, 3), ObjectVertical(5, 2, 0x5C, 5), ObjectVertical(6, 2, 0x5C, 5), Object(6, 4, 0xC6), ObjectWarp(1, 0x1F, 0xF6, 136, 112) ] re.updateOverlay(True) re.entities.append((0, 0, 0x41)) re.store(rom) re = RoomEditor(rom, 0x1F6) re.objects[-1].target_x -= 16 re.store(rom) # Fix the raft graphics (this overrides some unused graphic tiles) rom.banks[0x31][0x21C0:0x2200] = rom.banks[0x2E][0x07C0:0x0800] # Patch the owl entity to run our custom end handling. rom.patch(0x06, 0x27F5, 0x2A77, ASM(""" ld a, [$DB95] cp $0B ret nz ; If map is not fully loaded, return ld a, [$C124] and a ret nz ; Check if we are moving off the bottom of the map ldh a, [$99] cp $7D ret c ; Move link back so it does not move off the map ld a, $7D ldh [$99], a xor a ld e, a ld d, a raftSearchLoop: ld hl, $C280 add hl, de ld a, [hl] and a jr z, .skipEntity ld hl, $C3A0 add hl, de ld a, [hl] cp $6A jr nz, .skipEntity ; Raft found, check if near the bottom of the screen. ld hl, $C210 add hl, de ld a, [hl] cp $70 jr nc, raftOffWorld .skipEntity: inc e ld a, e cp $10 jr nz, raftSearchLoop ret raftOffWorld: ; Switch to the end credits ld a, $01 ld [$DB95], a ld a, $00 ld [$DB96], a ret """), fill_nop=True) # We need to run quickly trough part of the credits, or else it bugs out # Skip the whole windfish part. rom.patch(0x17, 0x0D39, None, ASM("ld a, $18\nld [$D00E], a\nret")) # And skip the zoomed out laying on the log rom.patch(0x17, 0x20ED, None, ASM("ld a, $00")) # Finally skip some waking up on the log. rom.patch(0x17, 0x23BC, None, ASM("jp $4CD9")) rom.patch(0x17, 0x2476, None, ASM("jp $4CD9"))
def updateWitch(rom): # Add a heartpiece at the toadstool, the item patches turn this into a 1 time toadstool item # Or depending on flags, in something else. re = RoomEditor(rom, 0x050) re.addEntity(2, 3, 0x35) re.store(rom) # Change what happens when you trade the toadstool with the witch # Note that the 2nd byte of this code gets patched with the item to give from the witch. rom.patch(0x05, 0x08D4, 0x08F0, ASM(""" ld e, $09 ; load the item to give the first time ; Get the room flags and mark the witch as done. ld hl, $DAA2 ld a, [hl] and $30 set 4, [hl] set 5, [hl] jr z, skip ld e, $09 ; give powder every time after the first time. skip: ld a, e ldh [$F1], a ld a, $02 rst 8 ld a, $03 rst 8 """), fill_nop=True) # Patch the toadstool to unload when you haven't delivered something to the witch yet. rom.patch(0x03, 0x1D4B, ASM(""" ld hl, $DB4B ld a, [$DB4C] or [hl] jp nz, $3F8D """), ASM(""" ld a, [$DAA2] and $20 jp z, $3F8D """), fill_nop=True) # Patch what happens when we pickup the toadstool, call our chest code to give a toadstool. rom.patch(0x03, 0x1D6F, 0x1D7D, ASM(""" ld a, $50 ldh [$F1], a ld a, $02 ; give item rst 8 ld hl, $DAA2 res 5, [hl] """), fill_nop=True)
def createDungeonOnlyOverworld(rom): # Skip the whole egg maze. rom.patch(0x14, 0x0453, "75", "73") # Some sprite patches (should generalize this) rom.room_sprite_data_overworld[0x72] = b'\xff\xff\xff\xff' rom.room_sprite_data_overworld[0x73] = rom.room_sprite_data_overworld[0x8C] rom.room_sprite_data_overworld[0xB1] = rom.room_sprite_data_overworld[0x92] instrument_rooms = [ 0x102, 0x12A, 0x159, 0x162, 0x182, 0x1B5, 0x22C, 0x230, 0x301 ] # Start with clearing all the maps, because this just generates a bunch of room in the rom. for n in range(0x100): re = RoomEditor(rom, n) re.entities = [] re.objects = [] if os.path.exists("patches/overworld/%02X.json" % (n)): re.loadFromJson("patches/overworld/%02X.json" % (n)) re.updateOverlay() entrances = list( filter( lambda obj: obj.type_id in (0xE1, 0xE2, 0xE3, 0xBA, 0xA8, 0xBE, 0xCB), re.objects)) for obj in re.objects: if isinstance(obj, ObjectWarp) and entrances: e = entrances.pop(0) other = RoomEditor(rom, obj.room) for o in other.objects: if isinstance(o, ObjectWarp) and o.warp_type == 0: o.room = n o.target_x = e.x * 16 + 8 o.target_y = e.y * 16 + 16 other.store(rom) if obj.room == 0x1F5: # Patch the boomang guy exit rom.patch( 0x0a, 0x3891, "E000F41820", "E000%02x%02x%02x" % (n, e.x * 16 + 8, e.y * 16 + 16)) if obj.warp_type == 1 and obj.map_nr < 8 or obj.map_nr == 0xFF: other = RoomEditor(rom, instrument_rooms[min(8, obj.map_nr)]) for o in other.objects: if isinstance(o, ObjectWarp) and o.warp_type == 0: o.room = n o.target_x = e.x * 16 + 8 o.target_y = e.y * 16 + 16 other.store(rom) if n == 0x06: re.objects.insert(0, Object(5, 3, 0xE1)) re.store(rom)
def addMultiworldShop(rom): # Make a copy of the shop into GrandpaUlrira house shop_room = RoomEditor(rom, 0x2A1) re = RoomEditor(rom, 0x2A9) re.objects = [ obj for obj in shop_room.objects if obj.x is not None and obj.type_id != 0xCE ] + re.getWarps() re.entities = [(1, 6, 0x77), (2, 6, 0x77)] re.animation_id = shop_room.animation_id re.floor_object = shop_room.floor_object re.store(rom) # Fix the tileset rom.banks[0x20][0x2EB3 + 0x2A9 - 0x100] = rom.banks[0x20][0x2EB3 + 0x2A1 - 0x100] # Load the shopkeeper sprites instead of Grandpa sprites entityData.SPRITE_DATA[0x77] = entityData.SPRITE_DATA[0x4D] labels = {} rom.patch(0x06, 0x2860, "00" * 0x215, ASM( """ shopItemsHandler: ; Render the shop items ld h, $00 loop: ; First load links position to render the item at ldh a, [$98] ; LinkX ldh [$EE], a ; X ldh a, [$99] ; LinkY sub $0E ldh [$EC], a ; Y ; Check if this is the item we have picked up ld a, [$C509] ; picked up item in shop dec a cp h jr z, .renderCarry ld a, h swap a add a, $20 ldh [$EE], a ; X ld a, $30 ldh [$EC], a ; Y .renderCarry: ld a, h push hl ldh [$F1], a ; variant cp $03 jr nc, .singleSprite ld de, ItemsDualSpriteData call $3BC0 ; render sprite pair jr .renderDone .singleSprite: ld de, ItemsSingleSpriteData call $3C77 ; render sprite .renderDone: pop hl .skipItem: inc h ld a, $07 cp h jr nz, loop ; check if we want to pickup or drop an item ldh a, [$CC] and $30 ; A or B button call nz, checkForPickup ; check if we have an item ld a, [$C509] ; carry item and a ret z ; Set that link has picked something up ld a, $01 ld [$C15C], a call $0CAF ; reset spin attack... ; Check if we are trying to exit the shop and so drop our item. ldh a, [$99] cp $78 ret c xor a ld [$C509], a ret checkForPickup: ldh a, [$9E] ; direction cp $02 ret nz ldh a, [$99] ; LinkY cp $48 ret nc ld a, $13 ldh [$F2], a ; play SFX ld a, [$C509] ; picked up shop item and a jr nz, .drop ldh a, [$98] ; LinkX sub $08 swap a and $07 ld [$C509], a ; picked up shop item ret .drop: xor a ld [$C509], a ret ItemsDualSpriteData: db $60, $08, $60, $28 ; zol db $68, $09 ; chicken (left) ItemsSingleSpriteData: ; (first 3 entries are still dual sprites) db $6A, $09 ; chicken (right) db $14, $02, $14, $22 ; piece of power ;Real single sprite data starts here db $00, $0F ; bomb db $38, $0A ; rupees db $20, $0C ; medicine db $28, $0C ; heart ;------------------------------------trying to buy something starts here talkHandler: ld a, [$C509] ; carry item add a, a ret z ; check if we have something to buy sub $02 ld hl, itemNames ld e, a ld d, b ; b=0 add hl, de ld e, [hl] inc hl ld d, [hl] ld hl, wCustomMessage call appendString dec hl call padString ld de, postMessage call appendString dec hl ld a, $fe ld [hl], a ld de, $FFEF add hl, de ldh a, [$EE] swap a and $0F add a, $30 ld [hl], a ld a, $C9 call $2385 ; open dialog call $3B12 ; increase entity state ret appendString: ld a, [de] inc de and a ret z ldi [hl], a jr appendString padString: ld a, l and $0F ret z ld a, $20 ldi [hl], a jr padString itemNames: dw itemZol dw itemChicken dw itemPieceOfPower dw itemBombs dw itemRupees dw itemMedicine dw itemHealth postMessage: db "For player X? Yes No ", $00 itemZol: db m"Slime storm|100 {RUPEES}", $00 itemChicken: db m"Coccu party|50 {RUPEES}", $00 itemPieceOfPower: db m"Piece of Power|50 {RUPEES}", $00 itemBombs: db m"20 Bombs|50 {RUPEES}", $00 itemRupees: db m"100 {RUPEES}|200 {RUPEES}", $00 itemMedicine: db m"Medicine|100 {RUPEES}", $00 itemHealth: db m"Health refill|10 {RUPEES}", $00 TalkResultHandler: ld hl, ItemPriceTableBCD ld a, [$C509] dec a add a, a ld c, a ; b=0 add hl, bc ldi a, [hl] ld d, [hl] ld e, a ld a, [$DB5D] cp d ret c jr nz, .highEnough ld a, [$DB5E] cp e ret c .highEnough: ; Got enough money, take it. ld hl, ItemPriceTableDEC ld a, [$C509] dec a ld c, a ; b=0 add hl, bc ld a, [hl] ld [$DB92], a ; No longer picked up item xor a ld [$C509], a ret ItemPriceTableBCD: dw $0100, $0050, $0050, $0050, $0200, $0100, $0010 ItemPriceTableDEC: db $64, $32, $32, $32, $C8, $64, $0A """, 0x6860, labels), fill_nop=True) # Patch GrandpaUlrira to work as a multiworld shop rom.patch(0x06, 0x1C0E, 0x1C89, ASM( """ ld a, $01 ld [$C50A], a ; this stops link from using items ;Draw shopkeeper ld de, OwnerSpriteData call $3BC0 ; render sprite pair ldh a, [$E7] ; frame counter swap a and $01 call $3B0C ; set sprite variant ldh a, [$F0] and a jr nz, checkTalkingResult call $641A ; prevent link from moving into the sprite call $645D ; check if talking to NPC call c, ${TALKHANDLER:04x} ; talk handling ldh a, [$EE] ; X cp $18 ret nz ; Jump to other code which is placed on the old owl code. As we do not have enough space here. jp ${SHOPITEMSHANDLER:04x} checkTalkingResult: ld a, [$C19F] and a ret nz ; still taking call $3B12 ; increase entity state ld [hl], $00 ld a, [$C177] ; dialog selection and a ret nz jp ${TALKRESULTHANDLER:04x} OwnerSpriteData: ;db $60, $03, $62, $03, $62, $23, $60, $23 ; down db $64, $03, $66, $03, $66, $23, $64, $23 ; up ;db $68, $03, $6A, $03, $6C, $03, $6E, $03 ; left ;db $6A, $23, $68, $23, $6E, $23, $6C, $23 ; right """.format(**labels), 0x5C0E), fill_nop=True)
def read(self, rom): re = RoomEditor(rom, 0x0F2) if re.hasEntity(0x31): return SWORD return super().read(rom)
def setSeashellGoal(rom, count): rom.texts[0x1A3] = formatText(b"You need %d seashells" % (count)) # Remove the seashell mansion handler (as it will take your seashells) but put a heartpiece instead re = RoomEditor(rom, 0x2E9) re.entities = [(4, 4, 0x35)] re.store(rom) rom.patch(0x19, 0x0ACB, 0x0C21, ASM( """ ldh a, [$F8] ; room status and $10 ret nz ldh a, [$F0] ; active entity state rst 0 dw state0, state1, state2, state3, state4 state0: ld a, [$C124] ; room transition state and a ret nz ldh a, [$99] ; link position Y cp $70 ret nc jp $3B12 ; increase entity state state1: call $0C05 ; get entity transition countdown jr nz, renderShells ld [hl], $10 call renderShells ld hl, $C2B0 ; private state 1 table add hl, bc ld a, [wSeashellsCount] cp [hl] jp z, $3B12 ; increase entity state ld a, [hl] ; increase the amount of compared shells inc a daa ld [hl], a ld hl, $C2C0 ; private state 2 table add hl, bc inc [hl] ; increase amount of displayed shells ld a, $2B ldh [$F4], a ; SFX ret state2: ld a, [wSeashellsCount] cp $%02d jr c, renderShells ; got enough shells call $3B12 ; increase entity state call $0C05 ; get entity transition countdown ld [hl], $40 jp renderShells state3: ld a, $23 ldh [$F2], a ; SFX: Dungeon opened ld hl, $D806 ; egg room status set 4, [hl] ld a, [hl] ldh [$F8], a ; current room status call $3B12 ; increase entity state ld a, $00 jp $4C2E state4: ret renderShells: ld hl, $C2C0 ; private state 2 table add hl, bc ld a, [hl] cp $14 jr c, .noMax ld a, $14 .noMax: and a ret z ld c, a ld hl, spriteRect call $3CE6 ; RenderActiveEntitySpritesRect ret spriteRect: db $10, $1E, $1E, $0C db $10, $2A, $1E, $0C db $10, $36, $1E, $0C db $10, $42, $1E, $0C db $10, $4E, $1E, $0C db $10, $5A, $1E, $0C db $10, $66, $1E, $0C db $10, $72, $1E, $0C db $10, $7E, $1E, $0C db $10, $8A, $1E, $0C db $24, $1E, $1E, $0C db $24, $2A, $1E, $0C db $24, $36, $1E, $0C db $24, $42, $1E, $0C db $24, $4E, $1E, $0C db $24, $5A, $1E, $0C db $24, $66, $1E, $0C db $24, $72, $1E, $0C db $24, $7E, $1E, $0C db $24, $8A, $1E, $0C """ % (count), 0x4ACB), fill_nop=True)
def exportRoom(self, room_nr): re = RoomEditor(self.__rom, room_nr) if room_nr < 0x100: tile_info_offset = self.__rom.banks[0x1A].find( b'\x7C\x7C\x7C\x7C\x7D\x7D\x7D\x7D') tile_info = self.__rom.banks[0x1A][ tile_info_offset:tile_info_offset + 0x100 * 4] else: tile_info_offset = self.__rom.banks[0x08].find( b'\x7F\x7F\x7F\x7F\x7E\x7E\x7E\x7E') tile_info = self.__rom.banks[0x08][ tile_info_offset:tile_info_offset + 0x100 * 4] if room_nr >= 0x100: rendered_map = RenderedMap(re.floor_object & 0x0F) else: rendered_map = RenderedMap(re.floor_object, True) def objHSize(type_id): if type_id == 0xF5: return 2 return 1 def objVSize(type_id): if type_id == 0xF5: return 2 return 1 if room_nr >= 0x100: if re.floor_object & 0xF0 == 0x00: rendered_map.addWalls(RenderedMap.WALL_LEFT | RenderedMap.WALL_RIGHT | RenderedMap.WALL_UP | RenderedMap.WALL_DOWN) if re.floor_object & 0xF0 == 0x10: rendered_map.addWalls(RenderedMap.WALL_LEFT | RenderedMap.WALL_RIGHT | RenderedMap.WALL_DOWN) if re.floor_object & 0xF0 == 0x20: rendered_map.addWalls(RenderedMap.WALL_LEFT | RenderedMap.WALL_UP | RenderedMap.WALL_DOWN) if re.floor_object & 0xF0 == 0x30: rendered_map.addWalls(RenderedMap.WALL_LEFT | RenderedMap.WALL_RIGHT | RenderedMap.WALL_UP) if re.floor_object & 0xF0 == 0x40: rendered_map.addWalls(RenderedMap.WALL_RIGHT | RenderedMap.WALL_UP | RenderedMap.WALL_DOWN) if re.floor_object & 0xF0 == 0x50: rendered_map.addWalls(RenderedMap.WALL_LEFT | RenderedMap.WALL_DOWN) if re.floor_object & 0xF0 == 0x60: rendered_map.addWalls(RenderedMap.WALL_RIGHT | RenderedMap.WALL_DOWN) if re.floor_object & 0xF0 == 0x70: rendered_map.addWalls(RenderedMap.WALL_RIGHT | RenderedMap.WALL_UP) if re.floor_object & 0xF0 == 0x80: rendered_map.addWalls(RenderedMap.WALL_LEFT | RenderedMap.WALL_UP) for obj in re.objects: if isinstance(obj, ObjectWarp): pass elif isinstance(obj, ObjectHorizontal): for n in range(0, obj.count): rendered_map.placeObject(obj.x + n * objHSize(obj.type_id), obj.y, obj.type_id) elif isinstance(obj, ObjectVertical): for n in range(0, obj.count): rendered_map.placeObject(obj.x, obj.y + n * objVSize(obj.type_id), obj.type_id) else: rendered_map.placeObject(obj.x, obj.y, obj.type_id) tiles = [0] * 20 * 16 for y in range(8): for x in range(10): obj = rendered_map.objects[(x, y)] tiles[x * 2 + y * 2 * 20] = tile_info[obj * 4] tiles[x * 2 + 1 + y * 2 * 20] = tile_info[obj * 4 + 1] tiles[x * 2 + (y * 2 + 1) * 20] = tile_info[obj * 4 + 2] tiles[x * 2 + 1 + (y * 2 + 1) * 20] = tile_info[obj * 4 + 3] if room_nr < 0x100: sub_tileset_offset = self.__rom.banks[0x20][0x2E73 + (room_nr & 0x0F) // 2 + ((room_nr >> 5) * 8)] << 4 tilemap = self.__tiles[0x0f][ sub_tileset_offset:sub_tileset_offset + 0x20] tilemap += self.__tiles[0x0c][0x120:0x180] tilemap += self.__tiles[0x0c][0x080:0x100] else: # TODO: The whole indoor tileset loading seems complex... tileset_nr = self.__rom.banks[0x20][0x2eB3 + room_nr - 0x100] tilemap = [None] * 0x100 tilemap[0x20:0x80] = self.__tiles[0x0D][0x000:0x060] if tileset_nr != 0xFF: tilemap[0x00:0x10] = self.__tiles[0x0D][0x100 + tileset_nr * 0x10:0x110 + tileset_nr * 0x10] tilemap[0x10:0x20] = self.__tiles[0x0D][0x210:0x220] tilemap[0xF0:0x100] = self.__tiles[0x12][0x380:0x390] if re.animation_id == 2: addr = 0x2B0 elif re.animation_id == 3: addr = 0x2C0 elif re.animation_id == 4: addr = 0x2D0 elif re.animation_id == 5: addr = 0x2E0 elif re.animation_id == 6: addr = 0x2F0 elif re.animation_id == 7: addr = 0x2D0 elif re.animation_id == 8: addr = 0x300 elif re.animation_id == 9: addr = 0x310 elif re.animation_id == 10: addr = 0x320 elif re.animation_id == 11: addr = 0x2A0 elif re.animation_id == 12: addr = 0x330 elif re.animation_id == 13: addr = 0x350 elif re.animation_id == 14: addr = 0x360 elif re.animation_id == 15: addr = 0x340 elif re.animation_id == 16: addr = 0x370 else: print(hex(room_nr), re.animation_id) addr = 0x000 tilemap[0x6C:0x70] = self.__tiles[0x0c][addr:addr + 4] assert len(tilemap) == 0x100 result = PIL.Image.new('L', (8 * 20, 8 * 16)) draw = PIL.ImageDraw.Draw(result) for y in range(16): for x in range(20): tile = tilemap[tiles[x + y * 20]] if tile is not None: result.paste(tile, (x * 8, y * 8)) warp_pos = [] for y in range(8): for x in range(10): if rendered_map.objects[(x, y)] in (0xE1, 0xE3, 0xBA, 0xD5, 0xA8, 0xBE, 0xCB): warp_pos.append((x, y)) for x, y, type_id in re.entities: draw.rectangle([(x * 16, y * 16), (x * 16 + 15, y * 16 + 15)], outline=0) draw.text((x * 16 + 3, y * 16 + 2), "%02X" % (type_id)) y = 8 for obj in re.objects: if isinstance(obj, ObjectWarp): draw.text((8, y), "W:" + str(obj)) y += 16 return result
def changeEntrances(rom, mapping): warp_to_indoor = {} warp_to_outdoor = {} for key in mapping.keys(): info = ENTRANCE_INFO[key] re = RoomEditor( rom, info.alt_room if info.alt_room is not None else info.room) warp = re.getWarps()[info.index if info.index not in (None, "all") else 0] warp_to_indoor[key] = warp assert info.target == warp.room, "%s != %03x" % (key, warp.room) re = RoomEditor(rom, warp.room) for warp in re.getWarps(): if warp.room == info.room: warp_to_outdoor[key] = warp assert key in warp_to_outdoor, "Missing warp to outdoor on %s" % (key) # First collect all the changes we need to do per room changes_per_room = {} def addChange(source_room, target_room, new_warp): if source_room not in changes_per_room: changes_per_room[source_room] = {} changes_per_room[source_room][target_room] = new_warp for key, target in mapping.items(): if key == target: continue info = ENTRANCE_INFO[key] # Change the entrance to point to the new indoor room addChange(info.room, warp_to_indoor[key].room, warp_to_indoor[target]) if info.alt_room: addChange(info.alt_room, warp_to_indoor[key].room, warp_to_indoor[target]) # Change the exit to point to the right outside addChange(warp_to_indoor[target].room, ENTRANCE_INFO[target].room, warp_to_outdoor[key]) if ENTRANCE_INFO[target].instrument_room is not None: addChange(ENTRANCE_INFO[target].instrument_room, ENTRANCE_INFO[target].room, warp_to_outdoor[key]) # Finally apply the changes, we need to do this once per room to prevent A->B->C issues. for room, changes in changes_per_room.items(): re = RoomEditor(rom, room) for idx, obj in enumerate(re.objects): if isinstance(obj, ObjectWarp) and obj.room in changes: re.objects[idx] = changes[obj.room].copy() re.store(rom)
def removeOwlEvents(rom): # Remove all the owl events from the entity tables. for room in range(0x100): re = RoomEditor(rom, room) if re.hasEntity(0x41): re.removeEntities(0x41) re.store(rom) # Clear texts used by the owl. Potentially reused somewhere o else. rom.texts[0x0D9] = b'\xff' # used by boomerang # 1 Used by empty chest (master stalfos message) # 8 unused (0x0C0-0x0C7) # 1 used by bowwow in chest # 1 used by item for other player message # 2 used by arrow chest messages # 2 used by tunics for idx in range(0x0BE, 0x0CE): rom.texts[idx] = b'\xff' # Patch the owl entity into a ghost to allow refill of powder/bombs/arrows rom.texts[0xC0] = formatText("Everybody hates me, so I give away free things in the hope people will love me. Want something?", ask="Okay No") rom.texts[0xC1] = formatText("Good for you.") rom.patch(0x06, 0x27F5, 0x2A77, ASM(""" ; Check if we have powder or bombs. ld e, INV_SIZE ld hl, $DB00 loop: ldi a, [hl] cp $02 ; bombs jr z, hasProperItem cp $0C ; powder jr z, hasProperItem cp $05 ; bow jr z, hasProperItem dec e jr nz, loop ret hasProperItem: ; Render ghost ld de, sprite call $3BC0 call $64C6 ; check if game is busy (pops this stack frame if busy) ldh a, [$E7] ; frame counter swap a and $01 call $3B0C ; set entity sprite variant call $641A ; check collision ldh a, [$F0] ;entity state rst 0 dw waitForTalk dw talking waitForTalk: call $645D ; check if talked to ret nc ld a, $C0 call $2385 ; open dialog call $3B12 ; increase entity state ret talking: ; Check if we are still talking ld a, [$C19F] and a ret nz call $3B12 ; increase entity state ld [hl], $00 ; set to state 0 ld a, [$C177] ; get which option we selected and a ret nz ; Give powder ld a, [$DB4C] cp $10 jr nc, doNotGivePowder ld a, $10 ld [$DB4C], a doNotGivePowder: ld a, [$DB4D] cp $10 jr nc, doNotGiveBombs ld a, $10 ld [$DB4D], a doNotGiveBombs: ld a, [$DB45] cp $10 jr nc, doNotGiveArrows ld a, $10 ld [$DB45], a doNotGiveArrows: ld a, $C1 call $2385 ; open dialog ret sprite: db $76, $09, $78, $09, $7A, $09, $7C, $09 """, 0x67F5), fill_nop=True) rom.patch(0x20, 0x0322 + 0x41 * 2, "734A", "564B") # Remove the owl init handler re = RoomEditor(rom, 0x2A3) re.entities.append((7, 6, 0x41)) re.store(rom)