Adventure: implement new game (#1531)
Adds Adventure for the Atari 2600, NTSC version. New randomizer, not based on prior works. Somewhat atypical of current AP rom patch games; The generator does not require the adventure rom, but writes some data to an .apadvn APContainer file that the client uses along with a base bsdiff patch to generate a final rom file.
This commit is contained in:
parent
206f8cf5ed
commit
d48e1e447f
|
@ -26,6 +26,7 @@
|
|||
*multisave
|
||||
*.archipelago
|
||||
*.apsave
|
||||
*.BIN
|
||||
|
||||
build
|
||||
bundle/components.wxs
|
||||
|
|
|
@ -0,0 +1,516 @@
|
|||
import asyncio
|
||||
import hashlib
|
||||
import json
|
||||
import time
|
||||
import os
|
||||
import bsdiff4
|
||||
import subprocess
|
||||
import zipfile
|
||||
from asyncio import StreamReader, StreamWriter, CancelledError
|
||||
from typing import List
|
||||
|
||||
|
||||
import Utils
|
||||
from NetUtils import ClientStatus
|
||||
from Utils import async_start
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
|
||||
get_base_parser
|
||||
from worlds.adventure import AdventureDeltaPatch
|
||||
|
||||
from worlds.adventure.Locations import base_location_id
|
||||
from worlds.adventure.Rom import AdventureForeignItemInfo, AdventureAutoCollectLocation, BatNoTouchLocation
|
||||
from worlds.adventure.Items import base_adventure_item_id, standard_item_max, item_table
|
||||
from worlds.adventure.Offsets import static_item_element_size, connector_port_offset
|
||||
|
||||
SYSTEM_MESSAGE_ID = 0
|
||||
|
||||
CONNECTION_TIMING_OUT_STATUS = \
|
||||
"Connection timing out. Please restart your emulator, then restart adventure_connector.lua"
|
||||
CONNECTION_REFUSED_STATUS = \
|
||||
"Connection Refused. Please start your emulator and make sure adventure_connector.lua is running"
|
||||
CONNECTION_RESET_STATUS = \
|
||||
"Connection was reset. Please restart your emulator, then restart adventure_connector.lua"
|
||||
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
||||
CONNECTION_CONNECTED_STATUS = "Connected"
|
||||
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
||||
|
||||
SCRIPT_VERSION = 1
|
||||
|
||||
|
||||
class AdventureCommandProcessor(ClientCommandProcessor):
|
||||
def __init__(self, ctx: CommonContext):
|
||||
super().__init__(ctx)
|
||||
|
||||
def _cmd_2600(self):
|
||||
"""Check 2600 Connection State"""
|
||||
if isinstance(self.ctx, AdventureContext):
|
||||
logger.info(f"2600 Status: {self.ctx.atari_status}")
|
||||
|
||||
def _cmd_aconnect(self):
|
||||
"""Discard current atari 2600 connection state"""
|
||||
if isinstance(self.ctx, AdventureContext):
|
||||
self.ctx.atari_sync_task.cancel()
|
||||
|
||||
|
||||
class AdventureContext(CommonContext):
|
||||
command_processor = AdventureCommandProcessor
|
||||
game = 'Adventure'
|
||||
lua_connector_port: int = 17242
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super().__init__(server_address, password)
|
||||
self.freeincarnates_used: int = -1
|
||||
self.freeincarnate_pending: int = 0
|
||||
self.foreign_items: [AdventureForeignItemInfo] = []
|
||||
self.autocollect_items: [AdventureAutoCollectLocation] = []
|
||||
self.atari_streams: (StreamReader, StreamWriter) = None
|
||||
self.atari_sync_task = None
|
||||
self.messages = {}
|
||||
self.locations_array = None
|
||||
self.atari_status = CONNECTION_INITIAL_STATUS
|
||||
self.awaiting_rom = False
|
||||
self.display_msgs = True
|
||||
self.deathlink_pending = False
|
||||
self.set_deathlink = False
|
||||
self.client_compatibility_mode = 0
|
||||
self.items_handling = 0b111
|
||||
self.checked_locations_sent: bool = False
|
||||
self.port_offset = 0
|
||||
self.bat_no_touch_locations: [BatNoTouchLocation] = []
|
||||
self.local_item_locations = {}
|
||||
self.dragon_speed_info = {}
|
||||
|
||||
options = Utils.get_options()
|
||||
self.display_msgs = options["adventure_options"]["display_msgs"]
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(AdventureContext, self).server_auth(password_requested)
|
||||
if not self.auth:
|
||||
self.auth = self.player_name
|
||||
if not self.auth:
|
||||
self.awaiting_rom = True
|
||||
logger.info('Awaiting connection to adventure_connector to get Player information')
|
||||
return
|
||||
|
||||
await self.send_connect()
|
||||
|
||||
def _set_message(self, msg: str, msg_id: int):
|
||||
if self.display_msgs:
|
||||
self.messages[(time.time(), msg_id)] = msg
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == 'Connected':
|
||||
self.locations_array = None
|
||||
if Utils.get_options()["adventure_options"].get("death_link", False):
|
||||
self.set_deathlink = True
|
||||
async_start(self.get_freeincarnates_used())
|
||||
elif cmd == "RoomInfo":
|
||||
self.seed_name = args['seed_name']
|
||||
elif cmd == 'Print':
|
||||
msg = args['text']
|
||||
if ': !' not in msg:
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == "ReceivedItems":
|
||||
msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}"
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
elif cmd == "Retrieved":
|
||||
self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"]
|
||||
if self.freeincarnates_used is None:
|
||||
self.freeincarnates_used = 0
|
||||
self.freeincarnates_used += self.freeincarnate_pending
|
||||
self.send_pending_freeincarnates()
|
||||
elif cmd == "SetReply":
|
||||
if args["key"] == f"adventure_{self.auth}_freeincarnates_used":
|
||||
self.freeincarnates_used = args["value"]
|
||||
if self.freeincarnates_used is None:
|
||||
self.freeincarnates_used = 0
|
||||
self.freeincarnates_used += self.freeincarnate_pending
|
||||
self.send_pending_freeincarnates()
|
||||
|
||||
def on_deathlink(self, data: dict):
|
||||
self.deathlink_pending = True
|
||||
super().on_deathlink(data)
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager
|
||||
|
||||
class AdventureManager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago Adventure Client"
|
||||
|
||||
self.ui = AdventureManager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
async def get_freeincarnates_used(self):
|
||||
if self.server and not self.server.socket.closed:
|
||||
await self.send_msgs([{"cmd": "SetNotify", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}])
|
||||
await self.send_msgs([{"cmd": "Get", "keys": [f"adventure_{self.auth}_freeincarnates_used"]}])
|
||||
|
||||
def send_pending_freeincarnates(self):
|
||||
if self.freeincarnate_pending > 0:
|
||||
async_start(self.send_pending_freeincarnates_impl(self.freeincarnate_pending))
|
||||
self.freeincarnate_pending = 0
|
||||
|
||||
async def send_pending_freeincarnates_impl(self, send_val: int) -> None:
|
||||
await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used",
|
||||
"default": 0, "want_reply": False,
|
||||
"operations": [{"operation": "add", "value": send_val}]}])
|
||||
|
||||
async def used_freeincarnate(self) -> None:
|
||||
if self.server and not self.server.socket.closed:
|
||||
await self.send_msgs([{"cmd": "Set", "key": f"adventure_{self.auth}_freeincarnates_used",
|
||||
"default": 0, "want_reply": True,
|
||||
"operations": [{"operation": "add", "value": 1}]}])
|
||||
else:
|
||||
self.freeincarnate_pending = self.freeincarnate_pending + 1
|
||||
|
||||
|
||||
def convert_item_id(ap_item_id: int):
|
||||
static_item_index = ap_item_id - base_adventure_item_id
|
||||
return static_item_index * static_item_element_size
|
||||
|
||||
|
||||
def get_payload(ctx: AdventureContext):
|
||||
current_time = time.time()
|
||||
items = []
|
||||
dragon_speed_update = {}
|
||||
diff_a_locked = ctx.diff_a_mode > 0
|
||||
diff_b_locked = ctx.diff_b_mode > 0
|
||||
freeincarnate_count = 0
|
||||
for item in ctx.items_received:
|
||||
item_id_str = str(item.item)
|
||||
if base_adventure_item_id < item.item <= standard_item_max:
|
||||
items.append(convert_item_id(item.item))
|
||||
elif item_id_str in ctx.dragon_speed_info:
|
||||
if item.item in dragon_speed_update:
|
||||
last_index = len(ctx.dragon_speed_info[item_id_str]) - 1
|
||||
dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][last_index]
|
||||
else:
|
||||
dragon_speed_update[item.item] = ctx.dragon_speed_info[item_id_str][0]
|
||||
elif item.item == item_table["Left Difficulty Switch"].id:
|
||||
diff_a_locked = False
|
||||
elif item.item == item_table["Right Difficulty Switch"].id:
|
||||
diff_b_locked = False
|
||||
elif item.item == item_table["Freeincarnate"].id:
|
||||
freeincarnate_count = freeincarnate_count + 1
|
||||
freeincarnates_available = 0
|
||||
|
||||
if ctx.freeincarnates_used >= 0:
|
||||
freeincarnates_available = freeincarnate_count - (ctx.freeincarnates_used + ctx.freeincarnate_pending)
|
||||
ret = json.dumps(
|
||||
{
|
||||
"items": items,
|
||||
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
|
||||
if key[0] > current_time - 10},
|
||||
"deathlink": ctx.deathlink_pending,
|
||||
"dragon_speeds": dragon_speed_update,
|
||||
"difficulty_a_locked": diff_a_locked,
|
||||
"difficulty_b_locked": diff_b_locked,
|
||||
"freeincarnates_available": freeincarnates_available,
|
||||
"bat_logic": ctx.bat_logic
|
||||
}
|
||||
)
|
||||
ctx.deathlink_pending = False
|
||||
return ret
|
||||
|
||||
|
||||
async def parse_locations(data: List, ctx: AdventureContext):
|
||||
locations = data
|
||||
|
||||
# for loc_name, loc_data in location_table.items():
|
||||
|
||||
# if flags["EventFlag"][280] & 1 and not ctx.finished_game:
|
||||
# await ctx.send_msgs([
|
||||
# {"cmd": "StatusUpdate",
|
||||
# "status": 30}
|
||||
# ])
|
||||
# ctx.finished_game = True
|
||||
if locations == ctx.locations_array:
|
||||
return
|
||||
ctx.locations_array = locations
|
||||
if locations is not None:
|
||||
await ctx.send_msgs([{"cmd": "LocationChecks", "locations": locations}])
|
||||
|
||||
|
||||
def send_ap_foreign_items(adventure_context):
|
||||
foreign_item_json_list = []
|
||||
autocollect_item_json_list = []
|
||||
bat_no_touch_locations_json_list = []
|
||||
for fi in adventure_context.foreign_items:
|
||||
foreign_item_json_list.append(fi.get_dict())
|
||||
for fi in adventure_context.autocollect_items:
|
||||
autocollect_item_json_list.append(fi.get_dict())
|
||||
for ntl in adventure_context.bat_no_touch_locations:
|
||||
bat_no_touch_locations_json_list.append(ntl.get_dict())
|
||||
payload = json.dumps(
|
||||
{
|
||||
"foreign_items": foreign_item_json_list,
|
||||
"autocollect_items": autocollect_item_json_list,
|
||||
"local_item_locations": adventure_context.local_item_locations,
|
||||
"bat_no_touch_locations": bat_no_touch_locations_json_list
|
||||
}
|
||||
)
|
||||
print("sending foreign items")
|
||||
msg = payload.encode()
|
||||
(reader, writer) = adventure_context.atari_streams
|
||||
writer.write(msg)
|
||||
writer.write(b'\n')
|
||||
|
||||
|
||||
def send_checked_locations_if_needed(adventure_context):
|
||||
if not adventure_context.checked_locations_sent and adventure_context.checked_locations is not None:
|
||||
if len(adventure_context.checked_locations) == 0:
|
||||
return
|
||||
checked_short_ids = []
|
||||
for location in adventure_context.checked_locations:
|
||||
checked_short_ids.append(location - base_location_id)
|
||||
print("Sending checked locations")
|
||||
payload = json.dumps(
|
||||
{
|
||||
"checked_locations": checked_short_ids,
|
||||
}
|
||||
)
|
||||
msg = payload.encode()
|
||||
(reader, writer) = adventure_context.atari_streams
|
||||
writer.write(msg)
|
||||
writer.write(b'\n')
|
||||
adventure_context.checked_locations_sent = True
|
||||
|
||||
|
||||
async def atari_sync_task(ctx: AdventureContext):
|
||||
logger.info("Starting Atari 2600 connector. Use /2600 for status information")
|
||||
while not ctx.exit_event.is_set():
|
||||
try:
|
||||
error_status = None
|
||||
if ctx.atari_streams:
|
||||
(reader, writer) = ctx.atari_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 1+ fields
|
||||
# 1. A keepalive response of the Players Name (always)
|
||||
# 2. romhash field with sha256 hash of the ROM memory region
|
||||
# 3. locations, messages, and deathLink
|
||||
# 4. freeincarnate, to indicate a freeincarnate was used
|
||||
data = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
data_decoded = json.loads(data.decode())
|
||||
if 'scriptVersion' not in data_decoded or data_decoded['scriptVersion'] != SCRIPT_VERSION:
|
||||
msg = "You are connecting with an incompatible Lua script version. Ensure your connector " \
|
||||
"Lua and AdventureClient are from the same Archipelago installation."
|
||||
logger.info(msg, extra={'compact_gui': True})
|
||||
ctx.gui_error('Error', msg)
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
if ctx.seed_name and bytes(ctx.seed_name, encoding='ASCII') != ctx.seed_name_from_data:
|
||||
msg = "The server is running a different multiworld than your client is. " \
|
||||
"(invalid seed_name)"
|
||||
logger.info(msg, extra={'compact_gui': True})
|
||||
ctx.gui_error('Error', msg)
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
if 'romhash' in data_decoded:
|
||||
if ctx.rom_hash.upper() != data_decoded['romhash'].upper():
|
||||
msg = "The rom hash does not match the client rom hash data"
|
||||
print("got " + data_decoded['romhash'])
|
||||
print("expected " + str(ctx.rom_hash))
|
||||
logger.info(msg, extra={'compact_gui': True})
|
||||
ctx.gui_error('Error', msg)
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
if ctx.auth is None:
|
||||
ctx.auth = ctx.player_name
|
||||
if ctx.awaiting_rom:
|
||||
await ctx.server_auth(False)
|
||||
if 'locations' in data_decoded and ctx.game and ctx.atari_status == CONNECTION_CONNECTED_STATUS \
|
||||
and not error_status and ctx.auth:
|
||||
# Not just a keep alive ping, parse
|
||||
async_start(parse_locations(data_decoded['locations'], ctx))
|
||||
if 'deathLink' in data_decoded and data_decoded['deathLink'] > 0 and 'DeathLink' in ctx.tags:
|
||||
dragon_name = "a dragon"
|
||||
if data_decoded['deathLink'] == 1:
|
||||
dragon_name = "Rhindle"
|
||||
elif data_decoded['deathLink'] == 2:
|
||||
dragon_name = "Yorgle"
|
||||
elif data_decoded['deathLink'] == 3:
|
||||
dragon_name = "Grundle"
|
||||
print (ctx.auth + " has been eaten by " + dragon_name )
|
||||
await ctx.send_death(ctx.auth + " has been eaten by " + dragon_name)
|
||||
# TODO - also if player reincarnates with a dragon onscreen ' dies to avoid being eaten by '
|
||||
if 'victory' in data_decoded and not ctx.finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
if 'freeincarnate' in data_decoded:
|
||||
await ctx.used_freeincarnate()
|
||||
if ctx.set_deathlink:
|
||||
await ctx.update_death_link(True)
|
||||
send_checked_locations_if_needed(ctx)
|
||||
except asyncio.TimeoutError:
|
||||
logger.debug("Read Timed Out, Reconnecting")
|
||||
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||
writer.close()
|
||||
ctx.atari_streams = None
|
||||
except ConnectionResetError as e:
|
||||
logger.debug("Read failed due to Connection Lost, Reconnecting")
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
writer.close()
|
||||
ctx.atari_streams = None
|
||||
except TimeoutError:
|
||||
logger.debug("Connection Timed Out, Reconnecting")
|
||||
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||
writer.close()
|
||||
ctx.atari_streams = None
|
||||
except ConnectionResetError:
|
||||
logger.debug("Connection Lost, Reconnecting")
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
writer.close()
|
||||
ctx.atari_streams = None
|
||||
except CancelledError:
|
||||
logger.debug("Connection Cancelled, Reconnecting")
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
writer.close()
|
||||
ctx.atari_streams = None
|
||||
pass
|
||||
except Exception as e:
|
||||
print("unknown exception " + e)
|
||||
raise
|
||||
if ctx.atari_status == CONNECTION_TENTATIVE_STATUS:
|
||||
if not error_status:
|
||||
logger.info("Successfully Connected to 2600")
|
||||
ctx.atari_status = CONNECTION_CONNECTED_STATUS
|
||||
ctx.checked_locations_sent = False
|
||||
send_ap_foreign_items(ctx)
|
||||
send_checked_locations_if_needed(ctx)
|
||||
else:
|
||||
ctx.atari_status = f"Was tentatively connected but error occurred: {error_status}"
|
||||
elif error_status:
|
||||
ctx.atari_status = error_status
|
||||
logger.info("Lost connection to 2600 and attempting to reconnect. Use /2600 for status updates")
|
||||
else:
|
||||
try:
|
||||
port = ctx.lua_connector_port + ctx.port_offset
|
||||
logger.debug(f"Attempting to connect to 2600 on port {port}")
|
||||
print(f"Attempting to connect to 2600 on port {port}")
|
||||
ctx.atari_streams = await asyncio.wait_for(
|
||||
asyncio.open_connection("localhost",
|
||||
port),
|
||||
timeout=10)
|
||||
ctx.atari_status = CONNECTION_TENTATIVE_STATUS
|
||||
except TimeoutError:
|
||||
logger.debug("Connection Timed Out, Trying Again")
|
||||
ctx.atari_status = CONNECTION_TIMING_OUT_STATUS
|
||||
continue
|
||||
except ConnectionRefusedError:
|
||||
logger.debug("Connection Refused, Trying Again")
|
||||
ctx.atari_status = CONNECTION_REFUSED_STATUS
|
||||
continue
|
||||
except CancelledError:
|
||||
pass
|
||||
except CancelledError:
|
||||
pass
|
||||
print("exiting atari sync task")
|
||||
|
||||
|
||||
async def run_game(romfile):
|
||||
auto_start = Utils.get_options()["adventure_options"].get("rom_start", True)
|
||||
rom_args = Utils.get_options()["adventure_options"].get("rom_args")
|
||||
if auto_start is True:
|
||||
import webbrowser
|
||||
webbrowser.open(romfile)
|
||||
elif os.path.isfile(auto_start):
|
||||
open_args = [auto_start, romfile]
|
||||
if rom_args is not None:
|
||||
open_args.insert(1, rom_args)
|
||||
subprocess.Popen(open_args,
|
||||
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
|
||||
|
||||
async def patch_and_run_game(patch_file, ctx):
|
||||
base_name = os.path.splitext(patch_file)[0]
|
||||
comp_path = base_name + '.a26'
|
||||
try:
|
||||
base_rom = AdventureDeltaPatch.get_source_data()
|
||||
except Exception as msg:
|
||||
logger.info(msg, extra={'compact_gui': True})
|
||||
ctx.gui_error('Error', msg)
|
||||
|
||||
with open("data/adventure_basepatch.bsdiff4", "rb") as file:
|
||||
basepatch = bytes(file.read())
|
||||
|
||||
base_patched_rom_data = bsdiff4.patch(base_rom, basepatch)
|
||||
|
||||
with zipfile.ZipFile(patch_file, 'r') as patch_archive:
|
||||
if not AdventureDeltaPatch.check_version(patch_archive):
|
||||
logger.error("apadvn version doesn't match this client. Make sure your generator and client are the same")
|
||||
raise Exception("apadvn version doesn't match this client.")
|
||||
|
||||
ctx.foreign_items = AdventureDeltaPatch.read_foreign_items(patch_archive)
|
||||
ctx.autocollect_items = AdventureDeltaPatch.read_autocollect_items(patch_archive)
|
||||
ctx.local_item_locations = AdventureDeltaPatch.read_local_item_locations(patch_archive)
|
||||
ctx.dragon_speed_info = AdventureDeltaPatch.read_dragon_speed_info(patch_archive)
|
||||
ctx.seed_name_from_data, ctx.player_name = AdventureDeltaPatch.read_rom_info(patch_archive)
|
||||
ctx.diff_a_mode, ctx.diff_b_mode = AdventureDeltaPatch.read_difficulty_switch_info(patch_archive)
|
||||
ctx.bat_logic = AdventureDeltaPatch.read_bat_logic(patch_archive)
|
||||
ctx.bat_no_touch_locations = AdventureDeltaPatch.read_bat_no_touch(patch_archive)
|
||||
ctx.rom_deltas = AdventureDeltaPatch.read_rom_deltas(patch_archive)
|
||||
ctx.auth = ctx.player_name
|
||||
|
||||
patched_rom_data = AdventureDeltaPatch.apply_rom_deltas(base_patched_rom_data, ctx.rom_deltas)
|
||||
rom_hash = hashlib.sha256()
|
||||
rom_hash.update(patched_rom_data)
|
||||
ctx.rom_hash = rom_hash.hexdigest()
|
||||
ctx.port_offset = patched_rom_data[connector_port_offset]
|
||||
|
||||
with open(comp_path, "wb") as patched_rom_file:
|
||||
patched_rom_file.write(patched_rom_data)
|
||||
|
||||
async_start(run_game(comp_path))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Utils.init_logging("AdventureClient")
|
||||
|
||||
async def main():
|
||||
parser = get_base_parser()
|
||||
parser.add_argument('patch_file', default="", type=str, nargs="?",
|
||||
help='Path to an ADVNTURE.BIN rom file')
|
||||
parser.add_argument('port', default=17242, type=int, nargs="?",
|
||||
help='port for adventure_connector connection')
|
||||
args = parser.parse_args()
|
||||
|
||||
ctx = AdventureContext(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.atari_sync_task = asyncio.create_task(atari_sync_task(ctx), name="Adventure Sync")
|
||||
|
||||
if args.patch_file:
|
||||
ext = args.patch_file.split(".")[len(args.patch_file.split(".")) - 1].lower()
|
||||
if ext == "apadvn":
|
||||
logger.info("apadvn file supplied, beginning patching process...")
|
||||
async_start(patch_and_run_game(args.patch_file, ctx))
|
||||
else:
|
||||
logger.warning(f"Unknown patch file extension {ext}")
|
||||
if args.port is int:
|
||||
ctx.lua_connector_port = args.port
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
|
||||
await ctx.shutdown()
|
||||
|
||||
if ctx.atari_sync_task:
|
||||
await ctx.atari_sync_task
|
||||
print("finished atari_sync_task (main)")
|
||||
|
||||
|
||||
import colorama
|
||||
|
||||
colorama.init()
|
||||
|
||||
asyncio.run(main())
|
||||
colorama.deinit()
|
|
@ -42,6 +42,7 @@ Currently, the following games are supported:
|
|||
* Kingdom Hearts 2
|
||||
* The Legend of Zelda: Link's Awakening DX
|
||||
* Clique
|
||||
* Adventure
|
||||
|
||||
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
|
||||
|
|
8
Utils.py
8
Utils.py
|
@ -332,7 +332,13 @@ def get_default_options() -> OptionsType:
|
|||
},
|
||||
"wargroove_options": {
|
||||
"root_directory": "C:/Program Files (x86)/Steam/steamapps/common/Wargroove"
|
||||
}
|
||||
},
|
||||
"adventure_options": {
|
||||
"rom_file": "ADVNTURE.BIN",
|
||||
"display_msgs": True,
|
||||
"rom_start": True,
|
||||
"rom_args": ""
|
||||
},
|
||||
}
|
||||
return options
|
||||
|
||||
|
|
Binary file not shown.
|
@ -0,0 +1,851 @@
|
|||
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 SCRIPT_VERSION = 1
|
||||
|
||||
local APItemValue = 0xA2
|
||||
local APItemRam = 0xE7
|
||||
local BatAPItemValue = 0xAB
|
||||
local BatAPItemRam = 0xEA
|
||||
local PlayerRoomAddr = 0x8A -- if in number room, we're not in play mode
|
||||
local WinAddr = 0xDE -- if not 0 (I think if 0xff specifically), we won (and should update once, immediately)
|
||||
|
||||
-- If any of these are 2, that dragon ate the player (should send update immediately
|
||||
-- once, and reset that when none of them are 2 again)
|
||||
|
||||
local DragonState = {0xA8, 0xAD, 0xB2}
|
||||
local last_dragon_state = {0, 0, 0}
|
||||
local carryAddress = 0x9D -- uses rom object table
|
||||
local batRoomAddr = 0xCB
|
||||
local batCarryAddress = 0xD0 -- uses ram object location
|
||||
local batInvalidCarryItem = 0x78
|
||||
local batItemCheckAddr = 0xf69f
|
||||
local batMatrixLen = 11 -- number of pairs
|
||||
local last_carry_item = 0xB4
|
||||
local frames_with_no_item = 0
|
||||
local ItemTableStart = 0xfe9d
|
||||
local PlayerSlotAddress = 0xfff9
|
||||
|
||||
local itemMessages = {}
|
||||
|
||||
local nullObjectId = 0xB4
|
||||
local ItemsReceived = nil
|
||||
local sha256hash = nil
|
||||
local foreign_items = nil
|
||||
local foreign_items_by_room = {}
|
||||
local bat_no_touch_locations_by_room = {}
|
||||
local bat_no_touch_items = {}
|
||||
local autocollect_items = {}
|
||||
local localItemLocations = {}
|
||||
|
||||
local prev_bat_room = 0xff
|
||||
local prev_player_room = 0
|
||||
local prev_ap_room_index = nil
|
||||
|
||||
local pending_foreign_items_collected = {}
|
||||
local pending_local_items_collected = {}
|
||||
local rendering_foreign_item = nil
|
||||
local skip_inventory_items = {}
|
||||
|
||||
local inventory = {}
|
||||
local next_inventory_item = nil
|
||||
|
||||
local input_button_address = 0xD7
|
||||
|
||||
local deathlink_rec = nil
|
||||
local deathlink_send = 0
|
||||
|
||||
local deathlink_sent = false
|
||||
|
||||
local prevstate = ""
|
||||
local curstate = STATE_UNINITIALIZED
|
||||
local atariSocket = nil
|
||||
local frame = 0
|
||||
|
||||
local ItemIndex = 0
|
||||
|
||||
local yorgle_speed_address = 0xf725
|
||||
local grundle_speed_address = 0xf740
|
||||
local rhindle_speed_address = 0xf70A
|
||||
|
||||
local read_switch_a = 0xf780
|
||||
local read_switch_b = 0xf764
|
||||
|
||||
local yorgle_speed = nil
|
||||
local grundle_speed = nil
|
||||
local rhindle_speed = nil
|
||||
|
||||
local slow_yorgle_id = tostring(118000000 + 0x103)
|
||||
local slow_grundle_id = tostring(118000000 + 0x104)
|
||||
local slow_rhindle_id = tostring(118000000 + 0x105)
|
||||
|
||||
local yorgle_dead = false
|
||||
local grundle_dead = false
|
||||
local rhindle_dead = false
|
||||
|
||||
local diff_a_locked = false
|
||||
local diff_b_locked = false
|
||||
|
||||
local bat_logic = 0
|
||||
|
||||
local is_dead = 0
|
||||
local freeincarnates_available = 0
|
||||
local send_freeincarnate_used = false
|
||||
local current_bat_ap_item = nil
|
||||
|
||||
local was_in_number_room = false
|
||||
|
||||
local u8 = nil
|
||||
local wU8 = nil
|
||||
local u16
|
||||
|
||||
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")
|
||||
|
||||
u8 = memory.read_u8
|
||||
wU8 = memory.write_u8
|
||||
u16 = memory.read_u16_le
|
||||
function uRangeRam(address, bytes)
|
||||
data = memory.read_bytes_as_array(address, bytes, "Main RAM")
|
||||
return data
|
||||
end
|
||||
function uRangeRom(address, bytes)
|
||||
data = memory.read_bytes_as_array(address+0xf000, bytes, "System Bus")
|
||||
return data
|
||||
end
|
||||
function uRangeAddress(address, bytes)
|
||||
data = memory.read_bytes_as_array(address, bytes, "System Bus")
|
||||
return data
|
||||
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 function createForeignItemsByRoom()
|
||||
foreign_items_by_room = {}
|
||||
if foreign_items == nil then
|
||||
return
|
||||
end
|
||||
for _, foreign_item in pairs(foreign_items) do
|
||||
if foreign_items_by_room[foreign_item.room_id] == nil then
|
||||
foreign_items_by_room[foreign_item.room_id] = {}
|
||||
end
|
||||
new_foreign_item = {}
|
||||
new_foreign_item.room_id = foreign_item.room_id
|
||||
new_foreign_item.room_x = foreign_item.room_x
|
||||
new_foreign_item.room_y = foreign_item.room_y
|
||||
new_foreign_item.short_location_id = foreign_item.short_location_id
|
||||
|
||||
table.insert(foreign_items_by_room[foreign_item.room_id], new_foreign_item)
|
||||
end
|
||||
end
|
||||
|
||||
function debugPrintNoTouchLocations()
|
||||
for room_id, list in pairs(bat_no_touch_locations_by_room) do
|
||||
for index, notouch_location in ipairs(list) do
|
||||
print("ROOM "..tostring(room_id).. "["..tostring(index).."]: "..tostring(notouch_location.short_location_id))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function processBlock(block)
|
||||
if block == nil then
|
||||
return
|
||||
end
|
||||
local block_identified = 0
|
||||
local msgBlock = block['messages']
|
||||
if msgBlock ~= nil then
|
||||
block_identified = 1
|
||||
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 itemsBlock = block["items"]
|
||||
if itemsBlock ~= nil then
|
||||
block_identified = 1
|
||||
ItemsReceived = itemsBlock
|
||||
end
|
||||
local apItemsBlock = block["foreign_items"]
|
||||
if apItemsBlock ~= nil then
|
||||
block_identified = 1
|
||||
print("got foreign items block")
|
||||
foreign_items = apItemsBlock
|
||||
createForeignItemsByRoom()
|
||||
end
|
||||
local autocollectItems = block["autocollect_items"]
|
||||
if autocollectItems ~= nil then
|
||||
block_identified = 1
|
||||
autocollect_items = {}
|
||||
for _, acitem in pairs(autocollectItems) do
|
||||
if autocollect_items[acitem.room_id] == nil then
|
||||
autocollect_items[acitem.room_id] = {}
|
||||
end
|
||||
table.insert(autocollect_items[acitem.room_id], acitem)
|
||||
end
|
||||
end
|
||||
local localLocalItemLocations = block["local_item_locations"]
|
||||
if localLocalItemLocations ~= nil then
|
||||
block_identified = 1
|
||||
localItemLocations = localLocalItemLocations
|
||||
print("got local item locations")
|
||||
end
|
||||
local checkedLocationsBlock = block["checked_locations"]
|
||||
if checkedLocationsBlock ~= nil then
|
||||
block_identified = 1
|
||||
for room_id, foreign_item_list in pairs(foreign_items_by_room) do
|
||||
for i, foreign_item in pairs(foreign_item_list) do
|
||||
short_id = foreign_item.short_location_id
|
||||
for j, checked_id in pairs(checkedLocationsBlock) do
|
||||
if checked_id == short_id then
|
||||
table.remove(foreign_item_list, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if foreign_items ~= nil then
|
||||
for i, foreign_item in pairs(foreign_items) do
|
||||
short_id = foreign_item.short_location_id
|
||||
for j, checked_id in pairs(checkedLocationsBlock) do
|
||||
if checked_id == short_id then
|
||||
foreign_items[i] = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local dragon_speeds_block = block["dragon_speeds"]
|
||||
if dragon_speeds_block ~= nil then
|
||||
block_identified = 1
|
||||
yorgle_speed = dragon_speeds_block[slow_yorgle_id]
|
||||
grundle_speed = dragon_speeds_block[slow_grundle_id]
|
||||
rhindle_speed = dragon_speeds_block[slow_rhindle_id]
|
||||
end
|
||||
local diff_a_block = block["difficulty_a_locked"]
|
||||
if diff_a_block ~= nil then
|
||||
block_identified = 1
|
||||
diff_a_locked = diff_a_block
|
||||
end
|
||||
local diff_b_block = block["difficulty_b_locked"]
|
||||
if diff_b_block ~= nil then
|
||||
block_identified = 1
|
||||
diff_b_locked = diff_b_block
|
||||
end
|
||||
local freeincarnates_available_block = block["freeincarnates_available"]
|
||||
if freeincarnates_available_block ~= nil then
|
||||
block_identified = 1
|
||||
if freeincarnates_available ~= freeincarnates_available_block then
|
||||
freeincarnates_available = freeincarnates_available_block
|
||||
local msg = {TTL=450, message="freeincarnates: "..tostring(freeincarnates_available), color=0xFFFF0000}
|
||||
itemMessages[-2] = msg
|
||||
end
|
||||
end
|
||||
local bat_logic_block = block["bat_logic"]
|
||||
if bat_logic_block ~= nil then
|
||||
block_identified = 1
|
||||
bat_logic = bat_logic_block
|
||||
end
|
||||
local bat_no_touch_locations_block = block["bat_no_touch_locations"]
|
||||
if bat_no_touch_locations_block ~= nil then
|
||||
block_identified = 1
|
||||
for _, notouch_location in pairs(bat_no_touch_locations_block) do
|
||||
local room_id = tonumber(notouch_location.room_id)
|
||||
if bat_no_touch_locations_by_room[room_id] == nil then
|
||||
bat_no_touch_locations_by_room[room_id] = {}
|
||||
end
|
||||
table.insert(bat_no_touch_locations_by_room[room_id], notouch_location)
|
||||
|
||||
if notouch_location.local_item ~= nil and notouch_location.local_item ~= 255 then
|
||||
bat_no_touch_items[tonumber(notouch_location.local_item)] = true
|
||||
-- print("no touch: "..tostring(notouch_location.local_item))
|
||||
end
|
||||
end
|
||||
-- debugPrintNoTouchLocations()
|
||||
end
|
||||
deathlink_rec = deathlink_rec or block["deathlink"]
|
||||
if( block_identified == 0 ) then
|
||||
print("unidentified block")
|
||||
print(block)
|
||||
end
|
||||
end
|
||||
|
||||
local function clearScreen()
|
||||
if is23Or24Or25 then
|
||||
return
|
||||
elseif is26To28 then
|
||||
drawText(0, 0, "", "black")
|
||||
end
|
||||
end
|
||||
|
||||
local function getMaxMessageLength()
|
||||
if is23Or24Or25 then
|
||||
return client.screenwidth()/11
|
||||
elseif is26To28 then
|
||||
return client.screenwidth()/12
|
||||
end
|
||||
end
|
||||
|
||||
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", nil, nil, nil, "client")
|
||||
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 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 getAllRam()
|
||||
uRangeRAM(0,128);
|
||||
return data
|
||||
end
|
||||
|
||||
local function arrayEqual(a1, a2)
|
||||
if #a1 ~= #a2 then
|
||||
return false
|
||||
end
|
||||
|
||||
for i, v in ipairs(a1) do
|
||||
if v ~= a2[i] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function alive_mode()
|
||||
return (u8(PlayerRoomAddr) ~= 0x00 and u8(WinAddr) == 0x00)
|
||||
end
|
||||
|
||||
local function generateLocationsChecked()
|
||||
list_of_locations = {}
|
||||
for s, f in pairs(pending_foreign_items_collected) do
|
||||
table.insert(list_of_locations, f.short_location_id + 118000000)
|
||||
end
|
||||
for s, f in pairs(pending_local_items_collected) do
|
||||
table.insert(list_of_locations, f + 118000000)
|
||||
end
|
||||
return list_of_locations
|
||||
end
|
||||
|
||||
function receive()
|
||||
l, e = atariSocket:receive()
|
||||
if e == 'closed' then
|
||||
if curstate == STATE_OK then
|
||||
print("Connection closed")
|
||||
end
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
elseif e == 'timeout' then
|
||||
return
|
||||
elseif e ~= nil then
|
||||
print(e)
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
end
|
||||
if l ~= nil then
|
||||
processBlock(json.decode(l))
|
||||
end
|
||||
-- Determine Message to send back
|
||||
|
||||
newSha256 = memory.hash_region(0xF000, 0x1000, "System Bus")
|
||||
if (sha256hash ~= nil and sha256hash ~= newSha256) then
|
||||
print("ROM changed, quitting")
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
end
|
||||
sha256hash = newSha256
|
||||
local retTable = {}
|
||||
retTable["scriptVersion"] = SCRIPT_VERSION
|
||||
retTable["romhash"] = sha256hash
|
||||
if (alive_mode()) then
|
||||
retTable["locations"] = generateLocationsChecked()
|
||||
end
|
||||
if (u8(WinAddr) ~= 0x00) then
|
||||
retTable["victory"] = 1
|
||||
end
|
||||
if( deathlink_sent or deathlink_send == 0 ) then
|
||||
retTable["deathLink"] = 0
|
||||
else
|
||||
print("Sending deathlink "..tostring(deathlink_send))
|
||||
retTable["deathLink"] = deathlink_send
|
||||
deathlink_sent = true
|
||||
end
|
||||
deathlink_send = 0
|
||||
|
||||
if send_freeincarnate_used == true then
|
||||
print("Sending freeincarnate used")
|
||||
retTable["freeincarnate"] = true
|
||||
send_freeincarnate_used = false
|
||||
end
|
||||
|
||||
msg = json.encode(retTable).."\n"
|
||||
local ret, error = atariSocket: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!")
|
||||
curstate = STATE_OK
|
||||
end
|
||||
end
|
||||
|
||||
function AutocollectFromRoom()
|
||||
if autocollect_items ~= nil and autocollect_items[prev_player_room] ~= nil then
|
||||
for _, item in pairs(autocollect_items[prev_player_room]) do
|
||||
pending_foreign_items_collected[item.short_location_id] = item
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SetYorgleSpeed()
|
||||
if yorgle_speed ~= nil then
|
||||
emu.setregister("A", yorgle_speed);
|
||||
end
|
||||
end
|
||||
|
||||
function SetGrundleSpeed()
|
||||
if grundle_speed ~= nil then
|
||||
emu.setregister("A", grundle_speed);
|
||||
end
|
||||
end
|
||||
|
||||
function SetRhindleSpeed()
|
||||
if rhindle_speed ~= nil then
|
||||
emu.setregister("A", rhindle_speed);
|
||||
end
|
||||
end
|
||||
|
||||
function SetDifficultySwitchB()
|
||||
if diff_b_locked then
|
||||
local a = emu.getregister("A")
|
||||
if a < 128 then
|
||||
emu.setregister("A", a + 128)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SetDifficultySwitchA()
|
||||
if diff_a_locked then
|
||||
local a = emu.getregister("A")
|
||||
if (a > 128 and a < 128 + 64) or (a < 64) then
|
||||
emu.setregister("A", a + 64)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TryFreeincarnate()
|
||||
if freeincarnates_available > 0 then
|
||||
freeincarnates_available = freeincarnates_available - 1
|
||||
for index, state_addr in pairs(DragonState) do
|
||||
if last_dragon_state[index] == 1 then
|
||||
send_freeincarnate_used = true
|
||||
memory.write_u8(state_addr, 1, "System Bus")
|
||||
local msg = {TTL=450, message="used freeincarnate", color=0xFF00FF00}
|
||||
itemMessages[-1] = msg
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
function GetLinkedObject()
|
||||
if emu.getregister("X") == batRoomAddr then
|
||||
bat_interest_item = emu.getregister("A")
|
||||
-- if the bat can't touch that item, we'll switch it to the number item, which should never be
|
||||
-- in the same room as the bat.
|
||||
if bat_no_touch_items[bat_interest_item] ~= nil then
|
||||
emu.setregister("A", 0xDD )
|
||||
emu.setregister("Y", 0xDD )
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CheckCollectAPItem(carry_item, target_item_value, target_item_ram, rendering_foreign_item)
|
||||
if( carry_item == target_item_value and rendering_foreign_item ~= nil ) then
|
||||
memory.write_u8(carryAddress, nullObjectId, "System Bus")
|
||||
memory.write_u8(target_item_ram, 0xFF, "System Bus")
|
||||
pending_foreign_items_collected[rendering_foreign_item.short_location_id] = rendering_foreign_item
|
||||
for index, fi in pairs(foreign_items_by_room[rendering_foreign_item.room_id]) do
|
||||
if( fi.short_location_id == rendering_foreign_item.short_location_id ) then
|
||||
table.remove(foreign_items_by_room[rendering_foreign_item.room_id], index)
|
||||
break
|
||||
end
|
||||
end
|
||||
for index, fi in pairs(foreign_items) do
|
||||
if( fi.short_location_id == rendering_foreign_item.short_location_id ) then
|
||||
foreign_items[index] = nil
|
||||
break
|
||||
end
|
||||
end
|
||||
prev_ap_room_index = 0
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function BatCanTouchForeign(foreign_item, bat_room)
|
||||
if bat_no_touch_locations_by_room[bat_room] == nil or bat_no_touch_locations_by_room[bat_room][1] == nil then
|
||||
return true
|
||||
end
|
||||
|
||||
for index, location in ipairs(bat_no_touch_locations_by_room[bat_room]) do
|
||||
if location.short_location_id == foreign_item.short_location_id then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
function main()
|
||||
memory.usememorydomain("System Bus")
|
||||
if (is23Or24Or25 or is26To28) == false then
|
||||
print("Must use a version of bizhawk 2.3.1 or higher")
|
||||
return
|
||||
end
|
||||
local playerSlot = memory.read_u8(PlayerSlotAddress)
|
||||
local port = 17242 + playerSlot
|
||||
print("Using port"..tostring(port))
|
||||
server, error = socket.bind('localhost', port)
|
||||
if( error ~= nil ) then
|
||||
print(error)
|
||||
end
|
||||
event.onmemoryexecute(SetYorgleSpeed, yorgle_speed_address);
|
||||
event.onmemoryexecute(SetGrundleSpeed, grundle_speed_address);
|
||||
event.onmemoryexecute(SetRhindleSpeed, rhindle_speed_address);
|
||||
event.onmemoryexecute(SetDifficultySwitchA, read_switch_a)
|
||||
event.onmemoryexecute(SetDifficultySwitchB, read_switch_b)
|
||||
event.onmemoryexecute(GetLinkedObject, batItemCheckAddr)
|
||||
-- TODO: Add an onmemoryexecute event to intercept the bat reading item rooms, and don't 'see' an item in the
|
||||
-- room if it is in bat_no_touch_locations_by_room. Although realistically, I may have to handle this in the rom
|
||||
-- for it to be totally reliable, because it won't work before the script connects (I might have to reset them?)
|
||||
-- TODO: Also remove those items from the bat_no_touch_locations_by_room if they have been collected
|
||||
while true do
|
||||
frame = frame + 1
|
||||
drawMessages()
|
||||
if not (curstate == prevstate) then
|
||||
print("Current state: "..curstate)
|
||||
prevstate = curstate
|
||||
end
|
||||
|
||||
local current_player_room = u8(PlayerRoomAddr)
|
||||
local bat_room = u8(batRoomAddr)
|
||||
local bat_carrying_item = u8(batCarryAddress)
|
||||
local bat_carrying_ap_item = (BatAPItemRam == bat_carrying_item)
|
||||
|
||||
if current_player_room == 0x1E then
|
||||
if u8(PlayerRoomAddr + 1) > 0x4B then
|
||||
memory.write_u8(PlayerRoomAddr + 1, 0x4B)
|
||||
end
|
||||
end
|
||||
|
||||
if current_player_room == 0x00 then
|
||||
if not was_in_number_room then
|
||||
print("reset "..tostring(bat_carrying_ap_item).." "..tostring(bat_carrying_item))
|
||||
memory.write_u8(batCarryAddress, batInvalidCarryItem)
|
||||
memory.write_u8(batCarryAddress+ 1, 0)
|
||||
createForeignItemsByRoom()
|
||||
memory.write_u8(BatAPItemRam, 0xff)
|
||||
memory.write_u8(APItemRam, 0xff)
|
||||
prev_ap_room_index = 0
|
||||
prev_player_room = 0
|
||||
rendering_foreign_item = nil
|
||||
was_in_number_room = true
|
||||
end
|
||||
else
|
||||
was_in_number_room = false
|
||||
end
|
||||
|
||||
if bat_room ~= prev_bat_room then
|
||||
if bat_carrying_ap_item then
|
||||
if foreign_items_by_room[prev_bat_room] ~= nil then
|
||||
for r,f in pairs(foreign_items_by_room[prev_bat_room]) do
|
||||
if f.short_location_id == current_bat_ap_item.short_location_id then
|
||||
-- print("removing item from "..tostring(r).." in "..tostring(prev_bat_room))
|
||||
table.remove(foreign_items_by_room[prev_bat_room], r)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if foreign_items_by_room[bat_room] == nil then
|
||||
foreign_items_by_room[bat_room] = {}
|
||||
end
|
||||
-- print("adding item to "..tostring(bat_room))
|
||||
table.insert(foreign_items_by_room[bat_room], current_bat_ap_item)
|
||||
else
|
||||
-- set AP item room and position for new room, or to invalid room
|
||||
if foreign_items_by_room[bat_room] ~= nil and foreign_items_by_room[bat_room][1] ~= nil
|
||||
and BatCanTouchForeign(foreign_items_by_room[bat_room][1], bat_room) then
|
||||
if current_bat_ap_item ~= foreign_items_by_room[bat_room][1] then
|
||||
current_bat_ap_item = foreign_items_by_room[bat_room][1]
|
||||
-- print("Changing bat item to "..tostring(current_bat_ap_item.short_location_id))
|
||||
end
|
||||
memory.write_u8(BatAPItemRam, bat_room)
|
||||
memory.write_u8(BatAPItemRam + 1, current_bat_ap_item.room_x)
|
||||
memory.write_u8(BatAPItemRam + 2, current_bat_ap_item.room_y)
|
||||
else
|
||||
memory.write_u8(BatAPItemRam, 0xff)
|
||||
if current_bat_ap_item ~= nil then
|
||||
-- print("clearing bat item")
|
||||
end
|
||||
current_bat_ap_item = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
prev_bat_room = bat_room
|
||||
|
||||
-- update foreign_items_by_room position and room id for bat item if bat carrying an item
|
||||
if bat_carrying_ap_item then
|
||||
-- this is setting the item using the bat's position, which is somewhat wrong, but I think
|
||||
-- there will be more problems with the room not matching sometimes if I use the actual item position
|
||||
current_bat_ap_item.room_id = bat_room
|
||||
current_bat_ap_item.room_x = u8(batRoomAddr + 1)
|
||||
current_bat_ap_item.room_y = u8(batRoomAddr + 2)
|
||||
end
|
||||
|
||||
if (alive_mode()) then
|
||||
if (current_player_room ~= prev_player_room) then
|
||||
memory.write_u8(APItemRam, 0xFF, "System Bus")
|
||||
prev_ap_room_index = 0
|
||||
prev_player_room = current_player_room
|
||||
AutocollectFromRoom()
|
||||
end
|
||||
local carry_item = memory.read_u8(carryAddress, "System Bus")
|
||||
bat_no_touch_items[carry_item] = nil
|
||||
if (next_inventory_item ~= nil) then
|
||||
if ( carry_item == nullObjectId and last_carry_item == nullObjectId ) then
|
||||
frames_with_no_item = frames_with_no_item + 1
|
||||
if (frames_with_no_item > 10) then
|
||||
frames_with_no_item = 10
|
||||
local input_value = memory.read_u8(input_button_address, "System Bus")
|
||||
if( input_value >= 64 and input_value < 128 ) then -- high bit clear, second highest bit set
|
||||
memory.write_u8(carryAddress, next_inventory_item)
|
||||
local item_ram_location = memory.read_u8(ItemTableStart + next_inventory_item)
|
||||
if( memory.read_u8(batCarryAddress) ~= 0x78 and
|
||||
memory.read_u8(batCarryAddress) == item_ram_location) then
|
||||
memory.write_u8(batCarryAddress, batInvalidCarryItem)
|
||||
memory.write_u8(batCarryAddress+ 1, 0)
|
||||
memory.write_u8(item_ram_location, current_player_room)
|
||||
memory.write_u8(item_ram_location + 1, memory.read_u8(PlayerRoomAddr + 1))
|
||||
memory.write_u8(item_ram_location + 2, memory.read_u8(PlayerRoomAddr + 2))
|
||||
end
|
||||
ItemIndex = ItemIndex + 1
|
||||
next_inventory_item = nil
|
||||
end
|
||||
end
|
||||
else
|
||||
frames_with_no_item = 0
|
||||
end
|
||||
end
|
||||
if( carry_item ~= last_carry_item ) then
|
||||
if ( localItemLocations ~= nil and localItemLocations[tostring(carry_item)] ~= nil ) then
|
||||
pending_local_items_collected[localItemLocations[tostring(carry_item)]] =
|
||||
localItemLocations[tostring(carry_item)]
|
||||
table.remove(localItemLocations, tostring(carry_item))
|
||||
skip_inventory_items[carry_item] = carry_item
|
||||
end
|
||||
end
|
||||
last_carry_item = carry_item
|
||||
|
||||
CheckCollectAPItem(carry_item, APItemValue, APItemRam, rendering_foreign_item)
|
||||
if CheckCollectAPItem(carry_item, BatAPItemValue, BatAPItemRam, current_bat_ap_item) and bat_carrying_ap_item then
|
||||
memory.write_u8(batCarryAddress, batInvalidCarryItem)
|
||||
memory.write_u8(batCarryAddress+ 1, 0)
|
||||
end
|
||||
|
||||
|
||||
rendering_foreign_item = nil
|
||||
if( foreign_items_by_room[current_player_room] ~= nil ) then
|
||||
if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil ) and memory.read_u8(APItemRam) ~= 0xff then
|
||||
foreign_items_by_room[current_player_room][prev_ap_room_index].room_x = memory.read_u8(APItemRam + 1)
|
||||
foreign_items_by_room[current_player_room][prev_ap_room_index].room_y = memory.read_u8(APItemRam + 2)
|
||||
end
|
||||
prev_ap_room_index = prev_ap_room_index + 1
|
||||
local invalid_index = -1
|
||||
if( foreign_items_by_room[current_player_room][prev_ap_room_index] == nil ) then
|
||||
prev_ap_room_index = 1
|
||||
end
|
||||
if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil and current_bat_ap_item ~= nil and
|
||||
foreign_items_by_room[current_player_room][prev_ap_room_index].short_location_id == current_bat_ap_item.short_location_id) then
|
||||
invalid_index = prev_ap_room_index
|
||||
prev_ap_room_index = prev_ap_room_index + 1
|
||||
if( foreign_items_by_room[current_player_room][prev_ap_room_index] == nil ) then
|
||||
prev_ap_room_index = 1
|
||||
end
|
||||
end
|
||||
|
||||
if( foreign_items_by_room[current_player_room][prev_ap_room_index] ~= nil and prev_ap_room_index ~= invalid_index ) then
|
||||
memory.write_u8(APItemRam, current_player_room)
|
||||
rendering_foreign_item = foreign_items_by_room[current_player_room][prev_ap_room_index]
|
||||
memory.write_u8(APItemRam + 1, rendering_foreign_item.room_x)
|
||||
memory.write_u8(APItemRam + 2, rendering_foreign_item.room_y)
|
||||
else
|
||||
memory.write_u8(APItemRam, 0xFF, "System Bus")
|
||||
end
|
||||
end
|
||||
if is_dead == 0 then
|
||||
dragons_revived = false
|
||||
player_dead = false
|
||||
new_dragon_state = {0,0,0}
|
||||
for index, dragon_state_addr in pairs(DragonState) do
|
||||
new_dragon_state[index] = memory.read_u8(dragon_state_addr, "System Bus" )
|
||||
if last_dragon_state[index] == 1 and new_dragon_state[index] ~= 1 then
|
||||
dragons_revived = true
|
||||
elseif last_dragon_state[index] ~= 1 and new_dragon_state[index] == 1 then
|
||||
dragon_real_index = index - 1
|
||||
print("Killed dragon: "..tostring(dragon_real_index))
|
||||
local dragon_item = {}
|
||||
dragon_item["short_location_id"] = 0xD0 + dragon_real_index
|
||||
pending_foreign_items_collected[dragon_item.short_location_id] = dragon_item
|
||||
end
|
||||
if new_dragon_state[index] == 2 then
|
||||
player_dead = true
|
||||
end
|
||||
end
|
||||
if dragons_revived and player_dead == false then
|
||||
TryFreeincarnate()
|
||||
end
|
||||
last_dragon_state = new_dragon_state
|
||||
end
|
||||
elseif (u8(PlayerRoomAddr) == 0x00) then -- not alive mode, in number room
|
||||
ItemIndex = 0 -- reset our inventory
|
||||
next_inventory_item = nil
|
||||
skip_inventory_items = {}
|
||||
end
|
||||
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
|
||||
if (frame % 5 == 0) then
|
||||
receive()
|
||||
if alive_mode() then
|
||||
local was_dead = is_dead
|
||||
is_dead = 0
|
||||
for index, dragonStateAddr in pairs(DragonState) do
|
||||
local dragonstateval = memory.read_u8(dragonStateAddr, "System Bus")
|
||||
if ( dragonstateval == 2) then
|
||||
is_dead = index
|
||||
end
|
||||
end
|
||||
if was_dead ~= 0 and is_dead == 0 then
|
||||
TryFreeincarnate()
|
||||
end
|
||||
if deathlink_rec == true and is_dead == 0 then
|
||||
print("setting dead from deathlink")
|
||||
deathlink_rec = false
|
||||
deathlink_sent = true
|
||||
is_dead = 1
|
||||
memory.write_u8(carryAddress, nullObjectId, "System Bus")
|
||||
memory.write_u8(DragonState[1], 2, "System Bus")
|
||||
end
|
||||
if (is_dead > 0 and deathlink_send == 0 and not deathlink_sent) then
|
||||
deathlink_send = is_dead
|
||||
print("setting deathlink_send to "..tostring(is_dead))
|
||||
elseif (is_dead == 0) then
|
||||
deathlink_send = 0
|
||||
deathlink_sent = false
|
||||
end
|
||||
if ItemsReceived ~= nil and ItemsReceived[ItemIndex + 1] ~= nil then
|
||||
while ItemsReceived[ItemIndex + 1] ~= nil and skip_inventory_items[ItemsReceived[ItemIndex + 1]] ~= nil do
|
||||
print("skip")
|
||||
ItemIndex = ItemIndex + 1
|
||||
end
|
||||
local static_id = ItemsReceived[ItemIndex + 1]
|
||||
if static_id ~= nil then
|
||||
inventory[static_id] = 1
|
||||
if next_inventory_item == nil then
|
||||
next_inventory_item = static_id
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
elseif (curstate == STATE_UNINITIALIZED) then
|
||||
if (frame % 60 == 0) then
|
||||
|
||||
print("Waiting for client.")
|
||||
|
||||
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
|
||||
atariSocket = client
|
||||
atariSocket:settimeout(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
emu.frameadvance()
|
||||
end
|
||||
end
|
||||
|
||||
main()
|
|
@ -0,0 +1,380 @@
|
|||
--
|
||||
-- json.lua
|
||||
--
|
||||
-- Copyright (c) 2015 rxi
|
||||
--
|
||||
-- This library is free software; you can redistribute it and/or modify it
|
||||
-- under the terms of the MIT license. See LICENSE for details.
|
||||
--
|
||||
|
||||
local json = { _version = "0.1.0" }
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Encode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local encode
|
||||
|
||||
local escape_char_map = {
|
||||
[ "\\" ] = "\\\\",
|
||||
[ "\"" ] = "\\\"",
|
||||
[ "\b" ] = "\\b",
|
||||
[ "\f" ] = "\\f",
|
||||
[ "\n" ] = "\\n",
|
||||
[ "\r" ] = "\\r",
|
||||
[ "\t" ] = "\\t",
|
||||
}
|
||||
|
||||
local escape_char_map_inv = { [ "\\/" ] = "/" }
|
||||
for k, v in pairs(escape_char_map) do
|
||||
escape_char_map_inv[v] = k
|
||||
end
|
||||
|
||||
|
||||
local function escape_char(c)
|
||||
return escape_char_map[c] or string.format("\\u%04x", c:byte())
|
||||
end
|
||||
|
||||
|
||||
local function encode_nil(val)
|
||||
return "null"
|
||||
end
|
||||
|
||||
|
||||
local function encode_table(val, stack)
|
||||
local res = {}
|
||||
stack = stack or {}
|
||||
|
||||
-- Circular reference?
|
||||
if stack[val] then error("circular reference") end
|
||||
|
||||
stack[val] = true
|
||||
|
||||
if val[1] ~= nil or next(val) == nil then
|
||||
-- Treat as array -- check keys are valid and it is not sparse
|
||||
local n = 0
|
||||
for k in pairs(val) do
|
||||
if type(k) ~= "number" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
n = n + 1
|
||||
end
|
||||
if n ~= #val then
|
||||
error("invalid table: sparse array")
|
||||
end
|
||||
-- Encode
|
||||
for i, v in ipairs(val) do
|
||||
table.insert(res, encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "[" .. table.concat(res, ",") .. "]"
|
||||
|
||||
else
|
||||
-- Treat as an object
|
||||
for k, v in pairs(val) do
|
||||
if type(k) ~= "string" then
|
||||
error("invalid table: mixed or invalid key types")
|
||||
end
|
||||
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
|
||||
end
|
||||
stack[val] = nil
|
||||
return "{" .. table.concat(res, ",") .. "}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function encode_string(val)
|
||||
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
|
||||
end
|
||||
|
||||
|
||||
local function encode_number(val)
|
||||
-- Check for NaN, -inf and inf
|
||||
if val ~= val or val <= -math.huge or val >= math.huge then
|
||||
error("unexpected number value '" .. tostring(val) .. "'")
|
||||
end
|
||||
return string.format("%.14g", val)
|
||||
end
|
||||
|
||||
|
||||
local type_func_map = {
|
||||
[ "nil" ] = encode_nil,
|
||||
[ "table" ] = encode_table,
|
||||
[ "string" ] = encode_string,
|
||||
[ "number" ] = encode_number,
|
||||
[ "boolean" ] = tostring,
|
||||
}
|
||||
|
||||
|
||||
encode = function(val, stack)
|
||||
local t = type(val)
|
||||
local f = type_func_map[t]
|
||||
if f then
|
||||
return f(val, stack)
|
||||
end
|
||||
error("unexpected type '" .. t .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.encode(val)
|
||||
return ( encode(val) )
|
||||
end
|
||||
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
-- Decode
|
||||
-------------------------------------------------------------------------------
|
||||
|
||||
local parse
|
||||
|
||||
local function create_set(...)
|
||||
local res = {}
|
||||
for i = 1, select("#", ...) do
|
||||
res[ select(i, ...) ] = true
|
||||
end
|
||||
return res
|
||||
end
|
||||
|
||||
local space_chars = create_set(" ", "\t", "\r", "\n")
|
||||
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
|
||||
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
|
||||
local literals = create_set("true", "false", "null")
|
||||
|
||||
local literal_map = {
|
||||
[ "true" ] = true,
|
||||
[ "false" ] = false,
|
||||
[ "null" ] = nil,
|
||||
}
|
||||
|
||||
|
||||
local function next_char(str, idx, set, negate)
|
||||
for i = idx, #str do
|
||||
if set[str:sub(i, i)] ~= negate then
|
||||
return i
|
||||
end
|
||||
end
|
||||
return #str + 1
|
||||
end
|
||||
|
||||
|
||||
local function decode_error(str, idx, msg)
|
||||
--local line_count = 1
|
||||
--local col_count = 1
|
||||
--for i = 1, idx - 1 do
|
||||
-- col_count = col_count + 1
|
||||
-- if str:sub(i, i) == "\n" then
|
||||
-- line_count = line_count + 1
|
||||
-- col_count = 1
|
||||
-- end
|
||||
-- end
|
||||
-- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) )
|
||||
end
|
||||
|
||||
|
||||
local function codepoint_to_utf8(n)
|
||||
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
|
||||
local f = math.floor
|
||||
if n <= 0x7f then
|
||||
return string.char(n)
|
||||
elseif n <= 0x7ff then
|
||||
return string.char(f(n / 64) + 192, n % 64 + 128)
|
||||
elseif n <= 0xffff then
|
||||
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
elseif n <= 0x10ffff then
|
||||
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
|
||||
f(n % 4096 / 64) + 128, n % 64 + 128)
|
||||
end
|
||||
error( string.format("invalid unicode codepoint '%x'", n) )
|
||||
end
|
||||
|
||||
|
||||
local function parse_unicode_escape(s)
|
||||
local n1 = tonumber( s:sub(3, 6), 16 )
|
||||
local n2 = tonumber( s:sub(9, 12), 16 )
|
||||
-- Surrogate pair?
|
||||
if n2 then
|
||||
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
|
||||
else
|
||||
return codepoint_to_utf8(n1)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
local function parse_string(str, i)
|
||||
local has_unicode_escape = false
|
||||
local has_surrogate_escape = false
|
||||
local has_escape = false
|
||||
local last
|
||||
for j = i + 1, #str do
|
||||
local x = str:byte(j)
|
||||
|
||||
if x < 32 then
|
||||
decode_error(str, j, "control character in string")
|
||||
end
|
||||
|
||||
if last == 92 then -- "\\" (escape char)
|
||||
if x == 117 then -- "u" (unicode escape sequence)
|
||||
local hex = str:sub(j + 1, j + 5)
|
||||
if not hex:find("%x%x%x%x") then
|
||||
decode_error(str, j, "invalid unicode escape in string")
|
||||
end
|
||||
if hex:find("^[dD][89aAbB]") then
|
||||
has_surrogate_escape = true
|
||||
else
|
||||
has_unicode_escape = true
|
||||
end
|
||||
else
|
||||
local c = string.char(x)
|
||||
if not escape_chars[c] then
|
||||
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
|
||||
end
|
||||
has_escape = true
|
||||
end
|
||||
last = nil
|
||||
|
||||
elseif x == 34 then -- '"' (end of string)
|
||||
local s = str:sub(i + 1, j - 1)
|
||||
if has_surrogate_escape then
|
||||
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
|
||||
end
|
||||
if has_unicode_escape then
|
||||
s = s:gsub("\\u....", parse_unicode_escape)
|
||||
end
|
||||
if has_escape then
|
||||
s = s:gsub("\\.", escape_char_map_inv)
|
||||
end
|
||||
return s, j + 1
|
||||
|
||||
else
|
||||
last = x
|
||||
end
|
||||
end
|
||||
decode_error(str, i, "expected closing quote for string")
|
||||
end
|
||||
|
||||
|
||||
local function parse_number(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local s = str:sub(i, x - 1)
|
||||
local n = tonumber(s)
|
||||
if not n then
|
||||
decode_error(str, i, "invalid number '" .. s .. "'")
|
||||
end
|
||||
return n, x
|
||||
end
|
||||
|
||||
|
||||
local function parse_literal(str, i)
|
||||
local x = next_char(str, i, delim_chars)
|
||||
local word = str:sub(i, x - 1)
|
||||
if not literals[word] then
|
||||
decode_error(str, i, "invalid literal '" .. word .. "'")
|
||||
end
|
||||
return literal_map[word], x
|
||||
end
|
||||
|
||||
|
||||
local function parse_array(str, i)
|
||||
local res = {}
|
||||
local n = 1
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local x
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of array?
|
||||
if str:sub(i, i) == "]" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read token
|
||||
x, i = parse(str, i)
|
||||
res[n] = x
|
||||
n = n + 1
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "]" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local function parse_object(str, i)
|
||||
local res = {}
|
||||
i = i + 1
|
||||
while 1 do
|
||||
local key, val
|
||||
i = next_char(str, i, space_chars, true)
|
||||
-- Empty / end of object?
|
||||
if str:sub(i, i) == "}" then
|
||||
i = i + 1
|
||||
break
|
||||
end
|
||||
-- Read key
|
||||
if str:sub(i, i) ~= '"' then
|
||||
decode_error(str, i, "expected string for key")
|
||||
end
|
||||
key, i = parse(str, i)
|
||||
-- Read ':' delimiter
|
||||
i = next_char(str, i, space_chars, true)
|
||||
if str:sub(i, i) ~= ":" then
|
||||
decode_error(str, i, "expected ':' after key")
|
||||
end
|
||||
i = next_char(str, i + 1, space_chars, true)
|
||||
-- Read value
|
||||
val, i = parse(str, i)
|
||||
-- Set
|
||||
res[key] = val
|
||||
-- Next token
|
||||
i = next_char(str, i, space_chars, true)
|
||||
local chr = str:sub(i, i)
|
||||
i = i + 1
|
||||
if chr == "}" then break end
|
||||
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
|
||||
end
|
||||
return res, i
|
||||
end
|
||||
|
||||
|
||||
local char_func_map = {
|
||||
[ '"' ] = parse_string,
|
||||
[ "0" ] = parse_number,
|
||||
[ "1" ] = parse_number,
|
||||
[ "2" ] = parse_number,
|
||||
[ "3" ] = parse_number,
|
||||
[ "4" ] = parse_number,
|
||||
[ "5" ] = parse_number,
|
||||
[ "6" ] = parse_number,
|
||||
[ "7" ] = parse_number,
|
||||
[ "8" ] = parse_number,
|
||||
[ "9" ] = parse_number,
|
||||
[ "-" ] = parse_number,
|
||||
[ "t" ] = parse_literal,
|
||||
[ "f" ] = parse_literal,
|
||||
[ "n" ] = parse_literal,
|
||||
[ "[" ] = parse_array,
|
||||
[ "{" ] = parse_object,
|
||||
}
|
||||
|
||||
|
||||
parse = function(str, idx)
|
||||
local chr = str:sub(idx, idx)
|
||||
local f = char_func_map[chr]
|
||||
if f then
|
||||
return f(str, idx)
|
||||
end
|
||||
decode_error(str, idx, "unexpected character '" .. chr .. "'")
|
||||
end
|
||||
|
||||
|
||||
function json.decode(str)
|
||||
if type(str) ~= "string" then
|
||||
error("expected argument of type string, got " .. type(str))
|
||||
end
|
||||
return ( parse(str, next_char(str, 1, space_chars, true)) )
|
||||
end
|
||||
|
||||
|
||||
return json
|
|
@ -0,0 +1,132 @@
|
|||
-----------------------------------------------------------------------------
|
||||
-- LuaSocket helper module
|
||||
-- Author: Diego Nehab
|
||||
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Declare module and import dependencies
|
||||
-----------------------------------------------------------------------------
|
||||
local base = _G
|
||||
local string = require("string")
|
||||
local math = require("math")
|
||||
local socket = require("socket.core")
|
||||
module("socket")
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Exported auxiliar functions
|
||||
-----------------------------------------------------------------------------
|
||||
function connect(address, port, laddress, lport)
|
||||
local sock, err = socket.tcp()
|
||||
if not sock then return nil, err end
|
||||
if laddress then
|
||||
local res, err = sock:bind(laddress, lport, -1)
|
||||
if not res then return nil, err end
|
||||
end
|
||||
local res, err = sock:connect(address, port)
|
||||
if not res then return nil, err end
|
||||
return sock
|
||||
end
|
||||
|
||||
function bind(host, port, backlog)
|
||||
local sock, err = socket.tcp()
|
||||
if not sock then return nil, err end
|
||||
sock:setoption("reuseaddr", true)
|
||||
local res, err = sock:bind(host, port)
|
||||
if not res then return nil, err end
|
||||
res, err = sock:listen(backlog)
|
||||
if not res then return nil, err end
|
||||
return sock
|
||||
end
|
||||
|
||||
try = newtry()
|
||||
|
||||
function choose(table)
|
||||
return function(name, opt1, opt2)
|
||||
if base.type(name) ~= "string" then
|
||||
name, opt1, opt2 = "default", name, opt1
|
||||
end
|
||||
local f = table[name or "nil"]
|
||||
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
|
||||
else return f(opt1, opt2) end
|
||||
end
|
||||
end
|
||||
|
||||
-----------------------------------------------------------------------------
|
||||
-- Socket sources and sinks, conforming to LTN12
|
||||
-----------------------------------------------------------------------------
|
||||
-- create namespaces inside LuaSocket namespace
|
||||
sourcet = {}
|
||||
sinkt = {}
|
||||
|
||||
BLOCKSIZE = 2048
|
||||
|
||||
sinkt["close-when-done"] = function(sock)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function(self, chunk, err)
|
||||
if not chunk then
|
||||
sock:close()
|
||||
return 1
|
||||
else return sock:send(chunk) end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sinkt["keep-open"] = function(sock)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function(self, chunk, err)
|
||||
if chunk then return sock:send(chunk)
|
||||
else return 1 end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sinkt["default"] = sinkt["keep-open"]
|
||||
|
||||
sink = choose(sinkt)
|
||||
|
||||
sourcet["by-length"] = function(sock, length)
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function()
|
||||
if length <= 0 then return nil end
|
||||
local size = math.min(socket.BLOCKSIZE, length)
|
||||
local chunk, err = sock:receive(size)
|
||||
if err then return nil, err end
|
||||
length = length - string.len(chunk)
|
||||
return chunk
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
sourcet["until-closed"] = function(sock)
|
||||
local done
|
||||
return base.setmetatable({
|
||||
getfd = function() return sock:getfd() end,
|
||||
dirty = function() return sock:dirty() end
|
||||
}, {
|
||||
__call = function()
|
||||
if done then return nil end
|
||||
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
|
||||
if not err then return chunk
|
||||
elseif err == "closed" then
|
||||
sock:close()
|
||||
done = 1
|
||||
return partial
|
||||
else return nil, err end
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
|
||||
sourcet["default"] = sourcet["until-closed"]
|
||||
|
||||
source = choose(sourcet)
|
19
host.yaml
19
host.yaml
|
@ -168,3 +168,22 @@ zillion_options:
|
|||
# You have to know the path to the emulator core library on the user's computer.
|
||||
rom_start: "retroarch"
|
||||
|
||||
adventure_options:
|
||||
# File name of the standard NTSC Adventure rom.
|
||||
# The licensed "The 80 Classic Games" CD-ROM contains this.
|
||||
# It may also have a .a26 extension
|
||||
rom_file: "ADVNTURE.BIN"
|
||||
# Set this to false to never autostart a rom (such as after patching)
|
||||
# True for operating system default program for '.a26'
|
||||
# Alternatively, a path to a program to open the .a26 file with (generally EmuHawk for multiworld)
|
||||
rom_start: true
|
||||
# Optional, additional args passed into rom_start before the .bin file
|
||||
# For example, this can be used to autoload the connector script in BizHawk
|
||||
# (see BizHawk --lua= option)
|
||||
rom_args: " "
|
||||
# Set this to true to display item received messages in Emuhawk
|
||||
display_msgs: true
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -87,6 +87,7 @@ Name: "client/sc2"; Description: "Starcraft 2"; Types: full playing
|
|||
Name: "client/wargroove"; Description: "Wargroove"; Types: full playing
|
||||
Name: "client/zl"; Description: "Zillion"; Types: full playing
|
||||
Name: "client/tloz"; Description: "The Legend of Zelda"; Types: full playing
|
||||
Name: "client/advn"; Description: "Adventure"; Types: full playing
|
||||
Name: "client/text"; Description: "Text, to !command and chat"; Types: full playing
|
||||
|
||||
[Dirs]
|
||||
|
@ -105,6 +106,7 @@ Source: "{code:GetRedROMPath}"; DestDir: "{app}"; DestName: "Pokemon Red (UE) [S
|
|||
Source: "{code:GetBlueROMPath}"; DestDir: "{app}"; DestName: "Pokemon Blue (UE) [S][!].gb"; Flags: external; Components: client/pkmn/blue or generator/pkmn_b
|
||||
Source: "{code:GetLADXROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The - Link's Awakening DX (USA, Europe) (SGB Enhanced).gbc"; Flags: external; Components: client/ladx or generator/ladx
|
||||
Source: "{code:GetTLoZROMPath}"; DestDir: "{app}"; DestName: "Legend of Zelda, The (U) (PRG0) [!].nes"; Flags: external; Components: client/tloz or generator/tloz
|
||||
Source: "{code:GetAdvnROMPath}"; DestDir: "{app}"; DestName: "ADVNTURE.BIN"; Flags: external; Components: client/advn
|
||||
Source: "{#source_path}\*"; Excludes: "*.sfc, *.log, data\sprites\alttpr, SNI, EnemizerCLI, Archipelago*.exe"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
|
||||
Source: "{#source_path}\SNI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\SNI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: client/sni
|
||||
Source: "{#source_path}\EnemizerCLI\*"; Excludes: "*.sfc, *.log"; DestDir: "{app}\EnemizerCLI"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: generator/lttp
|
||||
|
@ -128,6 +130,7 @@ Source: "{#source_path}\ArchipelagoStarcraft2Client.exe"; DestDir: "{app}"; Flag
|
|||
Source: "{#source_path}\ArchipelagoZelda1Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/tloz
|
||||
Source: "{#source_path}\ArchipelagoWargrooveClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/wargroove
|
||||
Source: "{#source_path}\ArchipelagoKH2Client.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/kh2
|
||||
Source: "{#source_path}\ArchipelagoAdventureClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/advn
|
||||
Source: "vc_redist.x64.exe"; DestDir: {tmp}; Flags: deleteafterinstall
|
||||
|
||||
[Icons]
|
||||
|
@ -145,6 +148,7 @@ Name: "{group}\{#MyAppName} ChecksFinder Client"; Filename: "{app}\ArchipelagoCh
|
|||
Name: "{group}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\ArchipelagoStarcraft2Client.exe"; Components: client/sc2
|
||||
Name: "{group}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Components: client/tloz
|
||||
Name: "{group}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Components: client/kh2
|
||||
Name: "{group}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Components: client/advn
|
||||
|
||||
Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopicon
|
||||
Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon; Components: server
|
||||
|
@ -160,6 +164,7 @@ Name: "{commondesktop}\{#MyAppName} Starcraft 2 Client"; Filename: "{app}\Archip
|
|||
Name: "{commondesktop}\{#MyAppName} The Legend of Zelda Client"; Filename: "{app}\ArchipelagoZelda1Client.exe"; Tasks: desktopicon; Components: client/tloz
|
||||
Name: "{commondesktop}\{#MyAppName} Wargroove Client"; Filename: "{app}\ArchipelagoWargrooveClient.exe"; Tasks: desktopicon; Components: client/wargroove
|
||||
Name: "{commondesktop}\{#MyAppName} Kingdom Hearts 2 Client"; Filename: "{app}\ArchipelagoKH2Client.exe"; Tasks: desktopicon; Components: client/kh2
|
||||
Name: "{commondesktop}\{#MyAppName} Adventure Client"; Filename: "{app}\ArchipelagoAdventureClient.exe"; Tasks: desktopicon; Components: client/advn
|
||||
|
||||
[Run]
|
||||
|
||||
|
@ -247,6 +252,11 @@ Root: HKCR; Subkey: "{#MyAppName}tlozpatch"; ValueData: "Arc
|
|||
Root: HKCR; Subkey: "{#MyAppName}tlozpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoZelda1Client.exe,0"; ValueType: string; ValueName: ""; Components: client/tloz
|
||||
Root: HKCR; Subkey: "{#MyAppName}tlozpatch\shell\open\command"; ValueData: """{app}\ArchipelagoZelda1Client.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/tloz
|
||||
|
||||
Root: HKCR; Subkey: ".apadvn"; ValueData: "{#MyAppName}advnpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/advn
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch"; ValueData: "Archipelago Adventure Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/advn
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoAdventureClient.exe,0"; ValueType: string; ValueName: ""; Components: client/advn
|
||||
Root: HKCR; Subkey: "{#MyAppName}advnpatch\shell\open\command"; ValueData: """{app}\ArchipelagoAdventureClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/advn
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: server
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: server
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: ""; Components: server
|
||||
|
@ -320,6 +330,9 @@ var LADXROMFilePage: TInputFileWizardPage;
|
|||
var tlozrom: string;
|
||||
var TLoZROMFilePage: TInputFileWizardPage;
|
||||
|
||||
var advnrom: string;
|
||||
var AdvnROMFilePage: TInputFileWizardPage;
|
||||
|
||||
function GetSNESMD5OfFile(const rom: string): string;
|
||||
var data: AnsiString;
|
||||
begin
|
||||
|
@ -490,6 +503,21 @@ begin
|
|||
'.z64');
|
||||
end;
|
||||
|
||||
function AddA26Page(name: string): TInputFileWizardPage;
|
||||
begin
|
||||
Result :=
|
||||
CreateInputFilePage(
|
||||
wpSelectComponents,
|
||||
'Select ROM File',
|
||||
'Where is your ' + name + ' located?',
|
||||
'Select the file, then click Next.');
|
||||
|
||||
Result.Add(
|
||||
'Location of ROM file:',
|
||||
'A2600 ROM files|*.BIN;*.a26|All files|*.*',
|
||||
'.BIN');
|
||||
end;
|
||||
|
||||
function NextButtonClick(CurPageID: Integer): Boolean;
|
||||
begin
|
||||
if (assigned(LttPROMFilePage)) and (CurPageID = LttPROMFilePage.ID) then
|
||||
|
@ -516,6 +544,8 @@ begin
|
|||
Result := not (LADXROMFilePage.Values[0] = '')
|
||||
else if (assigned(TLoZROMFilePage)) and (CurPageID = TLoZROMFilePage.ID) then
|
||||
Result := not (TLoZROMFilePage.Values[0] = '')
|
||||
else if (assigned(AdvnROMFilePage)) and (CurPageID = AdvnROMFilePage.ID) then
|
||||
Result := not (AdvnROMFilePage.Values[0] = '')
|
||||
else
|
||||
Result := True;
|
||||
end;
|
||||
|
@ -712,6 +742,22 @@ begin
|
|||
Result := '';
|
||||
end;
|
||||
|
||||
function GetAdvnROMPath(Param: string): string;
|
||||
begin
|
||||
if Length(advnrom) > 0 then
|
||||
Result := advnrom
|
||||
else if Assigned(AdvnROMFilePage) then
|
||||
begin
|
||||
R := CompareStr(GetMD5OfFile(AdvnROMFilePage.Values[0]), '157bddb7192754a45372be196797f284');
|
||||
if R <> 0 then
|
||||
MsgBox('Adventure ROM validation failed. Very likely wrong file.', mbInformation, MB_OK);
|
||||
|
||||
Result := AdvnROMFilePage.Values[0]
|
||||
end
|
||||
else
|
||||
Result := '';
|
||||
end;
|
||||
|
||||
procedure InitializeWizard();
|
||||
begin
|
||||
AddOoTRomPage();
|
||||
|
@ -759,6 +805,10 @@ begin
|
|||
tlozrom := CheckNESROM('Legend of Zelda, The (U) (PRG0) [!].nes', '337bd6f1a1163df31bf2633665589ab0');
|
||||
if Length(tlozrom) = 0 then
|
||||
TLoZROMFilePage:= AddNESRomPage('Legend of Zelda, The (U) (PRG0) [!].nes');
|
||||
|
||||
advnrom := CheckSMSRom('ADVNTURE.BIN', '157bddb7192754a45372be196797f284');
|
||||
if Length(advnrom) = 0 then
|
||||
AdvnROMFilePage:= AddA26Page('ADVNTURE.BIN');
|
||||
end;
|
||||
|
||||
|
||||
|
@ -789,4 +839,6 @@ begin
|
|||
Result := not (WizardIsComponentSelected('generator/ladx') or WizardIsComponentSelected('client/ladx'));
|
||||
if (assigned(TLoZROMFilePage)) and (PageID = TLoZROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('generator/tloz') or WizardIsComponentSelected('client/tloz'));
|
||||
if (assigned(AdvnROMFilePage)) and (PageID = AdvnROMFilePage.ID) then
|
||||
Result := not (WizardIsComponentSelected('client/advn'));
|
||||
end;
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
from typing import Optional
|
||||
from BaseClasses import ItemClassification, Item
|
||||
|
||||
base_adventure_item_id = 118000000
|
||||
|
||||
|
||||
class AdventureItem(Item):
|
||||
def __init__(self, name: str, classification: ItemClassification, code: Optional[int], player: int):
|
||||
super().__init__(name, classification, code, player)
|
||||
|
||||
|
||||
class ItemData:
|
||||
def __init__(self, id: int, classification: ItemClassification):
|
||||
self.classification = classification
|
||||
self.id = None if id is None else id + base_adventure_item_id
|
||||
self.table_index = id
|
||||
|
||||
|
||||
nothing_item_id = base_adventure_item_id
|
||||
|
||||
# base IDs are the index in the static item data table, which is
|
||||
# not the same order as the items in RAM (but offset 0 is a 16-bit address of
|
||||
# location of room and position data)
|
||||
item_table = {
|
||||
"Yellow Key": ItemData(0xB, ItemClassification.progression_skip_balancing),
|
||||
"White Key": ItemData(0xC, ItemClassification.progression),
|
||||
"Black Key": ItemData(0xD, ItemClassification.progression),
|
||||
"Bridge": ItemData(0xA, ItemClassification.progression),
|
||||
"Magnet": ItemData(0x11, ItemClassification.progression),
|
||||
"Sword": ItemData(0x9, ItemClassification.progression),
|
||||
"Chalice": ItemData(0x10, ItemClassification.progression_skip_balancing),
|
||||
# Non-ROM Adventure items, managed by lua
|
||||
"Left Difficulty Switch": ItemData(0x100, ItemClassification.filler),
|
||||
"Right Difficulty Switch": ItemData(0x101, ItemClassification.filler),
|
||||
# Can use these instead of 'nothing'
|
||||
"Freeincarnate": ItemData(0x102, ItemClassification.filler),
|
||||
# These should only be enabled if fast dragons is on?
|
||||
"Slow Yorgle": ItemData(0x103, ItemClassification.filler),
|
||||
"Slow Grundle": ItemData(0x104, ItemClassification.filler),
|
||||
"Slow Rhindle": ItemData(0x105, ItemClassification.filler),
|
||||
# this should only be enabled if opted into? For now, I'll just exclude them
|
||||
"Revive Dragons": ItemData(0x106, ItemClassification.trap),
|
||||
"nothing": ItemData(0x0, ItemClassification.filler)
|
||||
# Bat Trap
|
||||
# Bat Time Out
|
||||
# "Revive Dragons": ItemData(0x110, ItemClassification.trap)
|
||||
}
|
||||
|
||||
standard_item_max = item_table["Magnet"].id
|
||||
|
||||
|
||||
event_table = {
|
||||
}
|
|
@ -0,0 +1,214 @@
|
|||
from BaseClasses import Location
|
||||
|
||||
base_location_id = 118000000
|
||||
|
||||
|
||||
class AdventureLocation(Location):
|
||||
game: str = "Adventure"
|
||||
|
||||
|
||||
class WorldPosition:
|
||||
room_id: int
|
||||
room_x: int
|
||||
room_y: int
|
||||
|
||||
def __init__(self, room_id: int, room_x: int = None, room_y: int = None):
|
||||
self.room_id = room_id
|
||||
self.room_x = room_x
|
||||
self.room_y = room_y
|
||||
|
||||
def get_position(self, random):
|
||||
if self.room_x is None or self.room_y is None:
|
||||
return random.choice(standard_positions)
|
||||
else:
|
||||
return self.room_x, self.room_y
|
||||
|
||||
|
||||
class LocationData:
|
||||
def __init__(self, region, name, location_id, world_positions: [WorldPosition] = None, event=False,
|
||||
needs_bat_logic: bool = False):
|
||||
self.region: str = region
|
||||
self.name: str = name
|
||||
self.world_positions: [WorldPosition] = world_positions
|
||||
self.room_id: int = None
|
||||
self.room_x: int = None
|
||||
self.room_y: int = None
|
||||
self.location_id: int = location_id
|
||||
if location_id is None:
|
||||
self.short_location_id: int = None
|
||||
self.location_id: int = None
|
||||
else:
|
||||
self.short_location_id: int = location_id
|
||||
self.location_id: int = location_id + base_location_id
|
||||
self.event: bool = event
|
||||
if world_positions is None and not event:
|
||||
self.room_id: int = self.short_location_id
|
||||
self.needs_bat_logic: int = needs_bat_logic
|
||||
self.local_item: int = None
|
||||
|
||||
def get_position(self, random):
|
||||
if self.world_positions is None or len(self.world_positions) == 0:
|
||||
if self.room_id is None:
|
||||
return None
|
||||
self.room_x, self.room_y = random.choice(standard_positions)
|
||||
if self.room_id is None:
|
||||
selected_pos = random.choice(self.world_positions)
|
||||
self.room_id = selected_pos.room_id
|
||||
self.room_x, self.room_y = selected_pos.get_position(random)
|
||||
return self.room_x, self.room_y
|
||||
|
||||
def get_room_id(self, random):
|
||||
if self.world_positions is None or len(self.world_positions) == 0:
|
||||
return None
|
||||
if self.room_id is None:
|
||||
selected_pos = random.choice(self.world_positions)
|
||||
self.room_id = selected_pos.room_id
|
||||
self.room_x, self.room_y = selected_pos.get_position(random)
|
||||
return self.room_id
|
||||
|
||||
|
||||
standard_positions = [
|
||||
(0x80, 0x20),
|
||||
(0x20, 0x20),
|
||||
(0x20, 0x40),
|
||||
(0x20, 0x40),
|
||||
(0x30, 0x20)
|
||||
]
|
||||
|
||||
|
||||
# Gives the most difficult region the dragon can reach and get stuck in from the provided room without the
|
||||
# player unlocking something for it
|
||||
def dragon_room_to_region(room: int) -> str:
|
||||
if room <= 0x11:
|
||||
return "Overworld"
|
||||
elif room <= 0x12:
|
||||
return "YellowCastle"
|
||||
elif room <= 0x16 or room == 0x1B:
|
||||
return "BlackCastle"
|
||||
elif room <= 0x1A:
|
||||
return "WhiteCastleVault"
|
||||
elif room <= 0x1D:
|
||||
return "Overworld"
|
||||
elif room <= 0x1E:
|
||||
return "CreditsRoom"
|
||||
|
||||
|
||||
def get_random_room_in_regions(regions: [str], random) -> int:
|
||||
possible_rooms = {}
|
||||
for locname in location_table:
|
||||
if location_table[locname].region in regions:
|
||||
room = location_table[locname].get_room_id(random)
|
||||
if room is not None:
|
||||
possible_rooms[room] = location_table[locname].room_id
|
||||
return random.choice(list(possible_rooms.keys()))
|
||||
|
||||
|
||||
location_table = {
|
||||
"Blue Labyrinth 0": LocationData("Overworld", "Blue Labyrinth 0", 0x4,
|
||||
[WorldPosition(0x4, 0x83, 0x47), # exit upper right
|
||||
WorldPosition(0x4, 0x12, 0x47), # exit upper left
|
||||
WorldPosition(0x4, 0x65, 0x20), # exit bottom right
|
||||
WorldPosition(0x4, 0x2A, 0x20), # exit bottom left
|
||||
WorldPosition(0x5, 0x4B, 0x60), # T room, top
|
||||
WorldPosition(0x5, 0x28, 0x1F), # T room, bottom left
|
||||
WorldPosition(0x5, 0x70, 0x1F), # T room, bottom right
|
||||
]),
|
||||
"Blue Labyrinth 1": LocationData("Overworld", "Blue Labyrinth 1", 0x6,
|
||||
[WorldPosition(0x6, 0x8C, 0x20), # final turn bottom right
|
||||
WorldPosition(0x6, 0x03, 0x20), # final turn bottom left
|
||||
WorldPosition(0x6, 0x4B, 0x30), # final turn center
|
||||
WorldPosition(0x7, 0x4B, 0x40), # straightaway center
|
||||
WorldPosition(0x8, 0x40, 0x40), # entrance middle loop
|
||||
WorldPosition(0x8, 0x4B, 0x60), # entrance upper loop
|
||||
WorldPosition(0x8, 0x8C, 0x5E), # entrance right loop
|
||||
]),
|
||||
"Catacombs": LocationData("Overworld", "Catacombs", 0x9,
|
||||
[WorldPosition(0x9, 0x49, 0x40),
|
||||
WorldPosition(0x9, 0x4b, 0x20),
|
||||
WorldPosition(0xA),
|
||||
WorldPosition(0xA),
|
||||
WorldPosition(0xB, 0x40, 0x40),
|
||||
WorldPosition(0xB, 0x22, 0x1f),
|
||||
WorldPosition(0xB, 0x70, 0x1f)]),
|
||||
"Adjacent to Catacombs": LocationData("Overworld", "Adjacent to Catacombs", 0xC,
|
||||
[WorldPosition(0xC),
|
||||
WorldPosition(0xD)]),
|
||||
"Southwest of Catacombs": LocationData("Overworld", "Southwest of Catacombs", 0xE),
|
||||
"White Castle Gate": LocationData("Overworld", "White Castle Gate", 0xF),
|
||||
"Black Castle Gate": LocationData("Overworld", "Black Castle Gate", 0x10),
|
||||
"Yellow Castle Gate": LocationData("Overworld", "Yellow Castle Gate", 0x11),
|
||||
"Inside Yellow Castle": LocationData("YellowCastle", "Inside Yellow Castle", 0x12),
|
||||
"Dungeon0": LocationData("BlackCastle", "Dungeon0", 0x13,
|
||||
[WorldPosition(0x13),
|
||||
WorldPosition(0x14)]),
|
||||
"Dungeon Vault": LocationData("BlackCastleVault", "Dungeon Vault", 0xB5,
|
||||
[WorldPosition(0x15, 0x46, 0x1B)],
|
||||
needs_bat_logic=True),
|
||||
"Dungeon1": LocationData("BlackCastle", "Dungeon1", 0x15,
|
||||
[WorldPosition(0x15),
|
||||
WorldPosition(0x16)]),
|
||||
"RedMaze0": LocationData("WhiteCastle", "RedMaze0", 0x17,
|
||||
[WorldPosition(0x17, 0x70, 0x40), # right side third room
|
||||
WorldPosition(0x17, 0x18, 0x40), # left side third room
|
||||
WorldPosition(0x18, 0x20, 0x40),
|
||||
WorldPosition(0x18, 0x1A, 0x3F), # left side second room
|
||||
WorldPosition(0x18, 0x70, 0x3F), # right side second room
|
||||
]),
|
||||
"Red Maze Vault Entrance": LocationData("WhiteCastlePreVaultPeek", "Red Maze Vault Entrance", 0xB7,
|
||||
[WorldPosition(0x17, 0x50, 0x60)],
|
||||
needs_bat_logic=True),
|
||||
"Red Maze Vault": LocationData("WhiteCastleVault", "Red Maze Vault", 0x19,
|
||||
[WorldPosition(0x19, 0x4E, 0x35)],
|
||||
needs_bat_logic=True),
|
||||
"RedMaze1": LocationData("WhiteCastle", "RedMaze1", 0x1A), # entrance
|
||||
"Black Castle Foyer": LocationData("BlackCastle", "Black Castle Foyer", 0x1B),
|
||||
"Northeast of Catacombs": LocationData("Overworld", "Northeast of Catacombs", 0x1C),
|
||||
"Southeast of Catacombs": LocationData("Overworld", "Southeast of Catacombs", 0x1D),
|
||||
"Credits Left Side": LocationData("CreditsRoom", "Credits Left Side", 0x1E,
|
||||
[WorldPosition(0x1E, 0x25, 0x50)]),
|
||||
"Credits Right Side": LocationData("CreditsRoomFarSide", "Credits Right Side", 0xBE,
|
||||
[WorldPosition(0x1E, 0x70, 0x40)],
|
||||
needs_bat_logic=True),
|
||||
"Chalice Home": LocationData("YellowCastle", "Chalice Home", None, event=True),
|
||||
"Slay Yorgle": LocationData("Varies", "Slay Yorgle", 0xD1, event=False),
|
||||
"Slay Grundle": LocationData("Varies", "Slay Grundle", 0xD2, event=False),
|
||||
"Slay Rhindle": LocationData("Varies", "Slay Rhindle", 0xD0, event=False),
|
||||
}
|
||||
|
||||
# the old location table, for reference
|
||||
location_table_old = {
|
||||
"Blue Labyrinth 0": LocationData("Overworld", "Blue Labyrinth 0", 0x4),
|
||||
"Blue Labyrinth 1": LocationData("Overworld", "Blue Labyrinth 1", 0x5),
|
||||
"Blue Labyrinth 2": LocationData("Overworld", "Blue Labyrinth 2", 0x6),
|
||||
"Blue Labyrinth 3": LocationData("Overworld", "Blue Labyrinth 3", 0x7),
|
||||
"Blue Labyrinth 4": LocationData("Overworld", "Blue Labyrinth 4", 0x8),
|
||||
"Catacombs0": LocationData("Overworld", "Catacombs0", 0x9),
|
||||
"Catacombs1": LocationData("Overworld", "Catacombs1", 0xA),
|
||||
"Catacombs2": LocationData("Overworld", "Catacombs2", 0xB),
|
||||
"East of Catacombs": LocationData("Overworld", "East of Catacombs", 0xC),
|
||||
"West of Catacombs": LocationData("Overworld", "West of Catacombs", 0xD),
|
||||
"Southwest of Catacombs": LocationData("Overworld", "Southwest of Catacombs", 0xE),
|
||||
"White Castle Gate": LocationData("Overworld", "White Castle Gate", 0xF),
|
||||
"Black Castle Gate": LocationData("Overworld", "Black Castle Gate", 0x10),
|
||||
"Yellow Castle Gate": LocationData("Overworld", "Yellow Castle Gate", 0x11),
|
||||
"Inside Yellow Castle": LocationData("YellowCastle", "Inside Yellow Castle", 0x12),
|
||||
"Dungeon0": LocationData("BlackCastle", "Dungeon0", 0x13),
|
||||
"Dungeon1": LocationData("BlackCastle", "Dungeon1", 0x14),
|
||||
"Dungeon Vault": LocationData("BlackCastleVault", "Dungeon Vault", 0x15,
|
||||
[WorldPosition(0xB5, 0x46, 0x1B)]),
|
||||
"Dungeon2": LocationData("BlackCastle", "Dungeon2", 0x15),
|
||||
"Dungeon3": LocationData("BlackCastle", "Dungeon3", 0x16),
|
||||
"RedMaze0": LocationData("WhiteCastle", "RedMaze0", 0x17, [WorldPosition(0x17, 0x70, 0x40)]),
|
||||
"RedMaze1": LocationData("WhiteCastle", "RedMaze1", 0x18, [WorldPosition(0x18, 0x20, 0x40)]),
|
||||
"Red Maze Vault Entrance": LocationData("WhiteCastlePreVaultPeek", "Red Maze Vault Entrance",
|
||||
0x17, [WorldPosition(0xB7, 0x50, 0x60)]),
|
||||
"Red Maze Vault": LocationData("WhiteCastleVault", "Red Maze Vault", 0x19, [WorldPosition(0x19, 0x4E, 0x35)]),
|
||||
"RedMaze3": LocationData("WhiteCastle", "RedMaze3", 0x1A),
|
||||
"Black Castle Foyer": LocationData("BlackCastle", "Black Castle Foyer", 0x1B),
|
||||
"Northeast of Catacombs": LocationData("Overworld", "Northeast of Catacombs", 0x1C),
|
||||
"Southeast of Catacombs": LocationData("Overworld", "Southeast of Catacombs", 0x1D),
|
||||
"Credits Left Side": LocationData("CreditsRoom", "Credits Left Side", 0x1E, [WorldPosition(0x1E, 0x25, 0x50)]),
|
||||
"Credits Right Side": LocationData("CreditsRoomFarSide", "Credits Right Side", 0x1E,
|
||||
[WorldPosition(0xBE, 0x70, 0x40)]),
|
||||
"Chalice Home": LocationData("YellowCastle", "Chalice Home", None, event=True)
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
# probably I should generate this from the list file
|
||||
|
||||
static_item_data_location = 0xe9d
|
||||
static_item_element_size = 9
|
||||
static_first_dragon_index = 6
|
||||
item_position_table = 0x402
|
||||
items_ram_start = 0xa1
|
||||
connector_port_offset = 0xff9
|
||||
# dragon speeds are hardcoded directly in their respective movement subroutines, not in their item table or state data
|
||||
# so this is the second byte of an LDA immediate instruction
|
||||
yorgle_speed_data_location = 0x724
|
||||
grundle_speed_data_location = 0x73f
|
||||
rhindle_speed_data_location = 0x709
|
||||
|
||||
|
||||
# in case I need to place a rom address in the rom
|
||||
rom_address_space_start = 0xf000
|
||||
|
||||
start_castle_offset = 0x39c
|
||||
start_castle_values = [0x11, 0x10, 0x0F]
|
||||
"""yellow, black, white castle gate rooms"""
|
||||
|
||||
# indexed by static item table index. 0x00 indicates the position data is in ROM and is irrelevant to the randomizer
|
||||
item_ram_addresses = [
|
||||
0xD9, # lamp
|
||||
0x00, # portcullis 1
|
||||
0x00, # portcullis 2
|
||||
0x00, # portcullis 3
|
||||
0x00, # author name
|
||||
0x00, # GO object
|
||||
0xA4, # Rhindle
|
||||
0xA9, # Yorgle
|
||||
0xAE, # Grundle
|
||||
0xB6, # Sword
|
||||
0xBC, # Bridge
|
||||
0xBF, # Yellow Key
|
||||
0xC2, # White key
|
||||
0xC5, # Black key
|
||||
0xCB, # Bat
|
||||
0xA1, # Dot
|
||||
0xB9, # Chalice
|
||||
0xB3, # Magnet
|
||||
0xE7, # AP object 1
|
||||
0xEA, # AP bat object
|
||||
0xBC, # NULL object (end of table)
|
||||
]
|
|
@ -0,0 +1,244 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle
|
||||
|
||||
|
||||
class FreeincarnateMax(Range):
|
||||
"""How many maximum freeincarnate items to allow
|
||||
|
||||
When done generating items, any remaining item slots will be filled
|
||||
with freeincarnates, up to this maximum amount. Any remaining item
|
||||
slots after that will be 'nothing' items placed locally, so in multigame
|
||||
multiworlds, keeping this value high will allow more items from other games
|
||||
into Adventure.
|
||||
"""
|
||||
display_name = "Freeincarnate Maximum"
|
||||
range_start = 0
|
||||
range_end = 17
|
||||
default = 17
|
||||
|
||||
|
||||
class ItemRandoType(Choice):
|
||||
"""Choose how items are placed in the game
|
||||
|
||||
Not yet implemented. Currently only traditional supported
|
||||
Traditional: Adventure items are not in the map until
|
||||
they are collected (except local items) and are dropped
|
||||
on the player when collected. Adventure items are not checks.
|
||||
Inactive: Every item is placed, but is inactive until collected.
|
||||
Each item touched is a check. The bat ignores inactive items.
|
||||
|
||||
Supported values: traditional, inactive
|
||||
Default value: traditional
|
||||
"""
|
||||
|
||||
display_name = "Item type"
|
||||
option_traditional = 0x00
|
||||
option_inactive = 0x01
|
||||
default = option_traditional
|
||||
|
||||
|
||||
class DragonSlayCheck(DefaultOnToggle):
|
||||
"""If true, slaying each dragon for the first time is a check
|
||||
"""
|
||||
display_name = "Slay Dragon Checks"
|
||||
|
||||
|
||||
class TrapBatCheck(Choice):
|
||||
"""
|
||||
Locking the bat inside a castle may be a check
|
||||
|
||||
Not yet implemented
|
||||
If set to yes, the bat will not start inside a castle.
|
||||
Setting with_key requires the matching castle key to also be
|
||||
in the castle with the bat, achieved by dropping the key in the
|
||||
path of the portcullis as it falls. This setting is not recommended with the bat use_logic setting
|
||||
|
||||
Supported values: no, yes, with_key
|
||||
Default value: yes
|
||||
"""
|
||||
display_name = "Trap bat check"
|
||||
option_no_check = 0x0
|
||||
option_yes_key_optional = 0x1
|
||||
option_with_key = 0x2
|
||||
default = option_yes_key_optional
|
||||
|
||||
|
||||
class DragonRandoType(Choice):
|
||||
"""
|
||||
How to randomize the dragon starting locations
|
||||
|
||||
normal: Grundle is in the overworld, Yorgle in the white castle, and Rhindle in the black castle
|
||||
shuffle: A random dragon is placed in the overworld, one in the white castle, and one in the black castle
|
||||
overworldplus: Dragons can be placed anywhere, but at least one will be in the overworld
|
||||
randomized: Dragons can be anywhere except the credits room
|
||||
|
||||
|
||||
Supported values: normal, shuffle, overworldplus, randomized
|
||||
Default value: shuffle
|
||||
"""
|
||||
display_name = "Dragon Randomization"
|
||||
option_normal = 0x0
|
||||
option_shuffle = 0x1
|
||||
option_overworldplus = 0x2
|
||||
option_randomized = 0x3
|
||||
default = option_shuffle
|
||||
|
||||
|
||||
class BatLogic(Choice):
|
||||
"""How the bat is considered for logic
|
||||
|
||||
With cannot_break, the bat cannot pick up an item that starts out-of-logic until the player touches it
|
||||
With can_break, the bat is free to pick up any items, even if they are out-of-logic
|
||||
With use_logic, the bat can pick up anything just like can_break, and locations are no longer considered to require
|
||||
the magnet or bridge to collect, since the bat can retrieve these.
|
||||
A future option may allow the bat itself to be placed as an item.
|
||||
|
||||
Supported values: cannot_break, can_break, use_logic
|
||||
Default value: can_break
|
||||
"""
|
||||
display_name = "Bat Logic"
|
||||
option_cannot_break = 0x0
|
||||
option_can_break = 0x1
|
||||
option_use_logic = 0x2
|
||||
default = option_can_break
|
||||
|
||||
|
||||
class YorgleStartingSpeed(Range):
|
||||
"""
|
||||
Sets Yorgle's initial speed. Yorgle has a speed of 2 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Yorgle MaxSpeed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 2
|
||||
|
||||
|
||||
class YorgleMinimumSpeed(Range):
|
||||
"""
|
||||
Sets Yorgle's speed when all speed reducers are found. Yorgle has a speed of 2 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Yorgle Min Speed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 1
|
||||
|
||||
|
||||
class GrundleStartingSpeed(Range):
|
||||
"""
|
||||
Sets Grundle's initial speed. Grundle has a speed of 2 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Grundle MaxSpeed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 2
|
||||
|
||||
|
||||
class GrundleMinimumSpeed(Range):
|
||||
"""
|
||||
Sets Grundle's speed when all speed reducers are found. Grundle has a speed of 2 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Grundle Min Speed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 1
|
||||
|
||||
|
||||
class RhindleStartingSpeed(Range):
|
||||
"""
|
||||
Sets Rhindle's initial speed. Rhindle has a speed of 3 in the original game
|
||||
Default value: 3
|
||||
"""
|
||||
display_name = "Rhindle MaxSpeed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 3
|
||||
|
||||
|
||||
class RhindleMinimumSpeed(Range):
|
||||
"""
|
||||
Sets Rhindle's speed when all speed reducers are found. Rhindle has a speed of 3 in the original game
|
||||
Default value: 2
|
||||
"""
|
||||
display_name = "Rhindle Min Speed"
|
||||
range_start = 1
|
||||
range_end = 9
|
||||
default = 2
|
||||
|
||||
|
||||
class ConnectorMultiSlot(Toggle):
|
||||
"""If true, the client and lua connector will add lowest 8 bits of the player slot
|
||||
to the port number used to connect to each other, to simplify connecting multiple local
|
||||
clients to local BizHawks.
|
||||
Set in the yaml, since the connector has to read this out of the rom file before connecting.
|
||||
"""
|
||||
display_name = "Connector Multi-Slot"
|
||||
|
||||
|
||||
class DifficultySwitchA(Choice):
|
||||
"""Set availability of left difficulty switch
|
||||
This controls the speed of the dragons' bite animation
|
||||
|
||||
"""
|
||||
display_name = "Left Difficulty Switch"
|
||||
option_normal = 0x0
|
||||
option_locked_hard = 0x1
|
||||
option_hard_with_unlock_item = 0x2
|
||||
default = option_hard_with_unlock_item
|
||||
|
||||
|
||||
class DifficultySwitchB(Choice):
|
||||
"""Set availability of right difficulty switch
|
||||
On hard, dragons will run away from the sword
|
||||
|
||||
"""
|
||||
display_name = "Right Difficulty Switch"
|
||||
option_normal = 0x0
|
||||
option_locked_hard = 0x1
|
||||
option_hard_with_unlock_item = 0x2
|
||||
default = option_hard_with_unlock_item
|
||||
|
||||
|
||||
class StartCastle(Choice):
|
||||
"""Choose or randomize which castle to start in front of.
|
||||
|
||||
This affects both normal start and reincarnation. Starting
|
||||
at the black castle may give easy dot runs, while starting
|
||||
at the white castle may make them more dangerous! Also, not
|
||||
starting at the yellow castle can make delivering the chalice
|
||||
with a full inventory slightly less trivial.
|
||||
|
||||
This doesn't affect logic since all the castles are reachable
|
||||
from each other.
|
||||
"""
|
||||
display_name = "Start Castle"
|
||||
option_yellow = 0
|
||||
option_black = 1
|
||||
option_white = 2
|
||||
default = option_yellow
|
||||
|
||||
|
||||
adventure_option_definitions: Dict[str, type(Option)] = {
|
||||
"dragon_slay_check": DragonSlayCheck,
|
||||
"death_link": DeathLink,
|
||||
"bat_logic": BatLogic,
|
||||
"freeincarnate_max": FreeincarnateMax,
|
||||
"dragon_rando_type": DragonRandoType,
|
||||
"connector_multi_slot": ConnectorMultiSlot,
|
||||
"yorgle_speed": YorgleStartingSpeed,
|
||||
"yorgle_min_speed": YorgleMinimumSpeed,
|
||||
"grundle_speed": GrundleStartingSpeed,
|
||||
"grundle_min_speed": GrundleMinimumSpeed,
|
||||
"rhindle_speed": RhindleStartingSpeed,
|
||||
"rhindle_min_speed": RhindleMinimumSpeed,
|
||||
"difficulty_switch_a": DifficultySwitchA,
|
||||
"difficulty_switch_b": DifficultySwitchB,
|
||||
"start_castle": StartCastle,
|
||||
|
||||
}
|
|
@ -0,0 +1,160 @@
|
|||
from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType
|
||||
from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region
|
||||
|
||||
|
||||
def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True,
|
||||
one_way=False, name=None):
|
||||
source_region = world.get_region(source, player)
|
||||
target_region = world.get_region(target, player)
|
||||
|
||||
if name is None:
|
||||
name = source + " to " + target
|
||||
|
||||
connection = Entrance(
|
||||
player,
|
||||
name,
|
||||
source_region
|
||||
)
|
||||
|
||||
connection.access_rule = rule
|
||||
|
||||
source_region.exits.append(connection)
|
||||
connection.connect(target_region)
|
||||
if not one_way:
|
||||
connect(world, player, target, source, rule, True)
|
||||
|
||||
|
||||
def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None:
|
||||
for name, locdata in location_table.items():
|
||||
locdata.get_position(multiworld.random)
|
||||
|
||||
menu = Region("Menu", player, multiworld)
|
||||
|
||||
menu.exits.append(Entrance(player, "GameStart", menu))
|
||||
multiworld.regions.append(menu)
|
||||
|
||||
overworld = Region("Overworld", player, multiworld)
|
||||
overworld.exits.append(Entrance(player, "YellowCastlePort", overworld))
|
||||
overworld.exits.append(Entrance(player, "WhiteCastlePort", overworld))
|
||||
overworld.exits.append(Entrance(player, "BlackCastlePort", overworld))
|
||||
overworld.exits.append(Entrance(player, "CreditsWall", overworld))
|
||||
multiworld.regions.append(overworld)
|
||||
|
||||
yellow_castle = Region("YellowCastle", player, multiworld, "Yellow Castle")
|
||||
yellow_castle.exits.append(Entrance(player, "YellowCastleExit", yellow_castle))
|
||||
multiworld.regions.append(yellow_castle)
|
||||
|
||||
white_castle = Region("WhiteCastle", player, multiworld, "White Castle")
|
||||
white_castle.exits.append(Entrance(player, "WhiteCastleExit", white_castle))
|
||||
white_castle.exits.append(Entrance(player, "WhiteCastleSecretPassage", white_castle))
|
||||
white_castle.exits.append(Entrance(player, "WhiteCastlePeekPassage", white_castle))
|
||||
multiworld.regions.append(white_castle)
|
||||
|
||||
white_castle_pre_vault_peek = Region("WhiteCastlePreVaultPeek", player, multiworld, "White Castle Secret Peek")
|
||||
white_castle_pre_vault_peek.exits.append(Entrance(player, "WhiteCastleFromPeek", white_castle_pre_vault_peek))
|
||||
multiworld.regions.append(white_castle_pre_vault_peek)
|
||||
|
||||
white_castle_secret_room = Region("WhiteCastleVault", player, multiworld, "White Castle Vault",)
|
||||
white_castle_secret_room.exits.append(Entrance(player, "WhiteCastleReturnPassage", white_castle_secret_room))
|
||||
multiworld.regions.append(white_castle_secret_room)
|
||||
|
||||
black_castle = Region("BlackCastle", player, multiworld, "Black Castle")
|
||||
black_castle.exits.append(Entrance(player, "BlackCastleExit", black_castle))
|
||||
black_castle.exits.append(Entrance(player, "BlackCastleVaultEntrance", black_castle))
|
||||
multiworld.regions.append(black_castle)
|
||||
|
||||
black_castle_secret_room = Region("BlackCastleVault", player, multiworld, "Black Castle Vault")
|
||||
black_castle_secret_room.exits.append(Entrance(player, "BlackCastleReturnPassage", black_castle_secret_room))
|
||||
multiworld.regions.append(black_castle_secret_room)
|
||||
|
||||
credits_room = Region("CreditsRoom", player, multiworld, "Credits Room")
|
||||
credits_room.exits.append(Entrance(player, "CreditsExit", credits_room))
|
||||
credits_room.exits.append(Entrance(player, "CreditsToFarSide", credits_room))
|
||||
multiworld.regions.append(credits_room)
|
||||
|
||||
credits_room_far_side = Region("CreditsRoomFarSide", player, multiworld, "Credits Far Side")
|
||||
credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side))
|
||||
multiworld.regions.append(credits_room_far_side)
|
||||
|
||||
dragon_slay_check = multiworld.dragon_slay_check[player].value
|
||||
priority_locations = determine_priority_locations(multiworld, dragon_slay_check)
|
||||
|
||||
for name, location_data in location_table.items():
|
||||
require_sword = False
|
||||
if location_data.region == "Varies":
|
||||
if location_data.name == "Slay Yorgle":
|
||||
if not dragon_slay_check:
|
||||
continue
|
||||
region_name = dragon_room_to_region(dragon_rooms[0])
|
||||
elif location_data.name == "Slay Grundle":
|
||||
if not dragon_slay_check:
|
||||
continue
|
||||
region_name = dragon_room_to_region(dragon_rooms[1])
|
||||
elif location_data.name == "Slay Rhindle":
|
||||
if not dragon_slay_check:
|
||||
continue
|
||||
region_name = dragon_room_to_region(dragon_rooms[2])
|
||||
else:
|
||||
raise Exception(f"Unknown location region for {location_data.name}")
|
||||
r = multiworld.get_region(region_name, player)
|
||||
else:
|
||||
r = multiworld.get_region(location_data.region, player)
|
||||
|
||||
adventure_loc = AdventureLocation(player, location_data.name, location_data.location_id, r)
|
||||
if adventure_loc.name in priority_locations:
|
||||
adventure_loc.progress_type = LocationProgressType.PRIORITY
|
||||
r.locations.append(adventure_loc)
|
||||
|
||||
# In a tracker and plando-free world, I'd determine unused locations here and not add them.
|
||||
# But that would cause problems with both plandos and trackers. So I guess I'll stick
|
||||
# with filling in with 'nothing' in pre_fill.
|
||||
|
||||
# in the future, I may randomize the map some, and that will require moving
|
||||
# connections to later, probably
|
||||
|
||||
multiworld.get_entrance("GameStart", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
|
||||
multiworld.get_entrance("YellowCastlePort", player) \
|
||||
.connect(multiworld.get_region("YellowCastle", player))
|
||||
multiworld.get_entrance("YellowCastleExit", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
|
||||
multiworld.get_entrance("WhiteCastlePort", player) \
|
||||
.connect(multiworld.get_region("WhiteCastle", player))
|
||||
multiworld.get_entrance("WhiteCastleExit", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
|
||||
multiworld.get_entrance("WhiteCastleSecretPassage", player) \
|
||||
.connect(multiworld.get_region("WhiteCastleVault", player))
|
||||
multiworld.get_entrance("WhiteCastleReturnPassage", player) \
|
||||
.connect(multiworld.get_region("WhiteCastle", player))
|
||||
multiworld.get_entrance("WhiteCastlePeekPassage", player) \
|
||||
.connect(multiworld.get_region("WhiteCastlePreVaultPeek", player))
|
||||
multiworld.get_entrance("WhiteCastleFromPeek", player) \
|
||||
.connect(multiworld.get_region("WhiteCastle", player))
|
||||
|
||||
multiworld.get_entrance("BlackCastlePort", player) \
|
||||
.connect(multiworld.get_region("BlackCastle", player))
|
||||
multiworld.get_entrance("BlackCastleExit", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
multiworld.get_entrance("BlackCastleVaultEntrance", player) \
|
||||
.connect(multiworld.get_region("BlackCastleVault", player))
|
||||
multiworld.get_entrance("BlackCastleReturnPassage", player) \
|
||||
.connect(multiworld.get_region("BlackCastle", player))
|
||||
|
||||
multiworld.get_entrance("CreditsWall", player) \
|
||||
.connect(multiworld.get_region("CreditsRoom", player))
|
||||
multiworld.get_entrance("CreditsExit", player) \
|
||||
.connect(multiworld.get_region("Overworld", player))
|
||||
|
||||
multiworld.get_entrance("CreditsToFarSide", player) \
|
||||
.connect(multiworld.get_region("CreditsRoomFarSide", player))
|
||||
multiworld.get_entrance("CreditsFromFarSide", player) \
|
||||
.connect(multiworld.get_region("CreditsRoom", player))
|
||||
|
||||
|
||||
# Placeholder for adding sets of priority locations at generation, possibly as an option in the future
|
||||
def determine_priority_locations(world: MultiWorld, dragon_slay_check: bool) -> {}:
|
||||
priority_locations = {}
|
||||
return priority_locations
|
|
@ -0,0 +1,321 @@
|
|||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import zipfile
|
||||
from typing import Optional, Any
|
||||
|
||||
import Utils
|
||||
from .Locations import AdventureLocation, LocationData
|
||||
from Utils import OptionsType
|
||||
from worlds.Files import APDeltaPatch, AutoPatchRegister, APContainer
|
||||
from itertools import chain
|
||||
|
||||
import bsdiff4
|
||||
|
||||
ADVENTUREHASH: str = "157bddb7192754a45372be196797f284"
|
||||
|
||||
|
||||
class AdventureAutoCollectLocation:
|
||||
short_location_id: int = 0
|
||||
room_id: int = 0
|
||||
|
||||
def __init__(self, short_location_id: int, room_id: int):
|
||||
self.short_location_id = short_location_id
|
||||
self.room_id = room_id
|
||||
|
||||
def get_dict(self):
|
||||
return {
|
||||
"short_location_id": self.short_location_id,
|
||||
"room_id": self.room_id,
|
||||
}
|
||||
|
||||
|
||||
class AdventureForeignItemInfo:
|
||||
short_location_id: int = 0
|
||||
room_id: int = 0
|
||||
room_x: int = 0
|
||||
room_y: int = 0
|
||||
|
||||
def __init__(self, short_location_id: int, room_id: int, room_x: int, room_y: int):
|
||||
self.short_location_id = short_location_id
|
||||
self.room_id = room_id
|
||||
self.room_x = room_x
|
||||
self.room_y = room_y
|
||||
|
||||
def get_dict(self):
|
||||
return {
|
||||
"short_location_id": self.short_location_id,
|
||||
"room_id": self.room_id,
|
||||
"room_x": self.room_x,
|
||||
"room_y": self.room_y,
|
||||
}
|
||||
|
||||
|
||||
class BatNoTouchLocation:
|
||||
short_location_id: int
|
||||
room_id: int
|
||||
room_x: int
|
||||
room_y: int
|
||||
local_item: int
|
||||
|
||||
def __init__(self, short_location_id: int, room_id: int, room_x: int, room_y: int, local_item: int = None):
|
||||
self.short_location_id = short_location_id
|
||||
self.room_id = room_id
|
||||
self.room_x = room_x
|
||||
self.room_y = room_y
|
||||
self.local_item = local_item
|
||||
|
||||
def get_dict(self):
|
||||
ret_dict = {
|
||||
"short_location_id": self.short_location_id,
|
||||
"room_id": self.room_id,
|
||||
"room_x": self.room_x,
|
||||
"room_y": self.room_y,
|
||||
}
|
||||
if self.local_item is not None:
|
||||
ret_dict["local_item"] = self.local_item
|
||||
else:
|
||||
ret_dict["local_item"] = 255
|
||||
return ret_dict
|
||||
|
||||
|
||||
class AdventureDeltaPatch(APContainer, metaclass=AutoPatchRegister):
|
||||
hash = ADVENTUREHASH
|
||||
game = "Adventure"
|
||||
patch_file_ending = ".apadvn"
|
||||
zip_version: int = 2
|
||||
|
||||
# locations: [], autocollect: [], seed_name: bytes,
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
patch_only = True
|
||||
if "autocollect" in kwargs:
|
||||
patch_only = False
|
||||
self.foreign_items: [AdventureForeignItemInfo] = [AdventureForeignItemInfo(loc.short_location_id, loc.room_id, loc.room_x, loc.room_y)
|
||||
for loc in kwargs["locations"]]
|
||||
|
||||
self.autocollect_items: [AdventureAutoCollectLocation] = kwargs["autocollect"]
|
||||
self.seedName: bytes = kwargs["seed_name"]
|
||||
self.local_item_locations: {} = kwargs["local_item_locations"]
|
||||
self.dragon_speed_reducer_info: {} = kwargs["dragon_speed_reducer_info"]
|
||||
self.diff_a_mode: int = kwargs["diff_a_mode"]
|
||||
self.diff_b_mode: int = kwargs["diff_b_mode"]
|
||||
self.bat_logic: int = kwargs["bat_logic"]
|
||||
self.bat_no_touch_locations: [LocationData] = kwargs["bat_no_touch_locations"]
|
||||
self.rom_deltas: {int, int} = kwargs["rom_deltas"]
|
||||
del kwargs["locations"]
|
||||
del kwargs["autocollect"]
|
||||
del kwargs["seed_name"]
|
||||
del kwargs["local_item_locations"]
|
||||
del kwargs["dragon_speed_reducer_info"]
|
||||
del kwargs["diff_a_mode"]
|
||||
del kwargs["diff_b_mode"]
|
||||
del kwargs["bat_logic"]
|
||||
del kwargs["bat_no_touch_locations"]
|
||||
del kwargs["rom_deltas"]
|
||||
super(AdventureDeltaPatch, self).__init__(*args, **kwargs)
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(AdventureDeltaPatch, self).write_contents(opened_zipfile)
|
||||
# write Delta
|
||||
opened_zipfile.writestr("zip_version",
|
||||
self.zip_version.to_bytes(1, "little"),
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.foreign_items is not None:
|
||||
loc_bytes = []
|
||||
for foreign_item in self.foreign_items:
|
||||
loc_bytes.append(foreign_item.short_location_id)
|
||||
loc_bytes.append(foreign_item.room_id)
|
||||
loc_bytes.append(foreign_item.room_x)
|
||||
loc_bytes.append(foreign_item.room_y)
|
||||
opened_zipfile.writestr("adventure_locations",
|
||||
bytes(loc_bytes),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.autocollect_items is not None:
|
||||
loc_bytes = []
|
||||
for item in self.autocollect_items:
|
||||
loc_bytes.append(item.short_location_id)
|
||||
loc_bytes.append(item.room_id)
|
||||
opened_zipfile.writestr("adventure_autocollect",
|
||||
bytes(loc_bytes),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.player_name is not None:
|
||||
opened_zipfile.writestr("player",
|
||||
self.player_name, # UTF-8
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.seedName is not None:
|
||||
opened_zipfile.writestr("seedName",
|
||||
self.seedName,
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.local_item_locations is not None:
|
||||
opened_zipfile.writestr("local_item_locations",
|
||||
json.dumps(self.local_item_locations),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.dragon_speed_reducer_info is not None:
|
||||
opened_zipfile.writestr("dragon_speed_reducer_info",
|
||||
json.dumps(self.dragon_speed_reducer_info),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.diff_a_mode is not None:
|
||||
opened_zipfile.writestr("diff_a_mode",
|
||||
self.diff_a_mode.to_bytes(1, "little"),
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.diff_b_mode is not None:
|
||||
opened_zipfile.writestr("diff_b_mode",
|
||||
self.diff_b_mode.to_bytes(1, "little"),
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.bat_logic is not None:
|
||||
opened_zipfile.writestr("bat_logic",
|
||||
self.bat_logic.to_bytes(1, "little"),
|
||||
compress_type=zipfile.ZIP_STORED)
|
||||
if self.bat_no_touch_locations is not None:
|
||||
loc_bytes = []
|
||||
for loc in self.bat_no_touch_locations:
|
||||
loc_bytes.append(loc.short_location_id) # used for AP items managed by script
|
||||
loc_bytes.append(loc.room_id) # used for local items placed in rom
|
||||
loc_bytes.append(loc.room_x)
|
||||
loc_bytes.append(loc.room_y)
|
||||
loc_bytes.append(0xff if loc.local_item is None else loc.local_item)
|
||||
opened_zipfile.writestr("bat_no_touch_locations",
|
||||
bytes(loc_bytes),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
if self.rom_deltas is not None:
|
||||
# this is not an efficient way to do this AT ALL, but Adventure's data is so tiny it shouldn't matter
|
||||
# if you're looking at doing something like this for another game, consider encoding your rom changes
|
||||
# in a more efficient way
|
||||
opened_zipfile.writestr("rom_deltas",
|
||||
json.dumps(self.rom_deltas),
|
||||
compress_type=zipfile.ZIP_LZMA)
|
||||
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(AdventureDeltaPatch, self).read_contents(opened_zipfile)
|
||||
self.foreign_items = AdventureDeltaPatch.read_foreign_items(opened_zipfile)
|
||||
self.autocollect_items = AdventureDeltaPatch.read_autocollect_items(opened_zipfile)
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes()
|
||||
|
||||
@classmethod
|
||||
def check_version(cls, opened_zipfile: zipfile.ZipFile) -> bool:
|
||||
version_bytes = opened_zipfile.read("zip_version")
|
||||
version = 0
|
||||
if version_bytes is not None:
|
||||
version = int.from_bytes(version_bytes, "little")
|
||||
if version != cls.zip_version:
|
||||
return False
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def read_rom_info(cls, opened_zipfile: zipfile.ZipFile) -> (bytes, bytes, str):
|
||||
seedbytes: bytes = opened_zipfile.read("seedName")
|
||||
namebytes: bytes = opened_zipfile.read("player")
|
||||
namestr: str = namebytes.decode("utf-8")
|
||||
return seedbytes, namestr
|
||||
|
||||
@classmethod
|
||||
def read_difficulty_switch_info(cls, opened_zipfile: zipfile.ZipFile) -> (int, int):
|
||||
diff_a_bytes = opened_zipfile.read("diff_a_mode")
|
||||
diff_b_bytes = opened_zipfile.read("diff_b_mode")
|
||||
diff_a = 0
|
||||
diff_b = 0
|
||||
if diff_a_bytes is not None:
|
||||
diff_a = int.from_bytes(diff_a_bytes, "little")
|
||||
if diff_b_bytes is not None:
|
||||
diff_b = int.from_bytes(diff_b_bytes, "little")
|
||||
return diff_a, diff_b
|
||||
|
||||
@classmethod
|
||||
def read_bat_logic(cls, opened_zipfile: zipfile.ZipFile) -> int:
|
||||
bat_logic = opened_zipfile.read("bat_logic")
|
||||
if bat_logic is None:
|
||||
return 0
|
||||
return int.from_bytes(bat_logic, "little")
|
||||
|
||||
@classmethod
|
||||
def read_foreign_items(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]:
|
||||
foreign_items = []
|
||||
readbytes: bytes = opened_zipfile.read("adventure_locations")
|
||||
bytelist = list(readbytes)
|
||||
for i in range(round(len(bytelist) / 4)):
|
||||
offset = i * 4
|
||||
foreign_items.append(AdventureForeignItemInfo(bytelist[offset],
|
||||
bytelist[offset + 1],
|
||||
bytelist[offset + 2],
|
||||
bytelist[offset + 3]))
|
||||
return foreign_items
|
||||
|
||||
@classmethod
|
||||
def read_bat_no_touch(cls, opened_zipfile: zipfile.ZipFile) -> [BatNoTouchLocation]:
|
||||
locations = []
|
||||
readbytes: bytes = opened_zipfile.read("bat_no_touch_locations")
|
||||
bytelist = list(readbytes)
|
||||
for i in range(round(len(bytelist) / 5)):
|
||||
offset = i * 5
|
||||
locations.append(BatNoTouchLocation(bytelist[offset],
|
||||
bytelist[offset + 1],
|
||||
bytelist[offset + 2],
|
||||
bytelist[offset + 3],
|
||||
bytelist[offset + 4]))
|
||||
return locations
|
||||
|
||||
@classmethod
|
||||
def read_autocollect_items(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]:
|
||||
autocollect_items = []
|
||||
readbytes: bytes = opened_zipfile.read("adventure_autocollect")
|
||||
bytelist = list(readbytes)
|
||||
for i in range(round(len(bytelist) / 2)):
|
||||
offset = i * 2
|
||||
autocollect_items.append(AdventureAutoCollectLocation(bytelist[offset], bytelist[offset + 1]))
|
||||
return autocollect_items
|
||||
|
||||
@classmethod
|
||||
def read_local_item_locations(cls, opened_zipfile: zipfile.ZipFile) -> [AdventureForeignItemInfo]:
|
||||
readbytes: bytes = opened_zipfile.read("local_item_locations")
|
||||
readstr: str = readbytes.decode()
|
||||
return json.loads(readstr)
|
||||
|
||||
@classmethod
|
||||
def read_dragon_speed_info(cls, opened_zipfile: zipfile.ZipFile) -> {}:
|
||||
readbytes: bytes = opened_zipfile.read("dragon_speed_reducer_info")
|
||||
readstr: str = readbytes.decode()
|
||||
return json.loads(readstr)
|
||||
|
||||
@classmethod
|
||||
def read_rom_deltas(cls, opened_zipfile: zipfile.ZipFile) -> {int, int}:
|
||||
readbytes: bytes = opened_zipfile.read("rom_deltas")
|
||||
readstr: str = readbytes.decode()
|
||||
return json.loads(readstr)
|
||||
|
||||
@classmethod
|
||||
def apply_rom_deltas(cls, base_bytes: bytes, rom_deltas: {int, int}) -> bytearray:
|
||||
rom_bytes = bytearray(base_bytes)
|
||||
for offset, value in rom_deltas.items():
|
||||
int_offset = int(offset)
|
||||
rom_bytes[int_offset:int_offset+1] = int.to_bytes(value, 1, "little")
|
||||
return rom_bytes
|
||||
|
||||
|
||||
def apply_basepatch(base_rom_bytes: bytes) -> bytes:
|
||||
with open(os.path.join(os.path.dirname(__file__), "../../data/adventure_basepatch.bsdiff4"), "rb") as basepatch:
|
||||
delta: bytes = basepatch.read()
|
||||
return bsdiff4.patch(base_rom_bytes, delta)
|
||||
|
||||
|
||||
def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
file_name = get_base_rom_path(file_name)
|
||||
with open(file_name, "rb") as file:
|
||||
base_rom_bytes = bytes(file.read())
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if ADVENTUREHASH != basemd5.hexdigest():
|
||||
raise Exception(f"Supplied Base Rom does not match known MD5 for Adventure. "
|
||||
"Get the correct game and version, then dump it")
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options: OptionsType = Utils.get_options()
|
||||
if not file_name:
|
||||
file_name = options["adventure_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
|
@ -0,0 +1,98 @@
|
|||
from worlds.adventure import location_table
|
||||
from worlds.adventure.Options import BatLogic, DifficultySwitchB, DifficultySwitchA
|
||||
from worlds.generic.Rules import add_rule, set_rule, forbid_item
|
||||
from BaseClasses import LocationProgressType
|
||||
|
||||
|
||||
def set_rules(self) -> None:
|
||||
world = self.multiworld
|
||||
use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic
|
||||
|
||||
set_rule(world.get_entrance("YellowCastlePort", self.player),
|
||||
lambda state: state.has("Yellow Key", self.player))
|
||||
set_rule(world.get_entrance("BlackCastlePort", self.player),
|
||||
lambda state: state.has("Black Key", self.player))
|
||||
set_rule(world.get_entrance("WhiteCastlePort", self.player),
|
||||
lambda state: state.has("White Key", self.player))
|
||||
|
||||
# a future thing would be to make the bat an actual item, or at least allow it to
|
||||
# be placed in a castle, which would require some additions to the rules when
|
||||
# use_bat_logic is true
|
||||
if not use_bat_logic:
|
||||
set_rule(world.get_entrance("WhiteCastleSecretPassage", self.player),
|
||||
lambda state: state.has("Bridge", self.player))
|
||||
set_rule(world.get_entrance("WhiteCastlePeekPassage", self.player),
|
||||
lambda state: state.has("Bridge", self.player) or
|
||||
state.has("Magnet", self.player))
|
||||
set_rule(world.get_entrance("BlackCastleVaultEntrance", self.player),
|
||||
lambda state: state.has("Bridge", self.player) or
|
||||
state.has("Magnet", self.player))
|
||||
|
||||
dragon_slay_check = world.dragon_slay_check[self.player].value
|
||||
if dragon_slay_check:
|
||||
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
||||
set_rule(world.get_location("Slay Yorgle", self.player),
|
||||
lambda state: state.has("Sword", self.player) and
|
||||
state.has("Right Difficulty Switch", self.player))
|
||||
set_rule(world.get_location("Slay Grundle", self.player),
|
||||
lambda state: state.has("Sword", self.player) and
|
||||
state.has("Right Difficulty Switch", self.player))
|
||||
set_rule(world.get_location("Slay Rhindle", self.player),
|
||||
lambda state: state.has("Sword", self.player) and
|
||||
state.has("Right Difficulty Switch", self.player))
|
||||
else:
|
||||
set_rule(world.get_location("Slay Yorgle", self.player),
|
||||
lambda state: state.has("Sword", self.player))
|
||||
set_rule(world.get_location("Slay Grundle", self.player),
|
||||
lambda state: state.has("Sword", self.player))
|
||||
set_rule(world.get_location("Slay Rhindle", self.player),
|
||||
lambda state: state.has("Sword", self.player))
|
||||
|
||||
# really this requires getting the dot item, and having another item or enemy
|
||||
# in the room, but the dot would be *super evil*
|
||||
# to actually make randomized, since it is invisible. May add some options
|
||||
# for how that works in the distant future, but for now, just say you need
|
||||
# the bridge and black key to get to it, as that simplifies things a lot
|
||||
set_rule(world.get_entrance("CreditsWall", self.player),
|
||||
lambda state: state.has("Bridge", self.player) and
|
||||
state.has("Black Key", self.player))
|
||||
|
||||
if not use_bat_logic:
|
||||
set_rule(world.get_entrance("CreditsToFarSide", self.player),
|
||||
lambda state: state.has("Magnet", self.player))
|
||||
|
||||
# bridge literally does not fit in this space, I think. I'll just exclude it
|
||||
forbid_item(world.get_location("Dungeon Vault", self.player), "Bridge", self.player)
|
||||
# don't put magnet in locations that can pull in-logic items out of reach unless the bat is in play
|
||||
if not use_bat_logic:
|
||||
forbid_item(world.get_location("Dungeon Vault", self.player), "Magnet", self.player)
|
||||
forbid_item(world.get_location("Red Maze Vault Entrance", self.player), "Magnet", self.player)
|
||||
forbid_item(world.get_location("Credits Right Side", self.player), "Magnet", self.player)
|
||||
|
||||
# and obviously we don't want to start with the game already won
|
||||
forbid_item(world.get_location("Inside Yellow Castle", self.player), "Chalice", self.player)
|
||||
overworld = world.get_region("Overworld", self.player)
|
||||
|
||||
for loc in overworld.locations:
|
||||
forbid_item(loc, "Chalice", self.player)
|
||||
|
||||
add_rule(world.get_location("Chalice Home", self.player),
|
||||
lambda state: state.has("Chalice", self.player) and state.has("Yellow Key", self.player))
|
||||
|
||||
# world.random.choice(overworld.locations).progress_type = LocationProgressType.PRIORITY
|
||||
|
||||
# all_locations = world.get_locations(self.player).copy()
|
||||
# while priority_count < get_num_items():
|
||||
# loc = world.random.choice(all_locations)
|
||||
# if loc.progress_type == LocationProgressType.DEFAULT:
|
||||
# loc.progress_type = LocationProgressType.PRIORITY
|
||||
# priority_count += 1
|
||||
# all_locations.remove(loc)
|
||||
|
||||
# TODO: Add events for dragon_slay_check and trap_bat_check. Here? Elsewhere?
|
||||
# if self.dragon_slay_check == 1:
|
||||
# TODO - Randomize bat and dragon start rooms and use those to determine rules
|
||||
# TODO - for the requirements for the slay event (since we have to get to the
|
||||
# TODO - dragons and sword to kill them). Unless the dragons are set to be items,
|
||||
# TODO - which might be a funny option, then they can just be randoed like normal
|
||||
# TODO - just forbidden from the vaults and all credits room locations
|
|
@ -0,0 +1,391 @@
|
|||
import base64
|
||||
import copy
|
||||
import itertools
|
||||
import math
|
||||
import os
|
||||
from enum import IntFlag
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Set, Tuple
|
||||
|
||||
from BaseClasses import Entrance, Item, ItemClassification, MultiWorld, Region, Tutorial, \
|
||||
LocationProgressType
|
||||
from Main import __version__
|
||||
from Options import AssembleOptions
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from Fill import fill_restrictive
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB
|
||||
from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \
|
||||
AdventureAutoCollectLocation
|
||||
from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max
|
||||
from .Locations import location_table, base_location_id, LocationData, get_random_room_in_regions
|
||||
from .Offsets import static_item_data_location, items_ram_start, static_item_element_size, item_position_table, \
|
||||
static_first_dragon_index, connector_port_offset, yorgle_speed_data_location, grundle_speed_data_location, \
|
||||
rhindle_speed_data_location, item_ram_addresses, start_castle_values, start_castle_offset
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules
|
||||
|
||||
|
||||
from worlds.LauncherComponents import Component, components, SuffixIdentifier
|
||||
|
||||
# Adventure
|
||||
components.append(Component('Adventure Client', 'AdventureClient', file_identifier=SuffixIdentifier('.apadvn')))
|
||||
|
||||
|
||||
class AdventureWeb(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up Adventure for MultiWorld.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["JusticePS"]
|
||||
)]
|
||||
theme = "dirt"
|
||||
|
||||
|
||||
def get_item_position_data_start(table_index: int):
|
||||
item_ram_address = item_ram_addresses[table_index];
|
||||
return item_position_table + item_ram_address - items_ram_start
|
||||
|
||||
|
||||
class AdventureWorld(World):
|
||||
"""
|
||||
Adventure for the Atari 2600 is an early graphical adventure game.
|
||||
Find the enchanted chalice and return it to the yellow castle,
|
||||
using magic items to enter hidden rooms, retrieve out of
|
||||
reach items, or defeat the three dragons. Beware the bat
|
||||
who likes to steal your equipment!
|
||||
"""
|
||||
game: ClassVar[str] = "Adventure"
|
||||
web: ClassVar[WebWorld] = AdventureWeb()
|
||||
|
||||
option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions
|
||||
item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()}
|
||||
location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()}
|
||||
data_version: ClassVar[int] = 1
|
||||
required_client_version: Tuple[int, int, int] = (0, 3, 9)
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super().__init__(world, player)
|
||||
self.rom_name: Optional[bytearray] = bytearray("", "utf8" )
|
||||
self.dragon_rooms: [int] = [0x14, 0x19, 0x4]
|
||||
self.dragon_slay_check: Optional[int] = 0
|
||||
self.connector_multi_slot: Optional[int] = 0
|
||||
self.dragon_rando_type: Optional[int] = 0
|
||||
self.yorgle_speed: Optional[int] = 2
|
||||
self.yorgle_min_speed: Optional[int] = 2
|
||||
self.grundle_speed: Optional[int] = 2
|
||||
self.grundle_min_speed: Optional[int] = 2
|
||||
self.rhindle_speed: Optional[int] = 3
|
||||
self.rhindle_min_speed: Optional[int] = 3
|
||||
self.difficulty_switch_a: Optional[int] = 0
|
||||
self.difficulty_switch_b: Optional[int] = 0
|
||||
self.start_castle: Optional[int] = 0
|
||||
# dict of item names -> list of speed deltas
|
||||
self.dragon_speed_reducer_info: {} = {}
|
||||
self.created_items: int = 0
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, _multiworld: MultiWorld) -> None:
|
||||
# don't need rom anymore
|
||||
pass
|
||||
|
||||
def place_random_dragon(self, dragon_index: int):
|
||||
region_list = ["Overworld", "YellowCastle", "BlackCastle", "WhiteCastle"]
|
||||
self.dragon_rooms[dragon_index] = get_random_room_in_regions(region_list, self.multiworld.random)
|
||||
|
||||
def generate_early(self) -> None:
|
||||
self.rom_name = \
|
||||
bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21]
|
||||
self.rom_name.extend([0] * (21 - len(self.rom_name)))
|
||||
|
||||
self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value
|
||||
self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value
|
||||
self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value
|
||||
self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value
|
||||
self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value
|
||||
self.grundle_speed = self.multiworld.grundle_speed[self.player].value
|
||||
self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value
|
||||
self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value
|
||||
self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value
|
||||
self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value
|
||||
self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value
|
||||
self.start_castle = self.multiworld.start_castle[self.player].value
|
||||
self.created_items = 0
|
||||
|
||||
if self.dragon_slay_check == 0:
|
||||
item_table["Sword"].classification = ItemClassification.useful
|
||||
else:
|
||||
item_table["Sword"].classification = ItemClassification.progression
|
||||
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
||||
item_table["Right Difficulty Switch"].classification = ItemClassification.progression
|
||||
|
||||
if self.dragon_rando_type == DragonRandoType.option_shuffle:
|
||||
self.multiworld.random.shuffle(self.dragon_rooms)
|
||||
elif self.dragon_rando_type == DragonRandoType.option_overworldplus:
|
||||
dragon_indices = [0, 1, 2]
|
||||
overworld_forced_index = self.multiworld.random.choice(dragon_indices)
|
||||
dragon_indices.remove(overworld_forced_index)
|
||||
region_list = ["Overworld"]
|
||||
self.dragon_rooms[overworld_forced_index] = get_random_room_in_regions(region_list, self.multiworld.random)
|
||||
self.place_random_dragon(dragon_indices[0])
|
||||
self.place_random_dragon(dragon_indices[1])
|
||||
elif self.dragon_rando_type == DragonRandoType.option_randomized:
|
||||
self.place_random_dragon(0)
|
||||
self.place_random_dragon(1)
|
||||
self.place_random_dragon(2)
|
||||
|
||||
def create_items(self) -> None:
|
||||
for event in map(self.create_item, event_table):
|
||||
self.multiworld.itempool.append(event)
|
||||
exclude = [item for item in self.multiworld.precollected_items[self.player]]
|
||||
self.created_items = 0
|
||||
for item in map(self.create_item, item_table):
|
||||
if item.code == nothing_item_id:
|
||||
continue
|
||||
if item in exclude and item.code <= standard_item_max:
|
||||
exclude.remove(item) # this is destructive. create unique list above
|
||||
else:
|
||||
if item.code <= standard_item_max:
|
||||
self.multiworld.itempool.append(item)
|
||||
self.created_items += 1
|
||||
num_locations = len(location_table) - 1 # subtract out the chalice location
|
||||
if self.dragon_slay_check == 0:
|
||||
num_locations -= 3
|
||||
|
||||
if self.difficulty_switch_a == DifficultySwitchA.option_hard_with_unlock_item:
|
||||
self.multiworld.itempool.append(self.create_item("Left Difficulty Switch"))
|
||||
self.created_items += 1
|
||||
if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item:
|
||||
self.multiworld.itempool.append(self.create_item("Right Difficulty Switch"))
|
||||
self.created_items += 1
|
||||
|
||||
extra_filler_count = num_locations - self.created_items
|
||||
self.dragon_speed_reducer_info = {}
|
||||
# make sure yorgle doesn't take 2 if there's not enough for the others to get at least one
|
||||
if extra_filler_count <= 4:
|
||||
extra_filler_count = 1
|
||||
self.create_dragon_slow_items(self.yorgle_min_speed, self.yorgle_speed, "Slow Yorgle", extra_filler_count)
|
||||
extra_filler_count = num_locations - self.created_items
|
||||
|
||||
if extra_filler_count <= 3:
|
||||
extra_filler_count = 1
|
||||
self.create_dragon_slow_items(self.grundle_min_speed, self.grundle_speed, "Slow Grundle", extra_filler_count)
|
||||
extra_filler_count = num_locations - self.created_items
|
||||
|
||||
self.create_dragon_slow_items(self.rhindle_min_speed, self.rhindle_speed, "Slow Rhindle", extra_filler_count)
|
||||
extra_filler_count = num_locations - self.created_items
|
||||
|
||||
# traps would probably go here, if enabled
|
||||
freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value
|
||||
actual_freeincarnates = min(extra_filler_count, freeincarnate_max)
|
||||
self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)]
|
||||
self.created_items += actual_freeincarnates
|
||||
|
||||
def create_dragon_slow_items(self, min_speed: int, speed: int, item_name: str, maximum_items: int):
|
||||
if min_speed < speed:
|
||||
delta = speed - min_speed
|
||||
if delta > 2 and maximum_items >= 2:
|
||||
self.multiworld.itempool.append(self.create_item(item_name))
|
||||
self.multiworld.itempool.append(self.create_item(item_name))
|
||||
speed_with_one = speed - math.floor(delta / 2)
|
||||
self.dragon_speed_reducer_info[item_table[item_name].id] = [speed_with_one, min_speed]
|
||||
self.created_items += 2
|
||||
elif maximum_items >= 1:
|
||||
self.multiworld.itempool.append(self.create_item(item_name))
|
||||
self.dragon_speed_reducer_info[item_table[item_name].id] = [min_speed]
|
||||
self.created_items += 1
|
||||
|
||||
def create_regions(self) -> None:
|
||||
create_regions(self.multiworld, self.player, self.dragon_rooms)
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
self.multiworld.get_location("Chalice Home", self.player).place_locked_item(
|
||||
self.create_event("Victory", ItemClassification.progression))
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
def pre_fill(self):
|
||||
# Place empty items in filler locations here, to limit
|
||||
# the number of exported empty items and the density of stuff in overworld.
|
||||
max_location_count = len(location_table) - 1
|
||||
if self.dragon_slay_check == 0:
|
||||
max_location_count -= 3
|
||||
|
||||
force_empty_item_count = (max_location_count - self.created_items)
|
||||
if force_empty_item_count <= 0:
|
||||
return
|
||||
overworld = self.multiworld.get_region("Overworld", self.player)
|
||||
overworld_locations_copy = overworld.locations.copy()
|
||||
all_locations = self.multiworld.get_locations(self.player)
|
||||
|
||||
locations_copy = all_locations.copy()
|
||||
for loc in all_locations:
|
||||
if loc.item is not None or loc.progress_type != LocationProgressType.DEFAULT:
|
||||
locations_copy.remove(loc)
|
||||
if loc in overworld_locations_copy:
|
||||
overworld_locations_copy.remove(loc)
|
||||
|
||||
# guarantee at least one overworld location, so we can for sure get a key somewhere
|
||||
# if too much stuff is plando'd though, just let it go
|
||||
if len(overworld_locations_copy) >= 3:
|
||||
saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy)
|
||||
locations_copy.remove(saved_overworld_loc)
|
||||
overworld_locations_copy.remove(saved_overworld_loc)
|
||||
|
||||
# if we have few items, enforce another overworld slot, fill a hard slot, and ensure we have
|
||||
# at least one hard slot available
|
||||
if self.created_items < 15:
|
||||
hard_locations = []
|
||||
for loc in locations_copy:
|
||||
if "Vault" in loc.name or "Credits" in loc.name:
|
||||
hard_locations.append(loc)
|
||||
force_empty_item_count -= 1
|
||||
loc = self.multiworld.random.choice(hard_locations)
|
||||
loc.place_locked_item(self.create_item('nothing'))
|
||||
hard_locations.remove(loc)
|
||||
locations_copy.remove(loc)
|
||||
|
||||
loc = self.multiworld.random.choice(hard_locations)
|
||||
locations_copy.remove(loc)
|
||||
hard_locations.remove(loc)
|
||||
|
||||
saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy)
|
||||
locations_copy.remove(saved_overworld_loc)
|
||||
overworld_locations_copy.remove(saved_overworld_loc)
|
||||
|
||||
# if we have very few items, fill another two difficult slots
|
||||
if self.created_items < 10:
|
||||
for i in range(2):
|
||||
force_empty_item_count -= 1
|
||||
loc = self.multiworld.random.choice(hard_locations)
|
||||
loc.place_locked_item(self.create_item('nothing'))
|
||||
hard_locations.remove(loc)
|
||||
locations_copy.remove(loc)
|
||||
|
||||
# for the absolute minimum number of items, enforce a third overworld slot
|
||||
if self.created_items <= 7:
|
||||
saved_overworld_loc = self.multiworld.random.choice(overworld_locations_copy)
|
||||
locations_copy.remove(saved_overworld_loc)
|
||||
overworld_locations_copy.remove(saved_overworld_loc)
|
||||
|
||||
# finally, place nothing items
|
||||
while force_empty_item_count > 0 and locations_copy:
|
||||
force_empty_item_count -= 1
|
||||
# prefer somewhat to thin out the overworld.
|
||||
if len(overworld_locations_copy) > 0 and self.multiworld.random.randint(0, 10) < 4:
|
||||
loc = self.multiworld.random.choice(overworld_locations_copy)
|
||||
else:
|
||||
loc = self.multiworld.random.choice(locations_copy)
|
||||
loc.place_locked_item(self.create_item('nothing'))
|
||||
locations_copy.remove(loc)
|
||||
if loc in overworld_locations_copy:
|
||||
overworld_locations_copy.remove(loc)
|
||||
|
||||
def place_dragons(self, rom_deltas: {int, int}):
|
||||
for i in range(3):
|
||||
table_index = static_first_dragon_index + i
|
||||
item_position_data_start = get_item_position_data_start(table_index)
|
||||
rom_deltas[item_position_data_start] = self.dragon_rooms[i]
|
||||
|
||||
def set_dragon_speeds(self, rom_deltas: {int, int}):
|
||||
rom_deltas[yorgle_speed_data_location] = self.yorgle_speed
|
||||
rom_deltas[grundle_speed_data_location] = self.grundle_speed
|
||||
rom_deltas[rhindle_speed_data_location] = self.rhindle_speed
|
||||
|
||||
def set_start_castle(self, rom_deltas):
|
||||
start_castle_value = start_castle_values[self.start_castle]
|
||||
rom_deltas[start_castle_offset] = start_castle_value
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
rom_path: str = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.bin")
|
||||
foreign_item_locations: [LocationData] = []
|
||||
auto_collect_locations: [AdventureAutoCollectLocation] = []
|
||||
local_item_to_location: {int, int} = {}
|
||||
bat_no_touch_locs: [LocationData] = []
|
||||
bat_logic: int = self.multiworld.bat_logic[self.player].value
|
||||
try:
|
||||
rom_deltas: { int, int } = {}
|
||||
self.place_dragons(rom_deltas)
|
||||
self.set_dragon_speeds(rom_deltas)
|
||||
self.set_start_castle(rom_deltas)
|
||||
# start and stop indices are offsets in the ROM file, not Adventure ROM addresses (which start at f000)
|
||||
|
||||
# This places the local items (I still need to make it easy to inject the offset data)
|
||||
unplaced_local_items = dict(filter(lambda x: nothing_item_id < x[1].id <= standard_item_max,
|
||||
item_table.items()))
|
||||
for location in self.multiworld.get_locations(self.player):
|
||||
# 'nothing' items, which are autocollected when the room is entered
|
||||
if location.item.player == self.player and \
|
||||
location.item.name == "nothing":
|
||||
location_data = location_table[location.name]
|
||||
auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id,
|
||||
location_data.room_id))
|
||||
# standard Adventure items, which are placed in the rom
|
||||
elif location.item.player == self.player and \
|
||||
location.item.name != "nothing" and \
|
||||
location.item.code is not None and \
|
||||
location.item.code <= standard_item_max:
|
||||
# I need many of the intermediate values here.
|
||||
item_table_offset = item_table[location.item.name].table_index * static_item_element_size
|
||||
item_ram_address = item_ram_addresses[item_table[location.item.name].table_index]
|
||||
item_position_data_start = item_position_table + item_ram_address - items_ram_start
|
||||
location_data = location_table[location.name]
|
||||
room_x, room_y = location_data.get_position(self.multiworld.per_slot_randoms[self.player])
|
||||
if location_data.needs_bat_logic and bat_logic == 0x0:
|
||||
copied_location = copy.copy(location_data)
|
||||
copied_location.local_item = item_ram_address
|
||||
bat_no_touch_locs.append(copied_location)
|
||||
del unplaced_local_items[location.item.name]
|
||||
|
||||
rom_deltas[item_position_data_start] = location_data.room_id
|
||||
rom_deltas[item_position_data_start + 1] = room_x
|
||||
rom_deltas[item_position_data_start + 2] = room_y
|
||||
local_item_to_location[item_table_offset] = self.location_name_to_id[location.name] \
|
||||
- base_location_id
|
||||
# items from other worlds, and non-standard Adventure items handled by script, like difficulty switches
|
||||
elif location.item.code is not None:
|
||||
if location.item.code != nothing_item_id:
|
||||
location_data = location_table[location.name]
|
||||
foreign_item_locations.append(location_data)
|
||||
if location_data.needs_bat_logic and bat_logic == 0x0:
|
||||
bat_no_touch_locs.append(location_data)
|
||||
else:
|
||||
location_data = location_table[location.name]
|
||||
auto_collect_locations.append(AdventureAutoCollectLocation(location_data.short_location_id,
|
||||
location_data.room_id))
|
||||
# Adventure items that are in another world get put in an invalid room until needed
|
||||
for unplaced_item_name, unplaced_item in unplaced_local_items.items():
|
||||
item_position_data_start = get_item_position_data_start(unplaced_item.table_index)
|
||||
rom_deltas[item_position_data_start] = 0xff
|
||||
|
||||
if self.multiworld.connector_multi_slot[self.player].value:
|
||||
rom_deltas[connector_port_offset] = (self.player & 0xff)
|
||||
else:
|
||||
rom_deltas[connector_port_offset] = 0
|
||||
except Exception as e:
|
||||
raise e
|
||||
else:
|
||||
patch = AdventureDeltaPatch(os.path.splitext(rom_path)[0] + AdventureDeltaPatch.patch_file_ending,
|
||||
player=self.player, player_name=self.multiworld.player_name[self.player],
|
||||
locations=foreign_item_locations,
|
||||
autocollect=auto_collect_locations, local_item_locations=local_item_to_location,
|
||||
dragon_speed_reducer_info=self.dragon_speed_reducer_info,
|
||||
diff_a_mode=self.difficulty_switch_a, diff_b_mode=self.difficulty_switch_b,
|
||||
bat_logic=bat_logic, bat_no_touch_locations=bat_no_touch_locs,
|
||||
rom_deltas=rom_deltas,
|
||||
seed_name=bytes(self.multiworld.seed_name, encoding="ascii"))
|
||||
patch.write()
|
||||
finally:
|
||||
if os.path.exists(rom_path):
|
||||
os.unlink(rom_path)
|
||||
|
||||
# end of ordered Main.py calls
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
item_data: ItemData = item_table.get(name)
|
||||
return AdventureItem(name, item_data.classification, item_data.id, self.player)
|
||||
|
||||
def create_event(self, name: str, classification: ItemClassification) -> Item:
|
||||
return AdventureItem(name, classification, None, self.player)
|
|
@ -0,0 +1,62 @@
|
|||
# Adventure
|
||||
|
||||
## Where is the settings page?
|
||||
The [player settings page for Adventure](../player-settings) contains all the options you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
Adventure items may be distributed into additional locations not possible in the vanilla Adventure randomizer. All
|
||||
Adventure items are added to the multiworld item pool. Depending on the settings, dragon locations may be randomized,
|
||||
slaying dragons may award items, difficulty switches may require items to unlock, and limited use 'freeincarnates'
|
||||
can allow reincarnation without resurrecting dragons. Dragon speeds may also be randomized, and items may exist
|
||||
to reduce their speeds.
|
||||
|
||||
## What is the goal of Adventure when randomized?
|
||||
Same as vanilla; Find the Enchanted Chalice and return it to the Yellow Castle
|
||||
|
||||
## Which items can be in another player's world?
|
||||
All three keys, the chalice, the sword, the magnet, and the bridge can be found in another player's world. Depending on
|
||||
settings, dragon slowdowns, difficulty switch unlocks, and freeincarnates may also be found.
|
||||
|
||||
## What is considered a location check in Adventure?
|
||||
Most areas in Adventure have one or more locations which can contain an Adventure item or an Archipelago item.
|
||||
A few rooms have two potential locaions. If the location contains a 'nothing' Adventure item, it will send a check when
|
||||
that is seen. If it contains an item from another Adventure or other game, it will show a rough approximation of the
|
||||
Archipelago logo that can be touched for a check. Touching a local Adventure item also 'checks' it, allowing it to be
|
||||
retrieved after a select-reset or hard reset.
|
||||
|
||||
## Why isn't my item where the spoiler says it should be?
|
||||
If something isn't where the spoiler says, most likely the bat carried it somewhere else. The bat's ability to shuffle
|
||||
items around makes it somewhat unique in Archipelago. Touching the item, wherever it is, will award the location check
|
||||
for wherever the item was originally placed.
|
||||
|
||||
## Which notable items are not randomized?
|
||||
The bat, dot, and map are not yet randomized. If the chalice is local, it is randomized, but is always in either a
|
||||
castle or the credits screen. Forcing the chalice local in the yaml is recommended.
|
||||
|
||||
## What does another world's item look like in Adventure?
|
||||
It looks vaguely like a flashing Archipelago logo.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
A message is shown in the client log. While empty handed, the player can press the fire button to retrieve items in the
|
||||
order they were received. Once an item is retrieved this way, it cannot be retrieved again until pressing select to
|
||||
return to the 'GO' screen or doing a hard reset, either one of which will reset all items to their original positions.
|
||||
|
||||
## What are recommended settings to tweak for beginners to the rando?
|
||||
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
|
||||
local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or
|
||||
the credits room.
|
||||
|
||||
## My yellow key is stuck in a wall! Am I softlocked?
|
||||
Maybe! That's all part of Adventure. If you have access to the magnet, bridge, or bat, you might be able to retrieve
|
||||
it. In general, since the bat always starts outside of castles, you should always be able to find it unless you lock
|
||||
it in a castle yourself. This mod's inventory system allows you to quickly recover all the items
|
||||
you've collected after a hard reset or select-reset (except for the dot), so usually it's not as bad as in vanilla.
|
||||
|
||||
## How do I get into the credits room? There's a item I need in there.
|
||||
Searching for 'Adventure dot map' should bring up an AtariAge map with a good walkthrough, but here's the basics.
|
||||
Bring the bridge into the black castle. Find the small room in the dungeon that cannot be reached without the bridge,
|
||||
enter it, and push yourself into the bottom right corner to pick up the dot. The dot color matches the background,
|
||||
so you won't be able to see it if it isn't in a wall, so be careful not to drop it. Bring it to the room one south and
|
||||
one east of the yellow castle and drop it there. Bring 2-3 more objects (the bat and dragons also count for this) until
|
||||
it lets you walk through the right wall.
|
||||
If the item is on the right side, you'll need the magnet to get it.
|
|
@ -0,0 +1,70 @@
|
|||
# Setup Guide for Adventure: Archipelago
|
||||
|
||||
## Important
|
||||
|
||||
As we are using Bizhawk, this guide is only applicable to Windows and Linux systems.
|
||||
|
||||
## Required Software
|
||||
|
||||
- Bizhawk: [Bizhawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
|
||||
- Detailed installation instructions for Bizhawk can be found at the above link.
|
||||
- Windows users must run the prereq installer first, which can also be found at the above link.
|
||||
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
(select `Adventure Client` during installation).
|
||||
- An Adventure NTSC ROM file. The Archipelago community cannot provide these.
|
||||
|
||||
## Configuring Bizhawk
|
||||
|
||||
Once Bizhawk has been installed, open Bizhawk and change the following settings:
|
||||
|
||||
- Go to Config > Customize. Switch to the Advanced tab, then switch the Lua Core from "NLua+KopiLua" to
|
||||
"Lua+LuaInterface". Then restart Bizhawk. This is required for the Lua script to function correctly.
|
||||
**NOTE: Even if "Lua+LuaInterface" is already selected, toggle between the two options and reselect it. Fresh installs**
|
||||
**of newer versions of Bizhawk have a tendency to show "Lua+LuaInterface" as the default selected option but still load**
|
||||
**"NLua+KopiLua" until this step is done.**
|
||||
- Under Config > Customize, check the "Run in background" box. This will prevent disconnecting from the client while
|
||||
BizHawk is running in the background.
|
||||
|
||||
- It is recommended that you provide a path to BizHawk in your host.yaml for Adventure so the client can start it automatically
|
||||
|
||||
## Configuring your YAML file
|
||||
|
||||
### What is a YAML file and why do I need one?
|
||||
|
||||
Your YAML file contains a set of configuration options which provide the generator with information about how it should
|
||||
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
|
||||
an experience customized for their taste, and different players in the same multiworld can all have different options.
|
||||
|
||||
### Where do I get a YAML file?
|
||||
|
||||
You can generate a yaml or download a template by visiting the [Adventure Settings Page](/games/Adventure/player-settings)
|
||||
|
||||
### What are recommended settings to tweak for beginners to the rando?
|
||||
Setting difficulty_switch_a and lowering the dragons' speeds makes the dragons easier to avoid. Adding Chalice to
|
||||
local_items guarantees you'll visit at least one of the interesting castles, as it can only be placed in a castle or
|
||||
the credits room.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your Adventure patch file
|
||||
|
||||
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
|
||||
the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
|
||||
files. Your data file should have a `.apadvn` extension.
|
||||
|
||||
Drag your patch file to the AdventureClient.exe to start your client and start the ROM patch process. Once the process
|
||||
is finished (this can take a while), the client and the emulator will be started automatically (if you set the emulator
|
||||
path as recommended).
|
||||
|
||||
### Connect to the Multiserver
|
||||
|
||||
Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools"
|
||||
menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script.
|
||||
|
||||
Navigate to your Archipelago install folder and open `data/lua/ADVENTURE/adventure_connector.lua`.
|
||||
|
||||
To connect the client to the multiserver simply put `<address>:<port>` on the textfield on top and press enter (if the
|
||||
server uses password, type in the bottom textfield `/connect <address>:<port> [password]`)
|
||||
|
||||
Press Reset and begin playing
|
Loading…
Reference in New Issue