From e674e37e08fed77fa724021a29eeaa7d92b25bfa Mon Sep 17 00:00:00 2001
From: lordlou <87331798+lordlou@users.noreply.github.com>
Date: Thu, 28 Dec 2023 17:43:16 -0500
Subject: [PATCH 1/7] SMZ3: optimized message queues (#2611)
---
worlds/smz3/Client.py | 22 +++++++++++-----------
worlds/smz3/__init__.py | 3 ++-
worlds/smz3/data/zsm.ips | Bin 1470841 -> 1470841 bytes
3 files changed, 13 insertions(+), 12 deletions(-)
diff --git a/worlds/smz3/Client.py b/worlds/smz3/Client.py
index 687a43b00f..859cf234eb 100644
--- a/worlds/smz3/Client.py
+++ b/worlds/smz3/Client.py
@@ -69,7 +69,7 @@ class SMZ3SNIClient(SNIClient):
ctx.finished_game = True
return
- data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, 4)
+ data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, 4)
if data is None:
return
@@ -77,14 +77,14 @@ class SMZ3SNIClient(SNIClient):
recv_item = data[2] | (data[3] << 8)
while (recv_index < recv_item):
- item_address = recv_index * 8
- message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x700 + item_address, 8)
- is_z3_item = ((message[5] & 0x80) != 0)
- masked_part = (message[5] & 0x7F) if is_z3_item else message[5]
- item_index = ((message[4] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0)
+ item_address = recv_index * 2
+ message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xDA0 + item_address, 2)
+ is_z3_item = ((message[1] & 0x80) != 0)
+ masked_part = (message[1] & 0x7F) if is_z3_item else message[1]
+ item_index = ((message[0] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0)
recv_index += 1
- snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
+ snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
from .TotalSMZ3.Location import locations_start_id
from . import convertLocSMZ3IDToAPID
@@ -95,7 +95,7 @@ class SMZ3SNIClient(SNIClient):
snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
- data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x600, 4)
+ data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD36, 4)
if data is None:
return
@@ -106,10 +106,10 @@ class SMZ3SNIClient(SNIClient):
item = ctx.items_received[item_out_ptr]
item_id = item.item - items_start_id
- player_id = item.player if item.player <= SMZ3_ROM_PLAYER_LIMIT else 0
- snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 4, bytes([player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, (item_id >> 8) & 0xFF]))
+ player_id = item.player if item.player < SMZ3_ROM_PLAYER_LIMIT else 0
+ snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 2, bytes([player_id, item_id]))
item_out_ptr += 1
- snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x602, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF]))
+ snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD38, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF]))
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
ctx.location_names[item.location], item_out_ptr, len(ctx.items_received)))
diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py
index 2cc2ac97d9..39aa42c07a 100644
--- a/worlds/smz3/__init__.py
+++ b/worlds/smz3/__init__.py
@@ -80,7 +80,8 @@ class SMZ3World(World):
locationNamesGT: Set[str] = {loc.Name for loc in GanonsTower(None, None).Locations}
# first added for 0.2.6
- required_client_version = (0, 2, 6)
+ # optimized message queues for 0.4.4
+ required_client_version = (0, 4, 4)
def __init__(self, world: MultiWorld, player: int):
self.rom_name_available_event = threading.Event()
diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips
index fff36d95d15c96a9cc756c4fd0aa10ccda3e1751..87a4f924f1933fcf59493753c034192ef03a328c 100644
GIT binary patch
delta 508
zcmezQDDvl{$O+3CKWtnvO`6Fece8^`77OFb&8ro5T;lp7_~TYgayzHem*(8h?YW;B
zftU%1nSq!Eh*`Jier9WyVpQM$k(d2ABcuBCo&4;ojArfk`PqS(1Bf|+m}~oee(u$e
z7+t4#f8ss^v}AhZXKq7phulRkUOWh3xbRV<>Q_d#$F22txr?|~&B(}@zaV!Jh$Gbx
zB(gm?4jHUB%U#rOmAgm-s1C#hGW&r-K(2)VgJ|9M`=7au80$9(g4CV2$X)cI=#j$u
zPm&B5QrA9Vx{$E;J@bX=rLiX&KJcAn`M`Bj@B`aP@efQ5?n?zuvIE(iK(_T#!3*YV
z|4Du@vg-XO2~^6|pnFIGWFXh7{R|0
}CyxW8(DYbNo@#BF
z%ast;9fnzVP(cYC1Q6@4Z2!l}qn!#2XRUmm?OOS~-_#kk+ZCtt0x{oq#p(PkeT+NX
h)l>zxtEmcJ?O+sY|Fc#Qh(WY45Q}X8vsSeBFaS*}+?xOZ
delta 513
zcmezQDDvl{$O+3CUvFG7O`2&z+GYotEEdKkn^!CBxWx5d@W-u~LNMw6(95PtXkhZ9wC2f%gPzQ($WcCAvfLsd!2GPpxS3h$bG1jjZ1gSgE
zl(y(Y?jwcupClPBB&>bHbRl}}d*%yaOJh$meBe9D@`3B5;0Lyo;vbkAoR$ilWCyZ2
zfo!v-f)|X|{*(NmYt{Qt5~!4^LF14D$Uvau_A?|bYOUMP1SD$|PJykF0y&_8btM;2
zHOMPU4AZ;6a&PB$6aJz3pW%bZs{PBR`+nor=axefX#d9T!?t
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 2/7] 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 16905cc6da..4ff361c072 100644
--- a/worlds/ff1/__init__.py
+++ b/worlds/ff1/__init__.py
@@ -74,6 +74,7 @@ class FF1World(World):
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 3/7] 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 5f3cbb943f..ef96edab04 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 0000000000..ac73f6db50
--- /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 4/7] 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 fdd260ca91..4e1058fecf 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 5/7] 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 a5be06c7fb..544e710741 100644
--- a/worlds/kh2/Client.py
+++ b/worlds/kh2/Client.py
@@ -80,11 +80,6 @@ class KH2Context(CommonContext):
},
},
}
- self.front_of_inventory = {
- "Sora": 0x2546,
- "Donald": 0x2658,
- "Goofy": 0x276C,
- }
self.kh2seedname = None
self.kh2slotdata = None
self.itemamount = {}
@@ -169,6 +164,14 @@ class KH2Context(CommonContext):
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 @@ class KH2Context(CommonContext):
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 @@ class KH2Context(CommonContext):
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 @@ class KH2Context(CommonContext):
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 @@ class KH2Context(CommonContext):
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 @@ class KH2Context(CommonContext):
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 @@ class KH2Context(CommonContext):
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 @@ class KH2Context(CommonContext):
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 41207c6cb3..7c5551dbd5 100644
--- a/worlds/kh2/Rules.py
+++ b/worlds/kh2/Rules.py
@@ -268,7 +268,6 @@ class KH2WorldRules(KH2Rules):
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 @@ class KH2WorldRules(KH2Rules):
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 6/7] 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 eff400cb03..47af6e003d 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 7/7] 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 520ad22525..ac5e1aa507 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 locality_rules(world: MultiWorld):
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):