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>
This commit is contained in:
parent
3a68ce3faa
commit
efb2ab4505
|
@ -8,6 +8,7 @@
|
||||||
*.apm3
|
*.apm3
|
||||||
*.apmc
|
*.apmc
|
||||||
*.apz5
|
*.apz5
|
||||||
|
*.aptloz
|
||||||
*.pyc
|
*.pyc
|
||||||
*.pyd
|
*.pyd
|
||||||
*.sfc
|
*.sfc
|
||||||
|
|
|
@ -148,6 +148,8 @@ components: Iterable[Component] = (
|
||||||
Component('FF1 Client', 'FF1Client'),
|
Component('FF1 Client', 'FF1Client'),
|
||||||
# Pokémon
|
# Pokémon
|
||||||
Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
|
Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
|
||||||
|
# TLoZ
|
||||||
|
Component('Zelda 1 Client', 'Zelda1Client'),
|
||||||
# ChecksFinder
|
# ChecksFinder
|
||||||
Component('ChecksFinder Client', 'ChecksFinderClient'),
|
Component('ChecksFinder Client', 'ChecksFinderClient'),
|
||||||
# Starcraft 2
|
# Starcraft 2
|
||||||
|
|
|
@ -37,6 +37,7 @@ Currently, the following games are supported:
|
||||||
* Blasphemous
|
* Blasphemous
|
||||||
* Wargroove
|
* Wargroove
|
||||||
* Stardew Valley
|
* Stardew Valley
|
||||||
|
* The Legend of Zelda
|
||||||
|
|
||||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
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
|
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||||
|
|
5
Utils.py
5
Utils.py
|
@ -310,6 +310,11 @@ def get_default_options() -> OptionsType:
|
||||||
"lufia2ac_options": {
|
"lufia2ac_options": {
|
||||||
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
|
"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": {
|
"wargroove_options": {
|
||||||
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
|
@ -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()
|
Binary file not shown.
|
@ -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
|
|
@ -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)
|
11
host.yaml
11
host.yaml
|
@ -107,7 +107,7 @@ factorio_options:
|
||||||
filter_item_sends: false
|
filter_item_sends: false
|
||||||
# Whether to send chat messages from players on the Factorio server to Archipelago.
|
# Whether to send chat messages from players on the Factorio server to Archipelago.
|
||||||
bridge_chat_out: true
|
bridge_chat_out: true
|
||||||
minecraft_options:
|
minecraft_options:
|
||||||
forge_directory: "Minecraft Forge server"
|
forge_directory: "Minecraft Forge server"
|
||||||
max_heap_size: "2G"
|
max_heap_size: "2G"
|
||||||
# release channel, currently "release", or "beta"
|
# release channel, currently "release", or "beta"
|
||||||
|
@ -125,6 +125,15 @@ soe_options:
|
||||||
rom_file: "Secret of Evermore (USA).sfc"
|
rom_file: "Secret of Evermore (USA).sfc"
|
||||||
ffr_options:
|
ffr_options:
|
||||||
display_msgs: true
|
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:
|
dkc3_options:
|
||||||
# File name of the DKC3 US rom
|
# File name of the DKC3 US rom
|
||||||
rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
|
rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
}
|
|
@ -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}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
@ -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))
|
|
@ -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'
|
|
@ -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.
|
|
@ -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.<HighestVersion\>.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.
|
|
@ -0,0 +1 @@
|
||||||
|
bsdiff4>=1.2.2
|
Binary file not shown.
Loading…
Reference in New Issue