From 7ada5ce629efb904e487409037fdd1b98f2a347e Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:38:04 -0500 Subject: [PATCH 01/13] Bizhawk client and APWorld support. --- FF1Client.py | 267 ------------------------- setup.py | 1 - worlds/ff1/Client.py | 299 ++++++++++++++++++++++++++++ worlds/ff1/Items.py | 16 +- worlds/ff1/Locations.py | 12 +- worlds/ff1/__init__.py | 1 + worlds/ff1/docs/en_Final Fantasy.md | 9 +- worlds/ff1/docs/multiworld_en.md | 26 +-- 8 files changed, 329 insertions(+), 302 deletions(-) delete mode 100644 FF1Client.py create mode 100644 worlds/ff1/Client.py diff --git a/FF1Client.py b/FF1Client.py deleted file mode 100644 index b7c58e206123..000000000000 --- a/FF1Client.py +++ /dev/null @@ -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.init() - - asyncio.run(main(args)) - colorama.deinit() diff --git a/setup.py b/setup.py index 59c2d698d35b..da2772b211a6 100644 --- a/setup.py +++ b/setup.py @@ -66,7 +66,6 @@ "ArchipIDLE", "Archipelago", "Clique", - "Final Fantasy", "Lufia II Ancient Cave", "Meritous", "Ocarina of Time", diff --git a/worlds/ff1/Client.py b/worlds/ff1/Client.py new file mode 100644 index 000000000000..74e11936a1fc --- /dev/null +++ b/worlds/ff1/Client.py @@ -0,0 +1,299 @@ +import logging +from collections import deque +from copy import deepcopy +from typing import TYPE_CHECKING + +from MultiServer import Client +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 +nes_logger = logging.getLogger("NES") +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] + +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 = ["Smoke", "FullCure", "Blast", "Phoenix", + "Flare", "Black", "Refresh", "Guard", + "Wizard", "HighPotion", "Cloak", "Quick"] + +ext_consumables_lookup = {"Smoke": "Ext1", "FullCure": "Ext2", "Blast": "Ext3", "Phoenix": "Ext4", + "Flare": "Ext1", "Black": "Ext2", "Refresh": "Ext3", "Guard": "Ext4", + "Wizard": "Ext1", "HighPotion": "Ext2", "Cloak": "Ext3", "Quick": "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" + + def __init__(self): + self.wram = "RAM" + self.sram = "WRAM" + self.rom = "PRG ROM" + self.consumable_stack_amounts = None + self.weapons_queue = deque() + self.armor_queue = deque() + + + 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 + + return True + + async def game_watcher(self, ctx: "BizHawkClientContext") -> None: + if ctx.server is None: + return + + if ctx.slot is None: + return + try: + if await self.check_status_okay_to_process(ctx): + if self.consumable_stack_amounts is None: + self.consumable_stack_amounts = {} + self.consumable_stack_amounts["Shard"] = 1 + self.consumable_stack_amounts["Tent"] = (await self.read_rom(ctx, 0x47400, 1))[0] + 1 + self.consumable_stack_amounts["Cabin"] = (await self.read_rom(ctx, 0x47401, 1))[0] + 1 + self.consumable_stack_amounts["House"] = (await self.read_rom(ctx, 0x47402, 1))[0] + 1 + self.consumable_stack_amounts["Heal"] = (await self.read_rom(ctx, 0x47403, 1))[0] + 1 + self.consumable_stack_amounts["Pure"] = (await self.read_rom(ctx, 0x47404, 1))[0] + 1 + self.consumable_stack_amounts["Soft"] = (await self.read_rom(ctx, 0x47405, 1))[0] + 1 + self.consumable_stack_amounts["Ext1"] = (await self.read_rom(ctx, 0x47406, 1))[0] + 1 + self.consumable_stack_amounts["Ext2"] = (await self.read_rom(ctx, 0x47407, 1))[0] + 1 + self.consumable_stack_amounts["Ext3"] = (await self.read_rom(ctx, 0x47408, 1))[0] + 1 + self.consumable_stack_amounts["Ext4"] = (await self.read_rom(ctx, 0x47409, 1))[0] + 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 check_status_okay_to_process(self, ctx): + """ + 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) + """ + status_a = await self.read_sram_value(ctx, 0x102) + status_b = await self.read_sram_value(ctx, 0x0FC) + status_c = await self.read_sram_value(ctx, 0x0A3) + return (status_a != 0x00) and not (status_a == 0xF2 and status_b == 0xF2 and status_c == 0xF2) + + async def location_check(self, ctx): + locations_data = await self.read_sram_values(ctx, locations_array_start, locations_array_length) + 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 + # print(f"Location: {ctx.location_names[location]}") + # print(f"Index: {str(hex(index))}") + # print(f"value: {locations_array[index] & flag != 0}") + if locations_data[index] & flag != 0: + locations_checked.append(location) + + for location in locations_checked: + if location not in ctx.locations_checked: + ctx.locations_checked.add(location) + location_name = ctx.location_names.lookup_in_game(location) + nes_logger.info( + f'New Check: {location_name} ({len(ctx.locations_checked)}/' + f'{len(ctx.missing_locations) + len(ctx.checked_locations)})') + await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [location]}]) + + + + async def received_items_check(self, ctx): + items_received_count = await self.read_sram_value(ctx, items_obtained) + 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 + await self.write_sram(ctx, location, 1) + elif current_item_name in movement_items: + location = current_item_id - 0x1E0 + if current_item_name != "Canal": + await self.write_sram(ctx, location, 1) + else: + await self.write_sram(ctx, location, 0) + elif current_item_name in no_overworld_items: + if current_item_name == "Sigil": + location = 0x28 + else: + location = 0x12 + await self.write_sram(ctx, location, 1) + elif current_item_name in gold_items: + gold_amount = int(current_item_name[4:]) + current_gold = int.from_bytes(await self.read_sram_values(ctx, gp_location_low, 3), "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) + await self.write_sram(ctx, gp_location_low, lower_byte) + await self.write_sram(ctx, gp_location_middle, middle_byte) + await self.write_sram(ctx, gp_location_high, upper_byte) + elif current_item_name in consumables: + location = current_item_id - 0xE0 + current_value = await self.read_sram_value(ctx, location) + amount_to_add = self.consumable_stack_amounts[current_item_name] + new_value = min(current_value + amount_to_add, 99) + await self.write_sram(ctx, location, new_value) + 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(ctx, location) + amount_to_add = self.consumable_stack_amounts[ext_name] + new_value = min(current_value + amount_to_add, 99) + await self.write_sram(ctx, location, new_value) + 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) + await self.write_sram(ctx, items_obtained, items_received_count + 1) + + async def process_weapons_queue(self, ctx): + empty_slots = deque() + char1_slots = await self.read_sram_values(ctx, weapons_arrays_starts[0], 4) + char2_slots = await self.read_sram_values(ctx, weapons_arrays_starts[1], 4) + char3_slots = await self.read_sram_values(ctx, weapons_arrays_starts[2], 4) + char4_slots = await self.read_sram_values(ctx, weapons_arrays_starts[3], 4) + 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(ctx, current_slot, current_weapon) + + async def process_armor_queue(self, ctx): + empty_slots = deque() + char1_slots = await self.read_sram_values(ctx, armors_arrays_starts[0], 4) + char2_slots = await self.read_sram_values(ctx, armors_arrays_starts[1], 4) + char3_slots = await self.read_sram_values(ctx, armors_arrays_starts[2], 4) + char4_slots = await self.read_sram_values(ctx, armors_arrays_starts[3], 4) + 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(ctx, current_slot, current_armor) + + async def read_ram_values(self, ctx, location, size): + return (await bizhawk.read(ctx.bizhawk_ctx, [(location, size, self.wram)]))[0] + + async def read_ram_value(self, ctx, location): + value = ((await bizhawk.read(ctx.bizhawk_ctx, [(location, 1, self.wram)]))[0]) + return int.from_bytes(value) + + async def read_sram_values(self, ctx, location, size): + return (await bizhawk.read(ctx.bizhawk_ctx, [(location, size, self.sram)]))[0] + + async def read_sram_value(self, ctx, location): + value = ((await bizhawk.read(ctx.bizhawk_ctx, [(location, 1, self.sram)]))[0]) + return int.from_bytes(value) + + async def read_rom(self, ctx, location, size): + return (await bizhawk.read(ctx.bizhawk_ctx, [(location, size, self.rom)]))[0] + + async def write(self, ctx, location, value): + return await bizhawk.write(ctx.bizhawk_ctx, [(location, [value], self.wram)]) + + async def write_sram(self, ctx, location, value): + return await bizhawk.write(ctx.bizhawk_ctx, [(location, [value], self.sram)]) \ No newline at end of file diff --git a/worlds/ff1/Items.py b/worlds/ff1/Items.py index 469cf6f05193..21923c48e946 100644 --- a/worlds/ff1/Items.py +++ b/worlds/ff1/Items.py @@ -1,4 +1,6 @@ import json +import os +import pkgutil from pathlib import Path from typing import Dict, Set, NamedTuple, List @@ -39,13 +41,13 @@ class FF1Items: 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__, os.path.join("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: diff --git a/worlds/ff1/Locations.py b/worlds/ff1/Locations.py index b0353f94fbdb..5eb0b0008da2 100644 --- a/worlds/ff1/Locations.py +++ b/worlds/ff1/Locations.py @@ -1,4 +1,6 @@ import json +import os +import pkgutil from pathlib import Path from typing import Dict, NamedTuple, List, Optional @@ -20,11 +22,11 @@ class FF1Locations: 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__, os.path.join("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: diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index 3a5047506850..39df9020e529 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -7,6 +7,7 @@ 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): diff --git a/worlds/ff1/docs/en_Final Fantasy.md b/worlds/ff1/docs/en_Final Fantasy.md index 889bb46e0c35..a05aef63bc8c 100644 --- a/worlds/ff1/docs/en_Final Fantasy.md +++ b/worlds/ff1/docs/en_Final Fantasy.md @@ -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 diff --git a/worlds/ff1/docs/multiworld_en.md b/worlds/ff1/docs/multiworld_en.md index d3dc457f01be..1760374464ad 100644 --- a/worlds/ff1/docs/multiworld_en.md +++ b/worlds/ff1/docs/multiworld_en.md @@ -2,10 +2,11 @@ ## 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) + - Version 2.9.1 is recommended. + - 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 +14,7 @@ 1. Download and install the latest version of Archipelago. 1. On Windows, download Setup.Archipelago..exe and run it -2. Assign EmuHawk version 2.3.1 or higher as your default program for launching `.nes` files. +2. Assign EmuHawk or higher 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 +47,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 +55,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_ff1.lua` with the file picker. ## Play the game From 253a328dd1f9a52f49c78c2729f0588a3eb13522 Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:53:40 -0500 Subject: [PATCH 02/13] Removed now-unused connector lua. --- data/lua/connector_ff1.lua | 462 ------------------------------------- 1 file changed, 462 deletions(-) delete mode 100644 data/lua/connector_ff1.lua diff --git a/data/lua/connector_ff1.lua b/data/lua/connector_ff1.lua deleted file mode 100644 index afae5d3c81dc..000000000000 --- a/data/lua/connector_ff1.lua +++ /dev/null @@ -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() From d42edf01036726bec6329b7422a447e28dd84fef Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Wed, 8 Jan 2025 12:03:16 -0500 Subject: [PATCH 03/13] Forgot to remove the FF1 Client from the launcher. --- worlds/LauncherComponents.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 7f178f1739fc..53faf8f9a069 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -188,8 +188,6 @@ def install_apworld(apworld_path: str = "") -> None: 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 From 4d00b7d566a17286e0d3666c3584600822a6e31c Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Wed, 8 Jan 2025 15:58:43 -0500 Subject: [PATCH 04/13] Removed unnecessary os.path calls, set old client to be removed, and changed the logger in the client to actually do something. --- inno_setup.iss | 1 + worlds/ff1/Client.py | 3 +-- worlds/ff1/Items.py | 6 +----- worlds/ff1/Locations.py | 6 +----- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/inno_setup.iss b/inno_setup.iss index eb794650f3a6..45c7b907bfb1 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -86,6 +86,7 @@ Type: dirifempty; Name: "{app}" Type: files; Name: "{app}\lib\worlds\_bizhawk.apworld" Type: files; Name: "{app}\ArchipelagoLttPClient.exe" Type: files; Name: "{app}\ArchipelagoPokemonClient.exe" +type: files; Name: "{app}\ArchipelagoFF1Client.exe" Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua" Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy" Type: dirifempty; Name: "{app}\lib\worlds\rogue-legacy" diff --git a/worlds/ff1/Client.py b/worlds/ff1/Client.py index 74e11936a1fc..c9637fa227d2 100644 --- a/worlds/ff1/Client.py +++ b/worlds/ff1/Client.py @@ -14,7 +14,6 @@ base_id = 7000 -nes_logger = logging.getLogger("NES") logger = logging.getLogger("Client") @@ -172,7 +171,7 @@ async def location_check(self, ctx): if location not in ctx.locations_checked: ctx.locations_checked.add(location) location_name = ctx.location_names.lookup_in_game(location) - nes_logger.info( + logger.info( f'New Check: {location_name} ({len(ctx.locations_checked)}/' f'{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [location]}]) diff --git a/worlds/ff1/Items.py b/worlds/ff1/Items.py index 21923c48e946..5d674a17b386 100644 --- a/worlds/ff1/Items.py +++ b/worlds/ff1/Items.py @@ -1,7 +1,5 @@ import json -import os import pkgutil -from pathlib import Path from typing import Dict, Set, NamedTuple, List from BaseClasses import Item, ItemClassification @@ -39,9 +37,7 @@ 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() - file = pkgutil.get_data(__name__, os.path.join("data", "items.json")).decode("utf-8") + 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 diff --git a/worlds/ff1/Locations.py b/worlds/ff1/Locations.py index 5eb0b0008da2..47facad985e5 100644 --- a/worlds/ff1/Locations.py +++ b/worlds/ff1/Locations.py @@ -1,7 +1,5 @@ import json -import os import pkgutil -from pathlib import Path from typing import Dict, NamedTuple, List, Optional from BaseClasses import Region, Location, MultiWorld @@ -20,9 +18,7 @@ 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() - file = pkgutil.get_data(__name__, os.path.join("data", "locations.json")) + 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()] From 2b570745832b7ae916800940865a7a5159e3a0f5 Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:17:30 -0500 Subject: [PATCH 05/13] Updated according to review comments. --- worlds/ff1/Client.py | 29 +++++++++++++++++------------ worlds/ff1/docs/multiworld_en.md | 2 +- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/worlds/ff1/Client.py b/worlds/ff1/Client.py index c9637fa227d2..e775c9f8fd2c 100644 --- a/worlds/ff1/Client.py +++ b/worlds/ff1/Client.py @@ -95,6 +95,10 @@ async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: 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 @@ -109,16 +113,17 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: if self.consumable_stack_amounts is None: self.consumable_stack_amounts = {} self.consumable_stack_amounts["Shard"] = 1 - self.consumable_stack_amounts["Tent"] = (await self.read_rom(ctx, 0x47400, 1))[0] + 1 - self.consumable_stack_amounts["Cabin"] = (await self.read_rom(ctx, 0x47401, 1))[0] + 1 - self.consumable_stack_amounts["House"] = (await self.read_rom(ctx, 0x47402, 1))[0] + 1 - self.consumable_stack_amounts["Heal"] = (await self.read_rom(ctx, 0x47403, 1))[0] + 1 - self.consumable_stack_amounts["Pure"] = (await self.read_rom(ctx, 0x47404, 1))[0] + 1 - self.consumable_stack_amounts["Soft"] = (await self.read_rom(ctx, 0x47405, 1))[0] + 1 - self.consumable_stack_amounts["Ext1"] = (await self.read_rom(ctx, 0x47406, 1))[0] + 1 - self.consumable_stack_amounts["Ext2"] = (await self.read_rom(ctx, 0x47407, 1))[0] + 1 - self.consumable_stack_amounts["Ext3"] = (await self.read_rom(ctx, 0x47408, 1))[0] + 1 - self.consumable_stack_amounts["Ext4"] = (await self.read_rom(ctx, 0x47409, 1))[0] + 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) @@ -279,14 +284,14 @@ async def read_ram_values(self, ctx, location, size): async def read_ram_value(self, ctx, location): value = ((await bizhawk.read(ctx.bizhawk_ctx, [(location, 1, self.wram)]))[0]) - return int.from_bytes(value) + return int.from_bytes(value, "little") async def read_sram_values(self, ctx, location, size): return (await bizhawk.read(ctx.bizhawk_ctx, [(location, size, self.sram)]))[0] async def read_sram_value(self, ctx, location): value = ((await bizhawk.read(ctx.bizhawk_ctx, [(location, 1, self.sram)]))[0]) - return int.from_bytes(value) + return int.from_bytes(value, "little") async def read_rom(self, ctx, location, size): return (await bizhawk.read(ctx.bizhawk_ctx, [(location, size, self.rom)]))[0] diff --git a/worlds/ff1/docs/multiworld_en.md b/worlds/ff1/docs/multiworld_en.md index 1760374464ad..431d08403297 100644 --- a/worlds/ff1/docs/multiworld_en.md +++ b/worlds/ff1/docs/multiworld_en.md @@ -14,7 +14,7 @@ 1. Download and install the latest version of Archipelago. 1. On Windows, download Setup.Archipelago..exe and run it -2. Assign EmuHawk 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...** From 8f61aa7376e7e12a3e4c1be55c3023d2cb1085c1 Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:22:35 -0500 Subject: [PATCH 06/13] Dastardly copy/paste error. --- worlds/ff1/Client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ff1/Client.py b/worlds/ff1/Client.py index e775c9f8fd2c..43d1fa536ff8 100644 --- a/worlds/ff1/Client.py +++ b/worlds/ff1/Client.py @@ -98,7 +98,7 @@ async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: # Resetting these in case of switching ROMs self.consumable_stack_amounts = None self.weapons_queue = deque() - self.armor_queue = deque + self.armor_queue = deque() return True From cc7a33f3063b87d0b7796a24b2c53e7fe741c77e Mon Sep 17 00:00:00 2001 From: beauxq Date: Sat, 11 Jan 2025 06:51:16 -0800 Subject: [PATCH 07/13] ff1 client cleaning --- worlds/ff1/Client.py | 48 ++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/worlds/ff1/Client.py b/worlds/ff1/Client.py index 43d1fa536ff8..d48a0ead8159 100644 --- a/worlds/ff1/Client.py +++ b/worlds/ff1/Client.py @@ -1,9 +1,7 @@ import logging from collections import deque -from copy import deepcopy from typing import TYPE_CHECKING -from MultiServer import Client from NetUtils import ClientStatus import worlds._bizhawk as bizhawk @@ -73,7 +71,11 @@ class FF1Client(BizHawkClient): game = "Final Fantasy" system = "NES" - def __init__(self): + 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" @@ -81,7 +83,6 @@ def __init__(self): self.weapons_queue = deque() self.armor_queue = deque() - async def validate_rom(self, ctx: "BizHawkClientContext") -> bool: try: # Check ROM name/patch version @@ -134,21 +135,21 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: # The connector didn't respond. Exit handler and return to main loop to reconnect pass - async def check_status_okay_to_process(self, ctx): - """ + async def check_status_okay_to_process(self, ctx: "BizHawkClientContext") -> bool: + """``` 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) - """ + ```""" status_a = await self.read_sram_value(ctx, 0x102) status_b = await self.read_sram_value(ctx, 0x0FC) status_c = await self.read_sram_value(ctx, 0x0A3) return (status_a != 0x00) and not (status_a == 0xF2 and status_b == 0xF2 and status_c == 0xF2) - async def location_check(self, ctx): + async def location_check(self, ctx: "BizHawkClientContext") -> None: locations_data = await self.read_sram_values(ctx, locations_array_start, locations_array_length) - locations_checked = [] + locations_checked: list[int] = [] if len(locations_data) > 0xFE and locations_data[0xFE] & 0x02 != 0 and not ctx.finished_game: await ctx.send_msgs([ {"cmd": "StatusUpdate", @@ -181,9 +182,8 @@ async def location_check(self, ctx): f'{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [location]}]) - - - async def received_items_check(self, ctx): + async def received_items_check(self, ctx: "BizHawkClientContext") -> None: + assert self.consumable_stack_amounts, "shouldn't call this function without reading consumable_stack_amounts" items_received_count = await self.read_sram_value(ctx, items_obtained) if items_received_count < len(ctx.items_received): current_item = ctx.items_received[items_received_count] @@ -233,8 +233,8 @@ async def received_items_check(self, ctx): self.armor_queue.appendleft(current_item_id - 0x143) await self.write_sram(ctx, items_obtained, items_received_count + 1) - async def process_weapons_queue(self, ctx): - empty_slots = deque() + async def process_weapons_queue(self, ctx: "BizHawkClientContext") -> None: + empty_slots: deque[int] = deque() char1_slots = await self.read_sram_values(ctx, weapons_arrays_starts[0], 4) char2_slots = await self.read_sram_values(ctx, weapons_arrays_starts[1], 4) char3_slots = await self.read_sram_values(ctx, weapons_arrays_starts[2], 4) @@ -256,8 +256,8 @@ async def process_weapons_queue(self, ctx): current_weapon = self.weapons_queue.pop() await self.write_sram(ctx, current_slot, current_weapon) - async def process_armor_queue(self, ctx): - empty_slots = deque() + async def process_armor_queue(self, ctx: "BizHawkClientContext") -> None: + empty_slots: deque[int] = deque() char1_slots = await self.read_sram_values(ctx, armors_arrays_starts[0], 4) char2_slots = await self.read_sram_values(ctx, armors_arrays_starts[1], 4) char3_slots = await self.read_sram_values(ctx, armors_arrays_starts[2], 4) @@ -279,25 +279,25 @@ async def process_armor_queue(self, ctx): current_armor = self.armor_queue.pop() await self.write_sram(ctx, current_slot, current_armor) - async def read_ram_values(self, ctx, location, size): + async def read_ram_values(self, ctx: "BizHawkClientContext", location: int, size: int) -> bytes: return (await bizhawk.read(ctx.bizhawk_ctx, [(location, size, self.wram)]))[0] - async def read_ram_value(self, ctx, location): + async def read_ram_value(self, ctx: "BizHawkClientContext", location: int) -> int: value = ((await bizhawk.read(ctx.bizhawk_ctx, [(location, 1, self.wram)]))[0]) return int.from_bytes(value, "little") - async def read_sram_values(self, ctx, location, size): + async def read_sram_values(self, ctx: "BizHawkClientContext", location: int, size: int) -> bytes: return (await bizhawk.read(ctx.bizhawk_ctx, [(location, size, self.sram)]))[0] - async def read_sram_value(self, ctx, location): + async def read_sram_value(self, ctx: "BizHawkClientContext", location: int) -> int: value = ((await bizhawk.read(ctx.bizhawk_ctx, [(location, 1, self.sram)]))[0]) return int.from_bytes(value, "little") - async def read_rom(self, ctx, location, size): + async def read_rom(self, ctx: "BizHawkClientContext", location: int, size: int) -> bytes: return (await bizhawk.read(ctx.bizhawk_ctx, [(location, size, self.rom)]))[0] - async def write(self, ctx, location, value): + async def write(self, ctx: "BizHawkClientContext", location: int, value: int) -> None: return await bizhawk.write(ctx.bizhawk_ctx, [(location, [value], self.wram)]) - async def write_sram(self, ctx, location, value): - return await bizhawk.write(ctx.bizhawk_ctx, [(location, [value], self.sram)]) \ No newline at end of file + async def write_sram(self, ctx: "BizHawkClientContext", location: int, value: int) -> None: + return await bizhawk.write(ctx.bizhawk_ctx, [(location, [value], self.sram)]) From c89672a5fe0e0f577fc877dd056a09f46c5cc21e Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Sat, 8 Feb 2025 13:01:46 -0500 Subject: [PATCH 08/13] Changed client to use guarded reads/writes. Fixed a bug with extended consumables being switched. --- worlds/ff1/Client.py | 162 +++++++++++++++++++++++++------------------ 1 file changed, 96 insertions(+), 66 deletions(-) diff --git a/worlds/ff1/Client.py b/worlds/ff1/Client.py index d48a0ead8159..62e5bcbc0e0a 100644 --- a/worlds/ff1/Client.py +++ b/worlds/ff1/Client.py @@ -1,7 +1,9 @@ import logging from collections import deque -from typing import TYPE_CHECKING +from copy import deepcopy +from typing import TYPE_CHECKING, List +from MultiServer import Client from NetUtils import ClientStatus import worlds._bizhawk as bizhawk @@ -24,6 +26,9 @@ 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"] @@ -51,13 +56,13 @@ "Gold10000", "Gold12350", "Gold13000", "Gold13450", "Gold14050", "Gold14720", "Gold15000", "Gold17490", "Gold18010", "Gold19990", "Gold20000", "Gold20010", "Gold26000", "Gold45000", "Gold65000"] -extended_consumables = ["Smoke", "FullCure", "Blast", "Phoenix", - "Flare", "Black", "Refresh", "Guard", - "Wizard", "HighPotion", "Cloak", "Quick"] +extended_consumables = ["FullCure", "Phoenix", "Blast", "Smoke", + "Refresh", "Flare", "Black", "Guard", + "Quick", "HighPotion", "Wizard", "Cloak"] -ext_consumables_lookup = {"Smoke": "Ext1", "FullCure": "Ext2", "Blast": "Ext3", "Phoenix": "Ext4", - "Flare": "Ext1", "Black": "Ext2", "Refresh": "Ext3", "Guard": "Ext4", - "Wizard": "Ext1", "HighPotion": "Ext2", "Cloak": "Ext3", "Quick": "Ext4"} +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} @@ -82,6 +87,7 @@ def __init__(self) -> None: 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: @@ -136,20 +142,21 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: pass async def check_status_okay_to_process(self, ctx: "BizHawkClientContext") -> bool: - """``` - 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) - ```""" - status_a = await self.read_sram_value(ctx, 0x102) - status_b = await self.read_sram_value(ctx, 0x0FC) - status_c = await self.read_sram_value(ctx, 0x0A3) + status_a = await self.read_sram_value(ctx, status_a_location) + status_b = await self.read_sram_value(ctx, status_b_location) + status_c = await self.read_sram_value(ctx, status_c_location) + + # First character's name's first character will never have FF + # so this will cause all guarded read/writes to fail properly + self.guard_character = status_a if status_a != 0x00 else 0xFF + return (status_a != 0x00) and not (status_a == 0xF2 and status_b == 0xF2 and status_c == 0xF2) - async def location_check(self, ctx: "BizHawkClientContext") -> None: - locations_data = await self.read_sram_values(ctx, locations_array_start, locations_array_length) - locations_checked: list[int] = [] + 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", @@ -167,9 +174,6 @@ async def location_check(self, ctx: "BizHawkClientContext") -> None: # 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_data[index] & flag != 0: locations_checked.append(location) @@ -185,60 +189,73 @@ async def location_check(self, ctx: "BizHawkClientContext") -> None: async def received_items_check(self, ctx: "BizHawkClientContext") -> None: assert self.consumable_stack_amounts, "shouldn't call this function without reading consumable_stack_amounts" items_received_count = await self.read_sram_value(ctx, items_obtained) + 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 - await self.write_sram(ctx, location, 1) + write_list.append((location, [1], self.sram)) elif current_item_name in movement_items: location = current_item_id - 0x1E0 if current_item_name != "Canal": - await self.write_sram(ctx, location, 1) + write_list.append((location, [1], self.sram)) else: - await self.write_sram(ctx, location, 0) + 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 - await self.write_sram(ctx, location, 1) + write_list.append((location, [1], self.sram)) elif current_item_name in gold_items: gold_amount = int(current_item_name[4:]) - current_gold = int.from_bytes(await self.read_sram_values(ctx, gp_location_low, 3), "little") + current_gold = int.from_bytes(await self.read_sram_values_guarded(ctx, gp_location_low, 3), "little") + if current_gold is None: + return 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) - await self.write_sram(ctx, gp_location_low, lower_byte) - await self.write_sram(ctx, gp_location_middle, middle_byte) - await self.write_sram(ctx, gp_location_high, upper_byte) + 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(ctx, location) + 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) - await self.write_sram(ctx, location, new_value) + 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(ctx, location) + 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) - await self.write_sram(ctx, location, new_value) + 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) - await self.write_sram(ctx, items_obtained, items_received_count + 1) - - async def process_weapons_queue(self, ctx: "BizHawkClientContext") -> None: - empty_slots: deque[int] = deque() - char1_slots = await self.read_sram_values(ctx, weapons_arrays_starts[0], 4) - char2_slots = await self.read_sram_values(ctx, weapons_arrays_starts[1], 4) - char3_slots = await self.read_sram_values(ctx, weapons_arrays_starts[2], 4) - char4_slots = await self.read_sram_values(ctx, weapons_arrays_starts[3], 4) + write_list.append((items_obtained, [items_received_count + 1], self.sram)) + await self.write_sram_values_guarded(ctx, write_list) + + 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) @@ -254,14 +271,16 @@ async def process_weapons_queue(self, ctx: "BizHawkClientContext") -> None: 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(ctx, current_slot, current_weapon) - - async def process_armor_queue(self, ctx: "BizHawkClientContext") -> None: - empty_slots: deque[int] = deque() - char1_slots = await self.read_sram_values(ctx, armors_arrays_starts[0], 4) - char2_slots = await self.read_sram_values(ctx, armors_arrays_starts[1], 4) - char3_slots = await self.read_sram_values(ctx, armors_arrays_starts[2], 4) - char4_slots = await self.read_sram_values(ctx, armors_arrays_starts[3], 4) + 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) @@ -277,27 +296,38 @@ async def process_armor_queue(self, ctx: "BizHawkClientContext") -> None: 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(ctx, current_slot, current_armor) - - async def read_ram_values(self, ctx: "BizHawkClientContext", location: int, size: int) -> bytes: - return (await bizhawk.read(ctx.bizhawk_ctx, [(location, size, self.wram)]))[0] - - async def read_ram_value(self, ctx: "BizHawkClientContext", location: int) -> int: - value = ((await bizhawk.read(ctx.bizhawk_ctx, [(location, 1, self.wram)]))[0]) - return int.from_bytes(value, "little") + await self.write_sram_guarded(ctx, current_slot, current_armor) - async def read_sram_values(self, ctx: "BizHawkClientContext", location: int, size: int) -> bytes: - return (await bizhawk.read(ctx.bizhawk_ctx, [(location, size, self.sram)]))[0] - async def read_sram_value(self, ctx: "BizHawkClientContext", location: int) -> int: + 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_rom(self, ctx: "BizHawkClientContext", location: int, size: int) -> bytes: + 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(self, ctx: "BizHawkClientContext", location: int, value: int) -> None: - return await bizhawk.write(ctx.bizhawk_ctx, [(location, [value], self.wram)]) + 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(self, ctx: "BizHawkClientContext", location: int, value: int) -> None: - return await bizhawk.write(ctx.bizhawk_ctx, [(location, [value], 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)]) From bb5e7d46c772a5dcbac9151713421b0964420c54 Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Sat, 8 Feb 2025 17:06:36 -0500 Subject: [PATCH 09/13] Updated in accordance with review comments. --- worlds/ff1/Client.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/worlds/ff1/Client.py b/worlds/ff1/Client.py index 62e5bcbc0e0a..74949068d832 100644 --- a/worlds/ff1/Client.py +++ b/worlds/ff1/Client.py @@ -1,9 +1,7 @@ import logging from collections import deque -from copy import deepcopy -from typing import TYPE_CHECKING, List +from typing import TYPE_CHECKING -from MultiServer import Client from NetUtils import ClientStatus import worlds._bizhawk as bizhawk @@ -146,9 +144,7 @@ async def check_status_okay_to_process(self, ctx: "BizHawkClientContext") -> boo status_b = await self.read_sram_value(ctx, status_b_location) status_c = await self.read_sram_value(ctx, status_c_location) - # First character's name's first character will never have FF - # so this will cause all guarded read/writes to fail properly - self.guard_character = status_a if status_a != 0x00 else 0xFF + self.guard_character = status_a return (status_a != 0x00) and not (status_a == 0xF2 and status_b == 0xF2 and status_c == 0xF2) @@ -189,7 +185,7 @@ async def location_check(self, ctx: "BizHawkClientContext"): async def received_items_check(self, ctx: "BizHawkClientContext") -> None: assert self.consumable_stack_amounts, "shouldn't call this function without reading consumable_stack_amounts" items_received_count = await self.read_sram_value(ctx, items_obtained) - write_list: List[tuple[int, List[int], str]] = [] + 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 From bfe91255b6e10af1af592c4db4b0388ad8256c60 Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Sat, 8 Feb 2025 17:21:37 -0500 Subject: [PATCH 10/13] Remove old connector lua on install. --- inno_setup.iss | 1 + 1 file changed, 1 insertion(+) diff --git a/inno_setup.iss b/inno_setup.iss index 45c7b907bfb1..736b659e21a3 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -88,6 +88,7 @@ Type: files; Name: "{app}\ArchipelagoLttPClient.exe" Type: files; Name: "{app}\ArchipelagoPokemonClient.exe" type: files; Name: "{app}\ArchipelagoFF1Client.exe" Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua" +Type: files; Name: "{app}\data\lua\connector_ff1.lua" Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy" Type: dirifempty; Name: "{app}\lib\worlds\rogue-legacy" Type: files; Name: "{app}\lib\worlds\sc2wol.apworld" From 315ea54abe9033d06f7592fa1edf3894b41358e7 Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Sun, 9 Feb 2025 12:49:17 -0500 Subject: [PATCH 11/13] Updated documentation and cleaned up unneeded client code. Also implemented item received messages in Bizhawk. --- worlds/ff1/Client.py | 54 ++++++++++++++------------------ worlds/ff1/docs/multiworld_en.md | 9 +++--- 2 files changed, 28 insertions(+), 35 deletions(-) diff --git a/worlds/ff1/Client.py b/worlds/ff1/Client.py index 74949068d832..ba3c7414a62d 100644 --- a/worlds/ff1/Client.py +++ b/worlds/ff1/Client.py @@ -114,40 +114,32 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> None: if ctx.slot is None: return try: - if await self.check_status_okay_to_process(ctx): - 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) + self.guard_character = await self.read_sram_value(ctx, status_a_location) + + 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 check_status_okay_to_process(self, ctx: "BizHawkClientContext") -> bool: - status_a = await self.read_sram_value(ctx, status_a_location) - status_b = await self.read_sram_value(ctx, status_b_location) - status_c = await self.read_sram_value(ctx, status_c_location) - - self.guard_character = status_a - - return (status_a != 0x00) and not (status_a == 0xF2 and status_b == 0xF2 and status_c == 0xF2) - 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: @@ -242,7 +234,9 @@ async def received_items_check(self, ctx: "BizHawkClientContext") -> None: 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)) - await self.write_sram_values_guarded(ctx, write_list) + 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() diff --git a/worlds/ff1/docs/multiworld_en.md b/worlds/ff1/docs/multiworld_en.md index 431d08403297..1f1147bb31b5 100644 --- a/worlds/ff1/docs/multiworld_en.md +++ b/worlds/ff1/docs/multiworld_en.md @@ -2,11 +2,10 @@ ## Required Software -- Bizhawk: [Bizhawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) - - Version 2.9.1 is recommended. - - Detailed installation instructions for Bizhawk can be found at the above link. +- 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) +- 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. @@ -59,7 +58,7 @@ Once the Archipelago server has been hosted: extension `*.nes` 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_ff1.lua` with the file picker. +and navigate to `connector_bizhawk_generic.lua` with the file picker. ## Play the game From 60aeb9139f13d3762ef7cdec9e05aae485d56600 Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Sun, 9 Feb 2025 16:51:52 -0500 Subject: [PATCH 12/13] Updated to latest main to use new check_locations function. Fixed a bug with the guard against writing on title screen. Removed redundant read. --- worlds/ff1/Client.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/worlds/ff1/Client.py b/worlds/ff1/Client.py index ba3c7414a62d..72f9a1e75d70 100644 --- a/worlds/ff1/Client.py +++ b/worlds/ff1/Client.py @@ -115,6 +115,11 @@ async def game_watcher(self, ctx: "BizHawkClientContext") -> 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 = {} @@ -165,18 +170,17 @@ async def location_check(self, ctx: "BizHawkClientContext"): if locations_data[index] & flag != 0: locations_checked.append(location) - for location in locations_checked: - if location not in ctx.locations_checked: - 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)})') - await ctx.send_msgs([{"cmd": "LocationChecks", "locations": [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" - items_received_count = await self.read_sram_value(ctx, items_obtained) 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: From 58359da4e6d87a9232cee9dcfc3d1de8be91780a Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Sun, 9 Feb 2025 19:49:38 -0500 Subject: [PATCH 13/13] Current gold is now checked for properly. --- worlds/ff1/Client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/ff1/Client.py b/worlds/ff1/Client.py index 72f9a1e75d70..f7315f69f0ad 100644 --- a/worlds/ff1/Client.py +++ b/worlds/ff1/Client.py @@ -206,9 +206,10 @@ async def received_items_check(self, ctx: "BizHawkClientContext") -> None: write_list.append((location, [1], self.sram)) elif current_item_name in gold_items: gold_amount = int(current_item_name[4:]) - current_gold = int.from_bytes(await self.read_sram_values_guarded(ctx, gp_location_low, 3), "little") - if current_gold is None: + 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)