Skip to content

Commit

Permalink
Zillion: map tracker in client (#1136)
Browse files Browse the repository at this point in the history
* Option RangeWithSpecialMax

* amendment to typing in web options

* compare string with number

* lots of work on zillion

* fix zillion fill logic

* fix a few more issues in zillion fill logic

* can make zillion patch and use it

* put multi items in zillion rom

* work on ZillionClient

* logging and auth in client

* work on sending and receiving items

* implement item_handling flag

* fix locations ids to NuktiServer package

* use rewrite of zri

* cache logic rule data for performance

* use new id maps

* fix some problems with the big recent merge

* ZillionClient: use new context manager for Memory class

* fix ItemClassification for Zillion items
and some debug statements for asserts,
documentation on running scripts for manual testing
type correction in CommonContext

* fix some issues in client, start on docs, put rescue and item ram addresses in slot data

* use new location name system
fix item locations getting out of sync in progression balancing

* zillion client can read slot name from game

* zillion: new item names

* remove extra unneeded import

* newer options (room gen and starting cards)

* update comment in zillion patch

* zillion non static regions

* change some logging, update some comments

* allow ZillionClient to exit in certain situations

* todo note to fix options doc strings

* don't force auto forfeit

* rework validation of floppy requirement and item counts
and fix race condition in generate_output

* reorganize Zillion component structure
with System class

* documentation updates for Zillion

* attempt inno_setup.iss

* remove todo comment for something done

* update comment

* rework item count zillion options
and some small cleanups

* fix location check count

* data package version 1

* Zillion can pass unit tests without rom

* fix freeze if closing ZillionClient while it's waiting for server login

* specify commit hash for zilliandomizer package

* some changes to options validation

* Zillion doors saved on multiworld server

* add missing function in inno_setup
and name of vanilla continues in options

* rework zillion sync task and context

* Apply documentation suggestions from SoldierofOrder

Co-authored-by: SoldierofOrder <[email protected]>

* update zillion package

* workaround for asyncio udp bug

There is a bug in Python in Windows
python/cpython#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 the user asks for it with /sms

* a few of the smaller suggestions from review

* logic only looks at my locations
instead of all the multiworld locations

* some adjustments from pull request discussion
and some unit tests

* patch webhost changes from pull request discussion

* zillion logic tests

* better vblr test

* test interaction of character rescue items with logic

* move unit tests to new worlds folder

* comment improvements

* fix minor logic issue
and add memory read timeout

* capitalization in option display names
Opa-Opa is a proper noun

* client toggle side panel with /map

* displays map

* fix map transparency

* fix broken launcher

* better way to specify grid container

* start kivy typing

* have a map that updates with item checks

but it breaks other parts of the UI

* fix layout bug

* aspect ratio of image
and some type checking details

* Fix loading of map for compiled builds

Co-authored-by: SoldierofOrder <[email protected]>
Co-authored-by: Doug Hoskisson <[email protected]>
Co-authored-by: CaitSith2 <[email protected]>
  • Loading branch information
4 people authored Oct 27, 2022
1 parent 6134578 commit aeb78ea
Show file tree
Hide file tree
Showing 14 changed files with 218 additions and 7 deletions.
111 changes: 106 additions & 5 deletions ZillionClient.py
Original file line number Diff line number Diff line change
@@ -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, \
Expand All @@ -18,7 +18,7 @@
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):
Expand All @@ -29,6 +29,18 @@ def _cmd_sms(self) -> None:
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"
Expand Down Expand Up @@ -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:
Expand All @@ -69,6 +85,8 @@ def __init__(self,
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":
Expand Down Expand Up @@ -115,19 +133,87 @@ async def server_auth(self, password_requested: bool = False) -> None:
# 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)
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:
Expand Down Expand Up @@ -192,6 +278,21 @@ def on_package(self, cmd: str, args: Dict[str, Any]) -> None:
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()
Expand Down Expand Up @@ -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)
Expand Down
11 changes: 9 additions & 2 deletions kvui.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
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
Expand Down Expand Up @@ -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
Expand All @@ -325,7 +329,7 @@ def intercept_say(text):

super(GameManager, self).__init__()

def build(self):
def build(self) -> Layout:
self.container = ContainerLayout()

self.grid = MainLayout()
Expand Down Expand Up @@ -358,7 +362,10 @@ def build(self):
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
Expand Down
Empty file added typings/kivy/__init__.pyi
Empty file.
2 changes: 2 additions & 0 deletions typings/kivy/app.pyi
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class App:
async def async_run(self) -> None: ...
Empty file added typings/kivy/core/__init__.pyi
Empty file.
7 changes: 7 additions & 0 deletions typings/kivy/core/text.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
40 changes: 40 additions & 0 deletions typings/kivy/graphics.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
Empty file added typings/kivy/uix/__init__.pyi
Empty file.
8 changes: 8 additions & 0 deletions typings/kivy/uix/layout.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
12 changes: 12 additions & 0 deletions typings/kivy/uix/tabbedpanel.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
31 changes: 31 additions & 0 deletions typings/kivy/uix/widget.pyi
Original file line number Diff line number Diff line change
@@ -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: ...
Empty file added worlds/sa2b/Names/__init__.py
Empty file.
3 changes: 3 additions & 0 deletions worlds/zillion/config.py
Original file line number Diff line number Diff line change
@@ -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")
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit aeb78ea

Please sign in to comment.