From 3d1be0c468d0717fb9781886b006b498fa7751a4 Mon Sep 17 00:00:00 2001 From: wildham <64616385+wildham0@users.noreply.github.com> Date: Mon, 1 Jan 2024 12:13:35 -0500 Subject: [PATCH 1/6] FF1: Fix terminated_event access_rule not getting set (#2648) --- worlds/ff1/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index 16905cc6da0c..4ff361c07243 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -74,6 +74,7 @@ def create_regions(self): items = get_options(self.multiworld, 'items', self.player) goal_rule = generate_rule([[name for name in items.keys() if name in FF1_PROGRESSION_LIST and name != "Shard"]], self.player) + terminated_event.access_rule = goal_rule if "Shard" in items.keys(): def goal_rule_and_shards(state): return goal_rule(state) and state.has("Shard", self.player, 32) From c104e81145d6a5e89a25fb90c402abe2585ce007 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Mon, 1 Jan 2024 11:42:41 -0800 Subject: [PATCH 2/6] Zillion: move client to worlds/zillion (#2649) --- ZillionClient.py | 505 +-------------------------------------- worlds/zillion/client.py | 501 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 506 insertions(+), 500 deletions(-) create mode 100644 worlds/zillion/client.py diff --git a/ZillionClient.py b/ZillionClient.py index 5f3cbb943faa..ef96edab045e 100644 --- a/ZillionClient.py +++ b/ZillionClient.py @@ -1,505 +1,10 @@ -import asyncio -import base64 -import platform -from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast +import ModuleUpdate +ModuleUpdate.update() -# CommonClient import first to trigger ModuleUpdater -from CommonClient import CommonContext, server_loop, gui_enabled, \ - ClientCommandProcessor, logger, get_base_parser -from NetUtils import ClientStatus -import Utils -from Utils import async_start - -import colorama - -from zilliandomizer.zri.memory import Memory -from zilliandomizer.zri import events -from zilliandomizer.utils.loc_name_maps import id_to_loc -from zilliandomizer.options import Chars -from zilliandomizer.patch import RescueInfo - -from worlds.zillion.id_maps import make_id_to_others -from worlds.zillion.config import base_id, zillion_map - - -class ZillionCommandProcessor(ClientCommandProcessor): - ctx: "ZillionContext" - - def _cmd_sms(self) -> None: - """ Tell the client that Zillion is running in RetroArch. """ - logger.info("ready to look for game") - self.ctx.look_for_retroarch.set() - - def _cmd_map(self) -> None: - """ Toggle view of the map tracker. """ - self.ctx.ui_toggle_map() - - -class ToggleCallback(Protocol): - def __call__(self) -> None: ... - - -class SetRoomCallback(Protocol): - def __call__(self, rooms: List[List[int]]) -> None: ... - - -class ZillionContext(CommonContext): - game = "Zillion" - command_processor = ZillionCommandProcessor - items_handling = 1 # receive items from other players - - known_name: Optional[str] - """ This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """ - - from_game: "asyncio.Queue[events.EventFromGame]" - to_game: "asyncio.Queue[events.EventToGame]" - ap_local_count: int - """ local checks watched by server """ - next_item: int - """ index in `items_received` """ - ap_id_to_name: Dict[int, str] - ap_id_to_zz_id: Dict[int, int] - start_char: Chars = "JJ" - rescues: Dict[int, RescueInfo] = {} - loc_mem_to_id: Dict[int, int] = {} - got_room_info: asyncio.Event - """ flag for connected to server """ - got_slot_data: asyncio.Event - """ serves as a flag for whether I am logged in to the server """ - - look_for_retroarch: asyncio.Event - """ - There is a bug in Python in Windows - https://github.com/python/cpython/issues/91227 - that makes it so if I look for RetroArch before it's ready, - it breaks the asyncio udp transport system. - - As a workaround, we don't look for RetroArch until this event is set. - """ - - ui_toggle_map: ToggleCallback - ui_set_rooms: SetRoomCallback - """ parameter is y 16 x 8 numbers to show in each room """ - - def __init__(self, - server_address: str, - password: str) -> None: - super().__init__(server_address, password) - self.known_name = None - self.from_game = asyncio.Queue() - self.to_game = asyncio.Queue() - self.got_room_info = asyncio.Event() - self.got_slot_data = asyncio.Event() - self.ui_toggle_map = lambda: None - self.ui_set_rooms = lambda rooms: None - - self.look_for_retroarch = asyncio.Event() - if platform.system() != "Windows": - # asyncio udp bug is only on Windows - self.look_for_retroarch.set() - - self.reset_game_state() - - def reset_game_state(self) -> None: - for _ in range(self.from_game.qsize()): - self.from_game.get_nowait() - for _ in range(self.to_game.qsize()): - self.to_game.get_nowait() - self.got_slot_data.clear() - - self.ap_local_count = 0 - self.next_item = 0 - self.ap_id_to_name = {} - self.ap_id_to_zz_id = {} - self.rescues = {} - self.loc_mem_to_id = {} - - self.locations_checked.clear() - self.missing_locations.clear() - self.checked_locations.clear() - self.finished_game = False - self.items_received.clear() - - # override - def on_deathlink(self, data: Dict[str, Any]) -> None: - self.to_game.put_nowait(events.DeathEventToGame()) - return super().on_deathlink(data) - - # override - async def server_auth(self, password_requested: bool = False) -> None: - if password_requested and not self.password: - await super().server_auth(password_requested) - if not self.auth: - logger.info('waiting for connection to game...') - return - logger.info("logging in to server...") - await self.send_connect() - - # override - def run_gui(self) -> None: - from kvui import GameManager - from kivy.core.text import Label as CoreLabel - from kivy.graphics import Ellipse, Color, Rectangle - from kivy.uix.layout import Layout - from kivy.uix.widget import Widget - - class ZillionManager(GameManager): - logging_pairs = [ - ("Client", "Archipelago") - ] - base_title = "Archipelago Zillion Client" - - class MapPanel(Widget): - MAP_WIDTH: ClassVar[int] = 281 - - _number_textures: List[Any] = [] - rooms: List[List[int]] = [] - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - - self.rooms = [[0 for _ in range(8)] for _ in range(16)] - - self._make_numbers() - self.update_map() - - self.bind(pos=self.update_map) - # self.bind(size=self.update_bg) - - def _make_numbers(self) -> None: - self._number_textures = [] - for n in range(10): - label = CoreLabel(text=str(n), font_size=22, color=(0.1, 0.9, 0, 1)) - label.refresh() - self._number_textures.append(label.texture) - - def update_map(self, *args: Any) -> None: - self.canvas.clear() - - with self.canvas: - Color(1, 1, 1, 1) - Rectangle(source=zillion_map, - pos=self.pos, - size=(ZillionManager.MapPanel.MAP_WIDTH, - int(ZillionManager.MapPanel.MAP_WIDTH * 1.456))) # aspect ratio of that image - for y in range(16): - for x in range(8): - num = self.rooms[15 - y][x] - if num > 0: - Color(0, 0, 0, 0.4) - pos = [self.pos[0] + 17 + x * 32, self.pos[1] + 14 + y * 24] - Ellipse(size=[22, 22], pos=pos) - Color(1, 1, 1, 1) - pos = [self.pos[0] + 22 + x * 32, self.pos[1] + 12 + y * 24] - num_texture = self._number_textures[num] - Rectangle(texture=num_texture, size=num_texture.size, pos=pos) - - def build(self) -> Layout: - container = super().build() - self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=0) - self.main_area_container.add_widget(self.map_widget) - return container - - def toggle_map_width(self) -> None: - if self.map_widget.width == 0: - self.map_widget.width = ZillionManager.MapPanel.MAP_WIDTH - else: - self.map_widget.width = 0 - self.container.do_layout() - - def set_rooms(self, rooms: List[List[int]]) -> None: - self.map_widget.rooms = rooms - self.map_widget.update_map() - - self.ui = ZillionManager(self) - self.ui_toggle_map = lambda: self.ui.toggle_map_width() - self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms) - run_co: Coroutine[Any, Any, None] = self.ui.async_run() - self.ui_task = asyncio.create_task(run_co, name="UI") - - def on_package(self, cmd: str, args: Dict[str, Any]) -> None: - self.room_item_numbers_to_ui() - if cmd == "Connected": - logger.info("logged in to Archipelago server") - if "slot_data" not in args: - logger.warn("`Connected` packet missing `slot_data`") - return - slot_data = args["slot_data"] - - if "start_char" not in slot_data: - logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`") - return - self.start_char = slot_data['start_char'] - if self.start_char not in {"Apple", "Champ", "JJ"}: - logger.warn("invalid Zillion `Connected` packet, " - f"`slot_data` `start_char` has invalid value: {self.start_char}") - - if "rescues" not in slot_data: - logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`") - return - rescues = slot_data["rescues"] - self.rescues = {} - for rescue_id, json_info in rescues.items(): - assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}" - # TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch? - assert json_info["start_char"] == self.start_char, \ - f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}' - ri = RescueInfo(json_info["start_char"], - json_info["room_code"], - json_info["mask"]) - self.rescues[0 if rescue_id == "0" else 1] = ri - - if "loc_mem_to_id" not in slot_data: - logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`") - return - loc_mem_to_id = slot_data["loc_mem_to_id"] - self.loc_mem_to_id = {} - for mem_str, id_str in loc_mem_to_id.items(): - mem = int(mem_str) - id_ = int(id_str) - room_i = mem // 256 - assert 0 <= room_i < 74 - assert id_ in id_to_loc - self.loc_mem_to_id[mem] = id_ - - if len(self.loc_mem_to_id) != 394: - logger.warn("invalid Zillion `Connected` packet, " - f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}") - - self.got_slot_data.set() - - payload = { - "cmd": "Get", - "keys": [f"zillion-{self.auth}-doors"] - } - async_start(self.send_msgs([payload])) - elif cmd == "Retrieved": - if "keys" not in args: - logger.warning(f"invalid Retrieved packet to ZillionClient: {args}") - return - keys = cast(Dict[str, Optional[str]], args["keys"]) - doors_b64 = keys.get(f"zillion-{self.auth}-doors", None) - if doors_b64: - logger.info("received door data from server") - doors = base64.b64decode(doors_b64) - self.to_game.put_nowait(events.DoorEventToGame(doors)) - elif cmd == "RoomInfo": - self.seed_name = args["seed_name"] - self.got_room_info.set() - - def room_item_numbers_to_ui(self) -> None: - rooms = [[0 for _ in range(8)] for _ in range(16)] - for loc_id in self.missing_locations: - loc_id_small = loc_id - base_id - loc_name = id_to_loc[loc_id_small] - y = ord(loc_name[0]) - 65 - x = ord(loc_name[2]) - 49 - if y == 9 and x == 5: - # don't show main computer in numbers - continue - assert (0 <= y < 16) and (0 <= x < 8), f"invalid index from location name {loc_name}" - rooms[y][x] += 1 - # TODO: also add locations with locals lost from loading save state or reset - self.ui_set_rooms(rooms) - - def process_from_game_queue(self) -> None: - if self.from_game.qsize(): - event_from_game = self.from_game.get_nowait() - if isinstance(event_from_game, events.AcquireLocationEventFromGame): - server_id = event_from_game.id + base_id - loc_name = id_to_loc[event_from_game.id] - self.locations_checked.add(server_id) - if server_id in self.missing_locations: - self.ap_local_count += 1 - n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win - logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})') - async_start(self.send_msgs([ - {"cmd": 'LocationChecks', "locations": [server_id]} - ])) - else: - # This will happen a lot in Zillion, - # because all the key words are local and unwatched by the server. - logger.debug(f"DEBUG: {loc_name} not in missing") - elif isinstance(event_from_game, events.DeathEventFromGame): - async_start(self.send_death()) - elif isinstance(event_from_game, events.WinEventFromGame): - if not self.finished_game: - async_start(self.send_msgs([ - {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL} - ])) - self.finished_game = True - elif isinstance(event_from_game, events.DoorEventFromGame): - if self.auth: - doors_b64 = base64.b64encode(event_from_game.doors).decode() - payload = { - "cmd": "Set", - "key": f"zillion-{self.auth}-doors", - "operations": [{"operation": "replace", "value": doors_b64}] - } - async_start(self.send_msgs([payload])) - else: - logger.warning(f"WARNING: unhandled event from game {event_from_game}") - - def process_items_received(self) -> None: - if len(self.items_received) > self.next_item: - zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received] - for index in range(self.next_item, len(self.items_received)): - ap_id = self.items_received[index].item - from_name = self.player_names[self.items_received[index].player] - # TODO: colors in this text, like sni client? - logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}') - self.to_game.put_nowait( - events.ItemEventToGame(zz_item_ids) - ) - self.next_item = len(self.items_received) - - -def name_seed_from_ram(data: bytes) -> Tuple[str, str]: - """ returns player name, and end of seed string """ - if len(data) == 0: - # no connection to game - return "", "xxx" - null_index = data.find(b'\x00') - if null_index == -1: - logger.warning(f"invalid game id in rom {repr(data)}") - null_index = len(data) - name = data[:null_index].decode() - null_index_2 = data.find(b'\x00', null_index + 1) - if null_index_2 == -1: - null_index_2 = len(data) - seed_name = data[null_index + 1:null_index_2].decode() - - return name, seed_name - - -async def zillion_sync_task(ctx: ZillionContext) -> None: - logger.info("started zillion sync task") - - # to work around the Python bug where we can't check for RetroArch - if not ctx.look_for_retroarch.is_set(): - logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.") - await asyncio.wait(( - asyncio.create_task(ctx.look_for_retroarch.wait()), - asyncio.create_task(ctx.exit_event.wait()) - ), return_when=asyncio.FIRST_COMPLETED) - - last_log = "" - - def log_no_spam(msg: str) -> None: - nonlocal last_log - if msg != last_log: - last_log = msg - logger.info(msg) - - # to only show this message once per client run - help_message_shown = False - - with Memory(ctx.from_game, ctx.to_game) as memory: - while not ctx.exit_event.is_set(): - ram = await memory.read() - game_id = memory.get_rom_to_ram_data(ram) - name, seed_end = name_seed_from_ram(game_id) - if len(name): - if name == ctx.known_name: - ctx.auth = name - # this is the name we know - if ctx.server and ctx.server.socket: # type: ignore - if ctx.got_room_info.is_set(): - if ctx.seed_name and ctx.seed_name.endswith(seed_end): - # correct seed - if memory.have_generation_info(): - log_no_spam("everything connected") - await memory.process_ram(ram) - ctx.process_from_game_queue() - ctx.process_items_received() - else: # no generation info - if ctx.got_slot_data.is_set(): - memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id) - ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \ - make_id_to_others(ctx.start_char) - ctx.next_item = 0 - ctx.ap_local_count = len(ctx.checked_locations) - else: # no slot data yet - async_start(ctx.send_connect()) - log_no_spam("logging in to server...") - await asyncio.wait(( - asyncio.create_task(ctx.got_slot_data.wait()), - asyncio.create_task(ctx.exit_event.wait()), - asyncio.create_task(asyncio.sleep(6)) - ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets - else: # not correct seed name - log_no_spam("incorrect seed - did you mix up roms?") - else: # no room info - # If we get here, it looks like `RoomInfo` packet got lost - log_no_spam("waiting for room info from server...") - else: # server not connected - log_no_spam("waiting for server connection...") - else: # new game - log_no_spam("connected to new game") - await ctx.disconnect() - ctx.reset_server_state() - ctx.seed_name = None - ctx.got_room_info.clear() - ctx.reset_game_state() - memory.reset_game_state() - - ctx.auth = name - ctx.known_name = name - async_start(ctx.connect()) - await asyncio.wait(( - asyncio.create_task(ctx.got_room_info.wait()), - asyncio.create_task(ctx.exit_event.wait()), - asyncio.create_task(asyncio.sleep(6)) - ), return_when=asyncio.FIRST_COMPLETED) - else: # no name found in game - if not help_message_shown: - logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.') - help_message_shown = True - log_no_spam("looking for connection to game...") - await asyncio.sleep(0.3) - - await asyncio.sleep(0.09375) - logger.info("zillion sync task ending") - - -async def main() -> None: - parser = get_base_parser() - parser.add_argument('diff_file', default="", type=str, nargs="?", - help='Path to a .apzl Archipelago Binary Patch file') - # SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) - args = parser.parse_args() - print(args) - - if args.diff_file: - import Patch - logger.info("patch file was supplied - creating sms rom...") - meta, rom_file = Patch.create_rom_file(args.diff_file) - if "server" in meta: - args.connect = meta["server"] - logger.info(f"wrote rom file to {rom_file}") - - ctx = ZillionContext(args.connect, args.password) - if ctx.server_task is None: - ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") - - if gui_enabled: - ctx.run_gui() - ctx.run_cli() - - sync_task = asyncio.create_task(zillion_sync_task(ctx)) - - await ctx.exit_event.wait() - - ctx.server_address = None - logger.debug("waiting for sync task to end") - await sync_task - logger.debug("sync task ended") - await ctx.shutdown() +import Utils # noqa: E402 +from worlds.zillion.client import launch # noqa: E402 if __name__ == "__main__": Utils.init_logging("ZillionClient", exception_logger="Client") - - colorama.init() - asyncio.run(main()) - colorama.deinit() + launch() diff --git a/worlds/zillion/client.py b/worlds/zillion/client.py new file mode 100644 index 000000000000..ac73f6db50c8 --- /dev/null +++ b/worlds/zillion/client.py @@ -0,0 +1,501 @@ +import asyncio +import base64 +import platform +from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast + +from CommonClient import CommonContext, server_loop, gui_enabled, \ + ClientCommandProcessor, logger, get_base_parser +from NetUtils import ClientStatus +from Utils import async_start + +import colorama + +from zilliandomizer.zri.memory import Memory +from zilliandomizer.zri import events +from zilliandomizer.utils.loc_name_maps import id_to_loc +from zilliandomizer.options import Chars +from zilliandomizer.patch import RescueInfo + +from .id_maps import make_id_to_others +from .config import base_id, zillion_map + + +class ZillionCommandProcessor(ClientCommandProcessor): + ctx: "ZillionContext" + + def _cmd_sms(self) -> None: + """ Tell the client that Zillion is running in RetroArch. """ + logger.info("ready to look for game") + self.ctx.look_for_retroarch.set() + + def _cmd_map(self) -> None: + """ Toggle view of the map tracker. """ + self.ctx.ui_toggle_map() + + +class ToggleCallback(Protocol): + def __call__(self) -> None: ... + + +class SetRoomCallback(Protocol): + def __call__(self, rooms: List[List[int]]) -> None: ... + + +class ZillionContext(CommonContext): + game = "Zillion" + command_processor = ZillionCommandProcessor + items_handling = 1 # receive items from other players + + known_name: Optional[str] + """ This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """ + + from_game: "asyncio.Queue[events.EventFromGame]" + to_game: "asyncio.Queue[events.EventToGame]" + ap_local_count: int + """ local checks watched by server """ + next_item: int + """ index in `items_received` """ + ap_id_to_name: Dict[int, str] + ap_id_to_zz_id: Dict[int, int] + start_char: Chars = "JJ" + rescues: Dict[int, RescueInfo] = {} + loc_mem_to_id: Dict[int, int] = {} + got_room_info: asyncio.Event + """ flag for connected to server """ + got_slot_data: asyncio.Event + """ serves as a flag for whether I am logged in to the server """ + + look_for_retroarch: asyncio.Event + """ + There is a bug in Python in Windows + https://github.com/python/cpython/issues/91227 + that makes it so if I look for RetroArch before it's ready, + it breaks the asyncio udp transport system. + + As a workaround, we don't look for RetroArch until this event is set. + """ + + ui_toggle_map: ToggleCallback + ui_set_rooms: SetRoomCallback + """ parameter is y 16 x 8 numbers to show in each room """ + + def __init__(self, + server_address: str, + password: str) -> None: + super().__init__(server_address, password) + self.known_name = None + self.from_game = asyncio.Queue() + self.to_game = asyncio.Queue() + self.got_room_info = asyncio.Event() + self.got_slot_data = asyncio.Event() + self.ui_toggle_map = lambda: None + self.ui_set_rooms = lambda rooms: None + + self.look_for_retroarch = asyncio.Event() + if platform.system() != "Windows": + # asyncio udp bug is only on Windows + self.look_for_retroarch.set() + + self.reset_game_state() + + def reset_game_state(self) -> None: + for _ in range(self.from_game.qsize()): + self.from_game.get_nowait() + for _ in range(self.to_game.qsize()): + self.to_game.get_nowait() + self.got_slot_data.clear() + + self.ap_local_count = 0 + self.next_item = 0 + self.ap_id_to_name = {} + self.ap_id_to_zz_id = {} + self.rescues = {} + self.loc_mem_to_id = {} + + self.locations_checked.clear() + self.missing_locations.clear() + self.checked_locations.clear() + self.finished_game = False + self.items_received.clear() + + # override + def on_deathlink(self, data: Dict[str, Any]) -> None: + self.to_game.put_nowait(events.DeathEventToGame()) + return super().on_deathlink(data) + + # override + async def server_auth(self, password_requested: bool = False) -> None: + if password_requested and not self.password: + await super().server_auth(password_requested) + if not self.auth: + logger.info('waiting for connection to game...') + return + logger.info("logging in to server...") + await self.send_connect() + + # override + def run_gui(self) -> None: + from kvui import GameManager + from kivy.core.text import Label as CoreLabel + from kivy.graphics import Ellipse, Color, Rectangle + from kivy.uix.layout import Layout + from kivy.uix.widget import Widget + + class ZillionManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago Zillion Client" + + class MapPanel(Widget): + MAP_WIDTH: ClassVar[int] = 281 + + _number_textures: List[Any] = [] + rooms: List[List[int]] = [] + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + self.rooms = [[0 for _ in range(8)] for _ in range(16)] + + self._make_numbers() + self.update_map() + + self.bind(pos=self.update_map) + # self.bind(size=self.update_bg) + + def _make_numbers(self) -> None: + self._number_textures = [] + for n in range(10): + label = CoreLabel(text=str(n), font_size=22, color=(0.1, 0.9, 0, 1)) + label.refresh() + self._number_textures.append(label.texture) + + def update_map(self, *args: Any) -> None: + self.canvas.clear() + + with self.canvas: + Color(1, 1, 1, 1) + Rectangle(source=zillion_map, + pos=self.pos, + size=(ZillionManager.MapPanel.MAP_WIDTH, + int(ZillionManager.MapPanel.MAP_WIDTH * 1.456))) # aspect ratio of that image + for y in range(16): + for x in range(8): + num = self.rooms[15 - y][x] + if num > 0: + Color(0, 0, 0, 0.4) + pos = [self.pos[0] + 17 + x * 32, self.pos[1] + 14 + y * 24] + Ellipse(size=[22, 22], pos=pos) + Color(1, 1, 1, 1) + pos = [self.pos[0] + 22 + x * 32, self.pos[1] + 12 + y * 24] + num_texture = self._number_textures[num] + Rectangle(texture=num_texture, size=num_texture.size, pos=pos) + + def build(self) -> Layout: + container = super().build() + self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=0) + self.main_area_container.add_widget(self.map_widget) + return container + + def toggle_map_width(self) -> None: + if self.map_widget.width == 0: + self.map_widget.width = ZillionManager.MapPanel.MAP_WIDTH + else: + self.map_widget.width = 0 + self.container.do_layout() + + def set_rooms(self, rooms: List[List[int]]) -> None: + self.map_widget.rooms = rooms + self.map_widget.update_map() + + self.ui = ZillionManager(self) + self.ui_toggle_map = lambda: self.ui.toggle_map_width() + self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms) + run_co: Coroutine[Any, Any, None] = self.ui.async_run() + self.ui_task = asyncio.create_task(run_co, name="UI") + + def on_package(self, cmd: str, args: Dict[str, Any]) -> None: + self.room_item_numbers_to_ui() + if cmd == "Connected": + logger.info("logged in to Archipelago server") + if "slot_data" not in args: + logger.warn("`Connected` packet missing `slot_data`") + return + slot_data = args["slot_data"] + + if "start_char" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`") + return + self.start_char = slot_data['start_char'] + if self.start_char not in {"Apple", "Champ", "JJ"}: + logger.warn("invalid Zillion `Connected` packet, " + f"`slot_data` `start_char` has invalid value: {self.start_char}") + + if "rescues" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`") + return + rescues = slot_data["rescues"] + self.rescues = {} + for rescue_id, json_info in rescues.items(): + assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}" + # TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch? + assert json_info["start_char"] == self.start_char, \ + f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}' + ri = RescueInfo(json_info["start_char"], + json_info["room_code"], + json_info["mask"]) + self.rescues[0 if rescue_id == "0" else 1] = ri + + if "loc_mem_to_id" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`") + return + loc_mem_to_id = slot_data["loc_mem_to_id"] + self.loc_mem_to_id = {} + for mem_str, id_str in loc_mem_to_id.items(): + mem = int(mem_str) + id_ = int(id_str) + room_i = mem // 256 + assert 0 <= room_i < 74 + assert id_ in id_to_loc + self.loc_mem_to_id[mem] = id_ + + if len(self.loc_mem_to_id) != 394: + logger.warn("invalid Zillion `Connected` packet, " + f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}") + + self.got_slot_data.set() + + payload = { + "cmd": "Get", + "keys": [f"zillion-{self.auth}-doors"] + } + async_start(self.send_msgs([payload])) + elif cmd == "Retrieved": + if "keys" not in args: + logger.warning(f"invalid Retrieved packet to ZillionClient: {args}") + return + keys = cast(Dict[str, Optional[str]], args["keys"]) + doors_b64 = keys.get(f"zillion-{self.auth}-doors", None) + if doors_b64: + logger.info("received door data from server") + doors = base64.b64decode(doors_b64) + self.to_game.put_nowait(events.DoorEventToGame(doors)) + elif cmd == "RoomInfo": + self.seed_name = args["seed_name"] + self.got_room_info.set() + + def room_item_numbers_to_ui(self) -> None: + rooms = [[0 for _ in range(8)] for _ in range(16)] + for loc_id in self.missing_locations: + loc_id_small = loc_id - base_id + loc_name = id_to_loc[loc_id_small] + y = ord(loc_name[0]) - 65 + x = ord(loc_name[2]) - 49 + if y == 9 and x == 5: + # don't show main computer in numbers + continue + assert (0 <= y < 16) and (0 <= x < 8), f"invalid index from location name {loc_name}" + rooms[y][x] += 1 + # TODO: also add locations with locals lost from loading save state or reset + self.ui_set_rooms(rooms) + + def process_from_game_queue(self) -> None: + if self.from_game.qsize(): + event_from_game = self.from_game.get_nowait() + if isinstance(event_from_game, events.AcquireLocationEventFromGame): + server_id = event_from_game.id + base_id + loc_name = id_to_loc[event_from_game.id] + self.locations_checked.add(server_id) + if server_id in self.missing_locations: + self.ap_local_count += 1 + n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win + logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})') + async_start(self.send_msgs([ + {"cmd": 'LocationChecks', "locations": [server_id]} + ])) + else: + # This will happen a lot in Zillion, + # because all the key words are local and unwatched by the server. + logger.debug(f"DEBUG: {loc_name} not in missing") + elif isinstance(event_from_game, events.DeathEventFromGame): + async_start(self.send_death()) + elif isinstance(event_from_game, events.WinEventFromGame): + if not self.finished_game: + async_start(self.send_msgs([ + {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL} + ])) + self.finished_game = True + elif isinstance(event_from_game, events.DoorEventFromGame): + if self.auth: + doors_b64 = base64.b64encode(event_from_game.doors).decode() + payload = { + "cmd": "Set", + "key": f"zillion-{self.auth}-doors", + "operations": [{"operation": "replace", "value": doors_b64}] + } + async_start(self.send_msgs([payload])) + else: + logger.warning(f"WARNING: unhandled event from game {event_from_game}") + + def process_items_received(self) -> None: + if len(self.items_received) > self.next_item: + zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received] + for index in range(self.next_item, len(self.items_received)): + ap_id = self.items_received[index].item + from_name = self.player_names[self.items_received[index].player] + # TODO: colors in this text, like sni client? + logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}') + self.to_game.put_nowait( + events.ItemEventToGame(zz_item_ids) + ) + self.next_item = len(self.items_received) + + +def name_seed_from_ram(data: bytes) -> Tuple[str, str]: + """ returns player name, and end of seed string """ + if len(data) == 0: + # no connection to game + return "", "xxx" + null_index = data.find(b'\x00') + if null_index == -1: + logger.warning(f"invalid game id in rom {repr(data)}") + null_index = len(data) + name = data[:null_index].decode() + null_index_2 = data.find(b'\x00', null_index + 1) + if null_index_2 == -1: + null_index_2 = len(data) + seed_name = data[null_index + 1:null_index_2].decode() + + return name, seed_name + + +async def zillion_sync_task(ctx: ZillionContext) -> None: + logger.info("started zillion sync task") + + # to work around the Python bug where we can't check for RetroArch + if not ctx.look_for_retroarch.is_set(): + logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.") + await asyncio.wait(( + asyncio.create_task(ctx.look_for_retroarch.wait()), + asyncio.create_task(ctx.exit_event.wait()) + ), return_when=asyncio.FIRST_COMPLETED) + + last_log = "" + + def log_no_spam(msg: str) -> None: + nonlocal last_log + if msg != last_log: + last_log = msg + logger.info(msg) + + # to only show this message once per client run + help_message_shown = False + + with Memory(ctx.from_game, ctx.to_game) as memory: + while not ctx.exit_event.is_set(): + ram = await memory.read() + game_id = memory.get_rom_to_ram_data(ram) + name, seed_end = name_seed_from_ram(game_id) + if len(name): + if name == ctx.known_name: + ctx.auth = name + # this is the name we know + if ctx.server and ctx.server.socket: # type: ignore + if ctx.got_room_info.is_set(): + if ctx.seed_name and ctx.seed_name.endswith(seed_end): + # correct seed + if memory.have_generation_info(): + log_no_spam("everything connected") + await memory.process_ram(ram) + ctx.process_from_game_queue() + ctx.process_items_received() + else: # no generation info + if ctx.got_slot_data.is_set(): + memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id) + ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \ + make_id_to_others(ctx.start_char) + ctx.next_item = 0 + ctx.ap_local_count = len(ctx.checked_locations) + else: # no slot data yet + async_start(ctx.send_connect()) + log_no_spam("logging in to server...") + await asyncio.wait(( + asyncio.create_task(ctx.got_slot_data.wait()), + asyncio.create_task(ctx.exit_event.wait()), + asyncio.create_task(asyncio.sleep(6)) + ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets + else: # not correct seed name + log_no_spam("incorrect seed - did you mix up roms?") + else: # no room info + # If we get here, it looks like `RoomInfo` packet got lost + log_no_spam("waiting for room info from server...") + else: # server not connected + log_no_spam("waiting for server connection...") + else: # new game + log_no_spam("connected to new game") + await ctx.disconnect() + ctx.reset_server_state() + ctx.seed_name = None + ctx.got_room_info.clear() + ctx.reset_game_state() + memory.reset_game_state() + + ctx.auth = name + ctx.known_name = name + async_start(ctx.connect()) + await asyncio.wait(( + asyncio.create_task(ctx.got_room_info.wait()), + asyncio.create_task(ctx.exit_event.wait()), + asyncio.create_task(asyncio.sleep(6)) + ), return_when=asyncio.FIRST_COMPLETED) + else: # no name found in game + if not help_message_shown: + logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.') + help_message_shown = True + log_no_spam("looking for connection to game...") + await asyncio.sleep(0.3) + + await asyncio.sleep(0.09375) + logger.info("zillion sync task ending") + + +async def main() -> None: + parser = get_base_parser() + parser.add_argument('diff_file', default="", type=str, nargs="?", + help='Path to a .apzl Archipelago Binary Patch file') + # SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) + args = parser.parse_args() + print(args) + + if args.diff_file: + import Patch + logger.info("patch file was supplied - creating sms rom...") + meta, rom_file = Patch.create_rom_file(args.diff_file) + if "server" in meta: + args.connect = meta["server"] + logger.info(f"wrote rom file to {rom_file}") + + ctx = ZillionContext(args.connect, args.password) + if ctx.server_task is None: + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + sync_task = asyncio.create_task(zillion_sync_task(ctx)) + + await ctx.exit_event.wait() + + ctx.server_address = None + logger.debug("waiting for sync task to end") + await sync_task + logger.debug("sync task ended") + await ctx.shutdown() + + +def launch() -> None: + colorama.init() + asyncio.run(main()) + colorama.deinit() From 88c7484b3a105cea8adbb8ade5c2afd80fff4c4c Mon Sep 17 00:00:00 2001 From: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Tue, 2 Jan 2024 03:16:45 -0700 Subject: [PATCH 3/6] Shivers: Fixes rule logic for location 'puzzle solved three floor elevator' (#2657) Fixes rule logic for location 'puzzle solved three floor elevator'. Missing a parenthesis caused only the key requirement to be checked for the blue maze region. --- worlds/shivers/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py index fdd260ca91aa..4e1058fecfc8 100644 --- a/worlds/shivers/Rules.py +++ b/worlds/shivers/Rules.py @@ -157,7 +157,7 @@ def get_rules_lookup(player: int): "Puzzle Solved Underground Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player) and state.has("Key for Office Elevator", player))), "Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)), - "Puzzle Solved Three Floor Elevator": lambda state: ((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player) + "Puzzle Solved Three Floor Elevator": lambda state: (((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player)) and state.has("Key for Three Floor Elevator", player))) }, "lightning": { From e5c739ee31c43450dd5845768fa459f98e917dce Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Tue, 2 Jan 2024 05:19:57 -0500 Subject: [PATCH 4/6] KH2: Ability dupe fix and stat increase fix (#2621) Makes the client make sure the player has the correct amount of stat increase instead of letting the goa mod (apcompanion) do it abilities: checks the slot where abilities could dupe unless that slot is being used for an actual abiliity given to the player --- worlds/kh2/Client.py | 95 +++++++++++++++++++++++++++++++++++--------- worlds/kh2/Rules.py | 5 +-- 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index a5be06c7fb16..544e710741b4 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -80,11 +80,6 @@ def __init__(self, server_address, password): }, }, } - self.front_of_inventory = { - "Sora": 0x2546, - "Donald": 0x2658, - "Goofy": 0x276C, - } self.kh2seedname = None self.kh2slotdata = None self.itemamount = {} @@ -169,6 +164,14 @@ def __init__(self, server_address, password): self.ability_code_list = None self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"} + self.base_hp = 20 + self.base_mp = 100 + self.base_drive = 5 + self.base_accessory_slots = 1 + self.base_armor_slots = 1 + self.base_item_slots = 3 + self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, 0x2770, 0x2772] + async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: await super(KH2Context, self).server_auth(password_requested) @@ -219,6 +222,12 @@ def kh2_write_byte(self, address, value): def kh2_read_byte(self, address): return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1), "big") + def kh2_read_int(self, address): + return self.kh2.read_int(self.kh2.base_address + address) + + def kh2_write_int(self, address, value): + self.kh2.write_int(self.kh2.base_address + address, value) + def on_package(self, cmd: str, args: dict): if cmd in {"RoomInfo"}: self.kh2seedname = args['seed_name'] @@ -476,7 +485,7 @@ async def verifyLevel(self): async def give_item(self, item, location): try: - # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites + # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites itemname = self.lookup_id_to_item[item] itemdata = self.item_name_to_data[itemname] # itemcode = self.kh2_item_name_to_id[itemname] @@ -507,6 +516,8 @@ async def give_item(self, item, location): ability_slot = self.kh2_seed_save_cache["GoofyInvo"][1] self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["GoofyInvo"][1] -= 2 + if ability_slot in self.front_ability_slots: + self.front_ability_slots.remove(ability_slot) elif len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \ self.AbilityQuantityDict[itemname]: @@ -518,11 +529,14 @@ async def give_item(self, item, location): ability_slot = self.kh2_seed_save_cache["DonaldInvo"][0] self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["DonaldInvo"][0] -= 2 - elif itemname in self.goofy_ability_set: + else: ability_slot = self.kh2_seed_save_cache["GoofyInvo"][0] self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["GoofyInvo"][0] -= 2 + if ability_slot in self.front_ability_slots: + self.front_ability_slots.remove(ability_slot) + elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}: # if memaddr is in a bitmask location in memory if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]: @@ -615,7 +629,7 @@ async def verifyItems(self): master_sell = master_equipment | master_staff | master_shield await asyncio.create_task(self.IsInShop(master_sell)) - + # print(self.kh2_seed_save_cache["AmountInvo"]["Ability"]) for item_name in master_amount: item_data = self.item_name_to_data[item_name] amount_of_items = 0 @@ -673,10 +687,10 @@ async def verifyItems(self): self.kh2_write_short(self.Save + slot, item_data.memaddr) # removes the duped ability if client gave faster than the game. - for charInvo in {"Sora", "Donald", "Goofy"}: - if self.kh2_read_short(self.Save + self.front_of_inventory[charInvo]) != 0: - print(f"removed {self.Save + self.front_of_inventory[charInvo]} from {charInvo}") - self.kh2_write_short(self.Save + self.front_of_inventory[charInvo], 0) + for ability in self.front_ability_slots: + if self.kh2_read_short(self.Save + ability) != 0: + print(f"removed {self.Save + ability} from {ability}") + self.kh2_write_short(self.Save + ability, 0) # remove the dummy level 1 growths if they are in these invo slots. for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}: @@ -740,15 +754,60 @@ async def verifyItems(self): self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) for item_name in master_stat: - item_data = self.item_name_to_data[item_name] amount_of_items = 0 amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][item_name] + if self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5: + if item_name == ItemName.MaxHPUp: + if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical + Bonus = 5 + else: # Critical + Bonus = 2 + if self.kh2_read_int(self.Slot1 + 0x004) != self.base_hp + (Bonus * amount_of_items): + self.kh2_write_int(self.Slot1 + 0x004, self.base_hp + (Bonus * amount_of_items)) + + elif item_name == ItemName.MaxMPUp: + if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical + Bonus = 10 + else: # Critical + Bonus = 5 + if self.kh2_read_int(self.Slot1 + 0x184) != self.base_mp + (Bonus * amount_of_items): + self.kh2_write_int(self.Slot1 + 0x184, self.base_mp + (Bonus * amount_of_items)) + + elif item_name == ItemName.DriveGaugeUp: + current_max_drive = self.kh2_read_byte(self.Slot1 + 0x1B2) + # change when max drive is changed from 6 to 4 + if current_max_drive < 9 and current_max_drive != self.base_drive + amount_of_items: + self.kh2_write_byte(self.Slot1 + 0x1B2, self.base_drive + amount_of_items) + + elif item_name == ItemName.AccessorySlotUp: + current_accessory = self.kh2_read_byte(self.Save + 0x2501) + if current_accessory != self.base_accessory_slots + amount_of_items: + if 4 > current_accessory < self.base_accessory_slots + amount_of_items: + self.kh2_write_byte(self.Save + 0x2501, current_accessory + 1) + elif self.base_accessory_slots + amount_of_items < 4: + self.kh2_write_byte(self.Save + 0x2501, self.base_accessory_slots + amount_of_items) + + elif item_name == ItemName.ArmorSlotUp: + current_armor_slots = self.kh2_read_byte(self.Save + 0x2500) + if current_armor_slots != self.base_armor_slots + amount_of_items: + if 4 > current_armor_slots < self.base_armor_slots + amount_of_items: + self.kh2_write_byte(self.Save + 0x2500, current_armor_slots + 1) + elif self.base_armor_slots + amount_of_items < 4: + self.kh2_write_byte(self.Save + 0x2500, self.base_armor_slots + amount_of_items) + + elif item_name == ItemName.ItemSlotUp: + current_item_slots = self.kh2_read_byte(self.Save + 0x2502) + if current_item_slots != self.base_item_slots + amount_of_items: + if 8 > current_item_slots < self.base_item_slots + amount_of_items: + self.kh2_write_byte(self.Save + 0x2502, current_item_slots + 1) + elif self.base_item_slots + amount_of_items < 8: + self.kh2_write_byte(self.Save + 0x2502, self.base_item_slots + amount_of_items) + + # if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \ + # and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \ + # self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}: + # self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) - # if slot1 has 5 drive gauge and goa lost illusion is checked and they are not in a cutscene - if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \ - and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \ - self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}: - self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) if "PoptrackerVersionCheck" in self.kh2slotdata: if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3 self.kh2_write_byte(self.Save + 0x3607, 1) diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 41207c6cb3d0..7c5551dbd563 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -268,7 +268,6 @@ def set_kh2_rules(self) -> None: add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys()) def set_kh2_goal(self): - final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player) if self.multiworld.Goal[self.player] == "three_proofs": final_xemnas_location.access_rule = lambda state: self.kh2_has_all(three_proofs, state) @@ -291,8 +290,8 @@ def set_kh2_goal(self): else: self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) else: - final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and\ - state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) + final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and \ + state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) if self.multiworld.FinalXemnas[self.player]: self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1) else: From bf17582c5534d3dc35bdb597bcb2da097228e275 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 2 Jan 2024 03:32:03 -0700 Subject: [PATCH 5/6] BizHawkClient: Add some handling for non-string errors (#2656) --- data/lua/connector_bizhawk_generic.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/data/lua/connector_bizhawk_generic.lua b/data/lua/connector_bizhawk_generic.lua index eff400cb032b..47af6e003d8e 100644 --- a/data/lua/connector_bizhawk_generic.lua +++ b/data/lua/connector_bizhawk_generic.lua @@ -456,6 +456,7 @@ function send_receive () failed_guard_response = response end else + if type(response) ~= "string" then response = "Unknown error" end res[i] = {type = "ERROR", err = response} end end From 0df0955415cd4523531ab091f05090831fb5016d Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 2 Jan 2024 08:03:39 -0600 Subject: [PATCH 6/6] Core: check if a location is an event before excluding it (#2653) * Core: check if a location is an event before excluding it * log a warning * put the warning in the right spot --- worlds/generic/Rules.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index 520ad2252568..ac5e1aa50750 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -1,4 +1,5 @@ import collections +import logging import typing from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance @@ -81,15 +82,18 @@ def forbid(sender: int, receiver: int, items: typing.Set[str]): i.name not in sending_blockers[i.player] and old_rule(i) -def exclusion_rules(world: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None: +def exclusion_rules(multiworld: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None: for loc_name in exclude_locations: try: - location = world.get_location(loc_name, player) + location = multiworld.get_location(loc_name, player) except KeyError as e: # failed to find the given location. Check if it's a legitimate location - if loc_name not in world.worlds[player].location_name_to_id: + if loc_name not in multiworld.worlds[player].location_name_to_id: raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e else: - location.progress_type = LocationProgressType.EXCLUDED + if not location.event: + location.progress_type = LocationProgressType.EXCLUDED + else: + logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.") def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule):