diff --git a/ZillionClient.py b/ZillionClient.py index 8ad10650..e2ce697c 100644 --- a/ZillionClient.py +++ b/ZillionClient.py @@ -1,7 +1,7 @@ import asyncio import base64 import platform -from typing import Any, Coroutine, Dict, Optional, Tuple, Type, cast +from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, Type, cast # CommonClient import first to trigger ModuleUpdater from CommonClient import CommonContext, server_loop, gui_enabled, \ @@ -18,7 +18,7 @@ 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 +from worlds.zillion.config import base_id, zillion_map class ZillionCommandProcessor(ClientCommandProcessor): @@ -29,6 +29,18 @@ class ZillionCommandProcessor(ClientCommandProcessor): 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" @@ -61,6 +73,10 @@ class ZillionContext(CommonContext): 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: @@ -69,6 +85,8 @@ class ZillionContext(CommonContext): 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": @@ -115,6 +133,10 @@ class ZillionContext(CommonContext): # 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 = [ @@ -122,12 +144,76 @@ class ZillionContext(CommonContext): ] 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) - run_co: Coroutine[Any, Any, None] = self.ui.async_run() # type: ignore - # kivy types missing + 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: @@ -192,6 +278,21 @@ class ZillionContext(CommonContext): 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() @@ -251,7 +352,7 @@ def name_seed_from_ram(data: bytes) -> Tuple[str, str]: return "", "xxx" null_index = data.find(b'\x00') if null_index == -1: - logger.warning(f"invalid game id in rom {data}") + 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) diff --git a/kvui.py b/kvui.py index 3c1161f9..38208645 100644 --- a/kvui.py +++ b/kvui.py @@ -28,6 +28,7 @@ from kivy.factory import Factory from kivy.properties import BooleanProperty, ObjectProperty from kivy.uix.button import Button from kivy.uix.gridlayout import GridLayout +from kivy.uix.layout import Layout from kivy.uix.textinput import TextInput from kivy.uix.recycleview import RecycleView from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem @@ -299,6 +300,9 @@ class GameManager(App): base_title: str = "Archipelago Client" last_autofillable_command: str + main_area_container: GridLayout + """ subclasses can add more columns beside the tabs """ + def __init__(self, ctx: context_type): self.title = self.base_title self.ctx = ctx @@ -325,7 +329,7 @@ class GameManager(App): super(GameManager, self).__init__() - def build(self): + def build(self) -> Layout: self.container = ContainerLayout() self.grid = MainLayout() @@ -358,7 +362,10 @@ class GameManager(App): self.log_panels[display_name] = panel.content = UILog(bridge_logger) self.tabs.add_widget(panel) - self.grid.add_widget(self.tabs) + self.main_area_container = GridLayout(size_hint_y=1, rows=1) + self.main_area_container.add_widget(self.tabs) + + self.grid.add_widget(self.main_area_container) if len(self.logging_pairs) == 1: # Hide Tab selection if only one tab diff --git a/typings/kivy/__init__.pyi b/typings/kivy/__init__.pyi new file mode 100644 index 00000000..e69de29b diff --git a/typings/kivy/app.pyi b/typings/kivy/app.pyi new file mode 100644 index 00000000..bb41bf6b --- /dev/null +++ b/typings/kivy/app.pyi @@ -0,0 +1,2 @@ +class App: + async def async_run(self) -> None: ... diff --git a/typings/kivy/core/__init__.pyi b/typings/kivy/core/__init__.pyi new file mode 100644 index 00000000..e69de29b diff --git a/typings/kivy/core/text.pyi b/typings/kivy/core/text.pyi new file mode 100644 index 00000000..7b13ad34 --- /dev/null +++ b/typings/kivy/core/text.pyi @@ -0,0 +1,7 @@ +from typing import Tuple +from ..graphics import FillType_Shape +from ..uix.widget import Widget + + +class Label(FillType_Shape, Widget): + def __init__(self, *, text: str, font_size: int, color: Tuple[float, float, float, float]) -> None: ... diff --git a/typings/kivy/graphics.pyi b/typings/kivy/graphics.pyi new file mode 100644 index 00000000..19509106 --- /dev/null +++ b/typings/kivy/graphics.pyi @@ -0,0 +1,40 @@ +""" FillType_* is not a real kivy type - just something to fill unknown typing. """ + +from typing import Sequence + +FillType_Vec = Sequence[int] + + +class FillType_Drawable: + def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ... + + +class FillType_Texture(FillType_Drawable): + pass + + +class FillType_Shape(FillType_Drawable): + texture: FillType_Texture + + def __init__(self, + *, + texture: FillType_Texture = ..., + pos: FillType_Vec = ..., + size: FillType_Vec = ...) -> None: ... + + +class Ellipse(FillType_Shape): + pass + + +class Color: + def __init__(self, r: float, g: float, b: float, a: float) -> None: ... + + +class Rectangle(FillType_Shape): + def __init__(self, + *, + source: str = ..., + texture: FillType_Texture = ..., + pos: FillType_Vec = ..., + size: FillType_Vec = ...) -> None: ... diff --git a/typings/kivy/uix/__init__.pyi b/typings/kivy/uix/__init__.pyi new file mode 100644 index 00000000..e69de29b diff --git a/typings/kivy/uix/layout.pyi b/typings/kivy/uix/layout.pyi new file mode 100644 index 00000000..2a418a1d --- /dev/null +++ b/typings/kivy/uix/layout.pyi @@ -0,0 +1,8 @@ +from typing import Any +from .widget import Widget + + +class Layout(Widget): + def add_widget(self, widget: Widget) -> None: ... + + def do_layout(self, *largs: Any, **kwargs: Any) -> None: ... diff --git a/typings/kivy/uix/tabbedpanel.pyi b/typings/kivy/uix/tabbedpanel.pyi new file mode 100644 index 00000000..9183b4c8 --- /dev/null +++ b/typings/kivy/uix/tabbedpanel.pyi @@ -0,0 +1,12 @@ +from .layout import Layout +from .widget import Widget + + +class TabbedPanel(Layout): + pass + + +class TabbedPanelItem(Widget): + content: Widget + + def __init__(self, *, text: str = ...) -> None: ... diff --git a/typings/kivy/uix/widget.pyi b/typings/kivy/uix/widget.pyi new file mode 100644 index 00000000..54e3b781 --- /dev/null +++ b/typings/kivy/uix/widget.pyi @@ -0,0 +1,31 @@ +""" FillType_* is not a real kivy type - just something to fill unknown typing. """ + +from typing import Any, Optional, Protocol +from ..graphics import FillType_Drawable, FillType_Vec + + +class FillType_BindCallback(Protocol): + def __call__(self, *args: Any) -> None: ... + + +class FillType_Canvas: + def add(self, drawable: FillType_Drawable) -> None: ... + + def clear(self) -> None: ... + + def __enter__(self) -> None: ... + + def __exit__(self, *args: Any) -> None: ... + + +class Widget: + canvas: FillType_Canvas + width: int + pos: FillType_Vec + + def bind(self, + *, + pos: Optional[FillType_BindCallback] = ..., + size: Optional[FillType_BindCallback] = ...) -> None: ... + + def refresh(self) -> None: ... diff --git a/worlds/sa2b/Names/__init__.py b/worlds/sa2b/Names/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/worlds/zillion/config.py b/worlds/zillion/config.py index e08c4f42..ca02f9a9 100644 --- a/worlds/zillion/config.py +++ b/worlds/zillion/config.py @@ -1 +1,4 @@ +import os + base_id = 8675309 +zillion_map = os.path.join(os.path.dirname(__file__), "empty-zillion-map-row-col-labels-281.png") diff --git a/worlds/zillion/empty-zillion-map-row-col-labels-281.png b/worlds/zillion/empty-zillion-map-row-col-labels-281.png new file mode 100644 index 00000000..3084301f Binary files /dev/null and b/worlds/zillion/empty-zillion-map-row-col-labels-281.png differ