mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-11 18:38:22 -07:00
Merge branch 'main' into setup_more_apworld
This commit is contained in:
@@ -439,7 +439,7 @@ class MultiWorld():
|
||||
return self.regions.location_cache[player][location_name]
|
||||
|
||||
def get_all_state(self, use_cache: bool, allow_partial_entrances: bool = False,
|
||||
collect_pre_fill_items: bool = True) -> CollectionState:
|
||||
collect_pre_fill_items: bool = True, perform_sweep: bool = True) -> CollectionState:
|
||||
cached = getattr(self, "_all_state", None)
|
||||
if use_cache and cached:
|
||||
return cached.copy()
|
||||
@@ -453,7 +453,8 @@ class MultiWorld():
|
||||
subworld = self.worlds[player]
|
||||
for item in subworld.get_pre_fill_items():
|
||||
subworld.collect(ret, item)
|
||||
ret.sweep_for_advancements()
|
||||
if perform_sweep:
|
||||
ret.sweep_for_advancements()
|
||||
|
||||
if use_cache:
|
||||
self._all_state = ret
|
||||
@@ -736,6 +737,7 @@ class CollectionState():
|
||||
additional_copy_functions: List[Callable[[CollectionState, CollectionState], CollectionState]] = []
|
||||
|
||||
def __init__(self, parent: MultiWorld, allow_partial_entrances: bool = False):
|
||||
assert parent.worlds, "CollectionState created without worlds initialized in parent"
|
||||
self.prog_items = {player: Counter() for player in parent.get_all_ids()}
|
||||
self.multiworld = parent
|
||||
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
|
||||
@@ -1012,6 +1014,17 @@ class CollectionState():
|
||||
|
||||
return changed
|
||||
|
||||
def add_item(self, item: str, player: int, count: int = 1) -> None:
|
||||
"""
|
||||
Adds the item to state.
|
||||
|
||||
:param item: The item to be added.
|
||||
:param player: The player the item is for.
|
||||
:param count: How many of the item to add.
|
||||
"""
|
||||
assert count > 0
|
||||
self.prog_items[player][item] += count
|
||||
|
||||
def remove(self, item: Item):
|
||||
changed = self.multiworld.worlds[item.player].remove(self, item)
|
||||
if changed:
|
||||
@@ -1020,6 +1033,33 @@ class CollectionState():
|
||||
self.blocked_connections[item.player] = set()
|
||||
self.stale[item.player] = True
|
||||
|
||||
def remove_item(self, item: str, player: int, count: int = 1) -> None:
|
||||
"""
|
||||
Removes the item from state.
|
||||
|
||||
:param item: The item to be removed.
|
||||
:param player: The player the item is for.
|
||||
:param count: How many of the item to remove.
|
||||
"""
|
||||
assert count > 0
|
||||
self.prog_items[player][item] -= count
|
||||
if self.prog_items[player][item] < 1:
|
||||
del (self.prog_items[player][item])
|
||||
|
||||
def set_item(self, item: str, player: int, count: int) -> None:
|
||||
"""
|
||||
Sets the item in state equal to the provided count.
|
||||
|
||||
:param item: The item to modify.
|
||||
:param player: The player the item is for.
|
||||
:param count: How many of the item to now have.
|
||||
"""
|
||||
assert count >= 0
|
||||
if count == 0:
|
||||
del (self.prog_items[player][item])
|
||||
else:
|
||||
self.prog_items[player][item] = count
|
||||
|
||||
|
||||
class EntranceType(IntEnum):
|
||||
ONE_WAY = 1
|
||||
|
||||
@@ -266,38 +266,71 @@ class CommonContext:
|
||||
last_death_link: float = time.time() # last send/received death link on AP layer
|
||||
|
||||
# remaining type info
|
||||
slot_info: typing.Dict[int, NetworkSlot]
|
||||
server_address: typing.Optional[str]
|
||||
password: typing.Optional[str]
|
||||
hint_cost: typing.Optional[int]
|
||||
hint_points: typing.Optional[int]
|
||||
player_names: typing.Dict[int, str]
|
||||
slot_info: dict[int, NetworkSlot]
|
||||
"""Slot Info from the server for the current connection"""
|
||||
server_address: str | None
|
||||
"""Autoconnect address provided by the ctx constructor"""
|
||||
password: str | None
|
||||
"""Password used for Connecting, expected by server_auth"""
|
||||
hint_cost: int | None
|
||||
"""Current Hint Cost per Hint from the server"""
|
||||
hint_points: int | None
|
||||
"""Current avaliable Hint Points from the server"""
|
||||
player_names: dict[int, str]
|
||||
"""Current lookup of slot number to player display name from server (includes aliases)"""
|
||||
|
||||
finished_game: bool
|
||||
"""
|
||||
Bool to signal that status should be updated to Goal after reconnecting
|
||||
to be used to ensure that a StatusUpdate packet does not get lost when disconnected
|
||||
"""
|
||||
ready: bool
|
||||
team: typing.Optional[int]
|
||||
slot: typing.Optional[int]
|
||||
auth: typing.Optional[str]
|
||||
seed_name: typing.Optional[str]
|
||||
"""Bool to keep track of state for the /ready command"""
|
||||
team: int | None
|
||||
"""Team number of currently connected slot"""
|
||||
slot: int | None
|
||||
"""Slot number of currently connected slot"""
|
||||
auth: str | None
|
||||
"""Name used in Connect packet"""
|
||||
seed_name: str | None
|
||||
"""Seed name that will be validated on opening a socket if present"""
|
||||
|
||||
# locations
|
||||
locations_checked: typing.Set[int] # local state
|
||||
locations_scouted: typing.Set[int]
|
||||
items_received: typing.List[NetworkItem]
|
||||
missing_locations: typing.Set[int] # server state
|
||||
checked_locations: typing.Set[int] # server state
|
||||
server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations
|
||||
locations_info: typing.Dict[int, NetworkItem]
|
||||
locations_checked: set[int]
|
||||
"""
|
||||
Local container of location ids checked to signal that LocationChecks should be resent after reconnecting
|
||||
to be used to ensure that a LocationChecks packet does not get lost when disconnected
|
||||
"""
|
||||
locations_scouted: set[int]
|
||||
"""
|
||||
Local container of location ids scouted to signal that LocationScouts should be resent after reconnecting
|
||||
to be used to ensure that a LocationScouts packet does not get lost when disconnected
|
||||
"""
|
||||
items_received: list[NetworkItem]
|
||||
"""List of NetworkItems recieved from the server"""
|
||||
missing_locations: set[int]
|
||||
"""Container of Locations that are unchecked per server state"""
|
||||
checked_locations: set[int]
|
||||
"""Container of Locations that are checked per server state"""
|
||||
server_locations: set[int]
|
||||
"""Container of Locations that exist per server state; a combination between missing and checked locations"""
|
||||
locations_info: dict[int, NetworkItem]
|
||||
"""Dict of location id: NetworkItem info from LocationScouts request"""
|
||||
|
||||
# data storage
|
||||
stored_data: typing.Dict[str, typing.Any]
|
||||
stored_data_notification_keys: typing.Set[str]
|
||||
stored_data: dict[str, typing.Any]
|
||||
"""
|
||||
Data Storage values by key that were retrieved from the server
|
||||
any keys subscribed to with SetNotify will be kept up to date
|
||||
"""
|
||||
stored_data_notification_keys: set[str]
|
||||
"""Current container of watched Data Storage keys, managed by ctx.set_notify"""
|
||||
|
||||
# internals
|
||||
# current message box through kvui
|
||||
_messagebox: typing.Optional["kvui.MessageBox"] = None
|
||||
# message box reporting a loss of connection
|
||||
"""Current message box through kvui"""
|
||||
_messagebox_connection_loss: typing.Optional["kvui.MessageBox"] = None
|
||||
"""Message box reporting a loss of connection"""
|
||||
|
||||
def __init__(self, server_address: typing.Optional[str] = None, password: typing.Optional[str] = None) -> None:
|
||||
# server state
|
||||
|
||||
267
FF1Client.py
267
FF1Client.py
@@ -1,267 +0,0 @@
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import time
|
||||
from asyncio import StreamReader, StreamWriter
|
||||
from typing import List
|
||||
|
||||
|
||||
import Utils
|
||||
from Utils import async_start
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \
|
||||
get_base_parser
|
||||
|
||||
SYSTEM_MESSAGE_ID = 0
|
||||
|
||||
CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_ff1.lua"
|
||||
CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure connector_ff1.lua is running"
|
||||
CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_ff1.lua"
|
||||
CONNECTION_TENTATIVE_STATUS = "Initial Connection Made"
|
||||
CONNECTION_CONNECTED_STATUS = "Connected"
|
||||
CONNECTION_INITIAL_STATUS = "Connection has not been initiated"
|
||||
|
||||
DISPLAY_MSGS = True
|
||||
|
||||
|
||||
class FF1CommandProcessor(ClientCommandProcessor):
|
||||
def __init__(self, ctx: CommonContext):
|
||||
super().__init__(ctx)
|
||||
|
||||
def _cmd_nes(self):
|
||||
"""Check NES Connection State"""
|
||||
if isinstance(self.ctx, FF1Context):
|
||||
logger.info(f"NES Status: {self.ctx.nes_status}")
|
||||
|
||||
def _cmd_toggle_msgs(self):
|
||||
"""Toggle displaying messages in EmuHawk"""
|
||||
global DISPLAY_MSGS
|
||||
DISPLAY_MSGS = not DISPLAY_MSGS
|
||||
logger.info(f"Messages are now {'enabled' if DISPLAY_MSGS else 'disabled'}")
|
||||
|
||||
|
||||
class FF1Context(CommonContext):
|
||||
command_processor = FF1CommandProcessor
|
||||
game = 'Final Fantasy'
|
||||
items_handling = 0b111 # full remote
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super().__init__(server_address, password)
|
||||
self.nes_streams: (StreamReader, StreamWriter) = None
|
||||
self.nes_sync_task = None
|
||||
self.messages = {}
|
||||
self.locations_array = None
|
||||
self.nes_status = CONNECTION_INITIAL_STATUS
|
||||
self.awaiting_rom = False
|
||||
self.display_msgs = True
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(FF1Context, self).server_auth(password_requested)
|
||||
if not self.auth:
|
||||
self.awaiting_rom = True
|
||||
logger.info('Awaiting connection to NES to get Player information')
|
||||
return
|
||||
|
||||
await self.send_connect()
|
||||
|
||||
def _set_message(self, msg: str, msg_id: int):
|
||||
if DISPLAY_MSGS:
|
||||
self.messages[time.time(), msg_id] = msg
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd == 'Connected':
|
||||
async_start(parse_locations(self.locations_array, self, True))
|
||||
elif cmd == 'Print':
|
||||
msg = args['text']
|
||||
if ': !' not in msg:
|
||||
self._set_message(msg, SYSTEM_MESSAGE_ID)
|
||||
|
||||
def on_print_json(self, args: dict):
|
||||
if self.ui:
|
||||
self.ui.print_json(copy.deepcopy(args["data"]))
|
||||
else:
|
||||
text = self.jsontotextparser(copy.deepcopy(args["data"]))
|
||||
logger.info(text)
|
||||
relevant = args.get("type", None) in {"Hint", "ItemSend"}
|
||||
if relevant:
|
||||
item = args["item"]
|
||||
# goes to this world
|
||||
if self.slot_concerns_self(args["receiving"]):
|
||||
relevant = True
|
||||
# found in this world
|
||||
elif self.slot_concerns_self(item.player):
|
||||
relevant = True
|
||||
# not related
|
||||
else:
|
||||
relevant = False
|
||||
if relevant:
|
||||
item = args["item"]
|
||||
msg = self.raw_text_parser(copy.deepcopy(args["data"]))
|
||||
self._set_message(msg, item.item)
|
||||
|
||||
def run_gui(self):
|
||||
from kvui import GameManager
|
||||
|
||||
class FF1Manager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago Final Fantasy 1 Client"
|
||||
|
||||
self.ui = FF1Manager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
|
||||
def get_payload(ctx: FF1Context):
|
||||
current_time = time.time()
|
||||
return json.dumps(
|
||||
{
|
||||
"items": [item.item for item in ctx.items_received],
|
||||
"messages": {f'{key[0]}:{key[1]}': value for key, value in ctx.messages.items()
|
||||
if key[0] > current_time - 10}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
async def parse_locations(locations_array: List[int], ctx: FF1Context, force: bool):
|
||||
if locations_array == ctx.locations_array and not force:
|
||||
return
|
||||
else:
|
||||
# print("New values")
|
||||
ctx.locations_array = locations_array
|
||||
locations_checked = []
|
||||
if len(locations_array) > 0xFE and locations_array[0xFE] & 0x02 != 0 and not ctx.finished_game:
|
||||
await ctx.send_msgs([
|
||||
{"cmd": "StatusUpdate",
|
||||
"status": 30}
|
||||
])
|
||||
ctx.finished_game = True
|
||||
for location in ctx.missing_locations:
|
||||
# index will be - 0x100 or 0x200
|
||||
index = location
|
||||
if location < 0x200:
|
||||
# Location is a chest
|
||||
index -= 0x100
|
||||
flag = 0x04
|
||||
else:
|
||||
# Location is an NPC
|
||||
index -= 0x200
|
||||
flag = 0x02
|
||||
|
||||
# print(f"Location: {ctx.location_names[location]}")
|
||||
# print(f"Index: {str(hex(index))}")
|
||||
# print(f"value: {locations_array[index] & flag != 0}")
|
||||
if locations_array[index] & flag != 0:
|
||||
locations_checked.append(location)
|
||||
if locations_checked:
|
||||
# print([ctx.location_names[location] for location in locations_checked])
|
||||
await ctx.send_msgs([
|
||||
{"cmd": "LocationChecks",
|
||||
"locations": locations_checked}
|
||||
])
|
||||
|
||||
|
||||
async def nes_sync_task(ctx: FF1Context):
|
||||
logger.info("Starting nes connector. Use /nes for status information")
|
||||
while not ctx.exit_event.is_set():
|
||||
error_status = None
|
||||
if ctx.nes_streams:
|
||||
(reader, writer) = ctx.nes_streams
|
||||
msg = get_payload(ctx).encode()
|
||||
writer.write(msg)
|
||||
writer.write(b'\n')
|
||||
try:
|
||||
await asyncio.wait_for(writer.drain(), timeout=1.5)
|
||||
try:
|
||||
# Data will return a dict with up to two fields:
|
||||
# 1. A keepalive response of the Players Name (always)
|
||||
# 2. An array representing the memory values of the locations area (if in game)
|
||||
data = await asyncio.wait_for(reader.readline(), timeout=5)
|
||||
data_decoded = json.loads(data.decode())
|
||||
# print(data_decoded)
|
||||
if ctx.game is not None and 'locations' in data_decoded:
|
||||
# Not just a keep alive ping, parse
|
||||
async_start(parse_locations(data_decoded['locations'], ctx, False))
|
||||
if not ctx.auth:
|
||||
ctx.auth = ''.join([chr(i) for i in data_decoded['playerName'] if i != 0])
|
||||
if ctx.auth == '':
|
||||
logger.info("Invalid ROM detected. No player name built into the ROM. Please regenerate"
|
||||
"the ROM using the same link but adding your slot name")
|
||||
if ctx.awaiting_rom:
|
||||
await ctx.server_auth(False)
|
||||
except asyncio.TimeoutError:
|
||||
logger.debug("Read Timed Out, Reconnecting")
|
||||
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||
writer.close()
|
||||
ctx.nes_streams = None
|
||||
except ConnectionResetError as e:
|
||||
logger.debug("Read failed due to Connection Lost, Reconnecting")
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
writer.close()
|
||||
ctx.nes_streams = None
|
||||
except TimeoutError:
|
||||
logger.debug("Connection Timed Out, Reconnecting")
|
||||
error_status = CONNECTION_TIMING_OUT_STATUS
|
||||
writer.close()
|
||||
ctx.nes_streams = None
|
||||
except ConnectionResetError:
|
||||
logger.debug("Connection Lost, Reconnecting")
|
||||
error_status = CONNECTION_RESET_STATUS
|
||||
writer.close()
|
||||
ctx.nes_streams = None
|
||||
if ctx.nes_status == CONNECTION_TENTATIVE_STATUS:
|
||||
if not error_status:
|
||||
logger.info("Successfully Connected to NES")
|
||||
ctx.nes_status = CONNECTION_CONNECTED_STATUS
|
||||
else:
|
||||
ctx.nes_status = f"Was tentatively connected but error occured: {error_status}"
|
||||
elif error_status:
|
||||
ctx.nes_status = error_status
|
||||
logger.info("Lost connection to nes and attempting to reconnect. Use /nes for status updates")
|
||||
else:
|
||||
try:
|
||||
logger.debug("Attempting to connect to NES")
|
||||
ctx.nes_streams = await asyncio.wait_for(asyncio.open_connection("localhost", 52980), timeout=10)
|
||||
ctx.nes_status = CONNECTION_TENTATIVE_STATUS
|
||||
except TimeoutError:
|
||||
logger.debug("Connection Timed Out, Trying Again")
|
||||
ctx.nes_status = CONNECTION_TIMING_OUT_STATUS
|
||||
continue
|
||||
except ConnectionRefusedError:
|
||||
logger.debug("Connection Refused, Trying Again")
|
||||
ctx.nes_status = CONNECTION_REFUSED_STATUS
|
||||
continue
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Text Mode to use !hint and such with games that have no text entry
|
||||
Utils.init_logging("FF1Client")
|
||||
|
||||
options = Utils.get_options()
|
||||
DISPLAY_MSGS = options["ffr_options"]["display_msgs"]
|
||||
|
||||
async def main(args):
|
||||
ctx = FF1Context(args.connect, args.password)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop")
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
ctx.nes_sync_task = asyncio.create_task(nes_sync_task(ctx), name="NES Sync")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
|
||||
await ctx.shutdown()
|
||||
|
||||
if ctx.nes_sync_task:
|
||||
await ctx.nes_sync_task
|
||||
|
||||
|
||||
import colorama
|
||||
|
||||
parser = get_base_parser()
|
||||
args = parser.parse_args()
|
||||
colorama.just_fix_windows_console()
|
||||
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
12
Generate.py
12
Generate.py
@@ -224,10 +224,14 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
except Exception as e:
|
||||
raise Exception(f"Error setting {k} to {v} for player {player}") from e
|
||||
|
||||
if path == args.weights_file_path: # if name came from the weights file, just use base player name
|
||||
erargs.name[player] = f"Player{player}"
|
||||
elif player not in erargs.name: # if name was not specified, generate it from filename
|
||||
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||
# name was not specified
|
||||
if player not in erargs.name:
|
||||
if path == args.weights_file_path:
|
||||
# weights file, so we need to make the name unique
|
||||
erargs.name[player] = f"Player{player}"
|
||||
else:
|
||||
# use the filename
|
||||
erargs.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||
erargs.name[player] = handle_name(erargs.name[player], player, name_counter)
|
||||
|
||||
player += 1
|
||||
|
||||
@@ -392,7 +392,7 @@ def run_gui(path: str, args: Any) -> None:
|
||||
if file and component:
|
||||
run_component(component, file)
|
||||
else:
|
||||
logging.warning(f"unable to identify component for {file}")
|
||||
logging.warning(f"unable to identify component for {filename}")
|
||||
|
||||
def _on_keyboard(self, window: Window, key: int, scancode: int, codepoint: str, modifier: list[str]):
|
||||
# Activate search as soon as we start typing, no matter if we are focused on the search box or not.
|
||||
|
||||
@@ -14,6 +14,7 @@ import requests
|
||||
|
||||
import Utils
|
||||
from Utils import is_windows
|
||||
from settings import get_settings
|
||||
|
||||
atexit.register(input, "Press enter to exit.")
|
||||
|
||||
@@ -147,9 +148,11 @@ def find_jdk(version: str) -> str:
|
||||
if os.path.isfile(jdk_exe):
|
||||
return jdk_exe
|
||||
else:
|
||||
jdk_exe = shutil.which(options["minecraft_options"].get("java", "java"))
|
||||
jdk_exe = shutil.which(options.java)
|
||||
if not jdk_exe:
|
||||
raise Exception("Could not find Java. Is Java installed on the system?")
|
||||
jdk_exe = shutil.which("java") # try to fall back to system java
|
||||
if not jdk_exe:
|
||||
raise Exception("Could not find Java. Is Java installed on the system?")
|
||||
return jdk_exe
|
||||
|
||||
|
||||
@@ -285,8 +288,8 @@ if __name__ == '__main__':
|
||||
# Change to executable's working directory
|
||||
os.chdir(os.path.abspath(os.path.dirname(sys.argv[0])))
|
||||
|
||||
options = Utils.get_options()
|
||||
channel = args.channel or options["minecraft_options"]["release_channel"]
|
||||
options = get_settings().minecraft_options
|
||||
channel = args.channel or options.release_channel
|
||||
apmc_data = None
|
||||
data_version = args.data_version or None
|
||||
|
||||
@@ -299,8 +302,8 @@ if __name__ == '__main__':
|
||||
|
||||
versions = get_minecraft_versions(data_version, channel)
|
||||
|
||||
forge_dir = options["minecraft_options"]["forge_directory"]
|
||||
max_heap = options["minecraft_options"]["max_heap_size"]
|
||||
forge_dir = options.forge_directory
|
||||
max_heap = options.max_heap_size
|
||||
forge_version = args.forge or versions["forge"]
|
||||
java_version = args.java or versions["java"]
|
||||
mod_url = versions["url"]
|
||||
|
||||
@@ -458,8 +458,12 @@ class Context:
|
||||
self.generator_version = Version(*decoded_obj["version"])
|
||||
clients_ver = decoded_obj["minimum_versions"].get("clients", {})
|
||||
self.minimum_client_versions = {}
|
||||
if self.generator_version < Version(0, 6, 2):
|
||||
min_version = Version(0, 1, 6)
|
||||
else:
|
||||
min_version = min_client_version
|
||||
for player, version in clients_ver.items():
|
||||
self.minimum_client_versions[player] = max(Version(*version), min_client_version)
|
||||
self.minimum_client_versions[player] = max(Version(*version), min_version)
|
||||
|
||||
self.slot_info = decoded_obj["slot_info"]
|
||||
self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()}
|
||||
|
||||
@@ -12,6 +12,7 @@ from CommonClient import CommonContext, server_loop, gui_enabled, \
|
||||
import Utils
|
||||
from Utils import async_start
|
||||
from worlds import network_data_package
|
||||
from worlds.oot import OOTWorld
|
||||
from worlds.oot.Rom import Rom, compress_rom_file
|
||||
from worlds.oot.N64Patch import apply_patch_file
|
||||
from worlds.oot.Utils import data_path
|
||||
@@ -280,7 +281,7 @@ async def n64_sync_task(ctx: OoTContext):
|
||||
|
||||
|
||||
async def run_game(romfile):
|
||||
auto_start = Utils.get_options()["oot_options"].get("rom_start", True)
|
||||
auto_start = OOTWorld.settings.rom_start
|
||||
if auto_start is True:
|
||||
import webbrowser
|
||||
webbrowser.open(romfile)
|
||||
@@ -295,7 +296,7 @@ async def patch_and_run_game(apz5_file):
|
||||
decomp_path = base_name + '-decomp.z64'
|
||||
comp_path = base_name + '.z64'
|
||||
# Load vanilla ROM, patch file, compress ROM
|
||||
rom_file_name = Utils.get_options()["oot_options"]["rom_file"]
|
||||
rom_file_name = OOTWorld.settings.rom_file
|
||||
rom = Rom(rom_file_name)
|
||||
|
||||
sub_file = None
|
||||
|
||||
3
Utils.py
3
Utils.py
@@ -540,7 +540,8 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO,
|
||||
if add_timestamp:
|
||||
stream_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(stream_handler)
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
if hasattr(sys.stdout, "reconfigure"):
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
|
||||
# Relay unhandled exceptions to logger.
|
||||
if not getattr(sys.excepthook, "_wrapped", False): # skip if already modified
|
||||
|
||||
@@ -80,10 +80,8 @@ def register():
|
||||
"""Import submodules, triggering their registering on flask routing.
|
||||
Note: initializes worlds subsystem."""
|
||||
# has automatic patch integration
|
||||
import worlds.AutoWorld
|
||||
import worlds.Files
|
||||
app.jinja_env.filters['supports_apdeltapatch'] = lambda game_name: \
|
||||
game_name in worlds.Files.AutoPatchRegister.patch_types
|
||||
app.jinja_env.filters['is_applayercontainer'] = worlds.Files.is_ap_player_container
|
||||
|
||||
from WebHostLib.customserver import run_server_process
|
||||
# to trigger app routing picking up on it
|
||||
|
||||
@@ -17,9 +17,7 @@
|
||||
This page allows you to host a game which was not generated by the website. For example, if you have
|
||||
generated a game on your own computer, you may upload the zip file created by the generator to
|
||||
host the game here. This will also provide a tracker, and the ability for your players to download
|
||||
their patch files if the game is core-verified. For Custom Games, you can find the patch files in
|
||||
the output .zip file you are uploading here. You need to manually distribute those patch files to
|
||||
your players.
|
||||
their patch files.
|
||||
</p>
|
||||
<p>In addition to the zip file created by the generator, you may upload a multidata file here as well.</p>
|
||||
<div id="host-game-form-wrapper">
|
||||
|
||||
@@ -29,27 +29,15 @@
|
||||
{% if patch.game == "Minecraft" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APMC File...</a>
|
||||
{% elif patch.game == "Factorio" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download Factorio Mod...</a>
|
||||
{% elif patch.game == "Kingdom Hearts 2" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download Kingdom Hearts 2 Mod...</a>
|
||||
{% elif patch.game == "Ocarina of Time" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APZ5 File...</a>
|
||||
{% elif patch.game == "VVVVVV" and room.seed.slots|length == 1 %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APV6 File...</a>
|
||||
{% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APSM64EX File...</a>
|
||||
{% elif patch.game | supports_apdeltapatch %}
|
||||
{% elif patch.game | is_applayercontainer(patch.data, patch.player_id) %}
|
||||
<a href="{{ url_for("download_patch", patch_id=patch.id, room_id=room.id) }}" download>
|
||||
Download Patch File...</a>
|
||||
{% elif patch.game == "Final Fantasy Mystic Quest" %}
|
||||
<a href="{{ url_for("download_slot_file", room_id=room.id, player_id=patch.player_id) }}" download>
|
||||
Download APMQ File...</a>
|
||||
{% else %}
|
||||
No file to download for this game.
|
||||
{% endif %}
|
||||
|
||||
@@ -1,462 +0,0 @@
|
||||
local socket = require("socket")
|
||||
local json = require('json')
|
||||
local math = require('math')
|
||||
require("common")
|
||||
|
||||
local STATE_OK = "Ok"
|
||||
local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected"
|
||||
local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made"
|
||||
local STATE_UNINITIALIZED = "Uninitialized"
|
||||
|
||||
local ITEM_INDEX = 0x03
|
||||
local WEAPON_INDEX = 0x07
|
||||
local ARMOR_INDEX = 0x0B
|
||||
|
||||
local goldLookup = {
|
||||
[0x16C] = 10,
|
||||
[0x16D] = 20,
|
||||
[0x16E] = 25,
|
||||
[0x16F] = 30,
|
||||
[0x170] = 55,
|
||||
[0x171] = 70,
|
||||
[0x172] = 85,
|
||||
[0x173] = 110,
|
||||
[0x174] = 135,
|
||||
[0x175] = 155,
|
||||
[0x176] = 160,
|
||||
[0x177] = 180,
|
||||
[0x178] = 240,
|
||||
[0x179] = 255,
|
||||
[0x17A] = 260,
|
||||
[0x17B] = 295,
|
||||
[0x17C] = 300,
|
||||
[0x17D] = 315,
|
||||
[0x17E] = 330,
|
||||
[0x17F] = 350,
|
||||
[0x180] = 385,
|
||||
[0x181] = 400,
|
||||
[0x182] = 450,
|
||||
[0x183] = 500,
|
||||
[0x184] = 530,
|
||||
[0x185] = 575,
|
||||
[0x186] = 620,
|
||||
[0x187] = 680,
|
||||
[0x188] = 750,
|
||||
[0x189] = 795,
|
||||
[0x18A] = 880,
|
||||
[0x18B] = 1020,
|
||||
[0x18C] = 1250,
|
||||
[0x18D] = 1455,
|
||||
[0x18E] = 1520,
|
||||
[0x18F] = 1760,
|
||||
[0x190] = 1975,
|
||||
[0x191] = 2000,
|
||||
[0x192] = 2750,
|
||||
[0x193] = 3400,
|
||||
[0x194] = 4150,
|
||||
[0x195] = 5000,
|
||||
[0x196] = 5450,
|
||||
[0x197] = 6400,
|
||||
[0x198] = 6720,
|
||||
[0x199] = 7340,
|
||||
[0x19A] = 7690,
|
||||
[0x19B] = 7900,
|
||||
[0x19C] = 8135,
|
||||
[0x19D] = 9000,
|
||||
[0x19E] = 9300,
|
||||
[0x19F] = 9500,
|
||||
[0x1A0] = 9900,
|
||||
[0x1A1] = 10000,
|
||||
[0x1A2] = 12350,
|
||||
[0x1A3] = 13000,
|
||||
[0x1A4] = 13450,
|
||||
[0x1A5] = 14050,
|
||||
[0x1A6] = 14720,
|
||||
[0x1A7] = 15000,
|
||||
[0x1A8] = 17490,
|
||||
[0x1A9] = 18010,
|
||||
[0x1AA] = 19990,
|
||||
[0x1AB] = 20000,
|
||||
[0x1AC] = 20010,
|
||||
[0x1AD] = 26000,
|
||||
[0x1AE] = 45000,
|
||||
[0x1AF] = 65000
|
||||
}
|
||||
|
||||
local extensionConsumableLookup = {
|
||||
[432] = 0x3C,
|
||||
[436] = 0x3C,
|
||||
[440] = 0x3C,
|
||||
[433] = 0x3D,
|
||||
[437] = 0x3D,
|
||||
[441] = 0x3D,
|
||||
[434] = 0x3E,
|
||||
[438] = 0x3E,
|
||||
[442] = 0x3E,
|
||||
[435] = 0x3F,
|
||||
[439] = 0x3F,
|
||||
[443] = 0x3F
|
||||
}
|
||||
|
||||
local noOverworldItemsLookup = {
|
||||
[499] = 0x2B,
|
||||
[500] = 0x12,
|
||||
}
|
||||
|
||||
local consumableStacks = nil
|
||||
local prevstate = ""
|
||||
local curstate = STATE_UNINITIALIZED
|
||||
local ff1Socket = nil
|
||||
local frame = 0
|
||||
|
||||
local isNesHawk = false
|
||||
|
||||
|
||||
--Sets correct memory access functions based on whether NesHawk or QuickNES is loaded
|
||||
local function defineMemoryFunctions()
|
||||
local memDomain = {}
|
||||
local domains = memory.getmemorydomainlist()
|
||||
if domains[1] == "System Bus" then
|
||||
--NesHawk
|
||||
isNesHawk = true
|
||||
memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
|
||||
memDomain["saveram"] = function() memory.usememorydomain("Battery RAM") end
|
||||
memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
|
||||
elseif domains[1] == "WRAM" then
|
||||
--QuickNES
|
||||
memDomain["systembus"] = function() memory.usememorydomain("System Bus") end
|
||||
memDomain["saveram"] = function() memory.usememorydomain("WRAM") end
|
||||
memDomain["rom"] = function() memory.usememorydomain("PRG ROM") end
|
||||
end
|
||||
return memDomain
|
||||
end
|
||||
|
||||
local memDomain = defineMemoryFunctions()
|
||||
|
||||
local function StateOKForMainLoop()
|
||||
memDomain.saveram()
|
||||
local A = u8(0x102) -- Party Made
|
||||
local B = u8(0x0FC)
|
||||
local C = u8(0x0A3)
|
||||
return A ~= 0x00 and not (A== 0xF2 and B == 0xF2 and C == 0xF2)
|
||||
end
|
||||
|
||||
function generateLocationChecked()
|
||||
memDomain.saveram()
|
||||
data = uRange(0x01FF, 0x101)
|
||||
data[0] = nil
|
||||
return data
|
||||
end
|
||||
|
||||
function setConsumableStacks()
|
||||
memDomain.rom()
|
||||
consumableStacks = {}
|
||||
-- In order shards, tent, cabin, house, heal, pure, soft, ext1, ext2, ext3, ex4
|
||||
consumableStacks[0x35] = 1
|
||||
consumableStacks[0x36] = u8(0x47400) + 1
|
||||
consumableStacks[0x37] = u8(0x47401) + 1
|
||||
consumableStacks[0x38] = u8(0x47402) + 1
|
||||
consumableStacks[0x39] = u8(0x47403) + 1
|
||||
consumableStacks[0x3A] = u8(0x47404) + 1
|
||||
consumableStacks[0x3B] = u8(0x47405) + 1
|
||||
consumableStacks[0x3C] = u8(0x47406) + 1
|
||||
consumableStacks[0x3D] = u8(0x47407) + 1
|
||||
consumableStacks[0x3E] = u8(0x47408) + 1
|
||||
consumableStacks[0x3F] = u8(0x47409) + 1
|
||||
end
|
||||
|
||||
function getEmptyWeaponSlots()
|
||||
memDomain.saveram()
|
||||
ret = {}
|
||||
count = 1
|
||||
slot1 = uRange(0x118, 0x4)
|
||||
slot2 = uRange(0x158, 0x4)
|
||||
slot3 = uRange(0x198, 0x4)
|
||||
slot4 = uRange(0x1D8, 0x4)
|
||||
for i,v in pairs(slot1) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x118 + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
for i,v in pairs(slot2) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x158 + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
for i,v in pairs(slot3) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x198 + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
for i,v in pairs(slot4) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x1D8 + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
|
||||
function getEmptyArmorSlots()
|
||||
memDomain.saveram()
|
||||
ret = {}
|
||||
count = 1
|
||||
slot1 = uRange(0x11C, 0x4)
|
||||
slot2 = uRange(0x15C, 0x4)
|
||||
slot3 = uRange(0x19C, 0x4)
|
||||
slot4 = uRange(0x1DC, 0x4)
|
||||
for i,v in pairs(slot1) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x11C + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
for i,v in pairs(slot2) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x15C + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
for i,v in pairs(slot3) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x19C + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
for i,v in pairs(slot4) do
|
||||
if v == 0 then
|
||||
ret[count] = 0x1DC + i
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
return ret
|
||||
end
|
||||
local 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
|
||||
function processBlock(block)
|
||||
local msgBlock = block['messages']
|
||||
if msgBlock ~= nil then
|
||||
for i, v in pairs(msgBlock) do
|
||||
if itemMessages[i] == nil then
|
||||
local msg = {TTL=450, message=v, color=0xFFFF0000}
|
||||
itemMessages[i] = msg
|
||||
end
|
||||
end
|
||||
end
|
||||
local itemsBlock = block["items"]
|
||||
memDomain.saveram()
|
||||
isInGame = u8(0x102)
|
||||
if itemsBlock ~= nil and isInGame ~= 0x00 then
|
||||
if consumableStacks == nil then
|
||||
setConsumableStacks()
|
||||
end
|
||||
memDomain.saveram()
|
||||
-- print('ITEMBLOCK: ')
|
||||
-- print(itemsBlock)
|
||||
itemIndex = u8(ITEM_INDEX)
|
||||
-- print('ITEMINDEX: '..itemIndex)
|
||||
for i, v in pairs(slice(itemsBlock, itemIndex, #itemsBlock)) do
|
||||
-- Minus the offset and add to the correct domain
|
||||
local memoryLocation = v
|
||||
if v >= 0x100 and v <= 0x114 then
|
||||
-- This is a key item
|
||||
memoryLocation = memoryLocation - 0x0E0
|
||||
wU8(memoryLocation, 0x01)
|
||||
elseif v >= 0x1E0 and v <= 0x1F2 then
|
||||
-- This is a movement item
|
||||
-- Minus Offset (0x100) - movement offset (0xE0)
|
||||
memoryLocation = memoryLocation - 0x1E0
|
||||
-- Canal is a flipped bit
|
||||
if memoryLocation == 0x0C then
|
||||
wU8(memoryLocation, 0x00)
|
||||
else
|
||||
wU8(memoryLocation, 0x01)
|
||||
end
|
||||
elseif v >= 0x1F3 and v <= 0x1F4 then
|
||||
-- NoOverworld special items
|
||||
memoryLocation = noOverworldItemsLookup[v]
|
||||
wU8(memoryLocation, 0x01)
|
||||
elseif v >= 0x16C and v <= 0x1AF then
|
||||
-- This is a gold item
|
||||
amountToAdd = goldLookup[v]
|
||||
biggest = u8(0x01E)
|
||||
medium = u8(0x01D)
|
||||
smallest = u8(0x01C)
|
||||
currentValue = 0x10000 * biggest + 0x100 * medium + smallest
|
||||
newValue = currentValue + amountToAdd
|
||||
newBiggest = math.floor(newValue / 0x10000)
|
||||
newMedium = math.floor(math.fmod(newValue, 0x10000) / 0x100)
|
||||
newSmallest = math.floor(math.fmod(newValue, 0x100))
|
||||
wU8(0x01E, newBiggest)
|
||||
wU8(0x01D, newMedium)
|
||||
wU8(0x01C, newSmallest)
|
||||
elseif v >= 0x115 and v <= 0x11B then
|
||||
-- This is a regular consumable OR a shard
|
||||
-- Minus Offset (0x100) + item offset (0x20)
|
||||
memoryLocation = memoryLocation - 0x0E0
|
||||
currentValue = u8(memoryLocation)
|
||||
amountToAdd = consumableStacks[memoryLocation]
|
||||
if currentValue < 99 then
|
||||
wU8(memoryLocation, currentValue + amountToAdd)
|
||||
end
|
||||
elseif v >= 0x1B0 and v <= 0x1BB then
|
||||
-- This is an extension consumable
|
||||
memoryLocation = extensionConsumableLookup[v]
|
||||
currentValue = u8(memoryLocation)
|
||||
amountToAdd = consumableStacks[memoryLocation]
|
||||
if currentValue < 99 then
|
||||
value = currentValue + amountToAdd
|
||||
if value > 99 then
|
||||
value = 99
|
||||
end
|
||||
wU8(memoryLocation, value)
|
||||
end
|
||||
end
|
||||
end
|
||||
if #itemsBlock > itemIndex then
|
||||
wU8(ITEM_INDEX, #itemsBlock)
|
||||
end
|
||||
|
||||
memDomain.saveram()
|
||||
weaponIndex = u8(WEAPON_INDEX)
|
||||
emptyWeaponSlots = getEmptyWeaponSlots()
|
||||
lastUsedWeaponIndex = weaponIndex
|
||||
-- print('WEAPON_INDEX: '.. weaponIndex)
|
||||
memDomain.saveram()
|
||||
for i, v in pairs(slice(itemsBlock, weaponIndex, #itemsBlock)) do
|
||||
if v >= 0x11C and v <= 0x143 then
|
||||
-- Minus the offset and add to the correct domain
|
||||
local itemValue = v - 0x11B
|
||||
if #emptyWeaponSlots > 0 then
|
||||
slot = table.remove(emptyWeaponSlots, 1)
|
||||
wU8(slot, itemValue)
|
||||
lastUsedWeaponIndex = weaponIndex + i
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if lastUsedWeaponIndex ~= weaponIndex then
|
||||
wU8(WEAPON_INDEX, lastUsedWeaponIndex)
|
||||
end
|
||||
memDomain.saveram()
|
||||
armorIndex = u8(ARMOR_INDEX)
|
||||
emptyArmorSlots = getEmptyArmorSlots()
|
||||
lastUsedArmorIndex = armorIndex
|
||||
-- print('ARMOR_INDEX: '.. armorIndex)
|
||||
memDomain.saveram()
|
||||
for i, v in pairs(slice(itemsBlock, armorIndex, #itemsBlock)) do
|
||||
if v >= 0x144 and v <= 0x16B then
|
||||
-- Minus the offset and add to the correct domain
|
||||
local itemValue = v - 0x143
|
||||
if #emptyArmorSlots > 0 then
|
||||
slot = table.remove(emptyArmorSlots, 1)
|
||||
wU8(slot, itemValue)
|
||||
lastUsedArmorIndex = armorIndex + i
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if lastUsedArmorIndex ~= armorIndex then
|
||||
wU8(ARMOR_INDEX, lastUsedArmorIndex)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function receive()
|
||||
l, e = ff1Socket:receive()
|
||||
if e == 'closed' then
|
||||
if curstate == STATE_OK then
|
||||
print("Connection closed")
|
||||
end
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
elseif e == 'timeout' then
|
||||
print("timeout")
|
||||
return
|
||||
elseif e ~= nil then
|
||||
print(e)
|
||||
curstate = STATE_UNINITIALIZED
|
||||
return
|
||||
end
|
||||
processBlock(json.decode(l))
|
||||
|
||||
-- Determine Message to send back
|
||||
memDomain.rom()
|
||||
local playerName = uRange(0x7BCBF, 0x41)
|
||||
playerName[0] = nil
|
||||
local retTable = {}
|
||||
retTable["playerName"] = playerName
|
||||
if StateOKForMainLoop() then
|
||||
retTable["locations"] = generateLocationChecked()
|
||||
end
|
||||
msg = json.encode(retTable).."\n"
|
||||
local ret, error = ff1Socket:send(msg)
|
||||
if ret == nil then
|
||||
print(error)
|
||||
elseif curstate == STATE_INITIAL_CONNECTION_MADE then
|
||||
curstate = STATE_TENTATIVELY_CONNECTED
|
||||
elseif curstate == STATE_TENTATIVELY_CONNECTED then
|
||||
print("Connected!")
|
||||
itemMessages["(0,0)"] = {TTL=240, message="Connected", color="green"}
|
||||
curstate = STATE_OK
|
||||
end
|
||||
end
|
||||
|
||||
function main()
|
||||
if not checkBizHawkVersion() then
|
||||
return
|
||||
end
|
||||
server, error = socket.bind('localhost', 52980)
|
||||
|
||||
while true do
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
|
||||
frame = frame + 1
|
||||
drawMessages()
|
||||
if not (curstate == prevstate) then
|
||||
-- console.log("Current state: "..curstate)
|
||||
prevstate = curstate
|
||||
end
|
||||
if (curstate == STATE_OK) or (curstate == STATE_INITIAL_CONNECTION_MADE) or (curstate == STATE_TENTATIVELY_CONNECTED) then
|
||||
if (frame % 60 == 0) then
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "Blue")
|
||||
receive()
|
||||
else
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "Green")
|
||||
end
|
||||
elseif (curstate == STATE_UNINITIALIZED) then
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "White")
|
||||
if (frame % 60 == 0) then
|
||||
gui.drawEllipse(248, 9, 6, 6, "Black", "Yellow")
|
||||
|
||||
drawText(5, 8, "Waiting for client", 0xFFFF0000)
|
||||
drawText(5, 32, "Please start FF1Client.exe", 0xFFFF0000)
|
||||
|
||||
-- Advance so the messages are drawn
|
||||
emu.frameadvance()
|
||||
server:settimeout(2)
|
||||
print("Attempting to connect")
|
||||
local client, timeout = server:accept()
|
||||
if timeout == nil then
|
||||
-- print('Initial Connection Made')
|
||||
curstate = STATE_INITIAL_CONNECTION_MADE
|
||||
ff1Socket = client
|
||||
ff1Socket:settimeout(0)
|
||||
end
|
||||
end
|
||||
end
|
||||
emu.frameadvance()
|
||||
end
|
||||
end
|
||||
|
||||
main()
|
||||
@@ -231,11 +231,11 @@ Sent to clients after a client requested this message be sent to them, more info
|
||||
Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for.
|
||||
|
||||
#### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| type | str | The [PacketProblemType](#PacketProblemType) that was detected in the packet. |
|
||||
| original_cmd | Optional[str] | The `cmd` argument of the faulty packet, will be `None` if the `cmd` failed to be parsed. |
|
||||
| text | str | A descriptive message of the problem at hand. |
|
||||
| Name | Type | Notes |
|
||||
| ---- |-------------| ----- |
|
||||
| type | str | The [PacketProblemType](#PacketProblemType) that was detected in the packet. |
|
||||
| original_cmd | str \| None | The `cmd` argument of the faulty packet, will be `None` if the `cmd` failed to be parsed. |
|
||||
| text | str | A descriptive message of the problem at hand. |
|
||||
|
||||
##### PacketProblemType
|
||||
`PacketProblemType` indicates the type of problem that was detected in the faulty packet, the known problem types are below but others may be added in the future.
|
||||
@@ -551,14 +551,14 @@ In JSON this may look like:
|
||||
Message nodes sent along with [PrintJSON](#PrintJSON) packet to be reconstructed into a legible message. The nodes are intended to be read in the order they are listed in the packet.
|
||||
|
||||
```python
|
||||
from typing import TypedDict, Optional
|
||||
from typing import TypedDict
|
||||
class JSONMessagePart(TypedDict):
|
||||
type: Optional[str]
|
||||
text: Optional[str]
|
||||
color: Optional[str] # only available if type is a color
|
||||
flags: Optional[int] # only available if type is an item_id or item_name
|
||||
player: Optional[int] # only available if type is either item or location
|
||||
hint_status: Optional[HintStatus] # only available if type is hint_status
|
||||
type: str | None
|
||||
text: str | None
|
||||
color: str | None # only available if type is a color
|
||||
flags: int | None # only available if type is an item_id or item_name
|
||||
player: int | None # only available if type is either item or location
|
||||
hint_status: HintStatus | None # only available if type is hint_status
|
||||
```
|
||||
|
||||
`type` is used to denote the intent of the message part. This can be used to indicate special information which may be rendered differently depending on client. How these types are displayed in Archipelago's ALttP client is not the end-all be-all. Other clients may choose to interpret and display these messages differently.
|
||||
|
||||
@@ -333,7 +333,7 @@ within the world.
|
||||
### TextChoice
|
||||
Like choice allows you to predetermine options and has all of the same comparison methods and handling. Also accepts any
|
||||
user defined string as a valid option, so will either need to be validated by adding a validation step to the option
|
||||
class or within world, if necessary. Value for this class is `Union[str, int]` so if you need the value at a specified
|
||||
class or within world, if necessary. Value for this class is `str | int` so if you need the value at a specified
|
||||
point, `self.options.my_option.current_key` will always return a string.
|
||||
|
||||
### PlandoBosses
|
||||
|
||||
@@ -102,17 +102,16 @@ In worlds, this should only be used for the top level to avoid issues when upgra
|
||||
|
||||
### Bool
|
||||
|
||||
Since `bool` can not be subclassed, use the `settings.Bool` helper in a `typing.Union` to get a comment in host.yaml.
|
||||
Since `bool` can not be subclassed, use the `settings.Bool` helper in a union to get a comment in host.yaml.
|
||||
|
||||
```python
|
||||
import settings
|
||||
import typing
|
||||
|
||||
class MySettings(settings.Group):
|
||||
class MyBool(settings.Bool):
|
||||
"""Doc string"""
|
||||
|
||||
my_value: typing.Union[MyBool, bool] = True
|
||||
my_value: MyBool | bool = True
|
||||
```
|
||||
|
||||
### UserFilePath
|
||||
@@ -134,15 +133,15 @@ Checks the file against [md5s](#md5s) by default.
|
||||
|
||||
Resolves to an executable (varying file extension based on platform)
|
||||
|
||||
#### description: Optional\[str\]
|
||||
#### description: str | None
|
||||
|
||||
Human-readable name to use in file browser
|
||||
|
||||
#### copy_to: Optional\[str\]
|
||||
#### copy_to: str | None
|
||||
|
||||
Instead of storing the path, copy the file.
|
||||
|
||||
#### md5s: List[Union[str, bytes]]
|
||||
#### md5s: list[str | bytes]
|
||||
|
||||
Provide md5 hashes as hex digests or raw bytes for automatic validation.
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ Type: dirifempty; Name: "{app}"
|
||||
[InstallDelete]
|
||||
Type: files; Name: "{app}\*.exe"
|
||||
Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
|
||||
Type: files; Name: "{app}\data\lua\connector_ff1.lua"
|
||||
Type: filesandordirs; Name: "{app}\SNI\lua*"
|
||||
Type: filesandordirs; Name: "{app}\EnemizerCLI*"
|
||||
#include "installdelete.iss"
|
||||
|
||||
@@ -159,7 +159,6 @@ class WorldTestBase(unittest.TestCase):
|
||||
self.multiworld.game[self.player] = self.game
|
||||
self.multiworld.player_name = {self.player: "Tester"}
|
||||
self.multiworld.set_seed(seed)
|
||||
self.multiworld.state = CollectionState(self.multiworld)
|
||||
random.seed(self.multiworld.seed)
|
||||
self.multiworld.seed_name = get_seed_name(random) # only called to get same RNG progression as Generate.py
|
||||
args = Namespace()
|
||||
@@ -168,6 +167,7 @@ class WorldTestBase(unittest.TestCase):
|
||||
1: option.from_any(self.options.get(name, option.default))
|
||||
})
|
||||
self.multiworld.set_options(args)
|
||||
self.multiworld.state = CollectionState(self.multiworld)
|
||||
self.world = self.multiworld.worlds[self.player]
|
||||
for step in gen_steps:
|
||||
call_all(self.multiworld, step)
|
||||
|
||||
@@ -59,13 +59,13 @@ def run_locations_benchmark():
|
||||
multiworld.game[1] = game
|
||||
multiworld.player_name = {1: "Tester"}
|
||||
multiworld.set_seed(0)
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
args = argparse.Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types[game].options_dataclass.type_hints.items():
|
||||
setattr(args, name, {
|
||||
1: option.from_any(getattr(option, "default"))
|
||||
})
|
||||
multiworld.set_options(args)
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
|
||||
gc.collect()
|
||||
for step in self.gen_steps:
|
||||
|
||||
@@ -49,7 +49,6 @@ def setup_multiworld(worlds: Union[List[Type[World]], Type[World]], steps: Tuple
|
||||
multiworld.game = {player: world_type.game for player, world_type in enumerate(worlds, 1)}
|
||||
multiworld.player_name = {player: f"Tester{player}" for player in multiworld.player_ids}
|
||||
multiworld.set_seed(seed)
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
args = Namespace()
|
||||
for player, world_type in enumerate(worlds, 1):
|
||||
for key, option in world_type.options_dataclass.type_hints.items():
|
||||
@@ -57,6 +56,7 @@ def setup_multiworld(worlds: Union[List[Type[World]], Type[World]], steps: Tuple
|
||||
updated_options[player] = option.from_any(option.default)
|
||||
setattr(args, key, updated_options)
|
||||
multiworld.set_options(args)
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
for step in steps:
|
||||
call_all(multiworld, step)
|
||||
return multiworld
|
||||
|
||||
@@ -528,7 +528,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
"""Called when an item is collected in to state. Useful for things such as progressive items or currency."""
|
||||
name = self.collect_item(state, item)
|
||||
if name:
|
||||
state.prog_items[self.player][name] += 1
|
||||
state.add_item(name, self.player)
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -536,9 +536,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
"""Called when an item is removed from to state. Useful for things such as progressive items or currency."""
|
||||
name = self.collect_item(state, item, True)
|
||||
if name:
|
||||
state.prog_items[self.player][name] -= 1
|
||||
if state.prog_items[self.player][name] < 1:
|
||||
del (state.prog_items[self.player][name])
|
||||
state.remove_item(name, self.player)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import zipfile
|
||||
from enum import IntEnum
|
||||
import os
|
||||
import threading
|
||||
from io import BytesIO
|
||||
|
||||
from typing import ClassVar, Dict, List, Literal, Tuple, Any, Optional, Union, BinaryIO, overload, Sequence
|
||||
|
||||
@@ -70,6 +71,18 @@ class AutoPatchExtensionRegister(abc.ABCMeta):
|
||||
container_version: int = 6
|
||||
|
||||
|
||||
def is_ap_player_container(game: str, data: bytes, player: int):
|
||||
if not zipfile.is_zipfile(BytesIO(data)):
|
||||
return False
|
||||
with zipfile.ZipFile(BytesIO(data), mode='r') as zf:
|
||||
if "archipelago.json" in zf.namelist():
|
||||
manifest = json.loads(zf.read("archipelago.json"))
|
||||
if "game" in manifest and "player" in manifest:
|
||||
if game == manifest["game"] and player == manifest["player"]:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class InvalidDataError(Exception):
|
||||
"""
|
||||
Since games can override `read_contents` in APContainer,
|
||||
|
||||
@@ -224,16 +224,12 @@ components: List[Component] = [
|
||||
Component('OoT Client', 'OoTClient',
|
||||
file_identifier=SuffixIdentifier('.apz5')),
|
||||
Component('OoT Adjuster', 'OoTAdjuster'),
|
||||
# FF1
|
||||
Component('FF1 Client', 'FF1Client'),
|
||||
# TLoZ
|
||||
Component('Zelda 1 Client', 'Zelda1Client', file_identifier=SuffixIdentifier('.aptloz')),
|
||||
# ChecksFinder
|
||||
Component('ChecksFinder Client', 'ChecksFinderClient'),
|
||||
# Starcraft 2
|
||||
Component('Starcraft 2 Client', 'Starcraft2Client'),
|
||||
# Wargroove
|
||||
Component('Wargroove Client', 'WargrooveClient'),
|
||||
# Zillion
|
||||
Component('Zillion Client', 'ZillionClient',
|
||||
file_identifier=SuffixIdentifier('.apzl')),
|
||||
|
||||
@@ -477,7 +477,7 @@ act_completions = {
|
||||
"Act Completion (Rush Hour)": LocData(2000311210, "Rush Hour",
|
||||
dlc_flags=HatDLC.dlc2,
|
||||
hookshot=True,
|
||||
required_hats=[HatType.ICE, HatType.BREWING]),
|
||||
required_hats=[HatType.ICE, HatType.BREWING, HatType.DWELLER]),
|
||||
|
||||
"Act Completion (Time Rift - Rumbi Factory)": LocData(2000312736, "Time Rift - Rumbi Factory",
|
||||
dlc_flags=HatDLC.dlc2),
|
||||
|
||||
@@ -455,7 +455,7 @@ def set_moderate_rules(world: "HatInTimeWorld"):
|
||||
if "Pink Paw Station Thug" in key and is_location_valid(world, key):
|
||||
set_rule(world.multiworld.get_location(key, world.player), lambda state: True)
|
||||
|
||||
# Moderate: clear Rush Hour without Hookshot
|
||||
# Moderate: clear Rush Hour without Hookshot or Dweller Mask
|
||||
set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player),
|
||||
lambda state: state.has("Metro Ticket - Pink", world.player)
|
||||
and state.has("Metro Ticket - Yellow", world.player)
|
||||
|
||||
@@ -10,12 +10,12 @@ class LTTPTestBase(unittest.TestCase):
|
||||
from worlds.alttp.Options import Medallion
|
||||
self.multiworld = MultiWorld(1)
|
||||
self.multiworld.game[1] = "A Link to the Past"
|
||||
self.multiworld.state = CollectionState(self.multiworld)
|
||||
self.multiworld.set_seed(None)
|
||||
args = Namespace()
|
||||
for name, option in AutoWorldRegister.world_types["A Link to the Past"].options_dataclass.type_hints.items():
|
||||
setattr(args, name, {1: option.from_any(getattr(option, "default"))})
|
||||
self.multiworld.set_options(args)
|
||||
self.multiworld.state = CollectionState(self.multiworld)
|
||||
self.world = self.multiworld.worlds[1]
|
||||
# by default medallion access is randomized, for unittests we set it to vanilla
|
||||
self.world.options.misery_mire_medallion.value = Medallion.option_ether
|
||||
|
||||
328
worlds/ff1/Client.py
Normal file
328
worlds/ff1/Client.py
Normal file
@@ -0,0 +1,328 @@
|
||||
import logging
|
||||
from collections import deque
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from NetUtils import ClientStatus
|
||||
|
||||
import worlds._bizhawk as bizhawk
|
||||
from worlds._bizhawk.client import BizHawkClient
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from worlds._bizhawk.context import BizHawkClientContext
|
||||
|
||||
|
||||
base_id = 7000
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
|
||||
rom_name_location = 0x07FFE3
|
||||
locations_array_start = 0x200
|
||||
locations_array_length = 0x100
|
||||
items_obtained = 0x03
|
||||
gp_location_low = 0x1C
|
||||
gp_location_middle = 0x1D
|
||||
gp_location_high = 0x1E
|
||||
weapons_arrays_starts = [0x118, 0x158, 0x198, 0x1D8]
|
||||
armors_arrays_starts = [0x11C, 0x15C, 0x19C, 0x1DC]
|
||||
status_a_location = 0x102
|
||||
status_b_location = 0x0FC
|
||||
status_c_location = 0x0A3
|
||||
|
||||
key_items = ["Lute", "Crown", "Crystal", "Herb", "Key", "Tnt", "Adamant", "Slab", "Ruby", "Rod",
|
||||
"Floater", "Chime", "Tail", "Cube", "Bottle", "Oxyale", "EarthOrb", "FireOrb", "WaterOrb", "AirOrb"]
|
||||
|
||||
consumables = ["Shard", "Tent", "Cabin", "House", "Heal", "Pure", "Soft"]
|
||||
|
||||
weapons = ["WoodenNunchucks", "SmallKnife", "WoodenRod", "Rapier", "IronHammer", "ShortSword", "HandAxe", "Scimitar",
|
||||
"IronNunchucks", "LargeKnife", "IronStaff", "Sabre", "LongSword", "GreatAxe", "Falchon", "SilverKnife",
|
||||
"SilverSword", "SilverHammer", "SilverAxe", "FlameSword", "IceSword", "DragonSword", "GiantSword",
|
||||
"SunSword", "CoralSword", "WereSword", "RuneSword", "PowerRod", "LightAxe", "HealRod", "MageRod", "Defense",
|
||||
"WizardRod", "Vorpal", "CatClaw", "ThorHammer", "BaneSword", "Katana", "Xcalber", "Masamune"]
|
||||
|
||||
armor = ["Cloth", "WoodenArmor", "ChainArmor", "IronArmor", "SteelArmor", "SilverArmor", "FlameArmor", "IceArmor",
|
||||
"OpalArmor", "DragonArmor", "Copper", "Silver", "Gold", "Opal", "WhiteShirt", "BlackShirt", "WoodenShield",
|
||||
"IronShield", "SilverShield", "FlameShield", "IceShield", "OpalShield", "AegisShield", "Buckler", "ProCape",
|
||||
"Cap", "WoodenHelm", "IronHelm", "SilverHelm", "OpalHelm", "HealHelm", "Ribbon", "Gloves", "CopperGauntlets",
|
||||
"IronGauntlets", "SilverGauntlets", "ZeusGauntlets", "PowerGauntlets", "OpalGauntlets", "ProRing"]
|
||||
|
||||
gold_items = ["Gold10", "Gold20", "Gold25", "Gold30", "Gold55", "Gold70", "Gold85", "Gold110", "Gold135", "Gold155",
|
||||
"Gold160", "Gold180", "Gold240", "Gold255", "Gold260", "Gold295", "Gold300", "Gold315", "Gold330",
|
||||
"Gold350", "Gold385", "Gold400", "Gold450", "Gold500", "Gold530", "Gold575", "Gold620", "Gold680",
|
||||
"Gold750", "Gold795", "Gold880", "Gold1020", "Gold1250", "Gold1455", "Gold1520", "Gold1760", "Gold1975",
|
||||
"Gold2000", "Gold2750", "Gold3400", "Gold4150", "Gold5000", "Gold5450", "Gold6400", "Gold6720",
|
||||
"Gold7340", "Gold7690", "Gold7900", "Gold8135", "Gold9000", "Gold9300", "Gold9500", "Gold9900",
|
||||
"Gold10000", "Gold12350", "Gold13000", "Gold13450", "Gold14050", "Gold14720", "Gold15000", "Gold17490",
|
||||
"Gold18010", "Gold19990", "Gold20000", "Gold20010", "Gold26000", "Gold45000", "Gold65000"]
|
||||
|
||||
extended_consumables = ["FullCure", "Phoenix", "Blast", "Smoke",
|
||||
"Refresh", "Flare", "Black", "Guard",
|
||||
"Quick", "HighPotion", "Wizard", "Cloak"]
|
||||
|
||||
ext_consumables_lookup = {"FullCure": "Ext1", "Phoenix": "Ext2", "Blast": "Ext3", "Smoke": "Ext4",
|
||||
"Refresh": "Ext1", "Flare": "Ext2", "Black": "Ext3", "Guard": "Ext4",
|
||||
"Quick": "Ext1", "HighPotion": "Ext2", "Wizard": "Ext3", "Cloak": "Ext4"}
|
||||
|
||||
ext_consumables_locations = {"Ext1": 0x3C, "Ext2": 0x3D, "Ext3": 0x3E, "Ext4": 0x3F}
|
||||
|
||||
|
||||
movement_items = ["Ship", "Bridge", "Canal", "Canoe"]
|
||||
|
||||
no_overworld_items = ["Sigil", "Mark"]
|
||||
|
||||
|
||||
class FF1Client(BizHawkClient):
|
||||
game = "Final Fantasy"
|
||||
system = "NES"
|
||||
|
||||
weapons_queue: deque[int]
|
||||
armor_queue: deque[int]
|
||||
consumable_stack_amounts: dict[str, int] | None
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.wram = "RAM"
|
||||
self.sram = "WRAM"
|
||||
self.rom = "PRG ROM"
|
||||
self.consumable_stack_amounts = None
|
||||
self.weapons_queue = deque()
|
||||
self.armor_queue = deque()
|
||||
self.guard_character = 0x00
|
||||
|
||||
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||
try:
|
||||
# Check ROM name/patch version
|
||||
rom_name = ((await bizhawk.read(ctx.bizhawk_ctx, [(rom_name_location, 0x0D, self.rom)]))[0])
|
||||
rom_name = rom_name.decode("ascii")
|
||||
if rom_name != "FINAL FANTASY":
|
||||
return False # Not a Final Fantasy 1 ROM
|
||||
except bizhawk.RequestFailedError:
|
||||
return False # Not able to get a response, say no for now
|
||||
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b111
|
||||
ctx.want_slot_data = True
|
||||
# Resetting these in case of switching ROMs
|
||||
self.consumable_stack_amounts = None
|
||||
self.weapons_queue = deque()
|
||||
self.armor_queue = deque()
|
||||
|
||||
return True
|
||||
|
||||
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
|
||||
if ctx.server is None:
|
||||
return
|
||||
|
||||
if ctx.slot is None:
|
||||
return
|
||||
try:
|
||||
self.guard_character = await self.read_sram_value(ctx, status_a_location)
|
||||
# If the first character's name starts with a 0 value, we're at the title screen/character creation.
|
||||
# In that case, don't allow any read/writes.
|
||||
# We do this by setting the guard to 1 because that's neither a valid character nor the initial value.
|
||||
if self.guard_character == 0:
|
||||
self.guard_character = 0x01
|
||||
|
||||
if self.consumable_stack_amounts is None:
|
||||
self.consumable_stack_amounts = {}
|
||||
self.consumable_stack_amounts["Shard"] = 1
|
||||
other_consumable_amounts = await self.read_rom(ctx, 0x47400, 10)
|
||||
self.consumable_stack_amounts["Tent"] = other_consumable_amounts[0] + 1
|
||||
self.consumable_stack_amounts["Cabin"] = other_consumable_amounts[1] + 1
|
||||
self.consumable_stack_amounts["House"] = other_consumable_amounts[2] + 1
|
||||
self.consumable_stack_amounts["Heal"] = other_consumable_amounts[3] + 1
|
||||
self.consumable_stack_amounts["Pure"] = other_consumable_amounts[4] + 1
|
||||
self.consumable_stack_amounts["Soft"] = other_consumable_amounts[5] + 1
|
||||
self.consumable_stack_amounts["Ext1"] = other_consumable_amounts[6] + 1
|
||||
self.consumable_stack_amounts["Ext2"] = other_consumable_amounts[7] + 1
|
||||
self.consumable_stack_amounts["Ext3"] = other_consumable_amounts[8] + 1
|
||||
self.consumable_stack_amounts["Ext4"] = other_consumable_amounts[9] + 1
|
||||
|
||||
await self.location_check(ctx)
|
||||
await self.received_items_check(ctx)
|
||||
await self.process_weapons_queue(ctx)
|
||||
await self.process_armor_queue(ctx)
|
||||
|
||||
except bizhawk.RequestFailedError:
|
||||
# The connector didn't respond. Exit handler and return to main loop to reconnect
|
||||
pass
|
||||
|
||||
async def location_check(self, ctx: "BizHawkClientContext"):
|
||||
locations_data = await self.read_sram_values_guarded(ctx, locations_array_start, locations_array_length)
|
||||
if locations_data is None:
|
||||
return
|
||||
locations_checked = []
|
||||
if len(locations_data) > 0xFE and locations_data[0xFE] & 0x02 != 0 and not ctx.finished_game:
|
||||
await ctx.send_msgs([
|
||||
{"cmd": "StatusUpdate",
|
||||
"status": ClientStatus.CLIENT_GOAL}
|
||||
])
|
||||
ctx.finished_game = True
|
||||
for location in ctx.missing_locations:
|
||||
# index will be - 0x100 or 0x200
|
||||
index = location
|
||||
if location < 0x200:
|
||||
# Location is a chest
|
||||
index -= 0x100
|
||||
flag = 0x04
|
||||
else:
|
||||
# Location is an NPC
|
||||
index -= 0x200
|
||||
flag = 0x02
|
||||
if locations_data[index] & flag != 0:
|
||||
locations_checked.append(location)
|
||||
|
||||
found_locations = await ctx.check_locations(locations_checked)
|
||||
for location in found_locations:
|
||||
ctx.locations_checked.add(location)
|
||||
location_name = ctx.location_names.lookup_in_game(location)
|
||||
logger.info(
|
||||
f'New Check: {location_name} ({len(ctx.locations_checked)}/'
|
||||
f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
|
||||
|
||||
async def received_items_check(self, ctx: "BizHawkClientContext") -> None:
|
||||
assert self.consumable_stack_amounts, "shouldn't call this function without reading consumable_stack_amounts"
|
||||
write_list: list[tuple[int, list[int], str]] = []
|
||||
items_received_count = await self.read_sram_value_guarded(ctx, items_obtained)
|
||||
if items_received_count is None:
|
||||
return
|
||||
if items_received_count < len(ctx.items_received):
|
||||
current_item = ctx.items_received[items_received_count]
|
||||
current_item_id = current_item.item
|
||||
current_item_name = ctx.item_names.lookup_in_game(current_item_id, ctx.game)
|
||||
if current_item_name in key_items:
|
||||
location = current_item_id - 0xE0
|
||||
write_list.append((location, [1], self.sram))
|
||||
elif current_item_name in movement_items:
|
||||
location = current_item_id - 0x1E0
|
||||
if current_item_name != "Canal":
|
||||
write_list.append((location, [1], self.sram))
|
||||
else:
|
||||
write_list.append((location, [0], self.sram))
|
||||
elif current_item_name in no_overworld_items:
|
||||
if current_item_name == "Sigil":
|
||||
location = 0x28
|
||||
else:
|
||||
location = 0x12
|
||||
write_list.append((location, [1], self.sram))
|
||||
elif current_item_name in gold_items:
|
||||
gold_amount = int(current_item_name[4:])
|
||||
current_gold_value = await self.read_sram_values_guarded(ctx, gp_location_low, 3)
|
||||
if current_gold_value is None:
|
||||
return
|
||||
current_gold = int.from_bytes(current_gold_value, "little")
|
||||
new_gold = min(gold_amount + current_gold, 999999)
|
||||
lower_byte = new_gold % (2 ** 8)
|
||||
middle_byte = (new_gold // (2 ** 8)) % (2 ** 8)
|
||||
upper_byte = new_gold // (2 ** 16)
|
||||
write_list.append((gp_location_low, [lower_byte], self.sram))
|
||||
write_list.append((gp_location_middle, [middle_byte], self.sram))
|
||||
write_list.append((gp_location_high, [upper_byte], self.sram))
|
||||
elif current_item_name in consumables:
|
||||
location = current_item_id - 0xE0
|
||||
current_value = await self.read_sram_value_guarded(ctx, location)
|
||||
if current_value is None:
|
||||
return
|
||||
amount_to_add = self.consumable_stack_amounts[current_item_name]
|
||||
new_value = min(current_value + amount_to_add, 99)
|
||||
write_list.append((location, [new_value], self.sram))
|
||||
elif current_item_name in extended_consumables:
|
||||
ext_name = ext_consumables_lookup[current_item_name]
|
||||
location = ext_consumables_locations[ext_name]
|
||||
current_value = await self.read_sram_value_guarded(ctx, location)
|
||||
if current_value is None:
|
||||
return
|
||||
amount_to_add = self.consumable_stack_amounts[ext_name]
|
||||
new_value = min(current_value + amount_to_add, 99)
|
||||
write_list.append((location, [new_value], self.sram))
|
||||
elif current_item_name in weapons:
|
||||
self.weapons_queue.appendleft(current_item_id - 0x11B)
|
||||
elif current_item_name in armor:
|
||||
self.armor_queue.appendleft(current_item_id - 0x143)
|
||||
write_list.append((items_obtained, [items_received_count + 1], self.sram))
|
||||
write_successful = await self.write_sram_values_guarded(ctx, write_list)
|
||||
if write_successful:
|
||||
await bizhawk.display_message(ctx.bizhawk_ctx, f"Received {current_item_name}")
|
||||
|
||||
async def process_weapons_queue(self, ctx: "BizHawkClientContext"):
|
||||
empty_slots = deque()
|
||||
char1_slots = await self.read_sram_values_guarded(ctx, weapons_arrays_starts[0], 4)
|
||||
char2_slots = await self.read_sram_values_guarded(ctx, weapons_arrays_starts[1], 4)
|
||||
char3_slots = await self.read_sram_values_guarded(ctx, weapons_arrays_starts[2], 4)
|
||||
char4_slots = await self.read_sram_values_guarded(ctx, weapons_arrays_starts[3], 4)
|
||||
if char1_slots is None or char2_slots is None or char3_slots is None or char4_slots is None:
|
||||
return
|
||||
for i, slot in enumerate(char1_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(weapons_arrays_starts[0] + i)
|
||||
for i, slot in enumerate(char2_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(weapons_arrays_starts[1] + i)
|
||||
for i, slot in enumerate(char3_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(weapons_arrays_starts[2] + i)
|
||||
for i, slot in enumerate(char4_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(weapons_arrays_starts[3] + i)
|
||||
while len(empty_slots) > 0 and len(self.weapons_queue) > 0:
|
||||
current_slot = empty_slots.pop()
|
||||
current_weapon = self.weapons_queue.pop()
|
||||
await self.write_sram_guarded(ctx, current_slot, current_weapon)
|
||||
|
||||
async def process_armor_queue(self, ctx: "BizHawkClientContext"):
|
||||
empty_slots = deque()
|
||||
char1_slots = await self.read_sram_values_guarded(ctx, armors_arrays_starts[0], 4)
|
||||
char2_slots = await self.read_sram_values_guarded(ctx, armors_arrays_starts[1], 4)
|
||||
char3_slots = await self.read_sram_values_guarded(ctx, armors_arrays_starts[2], 4)
|
||||
char4_slots = await self.read_sram_values_guarded(ctx, armors_arrays_starts[3], 4)
|
||||
if char1_slots is None or char2_slots is None or char3_slots is None or char4_slots is None:
|
||||
return
|
||||
for i, slot in enumerate(char1_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(armors_arrays_starts[0] + i)
|
||||
for i, slot in enumerate(char2_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(armors_arrays_starts[1] + i)
|
||||
for i, slot in enumerate(char3_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(armors_arrays_starts[2] + i)
|
||||
for i, slot in enumerate(char4_slots):
|
||||
if slot == 0:
|
||||
empty_slots.appendleft(armors_arrays_starts[3] + i)
|
||||
while len(empty_slots) > 0 and len(self.armor_queue) > 0:
|
||||
current_slot = empty_slots.pop()
|
||||
current_armor = self.armor_queue.pop()
|
||||
await self.write_sram_guarded(ctx, current_slot, current_armor)
|
||||
|
||||
|
||||
async def read_sram_value(self, ctx: "BizHawkClientContext", location: int):
|
||||
value = ((await bizhawk.read(ctx.bizhawk_ctx, [(location, 1, self.sram)]))[0])
|
||||
return int.from_bytes(value, "little")
|
||||
|
||||
async def read_sram_values_guarded(self, ctx: "BizHawkClientContext", location: int, size: int):
|
||||
value = await bizhawk.guarded_read(ctx.bizhawk_ctx,
|
||||
[(location, size, self.sram)],
|
||||
[(status_a_location, [self.guard_character], self.sram)])
|
||||
if value is None:
|
||||
return None
|
||||
return value[0]
|
||||
|
||||
async def read_sram_value_guarded(self, ctx: "BizHawkClientContext", location: int):
|
||||
value = await bizhawk.guarded_read(ctx.bizhawk_ctx,
|
||||
[(location, 1, self.sram)],
|
||||
[(status_a_location, [self.guard_character], self.sram)])
|
||||
if value is None:
|
||||
return None
|
||||
return int.from_bytes(value[0], "little")
|
||||
|
||||
async def read_rom(self, ctx: "BizHawkClientContext", location: int, size: int):
|
||||
return (await bizhawk.read(ctx.bizhawk_ctx, [(location, size, self.rom)]))[0]
|
||||
|
||||
async def write_sram_guarded(self, ctx: "BizHawkClientContext", location: int, value: int):
|
||||
return await bizhawk.guarded_write(ctx.bizhawk_ctx,
|
||||
[(location, [value], self.sram)],
|
||||
[(status_a_location, [self.guard_character], self.sram)])
|
||||
|
||||
async def write_sram_values_guarded(self, ctx: "BizHawkClientContext", write_list):
|
||||
return await bizhawk.guarded_write(ctx.bizhawk_ctx,
|
||||
write_list,
|
||||
[(status_a_location, [self.guard_character], self.sram)])
|
||||
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import pkgutil
|
||||
from typing import Dict, Set, NamedTuple, List
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
@@ -37,15 +37,13 @@ class FF1Items:
|
||||
_item_table_lookup: Dict[str, ItemData] = {}
|
||||
|
||||
def _populate_item_table_from_data(self):
|
||||
base_path = Path(__file__).parent
|
||||
file_path = (base_path / "data/items.json").resolve()
|
||||
with open(file_path) as file:
|
||||
items = json.load(file)
|
||||
# Hardcode progression and categories for now
|
||||
self._item_table = [ItemData(name, code, "FF1Item", ItemClassification.progression if name in
|
||||
FF1_PROGRESSION_LIST else ItemClassification.useful if name in FF1_USEFUL_LIST else
|
||||
ItemClassification.filler) for name, code in items.items()]
|
||||
self._item_table_lookup = {item.name: item for item in self._item_table}
|
||||
file = pkgutil.get_data(__name__, "data/items.json").decode("utf-8")
|
||||
items = json.loads(file)
|
||||
# Hardcode progression and categories for now
|
||||
self._item_table = [ItemData(name, code, "FF1Item", ItemClassification.progression if name in
|
||||
FF1_PROGRESSION_LIST else ItemClassification.useful if name in FF1_USEFUL_LIST else
|
||||
ItemClassification.filler) for name, code in items.items()]
|
||||
self._item_table_lookup = {item.name: item for item in self._item_table}
|
||||
|
||||
def _get_item_table(self) -> List[ItemData]:
|
||||
if not self._item_table or not self._item_table_lookup:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
import pkgutil
|
||||
from typing import Dict, NamedTuple, List, Optional
|
||||
|
||||
from BaseClasses import Region, Location, MultiWorld
|
||||
@@ -18,13 +18,11 @@ class FF1Locations:
|
||||
_location_table_lookup: Dict[str, LocationData] = {}
|
||||
|
||||
def _populate_item_table_from_data(self):
|
||||
base_path = Path(__file__).parent
|
||||
file_path = (base_path / "data/locations.json").resolve()
|
||||
with open(file_path) as file:
|
||||
locations = json.load(file)
|
||||
# Hardcode progression and categories for now
|
||||
self._location_table = [LocationData(name, code) for name, code in locations.items()]
|
||||
self._location_table_lookup = {item.name: item for item in self._location_table}
|
||||
file = pkgutil.get_data(__name__, "data/locations.json")
|
||||
locations = json.loads(file)
|
||||
# Hardcode progression and categories for now
|
||||
self._location_table = [LocationData(name, code) for name, code in locations.items()]
|
||||
self._location_table_lookup = {item.name: item for item in self._location_table}
|
||||
|
||||
def _get_location_table(self) -> List[LocationData]:
|
||||
if not self._location_table or not self._location_table_lookup:
|
||||
|
||||
@@ -7,6 +7,7 @@ from .Items import ItemData, FF1Items, FF1_STARTER_ITEMS, FF1_PROGRESSION_LIST,
|
||||
from .Locations import EventId, FF1Locations, generate_rule, CHAOS_TERMINATED_EVENT
|
||||
from .Options import FF1Options
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from .Client import FF1Client
|
||||
|
||||
|
||||
class FF1Settings(settings.Group):
|
||||
|
||||
@@ -22,11 +22,6 @@ All items can appear in other players worlds, including consumables, shards, wea
|
||||
|
||||
## What does another world's item look like in Final Fantasy
|
||||
|
||||
All local and remote items appear the same. Final Fantasy will say that you received an item, then BOTH the client log and the
|
||||
emulator will display what was found external to the in-game text box.
|
||||
All local and remote items appear the same. Final Fantasy will say that you received an item, then the client log will
|
||||
display what was found external to the in-game text box.
|
||||
|
||||
## Unique Local Commands
|
||||
The following commands are only available when using the FF1Client for the Final Fantasy Randomizer.
|
||||
|
||||
- `/nes` Shows the current status of the NES connection.
|
||||
- `/toggle_msgs` Toggle displaying messages in EmuHawk
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
|
||||
## Required Software
|
||||
|
||||
- The FF1Client
|
||||
- Bundled with Archipelago: [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- The BizHawk emulator. Versions 2.3.1 and higher are supported. Version 2.7 is recommended
|
||||
- [BizHawk at TASVideos](https://tasvideos.org/BizHawk)
|
||||
- BizHawk: [BizHawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- Detailed installation instructions for BizHawk can be found at the above link.
|
||||
- Windows users must run the prerequisite installer first, which can also be found at the above link.
|
||||
- The built-in BizHawk client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- Your legally obtained Final Fantasy (USA Edition) ROM file, probably named `Final Fantasy (USA).nes`. Neither
|
||||
Archipelago.gg nor the Final Fantasy Randomizer Community can supply you with this.
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
1. Download and install the latest version of Archipelago.
|
||||
1. On Windows, download Setup.Archipelago.<HighestVersion\>.exe and run it
|
||||
2. Assign EmuHawk version 2.3.1 or higher as your default program for launching `.nes` files.
|
||||
2. Assign EmuHawk as your default program for launching `.nes` files.
|
||||
1. Extract your BizHawk folder to your Desktop, or somewhere you will remember. Below are optional additional steps
|
||||
for loading ROMs more conveniently
|
||||
1. Right-click on a ROM file and select **Open with...**
|
||||
@@ -46,7 +46,7 @@ please refer to the [game agnostic setup guide](/tutorial/Archipelago/setup/en).
|
||||
|
||||
Once the Archipelago server has been hosted:
|
||||
|
||||
1. Navigate to your Archipelago install folder and run `ArchipelagoFF1Client.exe`
|
||||
1. Navigate to your Archipelago install folder and run `ArchipelagoBizhawkClient.exe`
|
||||
2. Notice the `/connect command` on the server hosting page (It should look like `/connect archipelago.gg:*****`
|
||||
where ***** are numbers)
|
||||
3. Type the connect command into the client OR add the port to the pre-populated address on the top bar (it should
|
||||
@@ -54,16 +54,11 @@ Once the Archipelago server has been hosted:
|
||||
|
||||
### Running Your Game and Connecting to the Client Program
|
||||
|
||||
1. Open EmuHawk 2.3.1 or higher and load your ROM OR click your ROM file if it is already associated with the
|
||||
1. Open EmuHawk and load your ROM OR click your ROM file if it is already associated with the
|
||||
extension `*.nes`
|
||||
2. Navigate to where you installed Archipelago, then to `data/lua`, and drag+drop the `connector_ff1.lua` script onto
|
||||
the main EmuHawk window.
|
||||
1. You could instead open the Lua Console manually, click `Script` 〉 `Open Script`, and navigate to
|
||||
`connector_ff1.lua` with the file picker.
|
||||
2. If it gives a `NLua.Exceptions.LuaScriptException: .\socket.lua:13: module 'socket.core' not found:` exception
|
||||
close your emulator entirely, restart it and re-run these steps
|
||||
3. If it says `Must use a version of BizHawk 2.3.1 or higher`, double-check your BizHawk version by clicking **
|
||||
Help** -> **About**
|
||||
2. Navigate to where you installed Archipelago, then to `data/lua`, and drag+drop the `connector_bizhawk_generic.lua`
|
||||
script onto the main EmuHawk window. You can also instead open the Lua Console manually, click `Script` 〉 `Open Script`,
|
||||
and navigate to `connector_bizhawk_generic.lua` with the file picker.
|
||||
|
||||
## Play the game
|
||||
|
||||
|
||||
@@ -26,13 +26,13 @@ class KDL3TestBase(WorldTestBase):
|
||||
self.multiworld.game[1] = self.game
|
||||
self.multiworld.player_name = {1: "Tester"}
|
||||
self.multiworld.set_seed(seed)
|
||||
self.multiworld.state = CollectionState(self.multiworld)
|
||||
args = Namespace()
|
||||
for name, option in AutoWorld.AutoWorldRegister.world_types[self.game].options_dataclass.type_hints.items():
|
||||
setattr(args, name, {
|
||||
1: option.from_any(self.options.get(name, getattr(option, "default")))
|
||||
})
|
||||
self.multiworld.set_options(args)
|
||||
self.multiworld.state = CollectionState(self.multiworld)
|
||||
self.multiworld.plando_options = PlandoOptions.connections
|
||||
for step in gen_steps:
|
||||
call_all(self.multiworld, step)
|
||||
|
||||
@@ -6,12 +6,12 @@ from Options import Accessibility
|
||||
from Utils import output_path
|
||||
from settings import FilePath, Group
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from worlds.LauncherComponents import Component, Type, components
|
||||
from worlds.LauncherComponents import Component, Type, components, icon_paths
|
||||
from .client_setup import launch_game
|
||||
from .connections import CONNECTIONS, RANDOMIZED_CONNECTIONS, TRANSITIONS
|
||||
from .constants import ALL_ITEMS, ALWAYS_LOCATIONS, BOSS_LOCATIONS, FILLER, NOTES, PHOBEKINS, PROG_ITEMS, TRAPS, \
|
||||
USEFUL_ITEMS
|
||||
from .options import AvailablePortals, Goal, Logic, MessengerOptions, NotesNeeded, ShuffleTransitions
|
||||
from .options import AvailablePortals, Goal, Logic, MessengerOptions, NotesNeeded, option_groups, ShuffleTransitions
|
||||
from .portals import PORTALS, add_closed_portal_reqs, disconnect_portals, shuffle_portals, validate_portals
|
||||
from .regions import LEVELS, MEGA_SHARDS, LOCATIONS, REGION_CONNECTIONS
|
||||
from .rules import MessengerHardRules, MessengerOOBRules, MessengerRules
|
||||
@@ -20,9 +20,18 @@ from .subclasses import MessengerItem, MessengerRegion, MessengerShopLocation
|
||||
from .transitions import disconnect_entrances, shuffle_transitions
|
||||
|
||||
components.append(
|
||||
Component("The Messenger", component_type=Type.CLIENT, func=launch_game, game_name="The Messenger", supports_uri=True)
|
||||
Component(
|
||||
"The Messenger",
|
||||
component_type=Type.CLIENT,
|
||||
func=launch_game,
|
||||
game_name="The Messenger",
|
||||
supports_uri=True,
|
||||
icon="The Messenger",
|
||||
description="Launch The Messenger.\nInstalls and checks for updates for the randomizer.")
|
||||
)
|
||||
|
||||
icon_paths["The Messenger"] = f"ap:{__name__}/assets/component_icon.png"
|
||||
|
||||
|
||||
class MessengerSettings(Group):
|
||||
class GamePath(FilePath):
|
||||
@@ -35,6 +44,7 @@ class MessengerSettings(Group):
|
||||
|
||||
class MessengerWeb(WebWorld):
|
||||
theme = "ocean"
|
||||
rich_text_options_doc = True
|
||||
|
||||
bug_report_page = "https://github.com/alwaysintreble/TheMessengerRandomizerModAP/issues"
|
||||
|
||||
@@ -56,6 +66,7 @@ class MessengerWeb(WebWorld):
|
||||
)
|
||||
|
||||
tutorials = [tut_en, plando_en]
|
||||
option_groups = option_groups
|
||||
|
||||
|
||||
class MessengerWorld(World):
|
||||
@@ -426,13 +437,13 @@ class MessengerWorld(World):
|
||||
def collect(self, state: "CollectionState", item: "Item") -> bool:
|
||||
change = super().collect(state, item)
|
||||
if change and "Time Shard" in item.name:
|
||||
state.prog_items[self.player]["Shards"] += int(item.name.strip("Time Shard ()"))
|
||||
state.add_item("Shards", self.player, int(item.name.strip("Time Shard ()")))
|
||||
return change
|
||||
|
||||
def remove(self, state: "CollectionState", item: "Item") -> bool:
|
||||
change = super().remove(state, item)
|
||||
if change and "Time Shard" in item.name:
|
||||
state.prog_items[self.player]["Shards"] -= int(item.name.strip("Time Shard ()"))
|
||||
state.remove_item("Shards", self.player, int(item.name.strip("Time Shard ()")))
|
||||
return change
|
||||
|
||||
@classmethod
|
||||
|
||||
BIN
worlds/messenger/assets/component_icon.png
Normal file
BIN
worlds/messenger/assets/component_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
@@ -2,8 +2,11 @@ from dataclasses import dataclass
|
||||
|
||||
from schema import And, Optional, Or, Schema
|
||||
|
||||
from Options import Choice, DeathLinkMixin, DefaultOnToggle, ItemsAccessibility, OptionDict, PerGameCommonOptions, \
|
||||
PlandoConnections, Range, StartInventoryPool, Toggle
|
||||
from Options import (
|
||||
Choice, DeathLinkMixin, DefaultOnToggle, ItemsAccessibility, OptionDict, OptionGroup,
|
||||
PerGameCommonOptions,
|
||||
PlandoConnections, Range, StartInventoryPool, Toggle,
|
||||
)
|
||||
from . import RANDOMIZED_CONNECTIONS
|
||||
from .portals import CHECKPOINTS, PORTALS, SHOP_POINTS
|
||||
|
||||
@@ -48,8 +51,10 @@ class Logic(Choice):
|
||||
"""
|
||||
The level of logic to use when determining what locations in your world are accessible.
|
||||
|
||||
Normal: Can require damage boosts, but otherwise approachable for someone who has beaten the game.
|
||||
Hard: Expects more knowledge and tighter execution. Has leashing, normal clips and much tighter d-boosting in logic.
|
||||
**Normal:** Can require damage boosts, but otherwise approachable for someone who has beaten the game.
|
||||
|
||||
**Hard:** Expects more knowledge and tighter execution.
|
||||
Has leashing, normal clips and much tighter d-boosting in logic.
|
||||
"""
|
||||
display_name = "Logic Level"
|
||||
option_normal = 0
|
||||
@@ -76,7 +81,10 @@ class EarlyMed(Toggle):
|
||||
|
||||
|
||||
class AvailablePortals(Range):
|
||||
"""Number of portals that are available from the start. Autumn Hills, Howling Grotto, and Glacial Peak are always available. If portal outputs are not randomized, Searing Crags will also be available."""
|
||||
"""
|
||||
Number of portals that are available from the start. Autumn Hills, Howling Grotto, and Glacial Peak are always
|
||||
available. If portal outputs are not randomized, Searing Crags will also be available.
|
||||
"""
|
||||
display_name = "Available Starting Portals"
|
||||
range_start = 3
|
||||
range_end = 6
|
||||
@@ -89,10 +97,14 @@ class ShufflePortals(Choice):
|
||||
Entering a portal from its vanilla area will always lead to HQ, and will unlock it if relevant.
|
||||
Supports plando.
|
||||
|
||||
None: Portals will take you where they're supposed to.
|
||||
Shops: Portals can lead to any area except Music Box and Elemental Skylands, with each portal output guaranteed to not overlap with another portal's. Will only put you at a portal or a shop.
|
||||
Checkpoints: Like Shops except checkpoints without shops are also valid drop points.
|
||||
Anywhere: Like Checkpoints except it's possible for multiple portals to output to the same map.
|
||||
**None:** Portals will take you where they're supposed to.
|
||||
|
||||
**Shops:** Portals can lead to any area except Music Box and Elemental Skylands, with each portal output guaranteed
|
||||
to not overlap with another portal's. Will only put you at a portal or a shop.
|
||||
|
||||
**Checkpoints:** Like Shops except checkpoints without shops are also valid drop points.
|
||||
|
||||
**Anywhere:** Like Checkpoints except it's possible for multiple portals to output to the same map.
|
||||
"""
|
||||
display_name = "Shuffle Portal Outputs"
|
||||
option_none = 0
|
||||
@@ -107,9 +119,11 @@ class ShuffleTransitions(Choice):
|
||||
Whether the transitions between the levels should be randomized.
|
||||
Supports plando.
|
||||
|
||||
None: Level transitions lead where they should.
|
||||
Coupled: Returning through a transition will take you from whence you came.
|
||||
Decoupled: Any level transition can take you to any other level transition.
|
||||
**None:** Level transitions lead where they should.
|
||||
|
||||
**Coupled:** Returning through a transition will take you from whence you came.
|
||||
|
||||
**Decoupled:** Any level transition can take you to any other level transition.
|
||||
"""
|
||||
display_name = "Shuffle Level Transitions"
|
||||
option_none = 0
|
||||
@@ -119,7 +133,10 @@ class ShuffleTransitions(Choice):
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
"""Requirement to finish the game. To win with the power seal hunt goal, you must enter the Music Box through the shop chest."""
|
||||
"""
|
||||
Requirement to finish the game.
|
||||
To win with the power seal hunt goal, you must enter the Music Box through the shop chest.
|
||||
"""
|
||||
display_name = "Goal"
|
||||
option_open_music_box = 0
|
||||
option_power_seal_hunt = 1
|
||||
@@ -132,7 +149,8 @@ class MusicBox(DefaultOnToggle):
|
||||
|
||||
class NotesNeeded(Range):
|
||||
"""
|
||||
How many notes need to be found in order to access the Music Box. 6 are always needed to enter, so this places the others in your start inventory.
|
||||
How many notes need to be found in order to access the Music Box.
|
||||
6 are always needed to enter, so this places the others in your start inventory.
|
||||
"""
|
||||
display_name = "Notes Needed"
|
||||
range_start = 1
|
||||
@@ -240,3 +258,35 @@ class MessengerOptions(DeathLinkMixin, PerGameCommonOptions):
|
||||
shop_price_plan: PlannedShopPrices
|
||||
portal_plando: PortalPlando
|
||||
plando_connections: TransitionPlando
|
||||
|
||||
|
||||
option_groups = [
|
||||
OptionGroup(
|
||||
"Difficulty",
|
||||
[
|
||||
EarlyMed,
|
||||
Logic,
|
||||
LimitedMovement,
|
||||
],
|
||||
),
|
||||
OptionGroup(
|
||||
"Goal",
|
||||
[
|
||||
Goal,
|
||||
MusicBox,
|
||||
NotesNeeded,
|
||||
AmountSeals,
|
||||
RequiredSeals,
|
||||
],
|
||||
),
|
||||
OptionGroup(
|
||||
"Entrances",
|
||||
[
|
||||
AvailablePortals,
|
||||
ShufflePortals,
|
||||
ShuffleTransitions,
|
||||
PortalPlando,
|
||||
TransitionPlando,
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
@@ -27,9 +27,15 @@ class MinecraftSettings(settings.Group):
|
||||
any games played on the "beta" channel have a high likelihood of no longer working on the "release" channel.
|
||||
"""
|
||||
|
||||
forge_directory: ForgeDirectory = ForgeDirectory("Minecraft Forge server")
|
||||
class JavaExecutable(settings.OptionalUserFilePath):
|
||||
"""
|
||||
Path to Java executable. If not set, will attempt to fall back to Java system installation.
|
||||
"""
|
||||
|
||||
forge_directory: ForgeDirectory = ForgeDirectory("Minecraft NeoForge server")
|
||||
max_heap_size: str = "2G"
|
||||
release_channel: ReleaseChannel = ReleaseChannel("release")
|
||||
java: JavaExecutable = JavaExecutable("")
|
||||
|
||||
|
||||
class MinecraftWebWorld(WebWorld):
|
||||
|
||||
@@ -30,7 +30,6 @@ from .Patches import OoTContainer, patch_rom
|
||||
from .N64Patch import create_patch_file
|
||||
from .Cosmetics import patch_cosmetics
|
||||
|
||||
from settings import get_settings
|
||||
from BaseClasses import MultiWorld, CollectionState, Tutorial, LocationProgressType
|
||||
from Options import Range, Toggle, VerifyKeys, Accessibility, PlandoConnections, PlandoItems
|
||||
from Fill import fill_restrictive, fast_fill, FillError
|
||||
@@ -203,7 +202,8 @@ class OOTWorld(World):
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, multiworld: MultiWorld):
|
||||
rom = Rom(file=get_settings()['oot_options']['rom_file'])
|
||||
oot_settings = OOTWorld.settings
|
||||
rom = Rom(file=oot_settings.rom_file)
|
||||
|
||||
|
||||
# Option parsing, handling incompatible options, building useful-item table
|
||||
@@ -1089,7 +1089,8 @@ class OOTWorld(World):
|
||||
self.hint_rng = self.random
|
||||
|
||||
outfile_name = self.multiworld.get_out_file_name_base(self.player)
|
||||
rom = Rom(file=get_settings()['oot_options']['rom_file'])
|
||||
oot_settings = OOTWorld.settings
|
||||
rom = Rom(file=oot_settings.rom_file)
|
||||
try:
|
||||
if self.hints != 'none':
|
||||
buildWorldGossipHints(self)
|
||||
|
||||
@@ -120,165 +120,6 @@ class TestNoGingerIslandItemGeneration(SVTestBase):
|
||||
self.assertTrue(count == 0 or count == 2)
|
||||
|
||||
|
||||
class TestMonstersanityNone(SVTestBase):
|
||||
options = {
|
||||
options.Monstersanity.internal_name: options.Monstersanity.option_none,
|
||||
# Not really necessary, but it adds more locations, so we don't have to remove useful items.
|
||||
options.Fishsanity.internal_name: options.Fishsanity.option_all
|
||||
}
|
||||
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
# None is default
|
||||
return False
|
||||
|
||||
def test_when_generate_world_then_5_generic_weapons_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Weapon"), 5)
|
||||
|
||||
def test_when_generate_world_then_zero_specific_weapons_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Sword"), 0)
|
||||
self.assertEqual(item_pool.count("Progressive Club"), 0)
|
||||
self.assertEqual(item_pool.count("Progressive Dagger"), 0)
|
||||
|
||||
def test_when_generate_world_then_2_slingshots_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Slingshot"), 2)
|
||||
|
||||
def test_when_generate_world_then_3_shoes_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Footwear"), 3)
|
||||
|
||||
|
||||
class TestMonstersanityGoals(SVTestBase):
|
||||
options = {options.Monstersanity.internal_name: options.Monstersanity.option_goals}
|
||||
|
||||
def test_when_generate_world_then_no_generic_weapons_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Weapon"), 0)
|
||||
|
||||
def test_when_generate_world_then_5_specific_weapons_of_each_type_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Sword"), 5)
|
||||
self.assertEqual(item_pool.count("Progressive Club"), 5)
|
||||
self.assertEqual(item_pool.count("Progressive Dagger"), 5)
|
||||
|
||||
def test_when_generate_world_then_2_slingshots_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Slingshot"), 2)
|
||||
|
||||
def test_when_generate_world_then_4_shoes_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Footwear"), 4)
|
||||
|
||||
def test_when_generate_world_then_all_monster_checks_are_inaccessible(self):
|
||||
for location in self.get_real_locations():
|
||||
if LocationTags.MONSTERSANITY not in location_table[location.name].tags:
|
||||
continue
|
||||
with self.subTest(location.name):
|
||||
self.assertFalse(location.can_reach(self.multiworld.state))
|
||||
|
||||
|
||||
class TestMonstersanityOnePerCategory(SVTestBase):
|
||||
options = {options.Monstersanity.internal_name: options.Monstersanity.option_one_per_category}
|
||||
|
||||
def test_when_generate_world_then_no_generic_weapons_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Weapon"), 0)
|
||||
|
||||
def test_when_generate_world_then_5_specific_weapons_of_each_type_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Sword"), 5)
|
||||
self.assertEqual(item_pool.count("Progressive Club"), 5)
|
||||
self.assertEqual(item_pool.count("Progressive Dagger"), 5)
|
||||
|
||||
def test_when_generate_world_then_2_slingshots_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Slingshot"), 2)
|
||||
|
||||
def test_when_generate_world_then_4_shoes_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Footwear"), 4)
|
||||
|
||||
def test_when_generate_world_then_all_monster_checks_are_inaccessible(self):
|
||||
for location in self.get_real_locations():
|
||||
if LocationTags.MONSTERSANITY not in location_table[location.name].tags:
|
||||
continue
|
||||
with self.subTest(location.name):
|
||||
self.assertFalse(location.can_reach(self.multiworld.state))
|
||||
|
||||
|
||||
class TestMonstersanityProgressive(SVTestBase):
|
||||
options = {options.Monstersanity.internal_name: options.Monstersanity.option_progressive_goals}
|
||||
|
||||
def test_when_generate_world_then_no_generic_weapons_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Weapon"), 0)
|
||||
|
||||
def test_when_generate_world_then_5_specific_weapons_of_each_type_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Sword"), 5)
|
||||
self.assertEqual(item_pool.count("Progressive Club"), 5)
|
||||
self.assertEqual(item_pool.count("Progressive Dagger"), 5)
|
||||
|
||||
def test_when_generate_world_then_2_slingshots_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Slingshot"), 2)
|
||||
|
||||
def test_when_generate_world_then_4_shoes_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Footwear"), 4)
|
||||
|
||||
def test_when_generate_world_then_many_rings_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertIn("Hot Java Ring", item_pool)
|
||||
self.assertIn("Wedding Ring", item_pool)
|
||||
self.assertIn("Slime Charmer Ring", item_pool)
|
||||
|
||||
def test_when_generate_world_then_all_monster_checks_are_inaccessible(self):
|
||||
for location in self.get_real_locations():
|
||||
if LocationTags.MONSTERSANITY not in location_table[location.name].tags:
|
||||
continue
|
||||
with self.subTest(location.name):
|
||||
self.assertFalse(location.can_reach(self.multiworld.state))
|
||||
|
||||
|
||||
class TestMonstersanitySplit(SVTestBase):
|
||||
options = {options.Monstersanity.internal_name: options.Monstersanity.option_split_goals}
|
||||
|
||||
def test_when_generate_world_then_no_generic_weapons_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Weapon"), 0)
|
||||
|
||||
def test_when_generate_world_then_5_specific_weapons_of_each_type_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Sword"), 5)
|
||||
self.assertEqual(item_pool.count("Progressive Club"), 5)
|
||||
self.assertEqual(item_pool.count("Progressive Dagger"), 5)
|
||||
|
||||
def test_when_generate_world_then_2_slingshots_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Slingshot"), 2)
|
||||
|
||||
def test_when_generate_world_then_4_shoes_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Footwear"), 4)
|
||||
|
||||
def test_when_generate_world_then_many_rings_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertIn("Hot Java Ring", item_pool)
|
||||
self.assertIn("Wedding Ring", item_pool)
|
||||
self.assertIn("Slime Charmer Ring", item_pool)
|
||||
|
||||
def test_when_generate_world_then_all_monster_checks_are_inaccessible(self):
|
||||
for location in self.get_real_locations():
|
||||
if LocationTags.MONSTERSANITY not in location_table[location.name].tags:
|
||||
continue
|
||||
with self.subTest(location.name):
|
||||
self.assertFalse(location.can_reach(self.multiworld.state))
|
||||
|
||||
|
||||
class TestProgressiveElevator(SVTestBase):
|
||||
options = {
|
||||
options.ElevatorProgression.internal_name: options.ElevatorProgression.option_progressive,
|
||||
|
||||
132
worlds/stardew_valley/test/TestMonstersanity.py
Normal file
132
worlds/stardew_valley/test/TestMonstersanity.py
Normal file
@@ -0,0 +1,132 @@
|
||||
import unittest
|
||||
from typing import ClassVar
|
||||
|
||||
from .bases import SVTestBase
|
||||
from .. import options
|
||||
from ..locations import LocationTags, location_table
|
||||
from ..mods.mod_data import ModNames
|
||||
|
||||
|
||||
class SVMonstersanityTestBase(SVTestBase):
|
||||
expected_progressive_generic_weapon: ClassVar[int] = 0
|
||||
expected_progressive_specific_weapon: ClassVar[int] = 0
|
||||
expected_progressive_slingshot: ClassVar[int] = 0
|
||||
expected_progressive_footwear: ClassVar[int] = 0
|
||||
expected_rings: ClassVar[list[str]] = []
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
if cls is SVMonstersanityTestBase:
|
||||
raise unittest.SkipTest("Base tests disabled")
|
||||
|
||||
super().setUpClass()
|
||||
|
||||
def test_when_generate_world_then_expected_generic_weapons_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Weapon"), self.expected_progressive_generic_weapon)
|
||||
|
||||
def test_when_generate_world_then_expected_specific_weapons_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Sword"), self.expected_progressive_specific_weapon)
|
||||
self.assertEqual(item_pool.count("Progressive Club"), self.expected_progressive_specific_weapon)
|
||||
self.assertEqual(item_pool.count("Progressive Dagger"), self.expected_progressive_specific_weapon)
|
||||
|
||||
def test_when_generate_world_then_expected_slingshots_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Slingshot"), self.expected_progressive_slingshot)
|
||||
|
||||
def test_when_generate_world_then_expected_shoes_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
self.assertEqual(item_pool.count("Progressive Footwear"), self.expected_progressive_footwear)
|
||||
|
||||
def test_when_generate_world_then_many_rings_in_the_pool(self):
|
||||
item_pool = [item.name for item in self.multiworld.itempool]
|
||||
for expected_ring in self.expected_rings:
|
||||
self.assertIn(expected_ring, item_pool)
|
||||
|
||||
def test_when_generate_world_then_all_monster_checks_are_inaccessible_with_empty_inventory(self):
|
||||
for location in self.get_real_locations():
|
||||
if LocationTags.MONSTERSANITY not in location_table[location.name].tags:
|
||||
continue
|
||||
with self.subTest(location.name):
|
||||
self.assert_cannot_reach_location(location.name)
|
||||
|
||||
|
||||
class TestMonstersanityNone(SVMonstersanityTestBase):
|
||||
options = {
|
||||
options.Monstersanity: options.Monstersanity.option_none,
|
||||
# Not really necessary, but it adds more locations, so we don't have to remove useful items.
|
||||
options.Fishsanity: options.Fishsanity.option_all,
|
||||
}
|
||||
expected_progressive_generic_weapon = 5
|
||||
expected_progressive_slingshot = 2
|
||||
expected_progressive_footwear = 3
|
||||
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
# None is default
|
||||
return False
|
||||
|
||||
|
||||
class TestMonstersanityNoneWithSVE(SVMonstersanityTestBase):
|
||||
options = {
|
||||
options.Monstersanity: options.Monstersanity.option_none,
|
||||
options.Mods: ModNames.sve,
|
||||
}
|
||||
expected_progressive_generic_weapon = 6
|
||||
expected_progressive_slingshot = 2
|
||||
expected_progressive_footwear = 3
|
||||
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
# None is default
|
||||
return False
|
||||
|
||||
|
||||
class TestMonstersanityGoals(SVMonstersanityTestBase):
|
||||
options = {
|
||||
options.Monstersanity: options.Monstersanity.option_goals,
|
||||
}
|
||||
expected_progressive_specific_weapon = 5
|
||||
expected_progressive_slingshot = 2
|
||||
expected_progressive_footwear = 4
|
||||
|
||||
|
||||
class TestMonstersanityOnePerCategory(SVMonstersanityTestBase):
|
||||
options = {
|
||||
options.Monstersanity: options.Monstersanity.option_one_per_category,
|
||||
}
|
||||
expected_progressive_specific_weapon = 5
|
||||
expected_progressive_slingshot = 2
|
||||
expected_progressive_footwear = 4
|
||||
|
||||
|
||||
class TestMonstersanityProgressive(SVMonstersanityTestBase):
|
||||
options = {
|
||||
options.Monstersanity: options.Monstersanity.option_progressive_goals,
|
||||
}
|
||||
expected_progressive_specific_weapon = 5
|
||||
expected_progressive_slingshot = 2
|
||||
expected_progressive_footwear = 4
|
||||
expected_rings = ["Hot Java Ring", "Wedding Ring", "Slime Charmer Ring"]
|
||||
|
||||
|
||||
class TestMonstersanitySplit(SVMonstersanityTestBase):
|
||||
options = {
|
||||
options.Monstersanity: options.Monstersanity.option_split_goals,
|
||||
}
|
||||
expected_progressive_specific_weapon = 5
|
||||
expected_progressive_slingshot = 2
|
||||
expected_progressive_footwear = 4
|
||||
expected_rings = ["Hot Java Ring", "Wedding Ring", "Slime Charmer Ring"]
|
||||
|
||||
|
||||
class TestMonstersanitySplitWithSVE(SVMonstersanityTestBase):
|
||||
options = {
|
||||
options.Monstersanity: options.Monstersanity.option_split_goals,
|
||||
options.Mods: ModNames.sve,
|
||||
}
|
||||
expected_progressive_specific_weapon = 6
|
||||
expected_progressive_slingshot = 2
|
||||
expected_progressive_footwear = 4
|
||||
expected_rings = ["Hot Java Ring", "Wedding Ring", "Slime Charmer Ring"]
|
||||
@@ -293,12 +293,12 @@ def setup_multiworld(test_options: Iterable[Dict[str, int]] = None, seed=None) -
|
||||
multiworld = MultiWorld(len(test_options))
|
||||
multiworld.player_name = {}
|
||||
multiworld.set_seed(seed)
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
for i in range(1, len(test_options) + 1):
|
||||
multiworld.game[i] = StardewValleyWorld.game
|
||||
multiworld.player_name.update({i: f"Tester{i}"})
|
||||
args = fill_namespace_with_default(test_options)
|
||||
multiworld.set_options(args)
|
||||
multiworld.state = CollectionState(multiworld)
|
||||
|
||||
for step in gen_steps:
|
||||
call_all(multiworld, step)
|
||||
|
||||
@@ -16,9 +16,10 @@ from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_op
|
||||
get_hexagons_in_pool, HexagonQuestAbilityUnlockType, EntranceLayout)
|
||||
from .breakables import breakable_location_name_to_id, breakable_location_groups, breakable_location_table
|
||||
from .combat_logic import area_data, CombatState
|
||||
from . import ut_stuff
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from Options import PlandoConnection, OptionError, PerGameCommonOptions, Removed, Range
|
||||
from settings import Group, Bool
|
||||
from settings import Group, Bool, FilePath
|
||||
|
||||
|
||||
class TunicSettings(Group):
|
||||
@@ -27,9 +28,15 @@ class TunicSettings(Group):
|
||||
|
||||
class LimitGrassRando(Bool):
|
||||
"""Limits the impact of Grass Randomizer on the multiworld by disallowing local_fill percentages below 95."""
|
||||
|
||||
class UTPoptrackerPath(FilePath):
|
||||
"""Path to the user's TUNIC Poptracker Pack."""
|
||||
description = "TUNIC Poptracker Pack zip file"
|
||||
required = False
|
||||
|
||||
disable_local_spoiler: Union[DisableLocalSpoiler, bool] = False
|
||||
limit_grass_rando: Union[LimitGrassRando, bool] = True
|
||||
ut_poptracker_path: Union[UTPoptrackerPath, str] = UTPoptrackerPath()
|
||||
|
||||
|
||||
class TunicWeb(WebWorld):
|
||||
@@ -113,6 +120,7 @@ class TunicWorld(World):
|
||||
using_ut: bool # so we can check if we're using UT only once
|
||||
passthrough: Dict[str, Any]
|
||||
ut_can_gen_without_yaml = True # class var that tells it to ignore the player yaml
|
||||
tracker_world: ClassVar = ut_stuff.tracker_world
|
||||
|
||||
def generate_early(self) -> None:
|
||||
try:
|
||||
@@ -168,39 +176,7 @@ class TunicWorld(World):
|
||||
f"They have Direction Pairs enabled and the connection "
|
||||
f"{cxn.entrance} --> {cxn.exit} does not abide by this option.")
|
||||
|
||||
# Universal tracker stuff, shouldn't do anything in standard gen
|
||||
if hasattr(self.multiworld, "re_gen_passthrough"):
|
||||
if "TUNIC" in self.multiworld.re_gen_passthrough:
|
||||
self.using_ut = True
|
||||
self.passthrough = self.multiworld.re_gen_passthrough["TUNIC"]
|
||||
self.options.start_with_sword.value = self.passthrough["start_with_sword"]
|
||||
self.options.keys_behind_bosses.value = self.passthrough["keys_behind_bosses"]
|
||||
self.options.sword_progression.value = self.passthrough["sword_progression"]
|
||||
self.options.ability_shuffling.value = self.passthrough["ability_shuffling"]
|
||||
self.options.laurels_zips.value = self.passthrough["laurels_zips"]
|
||||
self.options.ice_grappling.value = self.passthrough["ice_grappling"]
|
||||
self.options.ladder_storage.value = self.passthrough["ladder_storage"]
|
||||
self.options.ladder_storage_without_items = self.passthrough["ladder_storage_without_items"]
|
||||
self.options.lanternless.value = self.passthrough["lanternless"]
|
||||
self.options.maskless.value = self.passthrough["maskless"]
|
||||
self.options.hexagon_quest.value = self.passthrough["hexagon_quest"]
|
||||
self.options.hexagon_quest_ability_type.value = self.passthrough.get("hexagon_quest_ability_type", 0)
|
||||
self.options.entrance_rando.value = self.passthrough["entrance_rando"]
|
||||
self.options.shuffle_ladders.value = self.passthrough["shuffle_ladders"]
|
||||
self.options.entrance_layout.value = EntranceLayout.option_standard
|
||||
if ("ziggurat2020_3, ziggurat2020_1_zig2_skip" in self.passthrough["Entrance Rando"].keys()
|
||||
or "ziggurat2020_3, ziggurat2020_1_zig2_skip" in self.passthrough["Entrance Rando"].values()):
|
||||
self.options.entrance_layout.value = EntranceLayout.option_fixed_shop
|
||||
self.options.decoupled = self.passthrough.get("decoupled", 0)
|
||||
self.options.laurels_location.value = LaurelsLocation.option_anywhere
|
||||
self.options.grass_randomizer.value = self.passthrough.get("grass_randomizer", 0)
|
||||
self.options.breakable_shuffle.value = self.passthrough.get("breakable_shuffle", 0)
|
||||
self.options.laurels_location.value = self.options.laurels_location.option_anywhere
|
||||
self.options.combat_logic.value = self.passthrough.get("combat_logic", 0)
|
||||
else:
|
||||
self.using_ut = False
|
||||
else:
|
||||
self.using_ut = False
|
||||
ut_stuff.setup_options_from_slot_data(self)
|
||||
|
||||
self.player_location_table = standard_location_name_to_id.copy()
|
||||
|
||||
|
||||
383
worlds/tunic/ut_stuff.py
Normal file
383
worlds/tunic/ut_stuff.py
Normal file
@@ -0,0 +1,383 @@
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from .options import EntranceLayout, LaurelsLocation
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TunicWorld
|
||||
|
||||
|
||||
def setup_options_from_slot_data(world: "TunicWorld") -> None:
|
||||
if hasattr(world.multiworld, "re_gen_passthrough"):
|
||||
if "TUNIC" in world.multiworld.re_gen_passthrough:
|
||||
world.using_ut = True
|
||||
world.passthrough = world.multiworld.re_gen_passthrough["TUNIC"]
|
||||
world.options.start_with_sword.value = world.passthrough["start_with_sword"]
|
||||
world.options.keys_behind_bosses.value = world.passthrough["keys_behind_bosses"]
|
||||
world.options.sword_progression.value = world.passthrough["sword_progression"]
|
||||
world.options.ability_shuffling.value = world.passthrough["ability_shuffling"]
|
||||
world.options.laurels_zips.value = world.passthrough["laurels_zips"]
|
||||
world.options.ice_grappling.value = world.passthrough["ice_grappling"]
|
||||
world.options.ladder_storage.value = world.passthrough["ladder_storage"]
|
||||
world.options.ladder_storage_without_items = world.passthrough["ladder_storage_without_items"]
|
||||
world.options.lanternless.value = world.passthrough["lanternless"]
|
||||
world.options.maskless.value = world.passthrough["maskless"]
|
||||
world.options.hexagon_quest.value = world.passthrough["hexagon_quest"]
|
||||
world.options.hexagon_quest_ability_type.value = world.passthrough.get("hexagon_quest_ability_type", 0)
|
||||
world.options.entrance_rando.value = world.passthrough["entrance_rando"]
|
||||
world.options.shuffle_ladders.value = world.passthrough["shuffle_ladders"]
|
||||
# world.options.shuffle_fuses.value = world.passthrough.get("shuffle_fuses", 0)
|
||||
# world.options.shuffle_bells.value = world.passthrough.get("shuffle_bells", 0)
|
||||
world.options.grass_randomizer.value = world.passthrough.get("grass_randomizer", 0)
|
||||
world.options.breakable_shuffle.value = world.passthrough.get("breakable_shuffle", 0)
|
||||
world.options.entrance_layout.value = EntranceLayout.option_standard
|
||||
if ("ziggurat2020_3, ziggurat2020_1_zig2_skip" in world.passthrough["Entrance Rando"].keys()
|
||||
or "ziggurat2020_3, ziggurat2020_1_zig2_skip" in world.passthrough["Entrance Rando"].values()):
|
||||
world.options.entrance_layout.value = EntranceLayout.option_fixed_shop
|
||||
world.options.decoupled = world.passthrough.get("decoupled", 0)
|
||||
world.options.laurels_location.value = LaurelsLocation.option_anywhere
|
||||
world.options.combat_logic.value = world.passthrough.get("combat_logic", 0)
|
||||
else:
|
||||
world.using_ut = False
|
||||
else:
|
||||
world.using_ut = False
|
||||
|
||||
|
||||
# for UT poptracker integration map tab switching
|
||||
def map_page_index(data: Any) -> int:
|
||||
mapping: dict[str, int] = {
|
||||
"Beneath the Earth": 1,
|
||||
"Beneath the Well": 2,
|
||||
"The Cathedral": 3,
|
||||
"Dark Tomb": 4,
|
||||
"Eastern Vault": 5,
|
||||
"Frog's Domain": 6,
|
||||
"Swamp": 7,
|
||||
"Overworld": 8,
|
||||
"The Quarry": 9,
|
||||
"Ruined Atoll": 10,
|
||||
"West Gardens": 11,
|
||||
"The Grand Library": 12,
|
||||
"East Forest": 13,
|
||||
"The Far Shore": 14,
|
||||
"The Rooted Ziggurat": 15,
|
||||
}
|
||||
return mapping.get(data, 0)
|
||||
|
||||
|
||||
# mapping of everything after the second to last slash and the location id
|
||||
# lua used for the name: string.match(full_name, "[^/]*/[^/]*$")
|
||||
poptracker_data: dict[str, int] = {
|
||||
"[Powered Secret Room] Chest/Follow the Purple Energy Road": 509342400,
|
||||
"[Entryway] Chest/Mind the Slorms": 509342401,
|
||||
"[Third Room] Beneath Platform Chest/Run from the tentacles!": 509342402,
|
||||
"[Third Room] Tentacle Chest/Water Sucks": 509342403,
|
||||
"[Entryway] Obscured Behind Waterfall/You can just go in there": 509342404,
|
||||
"[Save Room] Upper Floor Chest 1/Through the Power of Prayer": 509342405,
|
||||
"[Save Room] Upper Floor Chest 2/Above the Fox Shrine": 509342406,
|
||||
"[Second Room] Underwater Chest/Hidden Passage": 509342407,
|
||||
"[Back Corridor] Right Secret/Hidden Path": 509342408,
|
||||
"[Back Corridor] Left Secret/Behind the Slorms": 509342409,
|
||||
"[Second Room] Obscured Behind Waterfall/Just go in there": 509342410,
|
||||
"[Side Room] Chest By Pots/Just Climb up There": 509342411,
|
||||
"[Side Room] Chest By Phrends/So Many Phrends!": 509342412,
|
||||
"[Second Room] Page/Ruined Atoll Map": 509342413,
|
||||
"[Passage To Dark Tomb] Page Pickup/Siege Engine": 509342414,
|
||||
"[1F] Guarded By Lasers/Beside 3 Miasma Seekers": 509342415,
|
||||
"[1F] Near Spikes/Mind the Miasma Seeker": 509342416,
|
||||
"Birdcage Room/[2F] Bird Room": 509342417,
|
||||
"[2F] Entryway Upper Walkway/Overlooking Miasma": 509342418,
|
||||
"[1F] Library/By the Books": 509342419,
|
||||
"[2F] Library/Behind the Ladder": 509342420,
|
||||
"[2F] Guarded By Lasers/Before the big reveal...": 509342421,
|
||||
"Birdcage Room/[2F] Bird Room Secret": 509342422,
|
||||
"[1F] Library Secret/Pray to the Wallman": 509342423,
|
||||
"Spike Maze Near Exit/Watch out!": 509342424,
|
||||
"2nd Laser Room/Can you roll?": 509342425,
|
||||
"1st Laser Room/Use a bomb?": 509342426,
|
||||
"Spike Maze Upper Walkway/Just walk right!": 509342427,
|
||||
"Skulls Chest/Move the Grave": 509342428,
|
||||
"Spike Maze Near Stairs/In the Corner": 509342429,
|
||||
"1st Laser Room Obscured/Follow the red laser of death": 509342430,
|
||||
"Guardhouse 2 - Upper Floor/In the Mound": 509342431,
|
||||
"Guardhouse 2 - Bottom Floor Secret/Hidden Hallway": 509342432,
|
||||
"Guardhouse 1 Obscured/Upper Floor Obscured": 509342433,
|
||||
"Guardhouse 1/Upper Floor": 509342434,
|
||||
"Guardhouse 1 Ledge HC/Dancing Fox Spirit Holy Cross": 509342435,
|
||||
"Golden Obelisk Holy Cross/Use the Holy Cross": 509342436,
|
||||
"Ice Rod Grapple Chest/Freeze the Blob and ascend With Orb": 509342437,
|
||||
"Above Save Point/Chest": 509342438,
|
||||
"Above Save Point Obscured/Hidden Path": 509342439,
|
||||
"Guardhouse 1 Ledge/From Guardhouse 1 Chest": 509342440,
|
||||
"Near Save Point/Chest": 509342441,
|
||||
"Ambushed by Spiders/Beneath Spider Chest": 509342442,
|
||||
"Near Telescope/Up on the Wall": 509342443,
|
||||
"Ambushed by Spiders/Spider Chest": 509342444,
|
||||
"Lower Dash Chest/Dash Across": 509342445,
|
||||
"Lower Grapple Chest/Grapple Across": 509342446,
|
||||
"Bombable Wall/Follow the Flowers": 509342447,
|
||||
"Page On Teleporter/Page": 509342448,
|
||||
"Forest Belltower Save Point/Near Save Point": 509342449,
|
||||
"Forest Belltower - After Guard Captain/Chest": 509342450,
|
||||
"East Bell/Forest Belltower - Obscured Near Bell Top Floor": 509342451,
|
||||
"Forest Belltower Obscured/Obscured Beneath Bell Bottom Floor": 509342452,
|
||||
"Forest Belltower Page/Page Pickup": 509342453,
|
||||
"Forest Grave Path - Holy Cross Code by Grave/Single Money Chest": 509342454,
|
||||
"Forest Grave Path - Above Gate/Chest": 509342455,
|
||||
"Forest Grave Path - Obscured Chest/Behind the Trees": 509342456,
|
||||
"Forest Grave Path - Upper Walkway/From the top of the Guardhouse": 509342457,
|
||||
"The Hero's Sword/Forest Grave Path - Sword Pickup": 509342458,
|
||||
"The Hero's Sword/Hero's Grave - Tooth Relic": 509342459,
|
||||
"Fortress Courtyard - From East Belltower/Crack in the Wall": 509342460,
|
||||
"Fortress Leaf Piles - Secret Chest/Dusty": 509342461,
|
||||
"Fortress Arena/Hexagon Red": 509342462,
|
||||
"Fortress Arena/Siege Engine|Vault Key Pickup": 509342463,
|
||||
"Fortress East Shortcut - Chest Near Slimes/Mind the Custodians": 509342464,
|
||||
"[West Wing] Candles Holy Cross/Use the Holy Cross": 509342465,
|
||||
"Westmost Upper Room/[West Wing] Dark Room Chest 1": 509342466,
|
||||
"Westmost Upper Room/[West Wing] Dark Room Chest 2": 509342467,
|
||||
"[East Wing] Bombable Wall/Bomb the Wall": 509342468,
|
||||
"[West Wing] Page Pickup/He will never visit the Far Shore": 509342469,
|
||||
"Fortress Grave Path - Upper Walkway/Go Around the East Wing": 509342470,
|
||||
"Vault Hero's Grave/Fortress Grave Path - Chest Right of Grave": 509342471,
|
||||
"Vault Hero's Grave/Fortress Grave Path - Obscured Chest Left of Grave": 509342472,
|
||||
"Vault Hero's Grave/Hero's Grave - Flowers Relic": 509342473,
|
||||
"Bridge/Chest": 509342474,
|
||||
"Cell Chest 1/Drop the Shortcut Rope": 509342475,
|
||||
"Obscured Behind Waterfall/Muffling Bell": 509342476,
|
||||
"Back Room Chest/Lose the Lure or take 2 Damage": 509342477,
|
||||
"Cell Chest 2/Mind the Custodian": 509342478,
|
||||
"Near Vault/Already Stolen": 509342479,
|
||||
"Slorm Room/Tobias was Trapped Here Once...": 509342480,
|
||||
"Escape Chest/Don't Kick Fimbleton!": 509342481,
|
||||
"Grapple Above Hot Tub/Look Up": 509342482,
|
||||
"Above Vault/Obscured Doorway Ledge": 509342483,
|
||||
"Main Room Top Floor/Mind the Adult Frog": 509342484,
|
||||
"Main Room Bottom Floor/Altar Chest": 509342485,
|
||||
"Side Room Secret Passage/Upper Right Corner": 509342486,
|
||||
"Side Room Chest/Oh No! Our Frogs! They're Dead!": 509342487,
|
||||
"Side Room Grapple Secret/Grapple on Over": 509342488,
|
||||
"Magic Orb Pickup/Frult Meeting": 509342489,
|
||||
"The Librarian/Hexagon Green": 509342490,
|
||||
"Library Hall/Holy Cross Chest": 509342491,
|
||||
"Library Lab Chest by Shrine 2/Chest": 509342492,
|
||||
"Library Lab Chest by Shrine 1/Chest": 509342493,
|
||||
"Library Lab Chest by Shrine 3/Chest": 509342494,
|
||||
"Library Lab by Fuse/Behind Chalkboard": 509342495,
|
||||
"Library Lab Page 3/Page": 509342496,
|
||||
"Library Lab Page 1/Page": 509342497,
|
||||
"Library Lab Page 2/Page": 509342498,
|
||||
"Hero's Grave/Mushroom Relic": 509342499,
|
||||
"Mountain Door/Lower Mountain - Page Before Door": 509342500,
|
||||
"Changing Room/Normal Chest": 509342501,
|
||||
"Fortress Courtyard - Chest Near Cave/Next to the Obelisk": 509342502,
|
||||
"Fortress Courtyard - Near Fuse/Pray": 509342503,
|
||||
"Fortress Courtyard - Below Walkway/Under the Stairs": 509342504,
|
||||
"Fortress Courtyard - Page Near Cave/Heir-To-The-Heir": 509342505,
|
||||
"West Furnace/Lantern Pickup": 509342506,
|
||||
"Maze Cave/Maze Room Chest": 509342507,
|
||||
"Inside the Old House/Normal Chest": 509342508,
|
||||
"Inside the Old House/Shield Pickup": 509342509,
|
||||
"[West] Obscured Behind Windmill/Behind the Trees": 509342510,
|
||||
"[South] Beach Chest/Beside the Bridge": 509342511,
|
||||
"[West] Obscured Near Well/Hidden by Trees": 509342512,
|
||||
"[Central] Bombable Wall/Let the flowers guide you": 509342513,
|
||||
"[Northwest] Chest Near Turret/Mind the Autobolt...": 509342514,
|
||||
"[East] Chest Near Pots/Chest": 509342515,
|
||||
"[Northwest] Chest Near Golden Obelisk/Underneath the Staff": 509342516,
|
||||
"[Southwest] South Chest Near Guard/End of the Bridge": 509342517,
|
||||
"[Southwest] West Beach Guarded By Turret/Chest": 509342518,
|
||||
"[Southwest] Chest Guarded By Turret/Behind the Trees": 509342519,
|
||||
"[Northwest] Shadowy Corner Chest/Dark Ramps Chest": 509342520,
|
||||
"[Southwest] Obscured In Tunnel To Beach/Deep in the Wall": 509342521,
|
||||
"[Southwest] Grapple Chest Over Walkway/Jeffry": 509342522,
|
||||
"[Northwest] Chest Beneath Quarry Gate/Across the Bridge": 509342523,
|
||||
"[Southeast] Chest Near Swamp/Under the Bridge": 509342524,
|
||||
"[Southwest] From West Garden/Dash Across": 509342525,
|
||||
"[East] Grapple Chest/Grapple Across": 509342526,
|
||||
"[Southwest] West Beach Guarded By Turret 2/Get Across": 509342527,
|
||||
"Sand Hook/[Southwest] Beach Chest Near Flowers": 509342528,
|
||||
"[Southwest] Bombable Wall Near Fountain/Let the flowers guide you": 509342529,
|
||||
"[West] Chest After Bell/Post-Dong!": 509342530,
|
||||
"[Southwest] Tunnel Guarded By Turret/Below Jeffry": 509342531,
|
||||
"[East] Between ladders near Ruined Passage/Chest": 509342532,
|
||||
"[Northeast] Chest Above Patrol Cave/Behind Blue Rudelings": 509342533,
|
||||
"[Southwest] Beach Chest Beneath Guard/Under Bridge": 509342534,
|
||||
"[Central] Chest Across From Well/Across the Bridge": 509342535,
|
||||
"[Northwest] Chest Near Quarry Gate/Rudeling Camp": 509342536,
|
||||
"[East] Chest In Trees/Above Locked House": 509342537,
|
||||
"[West] Chest Behind Moss Wall/Around the Corner": 509342538,
|
||||
"[South] Beach Page/Page": 509342539,
|
||||
"[Southeast] Page on Pillar by Swamp/Dash Across": 509342540,
|
||||
"[Southwest] Key Pickup/Old House Key": 509342541,
|
||||
"[West] Key Pickup/Hero's Path Key": 509342542,
|
||||
"[East] Page Near Secret Shop/Page": 509342543,
|
||||
"Fountain/[Southwest] Fountain Page": 509342544,
|
||||
"[Northwest] Page on Pillar by Dark Tomb/A Terrible Power Rises": 509342545,
|
||||
"Magic Staff/[Northwest] Fire Wand Pickup": 509342546,
|
||||
"[West] Page on Teleporter/Treasures and Tools": 509342547,
|
||||
"[Northwest] Page By Well/If you seek to increase your power...": 509342548,
|
||||
"Patrol Cave/Normal Chest": 509342549,
|
||||
"Ruined Shop/Chest 1": 509342550,
|
||||
"Ruined Shop/Chest 2": 509342551,
|
||||
"Ruined Shop/Chest 3": 509342552,
|
||||
"Ruined Passage/Page Pickup": 509342553,
|
||||
"Shop/Potion 1": 509342554,
|
||||
"Shop/Potion 2": 509342555,
|
||||
"Shop/Coin 1": 509342556,
|
||||
"Shop/Coin 2": 509342557,
|
||||
"Special Shop/Secret Page Pickup": 509342558,
|
||||
"Stick House/Stick Chest": 509342559,
|
||||
"Sealed Temple/Page Pickup": 509342560,
|
||||
"Inside Hourglass Cave/Hourglass Chest": 509342561,
|
||||
"Secret Chest/Dash Across": 509342562,
|
||||
"Page Pickup/A Long, Long Time Ago...": 509342563,
|
||||
"Coins in the Well/10 Coins": 509342564,
|
||||
"Coins in the Well/15 Coins": 509342565,
|
||||
"Coins in the Well/3 Coins": 509342566,
|
||||
"Coins in the Well/6 Coins": 509342567,
|
||||
"Secret Gathering Place/20 Fairy Reward": 509342568,
|
||||
"Secret Gathering Place/10 Fairy Reward": 509342569,
|
||||
"[West] Moss Wall Holy Cross/Use the Holy Cross": 509342570,
|
||||
"[Southwest] Flowers Holy Cross/Use the Holy Cross": 509342571,
|
||||
"Fountain/[Southwest] Fountain Holy Cross": 509342572,
|
||||
"[Northeast] Flowers Holy Cross/Use the Holy Cross": 509342573,
|
||||
"[East] Weathervane Holy Cross/Use the Holy Cross": 509342574,
|
||||
"[West] Windmill Holy Cross/Sacred Geometry": 509342575,
|
||||
"Sand Hook/[Southwest] Haiku Holy Cross": 509342576,
|
||||
"[West] Windchimes Holy Cross/Power Up!": 509342577,
|
||||
"[South] Starting Platform Holy Cross/Back to Work": 509342578,
|
||||
"Magic Staff/[Northwest] Golden Obelisk Page": 509342579,
|
||||
"Inside the Old House/Holy Cross Door Page": 509342580,
|
||||
"Cube Cave/Holy Cross Chest": 509342581,
|
||||
"Southeast Cross Door/Chest 3": 509342582,
|
||||
"Southeast Cross Door/Chest 2": 509342583,
|
||||
"Southeast Cross Door/Chest 1": 509342584,
|
||||
"Maze Cave/Maze Room Holy Cross": 509342585,
|
||||
"Caustic Light Cave/Holy Cross Chest": 509342586,
|
||||
"Inside the Old House/Holy Cross Chest": 509342587,
|
||||
"Patrol Cave/Holy Cross Chest": 509342588,
|
||||
"Ruined Passage/Holy Cross Chest": 509342589,
|
||||
"Inside Hourglass Cave/Holy Cross Chest": 509342590,
|
||||
"Sealed Temple/Holy Cross Chest": 509342591,
|
||||
"Fountain Cross Door/Page Pickup": 509342592,
|
||||
"Secret Gathering Place/Holy Cross Chest": 509342593,
|
||||
"Mountain Door/Top of the Mountain - Page At The Peak": 509342594,
|
||||
"Monastery/Monastery Chest": 509342595,
|
||||
"[Back Entrance] Bushes Holy Cross/Use the Holy Cross": 509342596,
|
||||
"[Back Entrance] Chest/Peaceful Chest": 509342597,
|
||||
"[Central] Near Shortcut Ladder/By the Boxes": 509342598,
|
||||
"[East] Near Telescope/Spoopy": 509342599,
|
||||
"[East] Upper Floor/Reminds me of Blighttown": 509342600,
|
||||
"[Central] Below Entry Walkway/Even more Stairs!": 509342601,
|
||||
"[East] Obscured Near Winding Staircase/At the Bottom": 509342602,
|
||||
"[East] Obscured Beneath Scaffolding/In the Miasma Mound": 509342603,
|
||||
"[East] Obscured Near Telescope/Weird path?": 509342604,
|
||||
"[Back Entrance] Obscured Behind Wall/Happy Water!": 509342605,
|
||||
"[Central] Obscured Below Entry Walkway/Down the Stairs": 509342606,
|
||||
"[Central] Top Floor Overhang/End of the ruined bridge": 509342607,
|
||||
"[East] Near Bridge/Drop that Bridge!": 509342608,
|
||||
"[Central] Above Ladder/Climb Ladder": 509342609,
|
||||
"[Central] Obscured Behind Staircase/At the Bottom": 509342610,
|
||||
"[Central] Above Ladder Dash Chest/Dash Across": 509342611,
|
||||
"[West] Upper Area Bombable Wall/Boomy": 509342612,
|
||||
"[East] Bombable Wall/Flowers Guide Thee": 509342613,
|
||||
"Monastery/Hero's Grave - Ash Relic": 509342614,
|
||||
"[West] Shooting Range Secret Path/Obscured Path": 509342615,
|
||||
"[West] Near Shooting Range/End of bridge": 509342616,
|
||||
"[West] Below Shooting Range/Clever little sneak!": 509342617,
|
||||
"[Lowlands] Below Broken Ladder/Miasma Pits": 509342618,
|
||||
"[West] Upper Area Near Waterfall/Yummy Polygons": 509342619,
|
||||
"[Lowlands] Upper Walkway/Hate them Snipers": 509342620,
|
||||
"[West] Lower Area Below Bridge/Go Around": 509342621,
|
||||
"[West] Lower Area Isolated Chest/Burn Pots": 509342622,
|
||||
"[Lowlands] Near Elevator/End of the Tracks": 509342623,
|
||||
"[West] Lower Area After Bridge/Drop that Bridge!": 509342624,
|
||||
"Upper - Near Bridge Switch/You can shoot it": 509342625,
|
||||
"Upper - Beneath Bridge To Administrator/End of the First Floor": 509342626,
|
||||
"Tower - Inside Tower/I'm Scared": 509342627,
|
||||
"Lower - Near Corpses/They are Dead": 509342628,
|
||||
"Lower - Spider Ambush/Use the Gun": 509342629,
|
||||
"Lower - Left Of Checkpoint Before Fuse/Moment of Reprieve": 509342630,
|
||||
"Lower - After Guarded Fuse/Defeat those Mechs": 509342631,
|
||||
"Lower - Guarded By Double Turrets/Help": 509342632,
|
||||
"Lower - After 2nd Double Turret Chest/Haircut Time!": 509342633,
|
||||
"Lower - Guarded By Double Turrets 2/Oh god they're everywhere": 509342634,
|
||||
"Lower - Hexagon Blue/Scavenger Queen": 509342635,
|
||||
"[West] Near Kevin Block/Phonomath": 509342636,
|
||||
"[South] Upper Floor On Power Line/Hidden Ladder Chest": 509342637,
|
||||
"[South] Chest Near Big Crabs/His Name is Tom": 509342638,
|
||||
"[North] Guarded By Bird/Skraw!": 509342639,
|
||||
"[Northeast] Chest Beneath Brick Walkway/Mind the Crabbits": 509342640,
|
||||
"[Northwest] Bombable Wall/Flowers Guide Thee": 509342641,
|
||||
"[North] Obscured Beneath Bridge/In the shallow water": 509342642,
|
||||
"[South] Upper Floor On Bricks/Up the Ladder": 509342643,
|
||||
"[South] Near Birds/Danlarry and Thranmire ate Jerry!": 509342644,
|
||||
"[Northwest] Behind Envoy/Mind the Fairies": 509342645,
|
||||
"[Southwest] Obscured Behind Fuse/Saved by the Prayer": 509342646,
|
||||
"Locked Brick House/[East] Locked Room Upper Chest": 509342647,
|
||||
"[North] From Lower Overworld Entrance/Come from the Overworld": 509342648,
|
||||
"Locked Brick House/[East] Locked Room Lower Chest": 509342649,
|
||||
"[Northeast] Chest On Brick Walkway/Near Domain": 509342650,
|
||||
"[Southeast] Chest Near Fuse/Around the Tower": 509342651,
|
||||
"[Northeast] Key Pickup/Around the Hill": 509342652,
|
||||
"Cathedral Gauntlet/Gauntlet Reward": 509342653,
|
||||
"Secret Legend Trophy Chest/You can use the Holy Cross from the outside": 509342654,
|
||||
"[Upper Graveyard] Obscured Behind Hill/Between Two Hills": 509342655,
|
||||
"[South Graveyard] 4 Orange Skulls/DJ Khaled - Let's go Golfing!": 509342656,
|
||||
"[Central] Near Ramps Up/Up them Ramps": 509342657,
|
||||
"[Upper Graveyard] Near Shield Fleemers/Alternatively, Before the Cathedral": 509342658,
|
||||
"[South Graveyard] Obscured Behind Ridge/Hidden passage by ladder": 509342659,
|
||||
"[South Graveyard] Obscured Beneath Telescope/Through the Nook": 509342660,
|
||||
"[Entrance] Above Entryway/Dash Across": 509342661,
|
||||
"[Central] South Secret Passage/Wall Man Approves these Vibes": 509342662,
|
||||
"[South Graveyard] Upper Walkway On Pedestal/Gazing out over the Graves": 509342663,
|
||||
"[South Graveyard] Guarded By Tentacles/Isolated Island": 509342664,
|
||||
"[Upper Graveyard] Near Telescope/Overlooking the Graves": 509342665,
|
||||
"[Outside Cathedral] Near Moonlight Bridge Door/Down the Hidden Ladder": 509342666,
|
||||
"[Entrance] Obscured Inside Watchtower/Go Inside": 509342667,
|
||||
"[Entrance] South Near Fence/DAGGER STRAP!!!!!": 509342668,
|
||||
"[South Graveyard] Guarded By Big Skeleton/Super Clipping": 509342669,
|
||||
"[South Graveyard] Chest Near Graves/The Rest of Our Entire Life is Death": 509342670,
|
||||
"[Entrance] North Small Island/Mildly Hidden": 509342671,
|
||||
"First Hero's Grave/[Outside Cathedral] Obscured Behind Memorial": 509342672,
|
||||
"[Central] Obscured Behind Northern Mountain/Hug the Wall": 509342673,
|
||||
"[South Graveyard] Upper Walkway Dash Chest/Around the Hill": 509342674,
|
||||
"[South Graveyard] Above Big Skeleton/End of Ledge": 509342675,
|
||||
"[Central] Beneath Memorial/Do You Even Live?": 509342676,
|
||||
"First Hero's Grave/Hero's Grave - Feathers Relic": 509342677,
|
||||
"West Furnace/Chest": 509342678,
|
||||
"[West] Near Gardens Entrance/Effigy Skip": 509342679,
|
||||
"[Central Highlands] Holy Cross (Blue Lines)/Use the Holy Cross": 509342680,
|
||||
"[West Lowlands] Tree Holy Cross Chest/Use the Holy Cross": 509342681,
|
||||
"[Southeast Lowlands] Outside Cave/Mind the Chompignoms!": 509342682,
|
||||
"[Central Lowlands] Chest Beneath Faeries/As you walk by": 509342683,
|
||||
"[North] Behind Holy Cross Door/Extra Sword!": 509342684,
|
||||
"[Central Highlands] Top of Ladder Before Boss/Try to be This Strong": 509342685,
|
||||
"[Central Lowlands] Passage Beneath Bridge/Take the lower path": 509342686,
|
||||
"[North] Across From Page Pickup/I Love Fish!": 509342687,
|
||||
"[Central Lowlands] Below Left Walkway/Dash Across": 509342688,
|
||||
"[West] In Flooded Walkway/Dash through the water": 509342689,
|
||||
"[West] Past Flooded Walkway/Through the Shallow Water": 509342690,
|
||||
"[North] Obscured Beneath Hero's Memorial/Take the Long Way Around": 509342691,
|
||||
"[Central Lowlands] Chest Near Shortcut Bridge/Between a Rope and a Bridge Place": 509342692,
|
||||
"[West Highlands] Upper Left Walkway/By the Rudeling": 509342693,
|
||||
"[Central Lowlands] Chest Beneath Save Point/Behind the Way": 509342694,
|
||||
"[Central Highlands] Behind Guard Captain/Under Boss Ladder": 509342695,
|
||||
"[Central Highlands] After Garden Knight/Did Not Kill You": 509342696,
|
||||
"[South Highlands] Secret Chest Beneath Fuse/Pray to the Wall Man": 509342697,
|
||||
"[East Lowlands] Page Behind Ice Dagger House/Come from the Far Shore": 509342698,
|
||||
"[North] Page Pickup/Survival Tips": 509342699,
|
||||
"[Southeast Lowlands] Ice Dagger Pickup/Ice Dagger Cave": 509342700,
|
||||
"Hero's Grave/Effigy Relic": 509342701,
|
||||
}
|
||||
|
||||
|
||||
# for setting up the poptracker integration
|
||||
tracker_world = {
|
||||
"map_page_maps": ["maps/maps_pop.json"],
|
||||
"map_page_locations": ["locations/locations_pop_er.json"],
|
||||
"map_page_setting_key": "Slot:{player}:Current Map",
|
||||
"map_page_index": map_page_index,
|
||||
"external_pack_key": "ut_poptracker_path",
|
||||
"poptracker_name_mapping": poptracker_data
|
||||
}
|
||||
@@ -25,7 +25,7 @@ is strongly recommended in case they become corrupted.
|
||||
|
||||
## Linux Only: Select AppData equivalent when starting the client
|
||||
1. Shut down Wargroove if it is open.
|
||||
2. Start the ArchipelagoWargrooveClient from the Archipelago installation.
|
||||
2. Start the Archipelago Wargroove Client from the Archipelago Launcher.
|
||||
3. A file select dialogue will appear, claiming it cannot detect a path to the AppData folder.
|
||||
4. Navigate to your Steam install directory and select .
|
||||
`/steamapps/compatdata/607050/pfx/drive_c/users/steamuser/AppData/Roaming` as the save directory.
|
||||
@@ -36,7 +36,7 @@ is strongly recommended in case they become corrupted.
|
||||
## Installing the Archipelago Wargroove Mod and Campaign files
|
||||
|
||||
1. Shut down Wargroove if it is open.
|
||||
2. Start the ArchipelagoWargrooveClient.exe from the Archipelago installation.
|
||||
2. Start the Archipelago Wargroove Client from the Archipelago Launcher.
|
||||
This should install the mod and campaign for you.
|
||||
3. Start Wargroove.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user