Zillion: map tracker in client (#1136)

* 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 <107806872+SoldierofOrder@users.noreply.github.com>

* update zillion package

* workaround for asyncio udp bug

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 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 <107806872+SoldierofOrder@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <doughoskisson@novuslabs.com>
Co-authored-by: CaitSith2 <d_good@caitsith2.com>
This commit is contained in:
Doug Hoskisson 2022-10-27 02:30:22 -07:00 committed by GitHub
parent 6134578c60
commit aeb78eaa10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 218 additions and 7 deletions

View File

@ -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)

11
kvui.py
View File

@ -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

View File

2
typings/kivy/app.pyi Normal file
View File

@ -0,0 +1,2 @@
class App:
async def async_run(self) -> None: ...

View File

View File

@ -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
typings/kivy/graphics.pyi Normal file
View File

@ -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: ...

View File

View File

@ -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: ...

View File

@ -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: ...

View File

@ -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: ...

View File

View File

@ -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")

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB