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:
Rosalie-A 2023-03-05 07:31:31 -05:00 committed by GitHub
parent 3a68ce3faa
commit efb2ab4505
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 2994 additions and 1 deletions

1
.gitignore vendored
View File

@ -8,6 +8,7 @@
*.apm3
*.apmc
*.apz5
*.aptloz
*.pyc
*.pyd
*.sfc

View File

@ -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

View File

@ -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

View File

@ -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"
}

393
Zelda1Client.py Normal file
View File

@ -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()

View File

@ -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()

BIN
data/lua/TLoZ/core.dll Normal file

Binary file not shown.

380
data/lua/TLoZ/json.lua Normal file
View File

@ -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

132
data/lua/TLoZ/socket.lua Normal file
View File

@ -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)

View File

@ -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"

145
worlds/tloz/ItemPool.py Normal file
View File

@ -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

147
worlds/tloz/Items.py Normal file
View File

@ -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
}

350
worlds/tloz/Locations.py Normal file
View File

@ -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}

40
worlds/tloz/Options.py Normal file
View File

@ -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
}

78
worlds/tloz/Rom.py Normal file
View File

@ -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

147
worlds/tloz/Rules.py Normal file
View File

@ -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))

313
worlds/tloz/__init__.py Normal file
View File

@ -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'

View File

@ -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.

View File

@ -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.

View File

@ -0,0 +1 @@
bsdiff4>=1.2.2

Binary file not shown.