From efb2ab4505bcdb30ddb2771905f864f9b62df3cd Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Sun, 5 Mar 2023 07:31:31 -0500 Subject: [PATCH] TLoZ: Implementing The Legend of Zelda (#1354) Co-authored-by: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Co-authored-by: Alchav <59858495+Alchav@users.noreply.github.com> --- .gitignore | 1 + Launcher.py | 2 + README.md | 1 + Utils.py | 5 + Zelda1Client.py | 393 +++++++++++ data/lua/TLoZ/TheLegendOfZeldaConnector.lua | 702 ++++++++++++++++++++ data/lua/TLoZ/core.dll | Bin 0 -> 29184 bytes data/lua/TLoZ/json.lua | 380 +++++++++++ data/lua/TLoZ/socket.lua | 132 ++++ host.yaml | 11 +- worlds/tloz/ItemPool.py | 145 ++++ worlds/tloz/Items.py | 147 ++++ worlds/tloz/Locations.py | 350 ++++++++++ worlds/tloz/Options.py | 40 ++ worlds/tloz/Rom.py | 78 +++ worlds/tloz/Rules.py | 147 ++++ worlds/tloz/__init__.py | 313 +++++++++ worlds/tloz/docs/en_The Legend of Zelda.md | 43 ++ worlds/tloz/docs/multiworld_en.md | 104 +++ worlds/tloz/requirements.txt | 1 + worlds/tloz/z1_base_patch.bsdiff4 | Bin 0 -> 1648 bytes 21 files changed, 2994 insertions(+), 1 deletion(-) create mode 100644 Zelda1Client.py create mode 100644 data/lua/TLoZ/TheLegendOfZeldaConnector.lua create mode 100644 data/lua/TLoZ/core.dll create mode 100644 data/lua/TLoZ/json.lua create mode 100644 data/lua/TLoZ/socket.lua create mode 100644 worlds/tloz/ItemPool.py create mode 100644 worlds/tloz/Items.py create mode 100644 worlds/tloz/Locations.py create mode 100644 worlds/tloz/Options.py create mode 100644 worlds/tloz/Rom.py create mode 100644 worlds/tloz/Rules.py create mode 100644 worlds/tloz/__init__.py create mode 100644 worlds/tloz/docs/en_The Legend of Zelda.md create mode 100644 worlds/tloz/docs/multiworld_en.md create mode 100644 worlds/tloz/requirements.txt create mode 100644 worlds/tloz/z1_base_patch.bsdiff4 diff --git a/.gitignore b/.gitignore index e269202d..c964b929 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ *.apm3 *.apmc *.apz5 +*.aptloz *.pyc *.pyd *.sfc diff --git a/Launcher.py b/Launcher.py index 27510a88..c4d9b6fe 100644 --- a/Launcher.py +++ b/Launcher.py @@ -148,6 +148,8 @@ components: Iterable[Component] = ( Component('FF1 Client', 'FF1Client'), # Pokémon Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')), + # TLoZ + Component('Zelda 1 Client', 'Zelda1Client'), # ChecksFinder Component('ChecksFinder Client', 'ChecksFinderClient'), # Starcraft 2 diff --git a/README.md b/README.md index 8b72dcf6..9e6ed2b1 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Currently, the following games are supported: * Blasphemous * Wargroove * Stardew Valley +* The Legend of Zelda For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/Utils.py b/Utils.py index 098d5f01..3ec3d4ff 100644 --- a/Utils.py +++ b/Utils.py @@ -310,6 +310,11 @@ def get_default_options() -> OptionsType: "lufia2ac_options": { "rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc", }, + "tloz_options": { + "rom_file": "Legend of Zelda, The (U) (PRG0) [!].nes", + "rom_start": True, + "display_msgs": True, + }, "wargroove_options": { "root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove" } diff --git a/Zelda1Client.py b/Zelda1Client.py new file mode 100644 index 00000000..a325e4ae --- /dev/null +++ b/Zelda1Client.py @@ -0,0 +1,393 @@ +# Based (read: copied almost wholesale and edited) off the FF1 Client. + +import asyncio +import copy +import json +import logging +import os +import subprocess +import time +import typing +from asyncio import StreamReader, StreamWriter +from typing import List + +import Utils +from Utils import async_start +from worlds import lookup_any_location_id_to_name +from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \ + get_base_parser + +from worlds.tloz.Items import item_game_ids +from worlds.tloz.Locations import location_ids +from worlds.tloz import Items, Locations, Rom + +SYSTEM_MESSAGE_ID = 0 + +CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart Zelda_connector.lua" +CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure Zelda_connector.lua is running" +CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart Zelda_connector.lua" +CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" +CONNECTION_CONNECTED_STATUS = "Connected" +CONNECTION_INITIAL_STATUS = "Connection has not been initiated" + +DISPLAY_MSGS = True + +item_ids = item_game_ids +location_ids = location_ids +items_by_id = {id: item for item, id in item_ids.items()} +locations_by_id = {id: location for location, id in location_ids.items()} + + +class ZeldaCommandProcessor(ClientCommandProcessor): + + def _cmd_nes(self): + """Check NES Connection State""" + if isinstance(self.ctx, ZeldaContext): + logger.info(f"NES Status: {self.ctx.nes_status}") + + def _cmd_toggle_msgs(self): + """Toggle displaying messages in bizhawk""" + global DISPLAY_MSGS + DISPLAY_MSGS = not DISPLAY_MSGS + logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}") + + +class ZeldaContext(CommonContext): + command_processor = ZeldaCommandProcessor + items_handling = 0b101 # get sent remote and starting items + # Infinite Hyrule compatibility + overworld_item = 0x5F + armos_item = 0x24 + + def __init__(self, server_address, password): + super().__init__(server_address, password) + self.bonus_items = [] + self.nes_streams: (StreamReader, StreamWriter) = None + self.nes_sync_task = None + self.messages = {} + self.locations_array = None + self.nes_status = CONNECTION_INITIAL_STATUS + self.game = 'The Legend of Zelda' + self.awaiting_rom = False + self.shop_slots_left = 0 + self.shop_slots_middle = 0 + self.shop_slots_right = 0 + self.shop_slots = [self.shop_slots_left, self.shop_slots_middle, self.shop_slots_right] + self.slot_data = dict() + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(ZeldaContext, self).server_auth(password_requested) + if not self.auth: + self.awaiting_rom = True + logger.info('Awaiting connection to NES to get Player information') + return + + await self.send_connect() + + def _set_message(self, msg: str, msg_id: int): + if DISPLAY_MSGS: + self.messages[(time.time(), msg_id)] = msg + + def on_package(self, cmd: str, args: dict): + if cmd == 'Connected': + self.slot_data = args.get("slot_data", {}) + asyncio.create_task(parse_locations(self.locations_array, self, True)) + elif cmd == 'Print': + msg = args['text'] + if ': !' not in msg: + self._set_message(msg, SYSTEM_MESSAGE_ID) + + def on_print_json(self, args: dict): + if self.ui: + self.ui.print_json(copy.deepcopy(args["data"])) + else: + text = self.jsontotextparser(copy.deepcopy(args["data"])) + logger.info(text) + relevant = args.get("type", None) in {"Hint", "ItemSend"} + if relevant: + item = args["item"] + # goes to this world + if self.slot_concerns_self(args["receiving"]): + relevant = True + # found in this world + elif self.slot_concerns_self(item.player): + relevant = True + # not related + else: + relevant = False + if relevant: + item = args["item"] + msg = self.raw_text_parser(copy.deepcopy(args["data"])) + self._set_message(msg, item.item) + + def run_gui(self): + from kvui import GameManager + + class ZeldaManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago Zelda 1 Client" + + self.ui = ZeldaManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + +def get_payload(ctx: ZeldaContext): + current_time = time.time() + bonus_items = [item for item in ctx.bonus_items] + return json.dumps( + { + "items": [item.item for item in ctx.items_received], + "messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items() + if key[0] > current_time - 10}, + "shops": { + "left": ctx.shop_slots_left, + "middle": ctx.shop_slots_middle, + "right": ctx.shop_slots_right + }, + "bonusItems": bonus_items + } + ) + + +def reconcile_shops(ctx: ZeldaContext): + checked_location_names = [lookup_any_location_id_to_name[location] for location in ctx.checked_locations] + shops = [location for location in checked_location_names if "Shop" in location] + left_slots = [shop for shop in shops if "Left" in shop] + middle_slots = [shop for shop in shops if "Middle" in shop] + right_slots = [shop for shop in shops if "Right" in shop] + for shop in left_slots: + ctx.shop_slots_left |= get_shop_bit_from_name(shop) + for shop in middle_slots: + ctx.shop_slots_middle |= get_shop_bit_from_name(shop) + for shop in right_slots: + ctx.shop_slots_right |= get_shop_bit_from_name(shop) + + +def get_shop_bit_from_name(location_name): + if "Potion" in location_name: + return Rom.potion_shop + elif "Arrow" in location_name: + return Rom.arrow_shop + elif "Shield" in location_name: + return Rom.shield_shop + elif "Ring" in location_name: + return Rom.ring_shop + elif "Candle" in location_name: + return Rom.candle_shop + elif "Take" in location_name: + return Rom.take_any + return 0 # this should never be hit + + +async def parse_locations(locations_array, ctx: ZeldaContext, force: bool, zone="None"): + if locations_array == ctx.locations_array and not force: + return + else: + # print("New values") + ctx.locations_array = locations_array + locations_checked = [] + location = None + for location in ctx.missing_locations: + location_name = lookup_any_location_id_to_name[location] + + if location_name in Locations.overworld_locations and zone == "overworld": + status = locations_array[Locations.major_location_offsets[location_name]] + if location_name == "Ocean Heart Container": + status = locations_array[ctx.overworld_item] + if location_name == "Armos Knights": + status = locations_array[ctx.armos_item] + if status & 0x10: + ctx.locations_checked.add(location) + locations_checked.append(location) + elif location_name in Locations.underworld1_locations and zone == "underworld1": + status = locations_array[Locations.floor_location_game_offsets_early[location_name]] + if status & 0x10: + ctx.locations_checked.add(location) + locations_checked.append(location) + elif location_name in Locations.underworld2_locations and zone == "underworld2": + status = locations_array[Locations.floor_location_game_offsets_late[location_name]] + if status & 0x10: + ctx.locations_checked.add(location) + locations_checked.append(location) + elif (location_name in Locations.shop_locations or "Take" in location_name) and zone == "caves": + shop_bit = get_shop_bit_from_name(location_name) + slot = 0 + context_slot = 0 + if "Left" in location_name: + slot = "slot1" + context_slot = 0 + elif "Middle" in location_name: + slot = "slot2" + context_slot = 1 + elif "Right" in location_name: + slot = "slot3" + context_slot = 2 + if locations_array[slot] & shop_bit > 0: + locations_checked.append(location) + ctx.shop_slots[context_slot] |= shop_bit + if locations_array["takeAnys"] and locations_array["takeAnys"] >= 4: + if "Take Any" in location_name: + short_name = None + if "Left" in location_name: + short_name = "TakeAnyLeft" + elif "Middle" in location_name: + short_name = "TakeAnyMiddle" + elif "Right" in location_name: + short_name = "TakeAnyRight" + if short_name is not None: + item_code = ctx.slot_data[short_name] + if item_code > 0: + ctx.bonus_items.append(item_code) + locations_checked.append(location) + if locations_checked: + await ctx.send_msgs([ + {"cmd": "LocationChecks", + "locations": locations_checked} + ]) + + +async def nes_sync_task(ctx: ZeldaContext): + logger.info("Starting nes connector. Use /nes for status information") + while not ctx.exit_event.is_set(): + error_status = None + if ctx.nes_streams: + (reader, writer) = ctx.nes_streams + msg = get_payload(ctx).encode() + writer.write(msg) + writer.write(b'\n') + try: + await asyncio.wait_for(writer.drain(), timeout=1.5) + try: + # Data will return a dict with up to two fields: + # 1. A keepalive response of the Players Name (always) + # 2. An array representing the memory values of the locations area (if in game) + data = await asyncio.wait_for(reader.readline(), timeout=5) + data_decoded = json.loads(data.decode()) + if data_decoded["overworldHC"] is not None: + ctx.overworld_item = data_decoded["overworldHC"] + if data_decoded["overworldPB"] is not None: + ctx.armos_item = data_decoded["overworldPB"] + if data_decoded['gameMode'] == 19 and ctx.finished_game == False: + await ctx.send_msgs([ + {"cmd": "StatusUpdate", + "status": 30} + ]) + ctx.finished_game = True + if ctx.game is not None and 'overworld' in data_decoded: + # Not just a keep alive ping, parse + asyncio.create_task(parse_locations(data_decoded['overworld'], ctx, False, "overworld")) + if ctx.game is not None and 'underworld1' in data_decoded: + asyncio.create_task(parse_locations(data_decoded['underworld1'], ctx, False, "underworld1")) + if ctx.game is not None and 'underworld2' in data_decoded: + asyncio.create_task(parse_locations(data_decoded['underworld2'], ctx, False, "underworld2")) + if ctx.game is not None and 'caves' in data_decoded: + asyncio.create_task(parse_locations(data_decoded['caves'], ctx, False, "caves")) + if not ctx.auth: + ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0]) + if ctx.auth == '': + logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate" + "the ROM using the same link but adding your slot name") + if ctx.awaiting_rom: + await ctx.server_auth(False) + reconcile_shops(ctx) + except asyncio.TimeoutError: + logger.debug("Read Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.nes_streams = None + except ConnectionResetError as e: + logger.debug("Read failed due to Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.nes_streams = None + except TimeoutError: + logger.debug("Connection Timed Out, Reconnecting") + error_status = CONNECTION_TIMING_OUT_STATUS + writer.close() + ctx.nes_streams = None + except ConnectionResetError: + logger.debug("Connection Lost, Reconnecting") + error_status = CONNECTION_RESET_STATUS + writer.close() + ctx.nes_streams = None + if ctx.nes_status == CONNECTION_TENTATIVE_STATUS: + if not error_status: + logger.info("Successfully Connected to NES") + ctx.nes_status = CONNECTION_CONNECTED_STATUS + else: + ctx.nes_status = f"Was tentatively connected but error occured: {error_status}" + elif error_status: + ctx.nes_status = error_status + logger.info("Lost connection to nes and attempting to reconnect. Use /nes for status updates") + else: + try: + logger.debug("Attempting to connect to NES") + ctx.nes_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 52980), timeout=10) + ctx.nes_status = CONNECTION_TENTATIVE_STATUS + except TimeoutError: + logger.debug("Connection Timed Out, Trying Again") + ctx.nes_status = CONNECTION_TIMING_OUT_STATUS + continue + except ConnectionRefusedError: + logger.debug("Connection Refused, Trying Again") + ctx.nes_status = CONNECTION_REFUSED_STATUS + continue + + +if __name__ == '__main__': + # Text Mode to use !hint and such with games that have no text entry + Utils.init_logging("ZeldaClient") + + options = Utils.get_options() + DISPLAY_MSGS = options["tloz_options"]["display_msgs"] + + + async def run_game(romfile: str) -> None: + auto_start = typing.cast(typing.Union[bool, str], + Utils.get_options()["tloz_options"].get("rom_start", True)) + if auto_start is True: + import webbrowser + webbrowser.open(romfile) + elif isinstance(auto_start, str) and os.path.isfile(auto_start): + subprocess.Popen([auto_start, romfile], + stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + + async def main(args): + if args.diff_file: + import Patch + logging.info("Patch file was supplied. Creating nes rom..") + meta, romfile = Patch.create_rom_file(args.diff_file) + if "server" in meta: + args.connect = meta["server"] + logging.info(f"Wrote rom file to {romfile}") + async_start(run_game(romfile)) + ctx = ZeldaContext(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + ctx.nes_sync_task = asyncio.create_task(nes_sync_task(ctx), name="NES Sync") + + await ctx.exit_event.wait() + ctx.server_address = None + + await ctx.shutdown() + + if ctx.nes_sync_task: + await ctx.nes_sync_task + + + import colorama + + parser = get_base_parser() + parser.add_argument('diff_file', default="", type=str, nargs="?", + help='Path to a Archipelago Binary Patch file') + args = parser.parse_args() + colorama.init() + + asyncio.run(main(args)) + colorama.deinit() diff --git a/data/lua/TLoZ/TheLegendOfZeldaConnector.lua b/data/lua/TLoZ/TheLegendOfZeldaConnector.lua new file mode 100644 index 00000000..aee4412b --- /dev/null +++ b/data/lua/TLoZ/TheLegendOfZeldaConnector.lua @@ -0,0 +1,702 @@ +--Shamelessly based off the FF1 lua + +local socket = require("socket") +local json = require('json') +local math = require('math') + +local STATE_OK = "Ok" +local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" +local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made" +local STATE_UNINITIALIZED = "Uninitialized" + +local itemMessages = {} +local consumableStacks = nil +local prevstate = "" +local curstate = STATE_UNINITIALIZED +local zeldaSocket = nil +local frame = 0 +local gameMode = 0 + +local cave_index +local triforce_byte +local game_state + +local u8 = nil +local wU8 = nil +local isNesHawk = false + +local shopsChecked = {} +local shopSlotLeft = 0x0628 +local shopSlotMiddle = 0x0629 +local shopSlotRight = 0x062A + +--N.B.: you won't find these in a RAM map. They're flag values that the base patch derives from the cave ID. +local blueRingShopBit = 0x40 +local potionShopBit = 0x02 +local arrowShopBit = 0x08 +local candleShopBit = 0x10 +local shieldShopBit = 0x20 +local takeAnyCaveBit = 0x01 + + +local sword = 0x0657 +local bombs = 0x0658 +local maxBombs = 0x067C +local keys = 0x066E +local arrow = 0x0659 +local bow = 0x065A +local candle = 0x065B +local recorder = 0x065C +local food = 0x065D +local waterOfLife = 0x065E +local magicalRod = 0x065F +local raft = 0x0660 +local bookOfMagic = 0x0661 +local ring = 0x0662 +local stepladder = 0x0663 +local magicalKey = 0x0664 +local powerBracelet = 0x0665 +local letter = 0x0666 +local clockItem = 0x066C +local heartContainers = 0x066F +local partialHearts = 0x0670 +local triforceFragments = 0x0671 +local boomerang = 0x0674 +local magicalBoomerang = 0x0675 +local magicalShield = 0x0676 +local rupeesToAdd = 0x067D +local rupeesToSubtract = 0x067E +local itemsObtained = 0x0677 +local takeAnyCavesChecked = 0x0678 +local localTriforce = 0x0679 +local bonusItemsObtained = 0x067A + +itemAPids = { + ["Boomerang"] = 7100, + ["Bow"] = 7101, + ["Magical Boomerang"] = 7102, + ["Raft"] = 7103, + ["Stepladder"] = 7104, + ["Recorder"] = 7105, + ["Magical Rod"] = 7106, + ["Red Candle"] = 7107, + ["Book of Magic"] = 7108, + ["Magical Key"] = 7109, + ["Red Ring"] = 7110, + ["Silver Arrow"] = 7111, + ["Sword"] = 7112, + ["White Sword"] = 7113, + ["Magical Sword"] = 7114, + ["Heart Container"] = 7115, + ["Letter"] = 7116, + ["Magical Shield"] = 7117, + ["Candle"] = 7118, + ["Arrow"] = 7119, + ["Food"] = 7120, + ["Water of Life (Blue)"] = 7121, + ["Water of Life (Red)"] = 7122, + ["Blue Ring"] = 7123, + ["Triforce Fragment"] = 7124, + ["Power Bracelet"] = 7125, + ["Small Key"] = 7126, + ["Bomb"] = 7127, + ["Recovery Heart"] = 7128, + ["Five Rupees"] = 7129, + ["Rupee"] = 7130, + ["Clock"] = 7131, + ["Fairy"] = 7132 +} + +itemCodes = { + ["Boomerang"] = 0x1D, + ["Bow"] = 0x0A, + ["Magical Boomerang"] = 0x1E, + ["Raft"] = 0x0C, + ["Stepladder"] = 0x0D, + ["Recorder"] = 0x05, + ["Magical Rod"] = 0x10, + ["Red Candle"] = 0x07, + ["Book of Magic"] = 0x11, + ["Magical Key"] = 0x0B, + ["Red Ring"] = 0x13, + ["Silver Arrow"] = 0x09, + ["Sword"] = 0x01, + ["White Sword"] = 0x02, + ["Magical Sword"] = 0x03, + ["Heart Container"] = 0x1A, + ["Letter"] = 0x15, + ["Magical Shield"] = 0x1C, + ["Candle"] = 0x06, + ["Arrow"] = 0x08, + ["Food"] = 0x04, + ["Water of Life (Blue)"] = 0x1F, + ["Water of Life (Red)"] = 0x20, + ["Blue Ring"] = 0x12, + ["Triforce Fragment"] = 0x1B, + ["Power Bracelet"] = 0x14, + ["Small Key"] = 0x19, + ["Bomb"] = 0x00, + ["Recovery Heart"] = 0x22, + ["Five Rupees"] = 0x0F, + ["Rupee"] = 0x18, + ["Clock"] = 0x21, + ["Fairy"] = 0x23 +} + + +--Sets correct memory access functions based on whether NesHawk or QuickNES is loaded +local function defineMemoryFunctions() + local memDomain = {} + local domains = memory.getmemorydomainlist() + if domains[1] == "System Bus" then + --NesHawk + isNesHawk = true + memDomain["systembus"] = function() memory.usememorydomain("System Bus") end + memDomain["ram"] = function() memory.usememorydomain("RAM") end + memDomain["saveram"] = function() memory.usememorydomain("Battery RAM") end + memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end + elseif domains[1] == "WRAM" then + --QuickNES + memDomain["systembus"] = function() memory.usememorydomain("System Bus") end + memDomain["ram"] = function() memory.usememorydomain("RAM") end + memDomain["saveram"] = function() memory.usememorydomain("WRAM") end + memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end + end + return memDomain +end + +local memDomain = defineMemoryFunctions() +u8 = memory.read_u8 +wU8 = memory.write_u8 +uRange = memory.readbyterange + +itemIDNames = {} + +for key, value in pairs(itemAPids) do + itemIDNames[value] = key +end + + + +local function determineItem(array) + memdomain.ram() + currentItemsObtained = u8(itemsObtained) + +end + +local function gotSword() + local currentSword = u8(sword) + wU8(sword, math.max(currentSword, 1)) +end + +local function gotWhiteSword() + local currentSword = u8(sword) + wU8(sword, math.max(currentSword, 2)) +end + +local function gotMagicalSword() + wU8(sword, 3) +end + +local function gotBomb() + local currentBombs = u8(bombs) + local currentMaxBombs = u8(maxBombs) + wU8(bombs, math.min(currentBombs + 4, currentMaxBombs)) + wU8(0x505, 0x29) -- Fake bomb to show item get. +end + +local function gotArrow() + local currentArrow = u8(arrow) + wU8(arrow, math.max(currentArrow, 1)) +end + +local function gotSilverArrow() + wU8(arrow, 2) +end + +local function gotBow() + wU8(bow, 1) +end + +local function gotCandle() + local currentCandle = u8(candle) + wU8(candle, math.max(currentCandle, 1)) +end + +local function gotRedCandle() + wU8(candle, 2) +end + +local function gotRecorder() + wU8(recorder, 1) +end + +local function gotFood() + wU8(food, 1) +end + +local function gotWaterOfLifeBlue() + local currentWaterOfLife = u8(waterOfLife) + wU8(waterOfLife, math.max(currentWaterOfLife, 1)) +end + +local function gotWaterOfLifeRed() + wU8(waterOfLife, 2) +end + +local function gotMagicalRod() + wU8(magicalRod, 1) +end + +local function gotBookOfMagic() + wU8(bookOfMagic, 1) +end + +local function gotRaft() + wU8(raft, 1) +end + +local function gotBlueRing() + local currentRing = u8(ring) + wU8(ring, math.max(currentRing, 1)) + memDomain.saveram() + local currentTunicColor = u8(0x0B92) + if currentTunicColor == 0x29 then + wU8(0x0B92, 0x32) + wU8(0x0804, 0x32) + end +end + +local function gotRedRing() + wU8(ring, 2) + memDomain.saveram() + wU8(0x0B92, 0x16) + wU8(0x0804, 0x16) +end + +local function gotStepladder() + wU8(stepladder, 1) +end + +local function gotMagicalKey() + wU8(magicalKey, 1) +end + +local function gotPowerBracelet() + wU8(powerBracelet, 1) +end + +local function gotLetter() + wU8(letter, 1) +end + +local function gotHeartContainer() + local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4) + if currentHeartContainers < 16 then + currentHeartContainers = math.min(currentHeartContainers + 1, 16) + local currentHearts = bit.band(u8(heartContainers), 0x0F) + 1 + wU8(heartContainers, bit.lshift(currentHeartContainers, 4) + currentHearts) + end +end + +local function gotTriforceFragment() + local triforceByte = 0xFF + local newTriforceCount = u8(localTriforce) + 1 + wU8(localTriforce, newTriforceCount) +end + +local function gotBoomerang() + wU8(boomerang, 1) +end + +local function gotMagicalBoomerang() + wU8(magicalBoomerang, 1) +end + +local function gotMagicalShield() + wU8(magicalShield, 1) +end + +local function gotRecoveryHeart() + local currentHearts = bit.band(u8(heartContainers), 0x0F) + local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4) + if currentHearts < currentHeartContainers then + currentHearts = currentHearts + 1 + else + wU8(partialHearts, 0xFF) + end + currentHearts = bit.bor(bit.band(u8(heartContainers), 0xF0), currentHearts) + wU8(heartContainers, currentHearts) +end + +local function gotFairy() + local currentHearts = bit.band(u8(heartContainers), 0x0F) + local currentHeartContainers = bit.rshift(bit.band(u8(heartContainers), 0xF0), 4) + if currentHearts < currentHeartContainers then + currentHearts = currentHearts + 3 + if currentHearts > currentHeartContainers then + currentHearts = currentHeartContainers + wU8(partialHearts, 0xFF) + end + else + wU8(partialHearts, 0xFF) + end + currentHearts = bit.bor(bit.band(u8(heartContainers), 0xF0), currentHearts) + wU8(heartContainers, currentHearts) +end + +local function gotClock() + wU8(clockItem, 1) +end + +local function gotFiveRupees() + local currentRupeesToAdd = u8(rupeesToAdd) + wU8(rupeesToAdd, math.min(currentRupeesToAdd + 5, 255)) +end + +local function gotSmallKey() + wU8(keys, math.min(u8(keys) + 1, 9)) +end + +local function gotItem(item) + --Write itemCode to itemToLift + --Write 128 to itemLiftTimer + --Write 4 to sound effect queue + itemName = itemIDNames[item] + itemCode = itemCodes[itemName] + wU8(0x505, itemCode) + wU8(0x506, 128) + wU8(0x602, 4) + numberObtained = u8(itemsObtained) + 1 + wU8(itemsObtained, numberObtained) + if itemName == "Boomerang" then gotBoomerang() end + if itemName == "Bow" then gotBow() end + if itemName == "Magical Boomerang" then gotMagicalBoomerang() end + if itemName == "Raft" then gotRaft() end + if itemName == "Stepladder" then gotStepladder() end + if itemName == "Recorder" then gotRecorder() end + if itemName == "Magical Rod" then gotMagicalRod() end + if itemName == "Red Candle" then gotRedCandle() end + if itemName == "Book of Magic" then gotBookOfMagic() end + if itemName == "Magical Key" then gotMagicalKey() end + if itemName == "Red Ring" then gotRedRing() end + if itemName == "Silver Arrow" then gotSilverArrow() end + if itemName == "Sword" then gotSword() end + if itemName == "White Sword" then gotWhiteSword() end + if itemName == "Magical Sword" then gotMagicalSword() end + if itemName == "Heart Container" then gotHeartContainer() end + if itemName == "Letter" then gotLetter() end + if itemName == "Magical Shield" then gotMagicalShield() end + if itemName == "Candle" then gotCandle() end + if itemName == "Arrow" then gotArrow() end + if itemName == "Food" then gotFood() end + if itemName == "Water of Life (Blue)" then gotWaterOfLifeBlue() end + if itemName == "Water of Life (Red)" then gotWaterOfLifeRed() end + if itemName == "Blue Ring" then gotBlueRing() end + if itemName == "Triforce Fragment" then gotTriforceFragment() end + if itemName == "Power Bracelet" then gotPowerBracelet() end + if itemName == "Small Key" then gotSmallKey() end + if itemName == "Bomb" then gotBomb() end + if itemName == "Recovery Heart" then gotRecoveryHeart() end + if itemName == "Five Rupees" then gotFiveRupees() end + if itemName == "Fairy" then gotFairy() end + if itemName == "Clock" then gotClock() end +end + + +local function StateOKForMainLoop() + memDomain.ram() + local gameMode = u8(0x12) + return gameMode == 5 +end + +local function checkCaveItemObtained() + memDomain.ram() + local returnTable = {} + returnTable["slot1"] = u8(shopSlotLeft) + returnTable["slot2"] = u8(shopSlotMiddle) + returnTable["slot3"] = u8(shopSlotRight) + returnTable["takeAnys"] = u8(takeAnyCavesChecked) + return returnTable +end + +function table.empty (self) + for _, _ in pairs(self) do + return false + end + return true +end + +function slice (tbl, s, e) + local pos, new = 1, {} + for i = s + 1, e do + new[pos] = tbl[i] + pos = pos + 1 + end + return new +end + +local bizhawk_version = client.getversion() +local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5") +local is26To28 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7") or (bizhawk_version:sub(1,3)=="2.8") + +local function getMaxMessageLength() + if is23Or24Or25 then + return client.screenwidth()/11 + elseif is26To28 then + return client.screenwidth()/12 + end +end + +local function drawText(x, y, message, color) + if is23Or24Or25 then + gui.addmessage(message) + elseif is26To28 then + gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", "middle", "bottom", nil, "client") + end +end + +local function clearScreen() + if is23Or24Or25 then + return + elseif is26To28 then + drawText(0, 0, "", "black") + end +end + +local function drawMessages() + if table.empty(itemMessages) then + clearScreen() + return + end + local y = 10 + found = false + maxMessageLength = getMaxMessageLength() + for k, v in pairs(itemMessages) do + if v["TTL"] > 0 then + message = v["message"] + while true do + drawText(5, y, message:sub(1, maxMessageLength), v["color"]) + y = y + 16 + + message = message:sub(maxMessageLength + 1, message:len()) + if message:len() == 0 then + break + end + end + newTTL = 0 + if is26To28 then + newTTL = itemMessages[k]["TTL"] - 1 + end + itemMessages[k]["TTL"] = newTTL + found = true + end + end + if found == false then + clearScreen() + end +end + +function generateOverworldLocationChecked() + memDomain.ram() + data = uRange(0x067E, 0x81) + data[0] = nil + return data +end + +function getHCLocation() + memDomain.rom() + data = u8(0x1789A) + return data +end + +function getPBLocation() + memDomain.rom() + data = u8(0x10CB2) + return data +end + +function generateUnderworld16LocationChecked() + memDomain.ram() + data = uRange(0x06FE, 0x81) + data[0] = nil + return data +end + +function generateUnderworld79LocationChecked() + memDomain.ram() + data = uRange(0x077E, 0x81) + data[0] = nil + return data +end + +function updateTriforceFragments() + memDomain.ram() + local triforceByte = 0xFF + totalTriforceCount = u8(localTriforce) + local currentPieces = bit.rshift(triforceByte, 8 - math.min(8, totalTriforceCount)) + wU8(triforceFragments, currentPieces) +end + +function processBlock(block) + if block ~= nil then + local msgBlock = block['messages'] + if msgBlock ~= nil then + for i, v in pairs(msgBlock) do + if itemMessages[i] == nil then + local msg = {TTL=450, message=v, color=0xFFFF0000} + itemMessages[i] = msg + end + end + end + local bonusItems = block["bonusItems"] + if bonusItems ~= nil and isInGame then + for i, item in ipairs(bonusItems) do + memDomain.ram() + if i > u8(bonusItemsObtained) then + if u8(0x505) == 0 then + gotItem(item) + wU8(itemsObtained, u8(itemsObtained) - 1) + wU8(bonusItemsObtained, u8(bonusItemsObtained) + 1) + end + end + end + end + local itemsBlock = block["items"] + memDomain.saveram() + isInGame = StateOKForMainLoop() + updateTriforceFragments() + if itemsBlock ~= nil and isInGame then + memDomain.ram() + --get item from item code + --get function from item + --do function + for i, item in ipairs(itemsBlock) do + memDomain.ram() + if u8(0x505) == 0 then + if i > u8(itemsObtained) then + gotItem(item) + end + end + end + end + local shopsBlock = block["shops"] + if shopsBlock ~= nil then + wU8(shopSlotLeft, bit.bor(u8(shopSlotLeft), shopsBlock["left"])) + wU8(shopSlotMiddle, bit.bor(u8(shopSlotMiddle), shopsBlock["middle"])) + wU8(shopSlotRight, bit.bor(u8(shopSlotRight), shopsBlock["right"])) + end + end +end + +function difference(a, b) + local aa = {} + for k,v in pairs(a) do aa[v]=true end + for k,v in pairs(b) do aa[v]=nil end + local ret = {} + local n = 0 + for k,v in pairs(a) do + if aa[v] then n=n+1 ret[n]=v end + end + return ret +end + +function receive() + l, e = zeldaSocket:receive() + if e == 'closed' then + if curstate == STATE_OK then + print("Connection closed") + end + curstate = STATE_UNINITIALIZED + return + elseif e == 'timeout' then + print("timeout") + return + elseif e ~= nil then + print(e) + curstate = STATE_UNINITIALIZED + return + end + processBlock(json.decode(l)) + + -- Determine Message to send back + memDomain.rom() + local playerName = uRange(0x1F, 0x10) + playerName[0] = nil + local retTable = {} + retTable["playerName"] = playerName + if StateOKForMainLoop() then + retTable["overworld"] = generateOverworldLocationChecked() + retTable["underworld1"] = generateUnderworld16LocationChecked() + retTable["underworld2"] = generateUnderworld79LocationChecked() + end + retTable["caves"] = checkCaveItemObtained() + memDomain.ram() + if gameMode ~= 19 then + gameMode = u8(0x12) + end + retTable["gameMode"] = gameMode + retTable["overworldHC"] = getHCLocation() + retTable["overworldPB"] = getPBLocation() + retTable["itemsObtained"] = u8(itemsObtained) + msg = json.encode(retTable).."\n" + local ret, error = zeldaSocket:send(msg) + if ret == nil then + print(error) + elseif curstate == STATE_INITIAL_CONNECTION_MADE then + curstate = STATE_TENTATIVELY_CONNECTED + elseif curstate == STATE_TENTATIVELY_CONNECTED then + print("Connected!") + itemMessages["(0,0)"] = {TTL=240, message="Connected", color="green"} + curstate = STATE_OK + end +end + +function main() + if (is23Or24Or25 or is26To28) == false then + print("Must use a version of bizhawk 2.3.1 or higher") + return + end + server, error = socket.bind('localhost', 52980) + + while true do + gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow") + frame = frame + 1 + drawMessages() + if not (curstate == prevstate) then + -- console.log("Current state: "..curstate) + prevstate = curstate + end + if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then + if (frame % 60 == 0) then + gui.drawEllipse(248, 9, 6, 6, "Black", "Blue") + receive() + else + gui.drawEllipse(248, 9, 6, 6, "Black", "Green") + end + elseif (curstate == STATE_UNINITIALIZED) then + gui.drawEllipse(248, 9, 6, 6, "Black", "White") + if (frame % 60 == 0) then + gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow") + + drawText(5, 8, "Waiting for client", 0xFFFF0000) + drawText(5, 32, "Please start Zelda1Client.exe", 0xFFFF0000) + + -- Advance so the messages are drawn + emu.frameadvance() + server:settimeout(2) + print("Attempting to connect") + local client, timeout = server:accept() + if timeout == nil then + -- print('Initial Connection Made') + curstate = STATE_INITIAL_CONNECTION_MADE + zeldaSocket = client + zeldaSocket:settimeout(0) + end + end + end + emu.frameadvance() + end +end + +main() \ No newline at end of file diff --git a/data/lua/TLoZ/core.dll b/data/lua/TLoZ/core.dll new file mode 100644 index 0000000000000000000000000000000000000000..3e9569571ab0947dcb7bcd789dc9c06c009d072d GIT binary patch literal 29184 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 B{xARl literal 0 HcmV?d00001 diff --git a/data/lua/TLoZ/json.lua b/data/lua/TLoZ/json.lua new file mode 100644 index 00000000..0833bf6f --- /dev/null +++ b/data/lua/TLoZ/json.lua @@ -0,0 +1,380 @@ +-- +-- json.lua +-- +-- Copyright (c) 2015 rxi +-- +-- This library is free software; you can redistribute it and/or modify it +-- under the terms of the MIT license. See LICENSE for details. +-- + +local json = { _version = "0.1.0" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\\\", + [ "\"" ] = "\\\"", + [ "\b" ] = "\\b", + [ "\f" ] = "\\f", + [ "\n" ] = "\\n", + [ "\r" ] = "\\r", + [ "\t" ] = "\\t", +} + +local escape_char_map_inv = { [ "\\/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return escape_char_map[c] or string.format("\\u%04x", c:byte()) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if val[1] ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + --local line_count = 1 + --local col_count = 1 + --for i = 1, idx - 1 do + -- col_count = col_count + 1 + -- if str:sub(i, i) == "\n" then + -- line_count = line_count + 1 + -- col_count = 1 + -- end + -- end + -- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(3, 6), 16 ) + local n2 = tonumber( s:sub(9, 12), 16 ) + -- Surrogate pair? + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local has_unicode_escape = false + local has_surrogate_escape = false + local has_escape = false + local last + for j = i + 1, #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + end + + if last == 92 then -- "\\" (escape char) + if x == 117 then -- "u" (unicode escape sequence) + local hex = str:sub(j + 1, j + 5) + if not hex:find("%x%x%x%x") then + decode_error(str, j, "invalid unicode escape in string") + end + if hex:find("^[dD][89aAbB]") then + has_surrogate_escape = true + else + has_unicode_escape = true + end + else + local c = string.char(x) + if not escape_chars[c] then + decode_error(str, j, "invalid escape char '" .. c .. "' in string") + end + has_escape = true + end + last = nil + + elseif x == 34 then -- '"' (end of string) + local s = str:sub(i + 1, j - 1) + if has_surrogate_escape then + s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) + end + if has_unicode_escape then + s = s:gsub("\\u....", parse_unicode_escape) + end + if has_escape then + s = s:gsub("\\.", escape_char_map_inv) + end + return s, j + 1 + + else + last = x + end + end + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + -- Empty / end of array? + if str:sub(i, i) == "]" then + i = i + 1 + break + end + -- Read token + x, i = parse(str, i) + res[n] = x + n = n + 1 + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + -- Empty / end of object? + if str:sub(i, i) == "}" then + i = i + 1 + break + end + -- Read key + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + -- Read ':' delimiter + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + -- Read value + val, i = parse(str, i) + -- Set + res[key] = val + -- Next token + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + return ( parse(str, next_char(str, 1, space_chars, true)) ) +end + + +return json \ No newline at end of file diff --git a/data/lua/TLoZ/socket.lua b/data/lua/TLoZ/socket.lua new file mode 100644 index 00000000..a98e9521 --- /dev/null +++ b/data/lua/TLoZ/socket.lua @@ -0,0 +1,132 @@ +----------------------------------------------------------------------------- +-- LuaSocket helper module +-- Author: Diego Nehab +-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local math = require("math") +local socket = require("socket.core") +module("socket") + +----------------------------------------------------------------------------- +-- Exported auxiliar functions +----------------------------------------------------------------------------- +function connect(address, port, laddress, lport) + local sock, err = socket.tcp() + if not sock then return nil, err end + if laddress then + local res, err = sock:bind(laddress, lport, -1) + if not res then return nil, err end + end + local res, err = sock:connect(address, port) + if not res then return nil, err end + return sock +end + +function bind(host, port, backlog) + local sock, err = socket.tcp() + if not sock then return nil, err end + sock:setoption("reuseaddr", true) + local res, err = sock:bind(host, port) + if not res then return nil, err end + res, err = sock:listen(backlog) + if not res then return nil, err end + return sock +end + +try = newtry() + +function choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) + else return f(opt1, opt2) end + end +end + +----------------------------------------------------------------------------- +-- Socket sources and sinks, conforming to LTN12 +----------------------------------------------------------------------------- +-- create namespaces inside LuaSocket namespace +sourcet = {} +sinkt = {} + +BLOCKSIZE = 2048 + +sinkt["close-when-done"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then + sock:close() + return 1 + else return sock:send(chunk) end + end + }) +end + +sinkt["keep-open"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if chunk then return sock:send(chunk) + else return 1 end + end + }) +end + +sinkt["default"] = sinkt["keep-open"] + +sink = choose(sinkt) + +sourcet["by-length"] = function(sock, length) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if length <= 0 then return nil end + local size = math.min(socket.BLOCKSIZE, length) + local chunk, err = sock:receive(size) + if err then return nil, err end + length = length - string.len(chunk) + return chunk + end + }) +end + +sourcet["until-closed"] = function(sock) + local done + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if done then return nil end + local chunk, err, partial = sock:receive(socket.BLOCKSIZE) + if not err then return chunk + elseif err == "closed" then + sock:close() + done = 1 + return partial + else return nil, err end + end + }) +end + + +sourcet["default"] = sourcet["until-closed"] + +source = choose(sourcet) diff --git a/host.yaml b/host.yaml index 78fff669..5d9ec56e 100644 --- a/host.yaml +++ b/host.yaml @@ -107,7 +107,7 @@ factorio_options: filter_item_sends: false # Whether to send chat messages from players on the Factorio server to Archipelago. bridge_chat_out: true -minecraft_options: +minecraft_options: forge_directory: "Minecraft Forge server" max_heap_size: "2G" # release channel, currently "release", or "beta" @@ -125,6 +125,15 @@ soe_options: rom_file: "Secret of Evermore (USA).sfc" ffr_options: display_msgs: true +tloz_options: + # File name of the Zelda 1 + rom_file: "Legend of Zelda, The (U) (PRG0) [!].nes" + # Set this to false to never autostart a rom (such as after patching) + # true for operating system default program + # Alternatively, a path to a program to open the .nes file with + rom_start: true + # Display message inside of Bizhawk + display_msgs: true dkc3_options: # File name of the DKC3 US rom rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc" diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py new file mode 100644 index 00000000..21ceafdc --- /dev/null +++ b/worlds/tloz/ItemPool.py @@ -0,0 +1,145 @@ +from BaseClasses import ItemClassification +from .Locations import level_locations, all_level_locations, standard_level_locations, shop_locations + +# Swords are in starting_weapons +overworld_items = { + "Letter": 1, + "Power Bracelet": 1, + "Heart Container": 1, + "Sword": 1 +} + +# Bomb, Arrow, 1 Small Key and Red Water of Life are in guaranteed_shop_items +shop_items = { + "Magical Shield": 3, + "Food": 2, + "Small Key": 1, + "Candle": 1, + "Recovery Heart": 1, + "Blue Ring": 1, + "Water of Life (Blue)": 1 +} + +# Magical Rod and Red Candle are in starting_weapons, Triforce Fragments are added in its section of get_pool_core +major_dungeon_items = { + "Heart Container": 8, + "Bow": 1, + "Boomerang": 1, + "Magical Boomerang": 1, + "Raft": 1, + "Stepladder": 1, + "Recorder": 1, + "Magical Key": 1, + "Book of Magic": 1, + "Silver Arrow": 1, + "Red Ring": 1 +} + +minor_dungeon_items = { + "Bomb": 23, + "Small Key": 45, + "Five Rupees": 17 +} + +take_any_items = { + "Heart Container": 4 +} + +# Map/Compasses: 18 +# Reasoning: Adding some variety to the vanilla game. + +map_compass_replacements = { + "Fairy": 6, + "Clock": 3, + "Water of Life (Red)": 1, + "Water of Life (Blue)": 2, + "Bomb": 2, + "Small Key": 2, + "Five Rupees": 2 +} +basic_pool = { + item: overworld_items.get(item, 0) + shop_items.get(item, 0) + + major_dungeon_items.get(item, 0) + map_compass_replacements.get(item, 0) + for item in set(overworld_items) | set(shop_items) | set(major_dungeon_items) | set(map_compass_replacements) +} + +starting_weapons = ["Sword", "White Sword", "Magical Sword", "Magical Rod", "Red Candle"] +guaranteed_shop_items = ["Small Key", "Bomb", "Water of Life (Red)", "Arrow"] +starting_weapon_locations = ["Starting Sword Cave", "Letter Cave", "Armos Knights"] +dangerous_weapon_locations = [ + "Level 1 Compass", "Level 2 Bomb Drop (Keese)", "Level 3 Key Drop (Zols Entrance)", "Level 3 Compass"] + +def generate_itempool(tlozworld): + (pool, placed_items) = get_pool_core(tlozworld) + tlozworld.multiworld.itempool.extend([tlozworld.multiworld.create_item(item, tlozworld.player) for item in pool]) + for (location_name, item) in placed_items.items(): + location = tlozworld.multiworld.get_location(location_name, tlozworld.player) + location.place_locked_item(tlozworld.multiworld.create_item(item, tlozworld.player)) + if item == "Bomb": + location.item.classification = ItemClassification.progression + +def get_pool_core(world): + random = world.multiworld.random + + pool = [] + placed_items = {} + minor_items = dict(minor_dungeon_items) + + # Guaranteed Shop Items + reserved_store_slots = random.sample(shop_locations[0:9], 4) + for location, item in zip(reserved_store_slots, guaranteed_shop_items): + placed_items[location] = item + + # Starting Weapon + starting_weapon = random.choice(starting_weapons) + if world.multiworld.StartingPosition[world.player] == 0: + placed_items[starting_weapon_locations[0]] = starting_weapon + elif world.multiworld.StartingPosition[world.player] in [1, 2]: + if world.multiworld.StartingPosition[world.player] == 2: + for location in dangerous_weapon_locations: + if world.multiworld.ExpandedPool[world.player] or "Drop" not in location: + starting_weapon_locations.append(location) + placed_items[random.choice(starting_weapon_locations)] = starting_weapon + else: + pool.append(starting_weapon) + for other_weapons in starting_weapons: + if other_weapons != starting_weapon: + pool.append(other_weapons) + + # Triforce Fragments + fragment = "Triforce Fragment" + if world.multiworld.ExpandedPool[world.player]: + possible_level_locations = [location for location in all_level_locations + if location not in level_locations[8]] + else: + possible_level_locations = [location for location in standard_level_locations + if location not in level_locations[8]] + for level in range(1, 9): + if world.multiworld.TriforceLocations[world.player] == 0: + placed_items[f"Level {level} Triforce"] = fragment + elif world.multiworld.TriforceLocations[world.player] == 1: + placed_items[possible_level_locations.pop(random.randint(0, len(possible_level_locations) - 1))] = fragment + else: + pool.append(fragment) + + # Level 9 junk fill + if world.multiworld.ExpandedPool[world.player] > 0: + spots = random.sample(level_locations[8], len(level_locations[8]) // 2) + for spot in spots: + junk = random.choice(list(minor_items.keys())) + placed_items[spot] = junk + minor_items[junk] -= 1 + + # Finish Pool + final_pool = basic_pool + if world.multiworld.ExpandedPool[world.player]: + final_pool = { + item: basic_pool.get(item, 0) + minor_items.get(item, 0) + take_any_items.get(item, 0) + for item in set(basic_pool) | set(minor_items) | set(take_any_items) + } + final_pool["Five Rupees"] -= 1 + for item in final_pool.keys(): + for i in range(0, final_pool[item]): + pool.append(item) + + return pool, placed_items diff --git a/worlds/tloz/Items.py b/worlds/tloz/Items.py new file mode 100644 index 00000000..d896d11d --- /dev/null +++ b/worlds/tloz/Items.py @@ -0,0 +1,147 @@ +from BaseClasses import ItemClassification +import typing +from typing import Dict + +progression = ItemClassification.progression +filler = ItemClassification.filler +useful = ItemClassification.useful +trap = ItemClassification.trap + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + classification: ItemClassification + + +item_table: Dict[str, ItemData] = { + "Boomerang": ItemData(100, useful), + "Bow": ItemData(101, progression), + "Magical Boomerang": ItemData(102, useful), + "Raft": ItemData(103, progression), + "Stepladder": ItemData(104, progression), + "Recorder": ItemData(105, progression), + "Magical Rod": ItemData(106, progression), + "Red Candle": ItemData(107, progression), + "Book of Magic": ItemData(108, progression), + "Magical Key": ItemData(109, useful), + "Red Ring": ItemData(110, useful), + "Silver Arrow": ItemData(111, progression), + "Sword": ItemData(112, progression), + "White Sword": ItemData(113, progression), + "Magical Sword": ItemData(114, progression), + "Heart Container": ItemData(115, progression), + "Letter": ItemData(116, progression), + "Magical Shield": ItemData(117, useful), + "Candle": ItemData(118, progression), + "Arrow": ItemData(119, progression), + "Food": ItemData(120, progression), + "Water of Life (Blue)": ItemData(121, useful), + "Water of Life (Red)": ItemData(122, useful), + "Blue Ring": ItemData(123, useful), + "Triforce Fragment": ItemData(124, progression), + "Power Bracelet": ItemData(125, useful), + "Small Key": ItemData(126, filler), + "Bomb": ItemData(127, filler), + "Recovery Heart": ItemData(128, filler), + "Five Rupees": ItemData(129, filler), + "Rupee": ItemData(130, filler), + "Clock": ItemData(131, filler), + "Fairy": ItemData(132, filler) + +} + +item_game_ids = { + "Bomb": 0x00, + "Sword": 0x01, + "White Sword": 0x02, + "Magical Sword": 0x03, + "Food": 0x04, + "Recorder": 0x05, + "Candle": 0x06, + "Red Candle": 0x07, + "Arrow": 0x08, + "Silver Arrow": 0x09, + "Bow": 0x0A, + "Magical Key": 0x0B, + "Raft": 0x0C, + "Stepladder": 0x0D, + "Five Rupees": 0x0F, + "Magical Rod": 0x10, + "Book of Magic": 0x11, + "Blue Ring": 0x12, + "Red Ring": 0x13, + "Power Bracelet": 0x14, + "Letter": 0x15, + "Small Key": 0x19, + "Heart Container": 0x1A, + "Triforce Fragment": 0x1B, + "Magical Shield": 0x1C, + "Boomerang": 0x1D, + "Magical Boomerang": 0x1E, + "Water of Life (Blue)": 0x1F, + "Water of Life (Red)": 0x20, + "Recovery Heart": 0x22, + "Rupee": 0x18, + "Clock": 0x21, + "Fairy": 0x23 +} + +# Item prices are going to get a bit of a writeup here, because these are some seemingly arbitrary +# design decisions and future contributors may want to know how these were arrived at. + +# First, I based everything off of the Blue Ring. Since the Red Ring is twice as good as the Blue Ring, +# logic dictates it should cost twice as much. Since you can't make something cost 500 rupees, the only +# solution was to halve the price of the Blue Ring. Correspondingly, everything else sold in shops was +# also cut in half. + +# Then, I decided on a factor for swords. Since each sword does double the damage of its predecessor, each +# one should be at least double. Since the sword saves so much time when upgraded (as, unlike other items, +# you don't need to switch to it), I wanted a bit of a premium on upgrades. Thus, a 4x multiplier was chosen, +# allowing the basic Sword to stay cheap while making the Magical Sword be a hefty upgrade you'll +# feel the price of. + +# Since arrows do the same amount of damage as the White Sword and silver arrows are the same with the Magical Sword. +# they were given corresponding costs. + +# Utility items were based on the prices of the shield, keys, and food. Broadly useful utility items should cost more, +# while limited use utility items should cost less. After eyeballing those, a few editorial decisions were made as +# deliberate thumbs on the scale of game balance. Those exceptions will be noted below. In general, prices were chosen +# based on how a player would feel spending that amount of money as opposed to how useful an item actually is. + +item_prices = { + "Bomb": 10, + "Sword": 10, + "White Sword": 40, + "Magical Sword": 160, + "Food": 30, + "Recorder": 45, + "Candle": 30, + "Red Candle": 60, + "Arrow": 40, + "Silver Arrow": 160, + "Bow": 40, + "Magical Key": 250, # Replacing all small keys commands a high premium + "Raft": 80, + "Stepladder": 80, + "Five Rupees": 255, # This could cost anything above 5 Rupees and be fine, but 255 is the funniest + "Magical Rod": 100, # White Sword with forever beams should cost at least more than the White Sword itself + "Book of Magic": 60, + "Blue Ring": 125, + "Red Ring": 250, + "Power Bracelet": 25, + "Letter": 20, + "Small Key": 40, + "Heart Container": 80, + "Triforce Fragment": 200, # Since I couldn't make Zelda 1 track shop purchases, this is how to discourage repeat + # Triforce purchases. The punishment for endless Rupee grinding to avoid searching out + # Triforce pieces is that you're doing endless Rupee grinding to avoid playing the game + "Magical Shield": 45, + "Boomerang": 5, + "Magical Boomerang": 20, + "Water of Life (Blue)": 20, + "Water of Life (Red)": 34, + "Recovery Heart": 5, + "Rupee": 50, + "Clock": 0, + "Fairy": 10 +} diff --git a/worlds/tloz/Locations.py b/worlds/tloz/Locations.py new file mode 100644 index 00000000..bbd22aba --- /dev/null +++ b/worlds/tloz/Locations.py @@ -0,0 +1,350 @@ +from . import Rom + +major_locations = [ + "Starting Sword Cave", + "White Sword Pond", + "Magical Sword Grave", + "Take Any Item Left", + "Take Any Item Middle", + "Take Any Item Right", + "Armos Knights", + "Ocean Heart Container", + "Letter Cave", +] + +level_locations = [ + [ + "Level 1 Item (Bow)", "Level 1 Item (Boomerang)", "Level 1 Map", "Level 1 Compass", "Level 1 Boss", + "Level 1 Triforce", "Level 1 Key Drop (Keese Entrance)", "Level 1 Key Drop (Stalfos Middle)", + "Level 1 Key Drop (Moblins)", "Level 1 Key Drop (Stalfos Water)", + "Level 1 Key Drop (Stalfos Entrance)", "Level 1 Key Drop (Wallmasters)", + ], + [ + "Level 2 Item (Magical Boomerang)", "Level 2 Map", "Level 2 Compass", "Level 2 Boss", "Level 2 Triforce", + "Level 2 Key Drop (Ropes West)", "Level 2 Key Drop (Moldorms)", + "Level 2 Key Drop (Ropes Middle)", "Level 2 Key Drop (Ropes Entrance)", + "Level 2 Bomb Drop (Keese)", "Level 2 Bomb Drop (Moblins)", + "Level 2 Rupee Drop (Gels)", + ], + [ + "Level 3 Item (Raft)", "Level 3 Map", "Level 3 Compass", "Level 3 Boss", "Level 3 Triforce", + "Level 3 Key Drop (Zols and Keese West)", "Level 3 Key Drop (Keese North)", + "Level 3 Key Drop (Zols Central)", "Level 3 Key Drop (Zols South)", + "Level 3 Key Drop (Zols Entrance)", "Level 3 Bomb Drop (Darknuts West)", + "Level 3 Bomb Drop (Keese Corridor)", "Level 3 Bomb Drop (Darknuts Central)", + "Level 3 Rupee Drop (Zols and Keese East)" + ], + [ + "Level 4 Item (Stepladder)", "Level 4 Map", "Level 4 Compass", "Level 4 Boss", "Level 4 Triforce", + "Level 4 Key Drop (Keese Entrance)", "Level 4 Key Drop (Keese Central)", + "Level 4 Key Drop (Zols)", "Level 4 Key Drop (Keese North)", + ], + [ + "Level 5 Item (Recorder)", "Level 5 Map", "Level 5 Compass", "Level 5 Boss", "Level 5 Triforce", + "Level 5 Key Drop (Keese North)", "Level 5 Key Drop (Gibdos North)", + "Level 5 Key Drop (Gibdos Central)", "Level 5 Key Drop (Pols Voice Entrance)", + "Level 5 Key Drop (Gibdos Entrance)", "Level 5 Key Drop (Gibdos, Keese, and Pols Voice)", + "Level 5 Key Drop (Zols)", "Level 5 Bomb Drop (Gibdos)", + "Level 5 Bomb Drop (Dodongos)", "Level 5 Rupee Drop (Zols)", + ], + [ + "Level 6 Item (Magical Rod)", "Level 6 Map", "Level 6 Compass", "Level 6 Boss", "Level 6 Triforce", + "Level 6 Key Drop (Wizzrobes Entrance)", "Level 6 Key Drop (Keese)", + "Level 6 Key Drop (Wizzrobes North Island)", "Level 6 Key Drop (Wizzrobes North Stream)", + "Level 6 Key Drop (Vires)", "Level 6 Bomb Drop (Wizzrobes)", + "Level 6 Rupee Drop (Wizzrobes)" + ], + [ + "Level 7 Item (Red Candle)", "Level 7 Map", "Level 7 Compass", "Level 7 Boss", "Level 7 Triforce", + "Level 7 Key Drop (Ropes)", "Level 7 Key Drop (Goriyas)", "Level 7 Key Drop (Stalfos)", + "Level 7 Key Drop (Moldorms)", "Level 7 Bomb Drop (Goriyas South)", "Level 7 Bomb Drop (Keese and Spikes)", + "Level 7 Bomb Drop (Moldorms South)", "Level 7 Bomb Drop (Moldorms North)", + "Level 7 Bomb Drop (Goriyas North)", "Level 7 Bomb Drop (Dodongos)", + "Level 7 Bomb Drop (Digdogger)", "Level 7 Rupee Drop (Goriyas Central)", + "Level 7 Rupee Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)", + ], + [ + "Level 8 Item (Magical Key)", "Level 8 Map", "Level 8 Compass", "Level 8 Item (Book of Magic)", "Level 8 Boss", + "Level 8 Triforce", "Level 8 Key Drop (Darknuts West)", + "Level 8 Key Drop (Darknuts Far West)", "Level 8 Key Drop (Pols Voice South)", + "Level 8 Key Drop (Pols Voice and Keese)", "Level 8 Key Drop (Darknuts Central)", + "Level 8 Key Drop (Keese and Zols Entrance)", "Level 8 Bomb Drop (Darknuts North)", + "Level 8 Bomb Drop (Darknuts East)", "Level 8 Bomb Drop (Pols Voice North)", + "Level 8 Rupee Drop (Manhandla Entrance West)", "Level 8 Rupee Drop (Manhandla Entrance North)", + "Level 8 Rupee Drop (Darknuts and Gibdos)", + ], + [ + "Level 9 Item (Silver Arrow)", "Level 9 Item (Red Ring)", + "Level 9 Map", "Level 9 Compass", + "Level 9 Key Drop (Patra Southwest)", "Level 9 Key Drop (Like Likes and Zols East)", + "Level 9 Key Drop (Wizzrobes and Bubbles East)", "Level 9 Key Drop (Wizzrobes East Island)", + "Level 9 Bomb Drop (Blue Lanmolas)", "Level 9 Bomb Drop (Gels Lake)", + "Level 9 Bomb Drop (Like Likes and Zols Corridor)", "Level 9 Bomb Drop (Patra Northeast)", + "Level 9 Bomb Drop (Vires)", "Level 9 Rupee Drop (Wizzrobes West Island)", + "Level 9 Rupee Drop (Red Lanmolas)", "Level 9 Rupee Drop (Keese Southwest)", + "Level 9 Rupee Drop (Keese Central Island)", "Level 9 Rupee Drop (Wizzrobes Central)", + "Level 9 Rupee Drop (Wizzrobes North Island)", "Level 9 Rupee Drop (Gels East)" + ] +] + +all_level_locations = [] +for level in level_locations: + for location in level: + all_level_locations.append(location) + +standard_level_locations = [] +for level in level_locations: + for location in level: + if "Drop" not in location: + standard_level_locations.append(location) + +shop_locations = [ + "Arrow Shop Item Left", "Arrow Shop Item Middle", "Arrow Shop Item Right", + "Candle Shop Item Left", "Candle Shop Item Middle", "Candle Shop Item Right", + "Blue Ring Shop Item Left", "Blue Ring Shop Item Middle", "Blue Ring Shop Item Right", + "Shield Shop Item Left", "Shield Shop Item Middle", "Shield Shop Item Right", + "Potion Shop Item Left", "Potion Shop Item Middle", "Potion Shop Item Right" +] + +food_locations = [ + "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)", + "Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)", + "Level 7 Bomb Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)" +] + +floor_location_game_offsets_early = { + "Level 1 Item (Bow)": 0x7F, + "Level 1 Item (Boomerang)": 0x44, + "Level 1 Map": 0x43, + "Level 1 Compass": 0x54, + "Level 1 Boss": 0x35, + "Level 1 Triforce": 0x36, + "Level 1 Key Drop (Keese Entrance)": 0x72, + "Level 1 Key Drop (Moblins)": 0x23, + "Level 1 Key Drop (Stalfos Water)": 0x33, + "Level 1 Key Drop (Stalfos Entrance)": 0x74, + "Level 1 Key Drop (Stalfos Middle)": 0x53, + "Level 1 Key Drop (Wallmasters)": 0x45, + "Level 2 Item (Magical Boomerang)": 0x4F, + "Level 2 Map": 0x5F, + "Level 2 Compass": 0x6F, + "Level 2 Boss": 0x0E, + "Level 2 Triforce": 0x0D, + "Level 2 Key Drop (Ropes West)": 0x6C, + "Level 2 Key Drop (Moldorms)": 0x3E, + "Level 2 Key Drop (Ropes Middle)": 0x4E, + "Level 2 Key Drop (Ropes Entrance)": 0x7E, + "Level 2 Bomb Drop (Keese)": 0x3F, + "Level 2 Bomb Drop (Moblins)": 0x1E, + "Level 2 Rupee Drop (Gels)": 0x2F, + "Level 3 Item (Raft)": 0x0F, + "Level 3 Map": 0x4C, + "Level 3 Compass": 0x5A, + "Level 3 Boss": 0x4D, + "Level 3 Triforce": 0x3D, + "Level 3 Key Drop (Zols and Keese West)": 0x49, + "Level 3 Key Drop (Keese North)": 0x2A, + "Level 3 Key Drop (Zols Central)": 0x4B, + "Level 3 Key Drop (Zols South)": 0x6B, + "Level 3 Key Drop (Zols Entrance)": 0x7B, + "Level 3 Bomb Drop (Darknuts West)": 0x69, + "Level 3 Bomb Drop (Keese Corridor)": 0x4A, + "Level 3 Bomb Drop (Darknuts Central)": 0x5B, + "Level 3 Rupee Drop (Zols and Keese East)": 0x5D, + "Level 4 Item (Stepladder)": 0x60, + "Level 4 Map": 0x21, + "Level 4 Compass": 0x62, + "Level 4 Boss": 0x13, + "Level 4 Triforce": 0x03, + "Level 4 Key Drop (Keese Entrance)": 0x70, + "Level 4 Key Drop (Keese Central)": 0x51, + "Level 4 Key Drop (Zols)": 0x40, + "Level 4 Key Drop (Keese North)": 0x01, + "Level 5 Item (Recorder)": 0x04, + "Level 5 Map": 0x46, + "Level 5 Compass": 0x37, + "Level 5 Boss": 0x24, + "Level 5 Triforce": 0x14, + "Level 5 Key Drop (Keese North)": 0x16, + "Level 5 Key Drop (Gibdos North)": 0x26, + "Level 5 Key Drop (Gibdos Central)": 0x47, + "Level 5 Key Drop (Pols Voice Entrance)": 0x77, + "Level 5 Key Drop (Gibdos Entrance)": 0x66, + "Level 5 Key Drop (Gibdos, Keese, and Pols Voice)": 0x27, + "Level 5 Key Drop (Zols)": 0x55, + "Level 5 Bomb Drop (Gibdos)": 0x65, + "Level 5 Bomb Drop (Dodongos)": 0x56, + "Level 5 Rupee Drop (Zols)": 0x57, + "Level 6 Item (Magical Rod)": 0x75, + "Level 6 Map": 0x19, + "Level 6 Compass": 0x68, + "Level 6 Boss": 0x1C, + "Level 6 Triforce": 0x0C, + "Level 6 Key Drop (Wizzrobes Entrance)": 0x7A, + "Level 6 Key Drop (Keese)": 0x58, + "Level 6 Key Drop (Wizzrobes North Island)": 0x29, + "Level 6 Key Drop (Wizzrobes North Stream)": 0x1A, + "Level 6 Key Drop (Vires)": 0x2D, + "Level 6 Bomb Drop (Wizzrobes)": 0x3C, + "Level 6 Rupee Drop (Wizzrobes)": 0x28 +} + +floor_location_game_ids_early = {} +floor_location_game_ids_late = {} +for key, value in floor_location_game_offsets_early.items(): + floor_location_game_ids_early[key] = value + Rom.first_quest_dungeon_items_early + +floor_location_game_offsets_late = { + "Level 7 Item (Red Candle)": 0x4A, + "Level 7 Map": 0x18, + "Level 7 Compass": 0x5A, + "Level 7 Boss": 0x2A, + "Level 7 Triforce": 0x2B, + "Level 7 Key Drop (Ropes)": 0x78, + "Level 7 Key Drop (Goriyas)": 0x0A, + "Level 7 Key Drop (Stalfos)": 0x6D, + "Level 7 Key Drop (Moldorms)": 0x3A, + "Level 7 Bomb Drop (Goriyas South)": 0x69, + "Level 7 Bomb Drop (Keese and Spikes)": 0x68, + "Level 7 Bomb Drop (Moldorms South)": 0x7A, + "Level 7 Bomb Drop (Moldorms North)": 0x0B, + "Level 7 Bomb Drop (Goriyas North)": 0x1B, + "Level 7 Bomb Drop (Dodongos)": 0x0C, + "Level 7 Bomb Drop (Digdogger)": 0x6C, + "Level 7 Rupee Drop (Goriyas Central)": 0x38, + "Level 7 Rupee Drop (Dodongos)": 0x58, + "Level 7 Rupee Drop (Goriyas North)": 0x09, + "Level 8 Item (Magical Key)": 0x0F, + "Level 8 Item (Book of Magic)": 0x6F, + "Level 8 Map": 0x2E, + "Level 8 Compass": 0x5F, + "Level 8 Boss": 0x3C, + "Level 8 Triforce": 0x2C, + "Level 8 Key Drop (Darknuts West)": 0x5C, + "Level 8 Key Drop (Darknuts Far West)": 0x4B, + "Level 8 Key Drop (Pols Voice South)": 0x4C, + "Level 8 Key Drop (Pols Voice and Keese)": 0x5D, + "Level 8 Key Drop (Darknuts Central)": 0x5E, + "Level 8 Key Drop (Keese and Zols Entrance)": 0x7F, + "Level 8 Bomb Drop (Darknuts North)": 0x0E, + "Level 8 Bomb Drop (Darknuts East)": 0x3F, + "Level 8 Bomb Drop (Pols Voice North)": 0x1D, + "Level 8 Rupee Drop (Manhandla Entrance West)": 0x7D, + "Level 8 Rupee Drop (Manhandla Entrance North)": 0x6E, + "Level 8 Rupee Drop (Darknuts and Gibdos)": 0x4E, + "Level 9 Item (Silver Arrow)": 0x4F, + "Level 9 Item (Red Ring)": 0x00, + "Level 9 Map": 0x27, + "Level 9 Compass": 0x35, + "Level 9 Key Drop (Patra Southwest)": 0x61, + "Level 9 Key Drop (Like Likes and Zols East)": 0x56, + "Level 9 Key Drop (Wizzrobes and Bubbles East)": 0x47, + "Level 9 Key Drop (Wizzrobes East Island)": 0x57, + "Level 9 Bomb Drop (Blue Lanmolas)": 0x11, + "Level 9 Bomb Drop (Gels Lake)": 0x23, + "Level 9 Bomb Drop (Like Likes and Zols Corridor)": 0x25, + "Level 9 Bomb Drop (Patra Northeast)": 0x16, + "Level 9 Bomb Drop (Vires)": 0x37, + "Level 9 Rupee Drop (Wizzrobes West Island)": 0x40, + "Level 9 Rupee Drop (Red Lanmolas)": 0x12, + "Level 9 Rupee Drop (Keese Southwest)": 0x62, + "Level 9 Rupee Drop (Keese Central Island)": 0x34, + "Level 9 Rupee Drop (Wizzrobes Central)": 0x44, + "Level 9 Rupee Drop (Wizzrobes North Island)": 0x15, + "Level 9 Rupee Drop (Gels East)": 0x26 +} + +for key, value in floor_location_game_offsets_late.items(): + floor_location_game_ids_late[key] = value + Rom.first_quest_dungeon_items_late + +dungeon_items = {**floor_location_game_ids_early, **floor_location_game_ids_late} + +shop_location_ids = { + "Arrow Shop Item Left": 0x18637, + "Arrow Shop Item Middle": 0x18638, + "Arrow Shop Item Right": 0x18639, + "Candle Shop Item Left": 0x1863A, + "Candle Shop Item Middle": 0x1863B, + "Candle Shop Item Right": 0x1863C, + "Shield Shop Item Left": 0x1863D, + "Shield Shop Item Middle": 0x1863E, + "Shield Shop Item Right": 0x1863F, + "Blue Ring Shop Item Left": 0x18640, + "Blue Ring Shop Item Middle": 0x18641, + "Blue Ring Shop Item Right": 0x18642, + "Potion Shop Item Left": 0x1862E, + "Potion Shop Item Middle": 0x1862F, + "Potion Shop Item Right": 0x18630 +} + +shop_price_location_ids = { + "Arrow Shop Item Left": 0x18673, + "Arrow Shop Item Middle": 0x18674, + "Arrow Shop Item Right": 0x18675, + "Candle Shop Item Left": 0x18676, + "Candle Shop Item Middle": 0x18677, + "Candle Shop Item Right": 0x18678, + "Shield Shop Item Left": 0x18679, + "Shield Shop Item Middle": 0x1867A, + "Shield Shop Item Right": 0x1867B, + "Blue Ring Shop Item Left": 0x1867C, + "Blue Ring Shop Item Middle": 0x1867D, + "Blue Ring Shop Item Right": 0x1867E, + "Potion Shop Item Left": 0x1866A, + "Potion Shop Item Middle": 0x1866B, + "Potion Shop Item Right": 0x1866C +} + +secret_money_ids = { + "Secret Money 1": 0x18680, + "Secret Money 2": 0x18683, + "Secret Money 3": 0x18686 +} + +major_location_ids = { + "Starting Sword Cave": 0x18611, + "White Sword Pond": 0x18617, + "Magical Sword Grave": 0x1861A, + "Letter Cave": 0x18629, + "Take Any Item Left": 0x18613, + "Take Any Item Middle": 0x18614, + "Take Any Item Right": 0x18615, + "Armos Knights": 0x10D05, + "Ocean Heart Container": 0x1789A +} + +major_location_offsets = { + "Starting Sword Cave": 0x77, + "White Sword Pond": 0x0A, + "Magical Sword Grave": 0x21, + "Letter Cave": 0x0E, + # "Take Any Item Left": 0x7B, + # "Take Any Item Middle": 0x2C, + # "Take Any Item Right": 0x47, + "Armos Knights": 0x24, + "Ocean Heart Container": 0x5F +} + +overworld_locations = [ + "Starting Sword Cave", + "White Sword Pond", + "Magical Sword Grave", + "Letter Cave", + "Armos Knights", + "Ocean Heart Container" +] + +underworld1_locations = [*floor_location_game_offsets_early.keys()] + +underworld2_locations = [*floor_location_game_offsets_late.keys()] + +#cave_locations = ["Take Any Item Left", "Take Any Item Middle", "Take Any Item Right"] + [*shop_locations] + +location_table_base = [x for x in major_locations] + \ + [y for y in all_level_locations] + \ + [z for z in shop_locations] +location_table = {} +for i, location in enumerate(location_table_base): + location_table[location] = i + +location_ids = {**dungeon_items, **shop_location_ids, **major_location_ids} diff --git a/worlds/tloz/Options.py b/worlds/tloz/Options.py new file mode 100644 index 00000000..47eb9509 --- /dev/null +++ b/worlds/tloz/Options.py @@ -0,0 +1,40 @@ +import typing +from Options import Option, DefaultOnToggle, Choice + + +class ExpandedPool(DefaultOnToggle): + """Puts room clear drops into the pool of items and locations.""" + display_name = "Expanded Item Pool" + + +class TriforceLocations(Choice): + """Where Triforce fragments can be located. Note that Triforce pieces + obtained in a dungeon will heal and warp you out, while overworld Triforce pieces obtained will appear to have + no immediate effect. This is normal.""" + display_name = "Triforce Locations" + option_vanilla = 0 + option_dungeons = 1 + option_anywhere = 2 + + +class StartingPosition(Choice): + """How easy is the start of the game. + Safe means a weapon is guaranteed in Starting Sword Cave. + Unsafe means that a weapon is guaranteed between Starting Sword Cave, Letter Cave, and Armos Knight. + Dangerous adds these level locations to the unsafe pool (if they exist): +# Level 1 Compass, Level 2 Bomb Drop (Keese), Level 3 Key Drop (Zols Entrance), Level 3 Compass + Very Dangerous is the same as dangerous except it doesn't guarantee a weapon. It will only mean progression + will be there in single player seeds. In multi worlds, however, this means all bets are off and after checking + the dangerous spots, you could be stuck until someone sends you a weapon""" + display_name = "Starting Position" + option_safe = 0 + option_unsafe = 1 + option_dangerous = 2 + option_very_dangerous = 3 + + +tloz_options: typing.Dict[str, type(Option)] = { + "ExpandedPool": ExpandedPool, + "TriforceLocations": TriforceLocations, + "StartingPosition": StartingPosition +} diff --git a/worlds/tloz/Rom.py b/worlds/tloz/Rom.py new file mode 100644 index 00000000..0eaf5855 --- /dev/null +++ b/worlds/tloz/Rom.py @@ -0,0 +1,78 @@ +import zlib +import os + +import Utils +from Patch import APDeltaPatch + +NA10CHECKSUM = 'D7AE93DF' +ROM_PLAYER_LIMIT = 65535 +ROM_NAME = 0x10 +bit_positions = [0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80] +candle_shop = bit_positions[5] +arrow_shop = bit_positions[4] +potion_shop = bit_positions[1] +shield_shop = bit_positions[6] +ring_shop = bit_positions[7] +take_any = bit_positions[2] +first_quest_dungeon_items_early = 0x18910 +first_quest_dungeon_items_late = 0x18C10 +game_mode = 0x12 +sword = 0x0657 +bombs = 0x0658 +arrow = 0x0659 +bow = 0x065A +candle = 0x065B +recorder = 0x065C +food = 0x065D +potion = 0x065E +magical_rod = 0x065F +raft = 0x0660 +book_of_magic = 0x0661 +ring = 0x0662 +stepladder = 0x0663 +magical_key = 0x0664 +power_bracelet = 0x0665 +letter = 0x0666 +heart_containers = 0x066F +triforce_fragments = 0x0671 +boomerang = 0x0674 +magical_boomerang = 0x0675 +magical_shield = 0x0676 +rupees_to_add = 0x067D + + + + +class TLoZDeltaPatch(APDeltaPatch): + checksum = NA10CHECKSUM + hash = NA10CHECKSUM + game = "The Legend of Zelda" + patch_file_ending = ".aptloz" + result_file_ending = ".nes" + + @classmethod + def get_source_data(cls) -> bytes: + return get_base_rom_bytes() + + +def get_base_rom_bytes(file_name: str = "") -> bytes: + base_rom_bytes = getattr(get_base_rom_bytes, "base_rom_bytes", None) + if not base_rom_bytes: + file_name = get_base_rom_path(file_name) + base_rom_bytes = bytes(Utils.read_snes_rom(open(file_name, "rb"))) + + basechecksum = str(hex(zlib.crc32(base_rom_bytes))).upper()[2:] + if NA10CHECKSUM != basechecksum: + raise Exception('Supplied Base Rom does not match known CRC-32 for NA (1.0) release. ' + 'Get the correct game and version, then dump it') + get_base_rom_bytes.base_rom_bytes = base_rom_bytes + return base_rom_bytes + + +def get_base_rom_path(file_name: str = "") -> str: + options = Utils.get_options() + if not file_name: + file_name = options["tloz_options"]["rom_file"] + if not os.path.exists(file_name): + file_name = Utils.local_path(file_name) + return file_name \ No newline at end of file diff --git a/worlds/tloz/Rules.py b/worlds/tloz/Rules.py new file mode 100644 index 00000000..631a23d0 --- /dev/null +++ b/worlds/tloz/Rules.py @@ -0,0 +1,147 @@ +from typing import TYPE_CHECKING + +from ..generic.Rules import add_rule +from .Locations import food_locations, shop_locations +from .ItemPool import dangerous_weapon_locations + +if TYPE_CHECKING: + from . import TLoZWorld + +def set_rules(tloz_world: "TLoZWorld"): + player = tloz_world.player + world = tloz_world.multiworld + + # Boss events for a nicer spoiler log play through + for level in range(1, 9): + boss = world.get_location(f"Level {level} Boss", player) + boss_event = world.get_location(f"Level {level} Boss Status", player) + status = tloz_world.create_event(f"Boss {level} Defeated") + boss_event.place_locked_item(status) + add_rule(boss_event, lambda state, b=boss: state.can_reach(b, "Location", player)) + + # No dungeons without weapons except for the dangerous weapon locations if we're dangerous, no unsafe dungeons + for i, level in enumerate(tloz_world.levels[1:10]): + for location in level.locations: + if world.StartingPosition[player] < 1 or location.name not in dangerous_weapon_locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has_group("weapons", player)) + if i > 0: # Don't need an extra heart for Level 1 + add_rule(world.get_location(location.name, player), + lambda state, hearts=i: state.has("Heart Container", player, hearts) or + (state.has("Blue Ring", player) and + state.has("Heart Container", player, int(hearts / 2))) or + (state.has("Red Ring", player) and + state.has("Heart Container", player, int(hearts / 4))) + + ) + # No requiring anything in a shop until we can farm for money + for location in shop_locations: + add_rule(world.get_location(location, player), + lambda state: state.has_group("weapons", player)) + + # Everything from 4 on up has dark rooms + for level in tloz_world.levels[4:]: + for location in level.locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has_group("candles", player) + or (state.has("Magical Rod", player) and state.has("Book", player))) + + # Everything from 5 on up has gaps + for level in tloz_world.levels[5:]: + for location in level.locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Stepladder", player)) + + add_rule(world.get_location("Level 5 Boss", player), + lambda state: state.has("Recorder", player)) + + add_rule(world.get_location("Level 6 Boss", player), + lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + + add_rule(world.get_location("Level 7 Item (Red Candle)", player), + lambda state: state.has("Recorder", player)) + add_rule(world.get_location("Level 7 Boss", player), + lambda state: state.has("Recorder", player)) + if world.ExpandedPool[player]: + add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player), + lambda state: state.has("Recorder", player)) + add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player), + lambda state: state.has("Recorder", player)) + add_rule(world.get_location("Level 7 Rupee Drop (Dodongos)", player), + lambda state: state.has("Recorder", player)) + + for location in food_locations: + if world.ExpandedPool[player] or "Drop" not in location: + add_rule(world.get_location(location, player), + lambda state: state.has("Food", player)) + + add_rule(world.get_location("Level 8 Item (Magical Key)", player), + lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + if world.ExpandedPool[player]: + add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player), + lambda state: state.has("Bow", player) and state.has_group("arrows", player)) + + for location in tloz_world.levels[9].locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Triforce Fragment", player, 8) and + state.has_group("swords", player)) + + # Yes we are looping this range again for Triforce locations. No I can't add it to the boss event loop + for level in range(1, 9): + add_rule(world.get_location(f"Level {level} Triforce", player), + lambda state, l=level: state.has(f"Boss {l} Defeated", player)) + + # Sword, raft, and ladder spots + add_rule(world.get_location("White Sword Pond", player), + lambda state: state.has("Heart Container", player, 2)) + add_rule(world.get_location("Magical Sword Grave", player), + lambda state: state.has("Heart Container", player, 9)) + + stepladder_locations = ["Ocean Heart Container", "Level 4 Triforce", "Level 4 Boss", "Level 4 Map"] + stepladder_locations_expanded = ["Level 4 Key Drop (Keese North)"] + for location in stepladder_locations: + add_rule(world.get_location(location, player), + lambda state: state.has("Stepladder", player)) + if world.ExpandedPool[player]: + for location in stepladder_locations_expanded: + add_rule(world.get_location(location, player), + lambda state: state.has("Stepladder", player)) + + if world.StartingPosition[player] != 2: + # Don't allow Take Any Items until we can actually get in one + if world.ExpandedPool[player]: + add_rule(world.get_location("Take Any Item Left", player), + lambda state: state.has_group("candles", player) or + state.has("Raft", player)) + add_rule(world.get_location("Take Any Item Middle", player), + lambda state: state.has_group("candles", player) or + state.has("Raft", player)) + add_rule(world.get_location("Take Any Item Right", player), + lambda state: state.has_group("candles", player) or + state.has("Raft", player)) + for location in tloz_world.levels[4].locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Raft", player) or state.has("Recorder", player)) + for location in tloz_world.levels[7].locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Recorder", player)) + for location in tloz_world.levels[8].locations: + add_rule(world.get_location(location.name, player), + lambda state: state.has("Bow", player)) + + add_rule(world.get_location("Potion Shop Item Left", player), + lambda state: state.has("Letter", player)) + add_rule(world.get_location("Potion Shop Item Middle", player), + lambda state: state.has("Letter", player)) + add_rule(world.get_location("Potion Shop Item Right", player), + lambda state: state.has("Letter", player)) + + add_rule(world.get_location("Shield Shop Item Left", player), + lambda state: state.has_group("candles", player) or + state.has("Bomb", player)) + add_rule(world.get_location("Shield Shop Item Middle", player), + lambda state: state.has_group("candles", player) or + state.has("Bomb", player)) + add_rule(world.get_location("Shield Shop Item Right", player), + lambda state: state.has_group("candles", player) or + state.has("Bomb", player)) \ No newline at end of file diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py new file mode 100644 index 00000000..3304569c --- /dev/null +++ b/worlds/tloz/__init__.py @@ -0,0 +1,313 @@ +import logging +import os +import threading +import pkgutil +from typing import NamedTuple, Union, Dict, Any + +import bsdiff4 + +import Utils +from BaseClasses import Item, Location, Region, Entrance, MultiWorld, ItemClassification, Tutorial +from .ItemPool import generate_itempool, starting_weapons, dangerous_weapon_locations +from .Items import item_table, item_prices, item_game_ids +from .Locations import location_table, level_locations, major_locations, shop_locations, all_level_locations, \ + standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations +from .Options import tloz_options +from .Rom import TLoZDeltaPatch, get_base_rom_path, first_quest_dungeon_items_early, first_quest_dungeon_items_late +from .Rules import set_rules +from worlds.AutoWorld import World, WebWorld +from worlds.generic.Rules import add_rule + + +class TLoZWeb(WebWorld): + theme = "stone" + setup = Tutorial( + "Multiworld Setup Tutorial", + "A guide to setting up The Legend of Zelda for Archipelago on your computer.", + "English", + "multiworld_en.md", + "multiworld/en", + ["Rosalie and Figment"] + ) + + tutorials = [setup] + + +class TLoZWorld(World): + """ + The Legend of Zelda needs almost no introduction. Gather the eight fragments of the + Triforce of Courage, enter Death Mountain, defeat Ganon, and rescue Princess Zelda. + This randomizer shuffles all the items in the game around, leading to a new adventure + every time. + """ + option_definitions = tloz_options + game = "The Legend of Zelda" + topology_present = False + data_version = 1 + base_id = 7000 + web = TLoZWeb() + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = location_table + + item_name_groups = { + 'weapons': starting_weapons, + 'swords': { + "Sword", "White Sword", "Magical Sword" + }, + "candles": { + "Candle", "Red Candle" + }, + "arrows": { + "Arrow", "Silver Arrow" + } + } + + for k, v in item_name_to_id.items(): + item_name_to_id[k] = v + base_id + + for k, v in location_name_to_id.items(): + if v is not None: + location_name_to_id[k] = v + base_id + + def __init__(self, world: MultiWorld, player: int): + super().__init__(world, player) + self.generator_in_use = threading.Event() + self.rom_name_available_event = threading.Event() + self.levels = None + self.filler_items = None + + def create_item(self, name: str): + return TLoZItem(name, item_table[name].classification, self.item_name_to_id[name], self.player) + + def create_event(self, event: str): + return TLoZItem(event, ItemClassification.progression, None, self.player) + + def create_location(self, name, id, parent, event=False): + return_location = TLoZLocation(self.player, name, id, parent) + return_location.event = event + return return_location + + def create_regions(self): + menu = Region("Menu", self.player, self.multiworld) + overworld = Region("Overworld", self.player, self.multiworld) + self.levels = [None] # Yes I'm making a one-indexed array in a zero-indexed language. I hate me too. + for i in range(1, 10): + level = Region(f"Level {i}", self.player, self.multiworld) + self.levels.append(level) + new_entrance = Entrance(self.player, f"Level {i}", overworld) + new_entrance.connect(level) + overworld.exits.append(new_entrance) + self.multiworld.regions.append(level) + + for i, level in enumerate(level_locations): + for location in level: + if self.multiworld.ExpandedPool[self.player] or "Drop" not in location: + self.levels[i + 1].locations.append( + self.create_location(location, self.location_name_to_id[location], self.levels[i + 1])) + + for level in range(1, 9): + boss_event = self.create_location(f"Level {level} Boss Status", None, + self.multiworld.get_region(f"Level {level}", self.player), + True) + boss_event.show_in_spoiler = False + self.levels[level].locations.append(boss_event) + + for location in major_locations: + if self.multiworld.ExpandedPool[self.player] or "Take Any" not in location: + overworld.locations.append( + self.create_location(location, self.location_name_to_id[location], overworld)) + + for location in shop_locations: + overworld.locations.append( + self.create_location(location, self.location_name_to_id[location], overworld)) + + ganon = self.create_location("Ganon", None, self.multiworld.get_region("Level 9", self.player)) + zelda = self.create_location("Zelda", None, self.multiworld.get_region("Level 9", self.player)) + ganon.show_in_spoiler = False + zelda.show_in_spoiler = False + self.levels[9].locations.append(ganon) + self.levels[9].locations.append(zelda) + begin_game = Entrance(self.player, "Begin Game", menu) + menu.exits.append(begin_game) + begin_game.connect(overworld) + self.multiworld.regions.append(menu) + self.multiworld.regions.append(overworld) + + set_rules = set_rules + + def generate_basic(self): + ganon = self.multiworld.get_location("Ganon", self.player) + ganon.place_locked_item(self.create_event("Triforce of Power")) + add_rule(ganon, lambda state: state.has("Silver Arrow", self.player) and state.has("Bow", self.player)) + + self.multiworld.get_location("Zelda", self.player).place_locked_item(self.create_event("Rescued Zelda!")) + add_rule(self.multiworld.get_location("Zelda", self.player), + lambda state: ganon in state.locations_checked) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("Rescued Zelda!", self.player) + generate_itempool(self) + + def apply_base_patch(self, rom): + # The base patch source is on a different repo, so here's the summary of changes: + # Remove Triforce check for recorder, so you can always warp. + # Remove level check for Triforce Fragments (and maps and compasses, but this won't matter) + # Replace some code with a jump to free space + # Check if we're picking up a Triforce Fragment. If so, increment the local count + # In either case, we do the instructions we overwrote with the jump and then return to normal flow + # Remove map/compass check so they're always on + # Removing a bit from the boss roars flags, so we can have more dungeon items. This allows us to + # go past 0x1F items for dungeon items. + base_patch_location = os.path.dirname(__file__) + "/z1_base_patch.bsdiff4" + with open(base_patch_location, "rb") as base_patch: + rom_data = bsdiff4.patch(rom.read(), base_patch.read()) + rom_data = bytearray(rom_data) + # Set every item to the new nothing value, but keep room flags. Type 2 boss roars should + # become type 1 boss roars, so we at least keep the sound of roaring where it should be. + for i in range(0, 0x7F): + item = rom_data[first_quest_dungeon_items_early + i] + if item & 0b00100000: + rom_data[first_quest_dungeon_items_early + i] = item & 0b11011111 + rom_data[first_quest_dungeon_items_early + i] = item | 0b01000000 + if item & 0b00011111 == 0b00000011: # Change all Item 03s to Item 3F, the proper "nothing" + rom_data[first_quest_dungeon_items_early + i] = item | 0b00111111 + + item = rom_data[first_quest_dungeon_items_late + i] + if item & 0b00100000: + rom_data[first_quest_dungeon_items_late + i] = item & 0b11011111 + rom_data[first_quest_dungeon_items_late + i] = item | 0b01000000 + if item & 0b00011111 == 0b00000011: + rom_data[first_quest_dungeon_items_late + i] = item | 0b00111111 + return rom_data + + def apply_randomizer(self): + with open(get_base_rom_path(), 'rb') as rom: + rom_data = self.apply_base_patch(rom) + # Write each location's new data in + for location in self.multiworld.get_filled_locations(self.player): + # Zelda and Ganon aren't real locations + if location.name == "Ganon" or location.name == "Zelda": + continue + + # Neither are boss defeat events + if "Status" in location.name: + continue + + item = location.item.name + # Remote items are always going to look like Rupees. + if location.item.player != self.player: + item = "Rupee" + + item_id = item_game_ids[item] + location_id = location_ids[location.name] + + # Shop prices need to be set + if location.name in shop_locations: + if location.name[-5:] == "Right": + # Final item in stores has bit 6 and 7 set. It's what marks the cave a shop. + item_id = item_id | 0b11000000 + price_location = shop_price_location_ids[location.name] + item_price = item_prices[item] + if item == "Rupee": + item_class = location.item.classification + if item_class == ItemClassification.progression: + item_price = item_price * 2 + elif item_class == ItemClassification.useful: + item_price = item_price // 2 + elif item_class == ItemClassification.filler: + item_price = item_price // 2 + elif item_class == ItemClassification.trap: + item_price = item_price * 2 + rom_data[price_location] = item_price + if location.name == "Take Any Item Right": + # Same story as above: bit 6 is what makes this a Take Any cave + item_id = item_id | 0b01000000 + rom_data[location_id] = item_id + + # We shuffle the tiers of rupee caves. Caves that shared a value before still will. + secret_caves = self.multiworld.per_slot_randoms[self.player].sample(sorted(secret_money_ids), 3) + secret_cave_money_amounts = [20, 50, 100] + for i, amount in enumerate(secret_cave_money_amounts): + # Giving approximately double the money to keep grinding down + amount = amount * self.multiworld.per_slot_randoms[self.player].triangular(1.5, 2.5) + secret_cave_money_amounts[i] = int(amount) + for i, cave in enumerate(secret_caves): + rom_data[secret_money_ids[cave]] = secret_cave_money_amounts[i] + return rom_data + + def generate_output(self, output_directory: str): + try: + patched_rom = self.apply_randomizer() + outfilebase = 'AP_' + self.multiworld.seed_name + outfilepname = f'_P{self.player}' + outfilepname += f"_{self.multiworld.get_file_safe_player_name(self.player).replace(' ', '_')}" + outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.nes') + self.rom_name_text = f'LOZ{Utils.__version__.replace(".", "")[0:3]}_{self.player}_{self.multiworld.seed:11}\0' + self.romName = bytearray(self.rom_name_text, 'utf8')[:0x20] + self.romName.extend([0] * (0x20 - len(self.romName))) + self.rom_name = self.romName + patched_rom[0x10:0x30] = self.romName + self.playerName = bytearray(self.multiworld.player_name[self.player], 'utf8')[:0x20] + self.playerName.extend([0] * (0x20 - len(self.playerName))) + patched_rom[0x30:0x50] = self.playerName + patched_filename = os.path.join(output_directory, outputFilename) + with open(patched_filename, 'wb') as patched_rom_file: + patched_rom_file.write(patched_rom) + patch = TLoZDeltaPatch(os.path.splitext(outputFilename)[0] + TLoZDeltaPatch.patch_file_ending, + player=self.player, + player_name=self.multiworld.player_name[self.player], + patched_path=outputFilename) + patch.write() + os.unlink(patched_filename) + finally: + self.rom_name_available_event.set() + + def modify_multidata(self, multidata: dict): + import base64 + self.rom_name_available_event.wait() + new_name = base64.b64encode(bytes(self.rom_name)).decode() + multidata["connect_names"][new_name] = multidata["connect_names"][self.multiworld.player_name[self.player]] + + def get_filler_item_name(self) -> str: + if self.filler_items is None: + self.filler_items = [item for item in item_table if item_table[item].classification == ItemClassification.filler] + return self.multiworld.random.choice(self.filler_items) + + def fill_slot_data(self) -> Dict[str, Any]: + if self.multiworld.ExpandedPool[self.player]: + take_any_left = self.multiworld.get_location("Take Any Item Left", self.player).item + take_any_middle = self.multiworld.get_location("Take Any Item Middle", self.player).item + take_any_right = self.multiworld.get_location("Take Any Item Right", self.player).item + if take_any_left.player == self.player: + take_any_left = take_any_left.code + else: + take_any_left = -1 + if take_any_middle.player == self.player: + take_any_middle = take_any_middle.code + else: + take_any_middle = -1 + if take_any_right.player == self.player: + take_any_right = take_any_right.code + else: + take_any_right = -1 + + slot_data = { + "TakeAnyLeft": take_any_left, + "TakeAnyMiddle": take_any_middle, + "TakeAnyRight": take_any_right + } + else: + slot_data = { + "TakeAnyLeft": -1, + "TakeAnyMiddle": -1, + "TakeAnyRight": -1 + } + return slot_data + + +class TLoZItem(Item): + game = 'The Legend of Zelda' + + +class TLoZLocation(Location): + game = 'The Legend of Zelda' \ No newline at end of file diff --git a/worlds/tloz/docs/en_The Legend of Zelda.md b/worlds/tloz/docs/en_The Legend of Zelda.md new file mode 100644 index 00000000..e443c9b9 --- /dev/null +++ b/worlds/tloz/docs/en_The Legend of Zelda.md @@ -0,0 +1,43 @@ +# The Legend of Zelda (NES) + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +All acquirable pickups (except maps and compasses) are shuffled among each other. Logic is in place to ensure both +that the game is still completable, and that players aren't forced to enter dungeons under-geared. + +Shops can contain any item in the game, with prices added for the items unavailable in stores. Rupee caves are worth +more while shops cost less, making shop routing and money management important without requiring mindless grinding. + +## What items and locations get shuffled? + +In general, all item pickups in the game. More formally: + +- Every inventory item. +- Every item found in the five kinds of shops. +- Optionally, Triforce Fragments can be shuffled to be within dungeons, or anywhere. +- Optionally, enemy-held items and dungeon floor items can be included in the shuffle, along with their slots +- Maps and compasses have been replaced with bonus items, including Clocks and Fairies. + +## What items from The Legend of Zelda can appear in other players' worlds? + +All items can appear in other players' worlds. + +## What does another world's item look like in The Legend of Zelda? + +All local items appear as normal. All remote items, no matter the game they originate from, will take on the appearance +of a single Rupee. These single Rupees will have variable prices in shops: progression and trap items will cost more, +filler and useful items will cost less, and uncategorized items will be in the middle. + +## Are there any other changes made? + +- The map and compass for each dungeon start already acquired, and other items can be found in their place. +- The Recorder will warp you between all eight levels regardless of Triforce count + - It's possible for this to be your route to level 4! +- Pressing Select will cycle through your inventory. +- Shop purchases are tracked within sessions, indicated by the item being elevated from its normal position. +- What slots from a Take Any Cave have been chosen are similarly tracked. \ No newline at end of file diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md new file mode 100644 index 00000000..d3aa0afb --- /dev/null +++ b/worlds/tloz/docs/multiworld_en.md @@ -0,0 +1,104 @@ +# The Legend of Zelda (NES) Multiworld Setup Guide + +## Required Software + +- The Zelda1Client + - Bundled with Archipelago: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) +- The BizHawk emulator. Versions 2.3.1 and higher are supported. Version 2.7 is recommended + - [BizHawk Official Website](http://tasvideos.org/BizHawk.html) + +## Installation Procedures + +1. Download and install the latest version of Archipelago. + - On Windows, download Setup.Archipelago..exe and run it. +2. Assign Bizhawk version 2.3.1 or higher as your default program for launching `.nes` files. + - Extract your Bizhawk folder to your Desktop, or somewhere you will remember. Below are optional additional steps + for loading ROMs more conveniently. + 1. Right-click on a ROM file and select **Open with...** + 2. Check the box next to **Always use this app to open .nes files**. + 3. Scroll to the bottom of the list and click the grey text **Look for another App on this PC**. + 4. Browse for `EmuHawk.exe` located inside your Bizhawk folder (from step 1) and click **Open**. + +## Create a Config (.yaml) File + +### What is a config file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a config file? + +The Player Settings page on the website allows you to configure your personal settings and export a config file from +them. Player settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legen%20of%20Zelda/player-settings) + +### Verifying your config file + +If you would like to validate your config file to make sure it works, you may do so on the YAML Validator page. YAML +validator page: [YAML Validation page](/mysterycheck) + +## Generating a Single-Player Game + +1. Navigate to the Player Settings page, configure your options, and click the "Generate Game" button. + - Player Settings page: [The Legend of Zelda Player Settings Page](/games/The%20Legen%20of%20Zelda/player-settings) +2. You will be presented with a "Seed Info" page. +3. Click the "Create New Room" link. +4. You will be presented with a server page, from which you can download your patch file. +5. Double-click on your patch file, and the Zelda 1 Client will launch automatically, create your ROM from the + patch file, and open your emulator for you. +6. Since this is a single-player game, you will no longer need the client, so feel free to close it. + +## Joining a MultiWorld Game + +### Obtain your patch file and create your ROM + +When you join a multiworld game, you will be asked to provide your config file to whoever is hosting. Once that is done, +the host will provide you with either a link to download your patch file, or with a zip file containing everyone's patch +files. Your patch file should have a `.aptloz` extension. + +Put your patch file on your desktop or somewhere convenient, and double click it. This should automatically launch the +client, and will also create your ROM in the same place as your patch file. + + +## Running the Client Program and Connecting to the Server + +Once the Archipelago server has been hosted: + +1. Navigate to your Archipelago install folder and run `ArchipelagoZelda1Client.exe`. +2. Notice the `/connect command` on the server hosting page. (It should look like `/connect archipelago.gg:*****` + where ***** are numbers) +3. Type the connect command into the client OR add the port to the pre-populated address on the top bar (it should + already say `archipelago.gg`) and click `connect`. + +### Running Your Game and Connecting to the Client Program + +1. Open Bizhawk 2.3.1 or higher and load your ROM OR click your ROM file if it is already associated with the + extension `*.nes`. +2. Click on the Tools menu and click on **Lua Console**. +3. Click the folder button to open a new Lua script. (CTL-O or **Script** -> **Open Script**) +4. Navigate to the location you installed Archipelago to. Open `data/lua/TLOZ/tloz_connector.lua`. + 1. If it gives a `NLua.Exceptions.LuaScriptException: .\socket.lua:13: module 'socket.core' not found:` exception + close your emulator entirely, restart it and re-run these steps. + 2. If it says `Must use a version of bizhawk 2.3.1 or higher`, double-check your Bizhawk version by clicking ** + Help** -> **About**. + +## Play the game + +When the client shows both NES and server are connected, you are good to go. You can check the connection status of the +NES at any time by running `/nes`. + +### Other Client Commands + +All other commands may be found on the [Archipelago Server and Client Commands Guide.](/tutorial/Archipelago/commands/en) +. + +## Known Issues + +- Triforce Fragments and Heart Containers may be purchased multiple times. It is up to you if you wish to take advantage +of this; logic will not account for or require purchasing any slot more than once. Remote items, no matter what they +are, will always only be sent once. +- Obtaining a remote item will move the location of any existing item in that room. Should this make an item +inaccessible, simply exit and re-enter the room. This can be used to obtain the Ocean Heart Container item without the +stepladder; logic does not account for this. +- Whether you've purchased from a shop is tracked via Archipelago between sessions: if you revisit a single player game, +none of your shop pruchase statuses will be remembered. If you want them to be, connect to the client and server like +you would in a multiplayer game. \ No newline at end of file diff --git a/worlds/tloz/requirements.txt b/worlds/tloz/requirements.txt new file mode 100644 index 00000000..d1f50ea5 --- /dev/null +++ b/worlds/tloz/requirements.txt @@ -0,0 +1 @@ +bsdiff4>=1.2.2 \ No newline at end of file diff --git a/worlds/tloz/z1_base_patch.bsdiff4 b/worlds/tloz/z1_base_patch.bsdiff4 new file mode 100644 index 0000000000000000000000000000000000000000..1231c8699481ef3d89202bcb5ac7c8dc4ed83eca GIT binary patch literal 1648 zcmYL}e^k=<8pl6hKth8=@kgR|fgxeDLj09p?R-&0aFQZeLAs1k5s$N@X3IJg{6Xr~ zgw(F!Hp$Fil$vR#sjF!m{IRB~x0PC+mMgWpTGP5Kr*l8<+3TO@eV+52=RD6puV5jY z$KiN0^1+|%vG}v%0pgDkERp+rvcxo@xHR^ML;wWqGw=EelxDyNmKzhf5CmWVEHMyF z0uTvsONHATw`oq&lz`Zv4T_8cz?m)IW)j}N*UcC1Pa5H_Y6H{m!U&76+OlPHp)%AZgCZk@vdQH;K@k(Y|FW0Mgm-+k zi@x*~YvDr;Rz0}_)hj26r9p7EJ{u0asIY2SOuiI28P>GWq}D|W43Gh!l{kRh_;e!z zZvfB@8iBh(4*ufz?%*F8MU;AfjjbYZEQ{)C5JS(v+@jG493^fmOG(yvPms0srC3`9 zwZ2}VkW1x3BmlVZ8dy&6JdY3c=?vAo zNiI97^w7kU?}|!9VDPW2q(o@aS3sJSfSFtGWi5QKXfqMrCB5jv3F-SyIR%h zxVrq5NDK+NgYwy+m(_zpx55IqqB?uX!bVucNMRBA)%!V>B+6D%+`vEkaB6FAq&Rt( zy*oqqBENSEDs)5&(w}fKtgJ21_b_GWoGNa%H7oHdr2=nT#o62r=N`%RRfN!$x!$&U zYIlG53~PWsms*ri+A;k}Oo{Yi(}~~_(a!504icbfKg3X3@o0IL^+V)j>h>4;6&n4E zS1Rvvp3XDD4Xbh^*;=&YNzq+a#ZgAd@Xnou#U9amMWaUE(T%W74ZR>1*tm-YJ)Lri z@hBik{?(AhqiV$EZZ;6NT^E5y@etveuN4F)Z!-%q94&2e8$xjZ6T^0y!ak8U_B0mK z<3GCI*zA!GU{N&GFFON)C$pQ*@mse`W|AgvpI>jEoN+*TL{kzxDWuvZFPwdoz=Kzc zADm@8F1hjAyh4BaB`F2-4^hUpBwhdTT+;#s!OtajK+ziK>Z z*~9A_3#xg&bdH`2R9Qaz zuUK1h9KMJwYI{MDzA|1GaLi$^A!oO%{`bzeRzaBE7zP`U zA{V@I`Mi^Bf4@p~ur*fOg~s&t9LN#j+$&SJ|I*iZl(`Dv1)EGqJiVgew-Ek zQL*n~-@(oSV(rg)oP|w&*D9-S=Vrg*ejlIqNy@?V{~O!cK^wdF&S9RC-a*ucw5LV} z>}&qG&XD?iOMu_ZbXtQ`3)VL0tZS-Ky0ZHMD{C#IRvmNuea^|&57Wud9<#mPi_(&1 zaijt_H zb40oCi<$I*rMIV39wwh!%}N+|Jlfxz;q>-Kezhrb9@)b^_Un&lmk1?KVIfQ^}siYN70x<6rFGjxNe=RBb#>AwGDM2 z-|Z`29DRvYg;f@t`PI{gr1*VS#Kw~~rh@Xd#5`JN;4T~?W-8>;y(|3_M~r91R1Iz9 zz`A34=$sLye*G&=aX1_S5D)+)5RZT`xv(El!pB$uwt-OwkV(-a zHO57 nBP?pjsd<+KmYco}h+noV_*IK2yIqJ2aM}Eq%dO|_<&56|)Muy1 literal 0 HcmV?d00001