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 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 fixWrongWarp(rom): rom.patch(0x00, 0x18CE, ASM("cp $04"), ASM("cp $03")) re = RoomEditor(rom, 0x2b) for x in range(10): re.removeObject(x, 7) re.objects.append(ObjectHorizontal(0, 7, 0x2C, 10)) while len(re.getWarps()) < 4: re.objects.append(ObjectWarp(1, 3, 0x7a, 80, 124)) re.store(rom)
def readEntrances(rom): result = {} for key, info in ENTRANCE_INFO.items(): 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] for other_key, other_info in ENTRANCE_INFO.items(): if warp.room == other_info.target: result[key] = other_key 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) 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 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 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 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 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 warpHome(rom): # Patch the S&Q menu to allow 3 options rom.patch(0x01, 0x012A, 0x0150, ASM(""" ld hl, $C13F call $6BA8 ; make sound on keypress ldh a, [$CC] ; load joystick status and $04 ; if up jr z, noUp dec [hl] noUp: ldh a, [$CC] ; load joystick status and $08 ; if down jr z, noDown inc [hl] noDown: ld a, [hl] cp $ff jr nz, noWrapUp ld a, $02 noWrapUp: cp $03 jr nz, noWrapDown xor a noWrapDown: ld [hl], a jp $7E02 """), fill_nop=True) rom.patch(0x01, 0x3E02, 0x3E20, ASM(""" swap a add a, $48 ld hl, $C018 ldi [hl], a ld a, $24 ldi [hl], a ld a, $BE ldi [hl], a ld [hl], $00 ret """), fill_nop=True) rom.patch(0x01, 0x00B7, ASM(""" ld a, [$C13F] cp $01 jr z, $3B """), ASM(""" ld a, [$C13F] jp $7E20 """), fill_nop=True) re = RoomEditor(rom, 0x2a3) warp = re.getWarps()[0] rom.patch(0x01, 0x3E20, 0x4000, ASM(""" ; First, handle save & quit cp $01 jp z, $40F9 and a jp z, $40BE ; return to normal "return to game" handling ld a, [$C509] ; Check if we have an item in the shop and a jp nz, $40BE ; return to normal "return to game" handling ld a, $0B ld [$DB95], a call $0C7D ; Replace warp0 tile data, and put link on that tile. xor a ld [$D401], a ld [$D402], a ld a, $%02x ; Room ld [$D403], a ld a, $%02x ; X ld [$D404], a ld a, $%02x ; Y ld [$D405], a ldh a, [$98] swap a and $0F ld e, a ldh a, [$99] sub $08 and $F0 or e ld [$D416], a ld a, $07 ld [$DB96], a ret jp $40BE ; return to normal "return to game" handling """ % (warp.room, warp.target_x, warp.target_y)), fill_nop=True) # Patch the S&Q screen to have 3 options. be = BackgroundEditor(rom, 0x0D) for n in range(2, 18): be.tiles[0x99C0 + n] = be.tiles[0x9980 + n] be.tiles[0x99A0 + n] = be.tiles[0x9960 + n] be.tiles[0x9980 + n] = be.tiles[0x9940 + n] be.tiles[0x9960 + n] = be.tiles[0x98e0 + n] be.tiles[0x9960 + 10] = 0xCE be.tiles[0x9960 + 11] = 0xCF be.tiles[0x9960 + 12] = 0xC4 be.tiles[0x9960 + 13] = 0x7F be.tiles[0x9960 + 14] = 0x7F be.store(rom) sprite_data = [ 0b00000000, 0b01000100, 0b01000101, 0b01000101, 0b01111101, 0b01000101, 0b01000101, 0b01000100, 0b00000000, 0b11100100, 0b00010110, 0b00010101, 0b00010100, 0b00010100, 0b00010100, 0b11100100, ] for n in range(32): rom.banks[0x0F][0x08E0 + n] = sprite_data[n // 2]
def addMultiworldShop(rom, this_player, player_count): # Make a copy of the shop into GrandpaUlrira house re = RoomEditor(rom, 0x2A9) re.objects = [ ObjectHorizontal(1, 1, 0x00, 8), ObjectHorizontal(1, 2, 0x00, 8), ObjectHorizontal(1, 3, 0xCD, 8), Object(2, 0, 0xC7), Object(7, 0, 0xC7), Object(7, 7, 0xFD), ] + re.getWarps() re.entities = [(0, 6, 0xD4)] for n in range(player_count): if n != this_player: re.entities.append((n + 1, 6, 0xD4)) re.animation_id = 0x04 re.floor_object = 0x0D re.store(rom) # Fix the tileset rom.banks[0x20][0x2EB3 + 0x2A9 - 0x100] = rom.banks[0x20][0x2EB3 + 0x2A1 - 0x100] re = RoomEditor(rom, 0x0B1) re.getWarps()[0].target_x = 128 re.store(rom) # Load the shopkeeper sprites entityData.SPRITE_DATA[0xD4] = entityData.SPRITE_DATA[0x4D] rom.patch(0x03, 0x01CF, "00", "98") # Fix the hitbox of the ghost to be 16x16 # Patch Ghost to work as a multiworld shop rom.patch(0x19, 0x1E18, 0x20B0, ASM( """ ld a, $01 ld [$C50A], a ; this stops link from using items ldh a, [$EE] ; X cp $08 ; Jump to other code which is placed on the old owl code. As we do not have enough space here. jp z, shopItemsHandler ;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 $7CA2 ; prevent link from moving into the sprite call $7CF0 ; check if talking to NPC call c, talkHandler ; talk handling ret 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 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 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"10 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 ; set substract buffer ; Set the item to send ld hl, $DDFE ld a, [$C509] ; currently picked up item ldi [hl], a ldh a, [$EE] ; X position of NPC ldi [hl], a ld hl, $DDF7 set 2, [hl] ; 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 """, 0x5E18), fill_nop=True)