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:
JusticePS 2023-03-22 07:25:55 -07:00 committed by GitHub
parent 206f8cf5ed
commit d48e1e447f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 3619 additions and 2 deletions

1
.gitignore vendored
View File

@ -26,6 +26,7 @@
*multisave
*.archipelago
*.apsave
*.BIN
build
bundle/components.wxs

516
AdventureClient.py Normal file
View File

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

View File

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

View File

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

View File

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

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

@ -0,0 +1,380 @@
--
-- json.lua
--
-- Copyright (c) 2015 rxi
--
-- This library is free software; you can redistribute it and/or modify it
-- under the terms of the MIT license. See LICENSE for details.
--
local json = { _version = "0.1.0" }
-------------------------------------------------------------------------------
-- Encode
-------------------------------------------------------------------------------
local encode
local escape_char_map = {
[ "\\" ] = "\\\\",
[ "\"" ] = "\\\"",
[ "\b" ] = "\\b",
[ "\f" ] = "\\f",
[ "\n" ] = "\\n",
[ "\r" ] = "\\r",
[ "\t" ] = "\\t",
}
local escape_char_map_inv = { [ "\\/" ] = "/" }
for k, v in pairs(escape_char_map) do
escape_char_map_inv[v] = k
end
local function escape_char(c)
return escape_char_map[c] or string.format("\\u%04x", c:byte())
end
local function encode_nil(val)
return "null"
end
local function encode_table(val, stack)
local res = {}
stack = stack or {}
-- Circular reference?
if stack[val] then error("circular reference") end
stack[val] = true
if val[1] ~= nil or next(val) == nil then
-- Treat as array -- check keys are valid and it is not sparse
local n = 0
for k in pairs(val) do
if type(k) ~= "number" then
error("invalid table: mixed or invalid key types")
end
n = n + 1
end
if n ~= #val then
error("invalid table: sparse array")
end
-- Encode
for i, v in ipairs(val) do
table.insert(res, encode(v, stack))
end
stack[val] = nil
return "[" .. table.concat(res, ",") .. "]"
else
-- Treat as an object
for k, v in pairs(val) do
if type(k) ~= "string" then
error("invalid table: mixed or invalid key types")
end
table.insert(res, encode(k, stack) .. ":" .. encode(v, stack))
end
stack[val] = nil
return "{" .. table.concat(res, ",") .. "}"
end
end
local function encode_string(val)
return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"'
end
local function encode_number(val)
-- Check for NaN, -inf and inf
if val ~= val or val <= -math.huge or val >= math.huge then
error("unexpected number value '" .. tostring(val) .. "'")
end
return string.format("%.14g", val)
end
local type_func_map = {
[ "nil" ] = encode_nil,
[ "table" ] = encode_table,
[ "string" ] = encode_string,
[ "number" ] = encode_number,
[ "boolean" ] = tostring,
}
encode = function(val, stack)
local t = type(val)
local f = type_func_map[t]
if f then
return f(val, stack)
end
error("unexpected type '" .. t .. "'")
end
function json.encode(val)
return ( encode(val) )
end
-------------------------------------------------------------------------------
-- Decode
-------------------------------------------------------------------------------
local parse
local function create_set(...)
local res = {}
for i = 1, select("#", ...) do
res[ select(i, ...) ] = true
end
return res
end
local space_chars = create_set(" ", "\t", "\r", "\n")
local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",")
local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u")
local literals = create_set("true", "false", "null")
local literal_map = {
[ "true" ] = true,
[ "false" ] = false,
[ "null" ] = nil,
}
local function next_char(str, idx, set, negate)
for i = idx, #str do
if set[str:sub(i, i)] ~= negate then
return i
end
end
return #str + 1
end
local function decode_error(str, idx, msg)
--local line_count = 1
--local col_count = 1
--for i = 1, idx - 1 do
-- col_count = col_count + 1
-- if str:sub(i, i) == "\n" then
-- line_count = line_count + 1
-- col_count = 1
-- end
-- end
-- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) )
end
local function codepoint_to_utf8(n)
-- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa
local f = math.floor
if n <= 0x7f then
return string.char(n)
elseif n <= 0x7ff then
return string.char(f(n / 64) + 192, n % 64 + 128)
elseif n <= 0xffff then
return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128)
elseif n <= 0x10ffff then
return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128,
f(n % 4096 / 64) + 128, n % 64 + 128)
end
error( string.format("invalid unicode codepoint '%x'", n) )
end
local function parse_unicode_escape(s)
local n1 = tonumber( s:sub(3, 6), 16 )
local n2 = tonumber( s:sub(9, 12), 16 )
-- Surrogate pair?
if n2 then
return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000)
else
return codepoint_to_utf8(n1)
end
end
local function parse_string(str, i)
local has_unicode_escape = false
local has_surrogate_escape = false
local has_escape = false
local last
for j = i + 1, #str do
local x = str:byte(j)
if x < 32 then
decode_error(str, j, "control character in string")
end
if last == 92 then -- "\\" (escape char)
if x == 117 then -- "u" (unicode escape sequence)
local hex = str:sub(j + 1, j + 5)
if not hex:find("%x%x%x%x") then
decode_error(str, j, "invalid unicode escape in string")
end
if hex:find("^[dD][89aAbB]") then
has_surrogate_escape = true
else
has_unicode_escape = true
end
else
local c = string.char(x)
if not escape_chars[c] then
decode_error(str, j, "invalid escape char '" .. c .. "' in string")
end
has_escape = true
end
last = nil
elseif x == 34 then -- '"' (end of string)
local s = str:sub(i + 1, j - 1)
if has_surrogate_escape then
s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape)
end
if has_unicode_escape then
s = s:gsub("\\u....", parse_unicode_escape)
end
if has_escape then
s = s:gsub("\\.", escape_char_map_inv)
end
return s, j + 1
else
last = x
end
end
decode_error(str, i, "expected closing quote for string")
end
local function parse_number(str, i)
local x = next_char(str, i, delim_chars)
local s = str:sub(i, x - 1)
local n = tonumber(s)
if not n then
decode_error(str, i, "invalid number '" .. s .. "'")
end
return n, x
end
local function parse_literal(str, i)
local x = next_char(str, i, delim_chars)
local word = str:sub(i, x - 1)
if not literals[word] then
decode_error(str, i, "invalid literal '" .. word .. "'")
end
return literal_map[word], x
end
local function parse_array(str, i)
local res = {}
local n = 1
i = i + 1
while 1 do
local x
i = next_char(str, i, space_chars, true)
-- Empty / end of array?
if str:sub(i, i) == "]" then
i = i + 1
break
end
-- Read token
x, i = parse(str, i)
res[n] = x
n = n + 1
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "]" then break end
if chr ~= "," then decode_error(str, i, "expected ']' or ','") end
end
return res, i
end
local function parse_object(str, i)
local res = {}
i = i + 1
while 1 do
local key, val
i = next_char(str, i, space_chars, true)
-- Empty / end of object?
if str:sub(i, i) == "}" then
i = i + 1
break
end
-- Read key
if str:sub(i, i) ~= '"' then
decode_error(str, i, "expected string for key")
end
key, i = parse(str, i)
-- Read ':' delimiter
i = next_char(str, i, space_chars, true)
if str:sub(i, i) ~= ":" then
decode_error(str, i, "expected ':' after key")
end
i = next_char(str, i + 1, space_chars, true)
-- Read value
val, i = parse(str, i)
-- Set
res[key] = val
-- Next token
i = next_char(str, i, space_chars, true)
local chr = str:sub(i, i)
i = i + 1
if chr == "}" then break end
if chr ~= "," then decode_error(str, i, "expected '}' or ','") end
end
return res, i
end
local char_func_map = {
[ '"' ] = parse_string,
[ "0" ] = parse_number,
[ "1" ] = parse_number,
[ "2" ] = parse_number,
[ "3" ] = parse_number,
[ "4" ] = parse_number,
[ "5" ] = parse_number,
[ "6" ] = parse_number,
[ "7" ] = parse_number,
[ "8" ] = parse_number,
[ "9" ] = parse_number,
[ "-" ] = parse_number,
[ "t" ] = parse_literal,
[ "f" ] = parse_literal,
[ "n" ] = parse_literal,
[ "[" ] = parse_array,
[ "{" ] = parse_object,
}
parse = function(str, idx)
local chr = str:sub(idx, idx)
local f = char_func_map[chr]
if f then
return f(str, idx)
end
decode_error(str, idx, "unexpected character '" .. chr .. "'")
end
function json.decode(str)
if type(str) ~= "string" then
error("expected argument of type string, got " .. type(str))
end
return ( parse(str, next_char(str, 1, space_chars, true)) )
end
return json

