mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-04-21 18:03:31 -07:00
Compare commits
13 Commits
revert-491
...
0.6.2-rc2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e82d50a3c5 | ||
|
|
0a7aa9e3e2 | ||
|
|
13ca134d12 | ||
|
|
8671e9a391 | ||
|
|
a7de89f45c | ||
|
|
e9f51e3302 | ||
|
|
5491f8c459 | ||
|
|
de71677208 | ||
|
|
653ee2b625 | ||
|
|
62694b1ce7 | ||
|
|
9c0ad2b825 | ||
|
|
88b529593f | ||
|
|
0351698ef7 |
@@ -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
|
||||
@@ -558,7 +559,9 @@ class MultiWorld():
|
||||
else:
|
||||
return all((self.has_beaten_game(state, p) for p in range(1, self.players + 1)))
|
||||
|
||||
def can_beat_game(self, starting_state: Optional[CollectionState] = None) -> bool:
|
||||
def can_beat_game(self,
|
||||
starting_state: Optional[CollectionState] = None,
|
||||
locations: Optional[Iterable[Location]] = None) -> bool:
|
||||
if starting_state:
|
||||
if self.has_beaten_game(starting_state):
|
||||
return True
|
||||
@@ -567,7 +570,9 @@ class MultiWorld():
|
||||
state = CollectionState(self)
|
||||
if self.has_beaten_game(state):
|
||||
return True
|
||||
prog_locations = {location for location in self.get_locations() if location.item
|
||||
|
||||
base_locations = self.get_locations() if locations is None else locations
|
||||
prog_locations = {location for location in base_locations if location.item
|
||||
and location.item.advancement and location not in state.locations_checked}
|
||||
|
||||
while prog_locations:
|
||||
@@ -1602,21 +1607,19 @@ class Spoiler:
|
||||
|
||||
# in the second phase, we cull each sphere such that the game is still beatable,
|
||||
# reducing each range of influence to the bare minimum required inside it
|
||||
restore_later: Dict[Location, Item] = {}
|
||||
required_locations = {location for sphere in collection_spheres for location in sphere}
|
||||
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
|
||||
to_delete: Set[Location] = set()
|
||||
for location in sphere:
|
||||
# we remove the item at location and check if game is still beatable
|
||||
# we remove the location from required_locations to sweep from, and check if the game is still beatable
|
||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
|
||||
location.item.player)
|
||||
old_item = location.item
|
||||
location.item = None
|
||||
if multiworld.can_beat_game(state_cache[num]):
|
||||
required_locations.remove(location)
|
||||
if multiworld.can_beat_game(state_cache[num], required_locations):
|
||||
to_delete.add(location)
|
||||
restore_later[location] = old_item
|
||||
else:
|
||||
# still required, got to keep it around
|
||||
location.item = old_item
|
||||
required_locations.add(location)
|
||||
|
||||
# cull entries in spheres for spoiler walkthrough at end
|
||||
sphere -= to_delete
|
||||
@@ -1633,7 +1636,7 @@ class Spoiler:
|
||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
||||
precollected_items.remove(item)
|
||||
multiworld.state.remove(item)
|
||||
if not multiworld.can_beat_game():
|
||||
if not multiworld.can_beat_game(multiworld.state, required_locations):
|
||||
# Add the item back into `precollected_items` and collect it into `multiworld.state`.
|
||||
multiworld.push_precollected(item)
|
||||
else:
|
||||
@@ -1675,9 +1678,6 @@ class Spoiler:
|
||||
self.create_paths(state, collection_spheres)
|
||||
|
||||
# repair the multiworld again
|
||||
for location, item in restore_later.items():
|
||||
location.item = item
|
||||
|
||||
for item in removed_precollected:
|
||||
multiworld.push_precollected(item)
|
||||
|
||||
|
||||
@@ -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()
|
||||
60
Launcher.py
60
Launcher.py
@@ -115,34 +115,30 @@ components.extend([
|
||||
])
|
||||
|
||||
|
||||
def handle_uri(path: str, launch_args: tuple[str, ...]) -> None:
|
||||
def handle_uri(path: str) -> tuple[list[Component], Component]:
|
||||
url = urllib.parse.urlparse(path)
|
||||
queries = urllib.parse.parse_qs(url.query)
|
||||
launch_args = (path, *launch_args)
|
||||
client_component = []
|
||||
client_components = []
|
||||
text_client_component = None
|
||||
game = queries["game"][0]
|
||||
for component in components:
|
||||
if component.supports_uri and component.game_name == game:
|
||||
client_component.append(component)
|
||||
client_components.append(component)
|
||||
elif component.display_name == "Text Client":
|
||||
text_client_component = component
|
||||
return client_components, text_client_component
|
||||
|
||||
|
||||
if not client_component:
|
||||
run_component(text_client_component, *launch_args)
|
||||
return
|
||||
else:
|
||||
from kvui import ButtonsPrompt
|
||||
component_options = {
|
||||
text_client_component.display_name: text_client_component,
|
||||
**{component.display_name: component for component in client_component}
|
||||
}
|
||||
popup = ButtonsPrompt("Connect to Multiworld",
|
||||
"Select client to open and connect with.",
|
||||
lambda component_name: run_component(component_options[component_name], *launch_args),
|
||||
*component_options.keys())
|
||||
popup.open()
|
||||
def build_uri_popup(component_list: list[Component], launch_args: tuple[str, ...]) -> None:
|
||||
from kvui import ButtonsPrompt
|
||||
component_options = {
|
||||
component.display_name: component for component in component_list
|
||||
}
|
||||
popup = ButtonsPrompt("Connect to Multiworld",
|
||||
"Select client to open and connect with.",
|
||||
lambda component_name: run_component(component_options[component_name], *launch_args),
|
||||
*component_options.keys())
|
||||
popup.open()
|
||||
|
||||
|
||||
def identify(path: None | str) -> tuple[None | str, None | Component]:
|
||||
@@ -212,7 +208,7 @@ def create_shortcut(button: Any, component: Component) -> None:
|
||||
refresh_components: Callable[[], None] | None = None
|
||||
|
||||
|
||||
def run_gui(path: str, args: Any) -> None:
|
||||
def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, ScrollBox)
|
||||
from kivy.properties import ObjectProperty
|
||||
from kivy.core.window import Window
|
||||
@@ -245,12 +241,12 @@ def run_gui(path: str, args: Any) -> None:
|
||||
cards: list[LauncherCard]
|
||||
current_filter: Sequence[str | Type] | None
|
||||
|
||||
def __init__(self, ctx=None, path=None, args=None):
|
||||
def __init__(self, ctx=None, components=None, args=None):
|
||||
self.title = self.base_title + " " + Utils.__version__
|
||||
self.ctx = ctx
|
||||
self.icon = r"data/icon.png"
|
||||
self.favorites = []
|
||||
self.launch_uri = path
|
||||
self.launch_components = components
|
||||
self.launch_args = args
|
||||
self.cards = []
|
||||
self.current_filter = (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
|
||||
@@ -372,9 +368,9 @@ def run_gui(path: str, args: Any) -> None:
|
||||
return self.top_screen
|
||||
|
||||
def on_start(self):
|
||||
if self.launch_uri:
|
||||
handle_uri(self.launch_uri, self.launch_args)
|
||||
self.launch_uri = None
|
||||
if self.launch_components:
|
||||
build_uri_popup(self.launch_components, self.launch_args)
|
||||
self.launch_components = None
|
||||
self.launch_args = None
|
||||
|
||||
@staticmethod
|
||||
@@ -392,7 +388,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.
|
||||
@@ -415,7 +411,7 @@ def run_gui(path: str, args: Any) -> None:
|
||||
for filter in self.current_filter))
|
||||
super().on_stop()
|
||||
|
||||
Launcher(path=path, args=args).run()
|
||||
Launcher(components=launch_components, args=args).run()
|
||||
|
||||
# avoiding Launcher reference leak
|
||||
# and don't try to do something with widgets after window closed
|
||||
@@ -442,7 +438,15 @@ def main(args: argparse.Namespace | dict | None = None):
|
||||
|
||||
path = args.get("Patch|Game|Component|url", None)
|
||||
if path is not None:
|
||||
if not path.startswith("archipelago://"):
|
||||
if path.startswith("archipelago://"):
|
||||
args["args"] = (path, *args.get("args", ()))
|
||||
# add the url arg to the passthrough args
|
||||
components, text_client_component = handle_uri(path)
|
||||
if not components:
|
||||
args["component"] = text_client_component
|
||||
else:
|
||||
args['launch_components'] = [text_client_component, *components]
|
||||
else:
|
||||
file, component = identify(path)
|
||||
if file:
|
||||
args['file'] = file
|
||||
@@ -458,7 +462,7 @@ def main(args: argparse.Namespace | dict | None = None):
|
||||
elif "component" in args:
|
||||
run_component(args["component"], *args["args"])
|
||||
elif not args["update_settings"]:
|
||||
run_gui(path, args.get("args", ()))
|
||||
run_gui(args.get("launch_components", None), args.get("args", ()))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -82,6 +82,7 @@ Currently, the following games are supported:
|
||||
* The Legend of Zelda: The Wind Waker
|
||||
* Jak and Daxter: The Precursor Legacy
|
||||
* Super Mario Land 2: 6 Golden Coins
|
||||
* shapez
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
@@ -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()
|
||||
@@ -160,6 +160,9 @@
|
||||
# Saving Princess
|
||||
/worlds/saving_princess/ @LeonarthCG
|
||||
|
||||
# shapez
|
||||
/worlds/shapez/ @BlastSlimey
|
||||
|
||||
# Shivers
|
||||
/worlds/shivers/ @GodlFire @korydondzila
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
3
setup.py
3
setup.py
@@ -64,7 +64,6 @@ non_apworlds: set[str] = {
|
||||
"ArchipIDLE",
|
||||
"Archipelago",
|
||||
"Clique",
|
||||
"Final Fantasy",
|
||||
"Lufia II Ancient Cave",
|
||||
"Meritous",
|
||||
"Ocarina of Time",
|
||||
@@ -482,7 +481,7 @@ tmp="${{exe#*/}}"
|
||||
if [ ! "${{#tmp}}" -lt "${{#exe}}" ]; then
|
||||
exe="{default_exe.parent}/$exe"
|
||||
fi
|
||||
export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$APPDIR/{default_exe.parent}/lib"
|
||||
export LD_LIBRARY_PATH="${{LD_LIBRARY_PATH:+$LD_LIBRARY_PATH:}}$APPDIR/{default_exe.parent}/lib"
|
||||
$APPDIR/$exe "$@"
|
||||
""")
|
||||
launcher_filename.chmod(0o755)
|
||||
|
||||
@@ -224,8 +224,6 @@ 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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -281,7 +281,7 @@ class MessengerWorld(World):
|
||||
disconnect_entrances(self)
|
||||
add_closed_portal_reqs(self)
|
||||
# i need portal shuffle to happen after rules exist so i can validate it
|
||||
attempts = 5
|
||||
attempts = 20
|
||||
if self.options.shuffle_portals:
|
||||
self.portal_mapping = []
|
||||
self.spoiler_portal_mapping = {}
|
||||
|
||||
@@ -3,7 +3,7 @@ from ...data import villagers_data, fish_data
|
||||
from ...data.building import Building
|
||||
from ...data.game_item import GenericSource, ItemTag, Tag, CustomRuleSource
|
||||
from ...data.harvest import ForagingSource, SeasonalForagingSource, ArtifactSpotSource
|
||||
from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement
|
||||
from ...data.requirement import ToolRequirement, BookRequirement, SkillRequirement, YearRequirement
|
||||
from ...data.shop import ShopSource, MysteryBoxSource, ArtifactTroveSource, PrizeMachineSource, FishingTreasureChestSource
|
||||
from ...strings.artisan_good_names import ArtisanGood
|
||||
from ...strings.book_names import Book
|
||||
@@ -209,7 +209,7 @@ pelican_town = ContentPack(
|
||||
# Books
|
||||
Book.animal_catalogue: (
|
||||
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
|
||||
ShopSource(money_price=5000, shop_region=Region.ranch),),
|
||||
ShopSource(money_price=5000, shop_region=Region.ranch, other_requirements=(YearRequirement(2),)),),
|
||||
Book.book_of_mysteries: (
|
||||
Tag(ItemTag.BOOK, ItemTag.BOOK_POWER),
|
||||
MysteryBoxSource(amount=38),), # After 38 boxes, there are 49.99% chances player received the book.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import unittest
|
||||
from typing import ClassVar
|
||||
|
||||
from . import SVTestBase
|
||||
from .bases import SVTestBase
|
||||
from .. import options
|
||||
from ..locations import LocationTags, location_table
|
||||
from ..mods.mod_data import ModNames
|
||||
|
||||
Reference in New Issue
Block a user