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
|
||||
*.apmc
|
||||
*.apz5
|
||||
*.aptloz
|
||||
*.pyc
|
||||
*.pyd
|
||||
*.sfc
|
||||
|
|
|
@ -148,6 +148,8 @@ components: Iterable[Component] = (
|
|||
Component('FF1 Client', 'FF1Client'),
|
||||
# Pokémon
|
||||
Component('Pokemon Client', 'PokemonClient', file_identifier=SuffixIdentifier('.apred', '.apblue')),
|
||||
# TLoZ
|
||||
Component('Zelda 1 Client', 'Zelda1Client'),
|
||||
# ChecksFinder
|
||||
Component('ChecksFinder Client', 'ChecksFinderClient'),
|
||||
# Starcraft 2
|
||||
|
|
|
@ -37,6 +37,7 @@ Currently, the following games are supported:
|
|||
* Blasphemous
|
||||
* Wargroove
|
||||
* Stardew Valley
|
||||
* The Legend of Zelda
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
|
5
Utils.py
5
Utils.py
|
@ -310,6 +310,11 @@ def get_default_options() -> OptionsType:
|
|||
"lufia2ac_options": {
|
||||
"rom_file": "Lufia II - Rise of the Sinistrals (USA).sfc",
|
||||
},
|
||||
"tloz_options": {
|
||||
"rom_file": "Legend of Zelda, The (U) (PRG0) [!].nes",
|
||||
"rom_start": True,
|
||||
"display_msgs": True,
|
||||
},
|
||||
"wargroove_options": {
|
||||
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
||||
}
|
||||
|
|
|
@ -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
|
||||
# Whether to send chat messages from players on the Factorio server to Archipelago.
|
||||
bridge_chat_out: true
|
||||
minecraft_options:
|
||||
minecraft_options:
|
||||
forge_directory: "Minecraft Forge server"
|
||||
max_heap_size: "2G"
|
||||
# release channel, currently "release", or "beta"
|
||||
|
@ -125,6 +125,15 @@ soe_options:
|
|||
rom_file: "Secret of Evermore (USA).sfc"
|
||||
ffr_options:
|
||||
display_msgs: true
|
||||
tloz_options:
|
||||
# File name of the Zelda 1
|
||||
rom_file: "Legend of Zelda, The (U) (PRG0) [!].nes"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# true for operating system default program
|
||||
# Alternatively, a path to a program to open the .nes file with
|
||||
rom_start: true
|
||||
# Display message inside of Bizhawk
|
||||
display_msgs: true
|
||||
dkc3_options:
|
||||
# File name of the DKC3 US rom
|
||||
rom_file: "Donkey Kong Country 3 - Dixie Kong's Double Trouble! (USA) (En,Fr).sfc"
|
||||
|
|
|
@ -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