View File

@ -0,0 +1,132 @@
-----------------------------------------------------------------------------
-- LuaSocket helper module
-- Author: Diego Nehab
-- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
-- Declare module and import dependencies
-----------------------------------------------------------------------------
local base = _G
local string = require("string")
local math = require("math")
local socket = require("socket.core")
module("socket")
-----------------------------------------------------------------------------
-- Exported auxiliar functions
-----------------------------------------------------------------------------
function connect(address, port, laddress, lport)
local sock, err = socket.tcp()
if not sock then return nil, err end
if laddress then
local res, err = sock:bind(laddress, lport, -1)
if not res then return nil, err end
end
local res, err = sock:connect(address, port)
if not res then return nil, err end
return sock
end
function bind(host, port, backlog)
local sock, err = socket.tcp()
if not sock then return nil, err end
sock:setoption("reuseaddr", true)
local res, err = sock:bind(host, port)
if not res then return nil, err end
res, err = sock:listen(backlog)
if not res then return nil, err end
return sock
end
try = newtry()
function choose(table)
return function(name, opt1, opt2)
if base.type(name) ~= "string" then
name, opt1, opt2 = "default", name, opt1
end
local f = table[name or "nil"]
if not f then base.error("unknown key (".. base.tostring(name) ..")", 3)
else return f(opt1, opt2) end
end
end
-----------------------------------------------------------------------------
-- Socket sources and sinks, conforming to LTN12
-----------------------------------------------------------------------------
-- create namespaces inside LuaSocket namespace
sourcet = {}
sinkt = {}
BLOCKSIZE = 2048
sinkt["close-when-done"] = function(sock)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function(self, chunk, err)
if not chunk then
sock:close()
return 1
else return sock:send(chunk) end
end
})
end
sinkt["keep-open"] = function(sock)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function(self, chunk, err)
if chunk then return sock:send(chunk)
else return 1 end
end
})
end
sinkt["default"] = sinkt["keep-open"]
sink = choose(sinkt)
sourcet["by-length"] = function(sock, length)
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function()
if length <= 0 then return nil end
local size = math.min(socket.BLOCKSIZE, length)
local chunk, err = sock:receive(size)
if err then return nil, err end
length = length - string.len(chunk)
return chunk
end
})
end
sourcet["until-closed"] = function(sock)
local done
return base.setmetatable({
getfd = function() return sock:getfd() end,
dirty = function() return sock:dirty() end
}, {
__call = function()
if done then return nil end
local chunk, err, partial = sock:receive(socket.BLOCKSIZE)
if not err then return chunk
elseif err == "closed" then
sock:close()
done = 1
return partial
else return nil, err end
end
})
end
sourcet["default"] = sourcet["until-closed"]
source = choose(sourcet)

View File

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

View File

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

53
worlds/adventure/Items.py Normal file
View File

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

View File

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

View File

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

244
worlds/adventure/Options.py Normal file
View File

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

160
worlds/adventure/Regions.py Normal file
View File

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

321
worlds/adventure/Rom.py Normal file
View File

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

98
worlds/adventure/Rules.py Normal file
View File

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

View File

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

View File

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

View File

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