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:
parent
6134578c60
commit
aeb78eaa10
111
ZillionClient.py
111
ZillionClient.py
|
@ -1,7 +1,7 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import platform
|
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
|
# CommonClient import first to trigger ModuleUpdater
|
||||||
from CommonClient import CommonContext, server_loop, gui_enabled, \
|
from CommonClient import CommonContext, server_loop, gui_enabled, \
|
||||||
|
@ -18,7 +18,7 @@ from zilliandomizer.options import Chars
|
||||||
from zilliandomizer.patch import RescueInfo
|
from zilliandomizer.patch import RescueInfo
|
||||||
|
|
||||||
from worlds.zillion.id_maps import make_id_to_others
|
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):
|
class ZillionCommandProcessor(ClientCommandProcessor):
|
||||||
|
@ -29,6 +29,18 @@ class ZillionCommandProcessor(ClientCommandProcessor):
|
||||||
logger.info("ready to look for game")
|
logger.info("ready to look for game")
|
||||||
self.ctx.look_for_retroarch.set()
|
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):
|
class ZillionContext(CommonContext):
|
||||||
game = "Zillion"
|
game = "Zillion"
|
||||||
|
@ -61,6 +73,10 @@ class ZillionContext(CommonContext):
|
||||||
As a workaround, we don't look for RetroArch until this event is set.
|
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,
|
def __init__(self,
|
||||||
server_address: str,
|
server_address: str,
|
||||||
password: str) -> None:
|
password: str) -> None:
|
||||||
|
@ -69,6 +85,8 @@ class ZillionContext(CommonContext):
|
||||||
self.to_game = asyncio.Queue()
|
self.to_game = asyncio.Queue()
|
||||||
self.got_room_info = asyncio.Event()
|
self.got_room_info = asyncio.Event()
|
||||||
self.got_slot_data = 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()
|
self.look_for_retroarch = asyncio.Event()
|
||||||
if platform.system() != "Windows":
|
if platform.system() != "Windows":
|
||||||
|
@ -115,6 +133,10 @@ class ZillionContext(CommonContext):
|
||||||
# override
|
# override
|
||||||
def run_gui(self) -> None:
|
def run_gui(self) -> None:
|
||||||
from kvui import GameManager
|
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):
|
class ZillionManager(GameManager):
|
||||||
logging_pairs = [
|
logging_pairs = [
|
||||||
|
@ -122,12 +144,76 @@ class ZillionContext(CommonContext):
|
||||||
]
|
]
|
||||||
base_title = "Archipelago Zillion Client"
|
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 = ZillionManager(self)
|
||||||
run_co: Coroutine[Any, Any, None] = self.ui.async_run() # type: ignore
|
self.ui_toggle_map = lambda: self.ui.toggle_map_width()
|
||||||
# kivy types missing
|
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")
|
self.ui_task = asyncio.create_task(run_co, name="UI")
|
||||||
|
|
||||||
def on_package(self, cmd: str, args: Dict[str, Any]) -> None:
|
def on_package(self, cmd: str, args: Dict[str, Any]) -> None:
|
||||||
|
self.room_item_numbers_to_ui()
|
||||||
if cmd == "Connected":
|
if cmd == "Connected":
|
||||||
logger.info("logged in to Archipelago server")
|
logger.info("logged in to Archipelago server")
|
||||||
if "slot_data" not in args:
|
if "slot_data" not in args:
|
||||||
|
@ -192,6 +278,21 @@ class ZillionContext(CommonContext):
|
||||||
self.seed_name = args["seed_name"]
|
self.seed_name = args["seed_name"]
|
||||||
self.got_room_info.set()
|
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:
|
def process_from_game_queue(self) -> None:
|
||||||
if self.from_game.qsize():
|
if self.from_game.qsize():
|
||||||
event_from_game = self.from_game.get_nowait()
|
event_from_game = self.from_game.get_nowait()
|
||||||
|
@ -251,7 +352,7 @@ def name_seed_from_ram(data: bytes) -> Tuple[str, str]:
|
||||||
return "", "xxx"
|
return "", "xxx"
|
||||||
null_index = data.find(b'\x00')
|
null_index = data.find(b'\x00')
|
||||||
if null_index == -1:
|
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)
|
null_index = len(data)
|
||||||
name = data[:null_index].decode()
|
name = data[:null_index].decode()
|
||||||
null_index_2 = data.find(b'\x00', null_index + 1)
|
null_index_2 = data.find(b'\x00', null_index + 1)
|
||||||
|
|
11
kvui.py
11
kvui.py
|
@ -28,6 +28,7 @@ from kivy.factory import Factory
|
||||||
from kivy.properties import BooleanProperty, ObjectProperty
|
from kivy.properties import BooleanProperty, ObjectProperty
|
||||||
from kivy.uix.button import Button
|
from kivy.uix.button import Button
|
||||||
from kivy.uix.gridlayout import GridLayout
|
from kivy.uix.gridlayout import GridLayout
|
||||||
|
from kivy.uix.layout import Layout
|
||||||
from kivy.uix.textinput import TextInput
|
from kivy.uix.textinput import TextInput
|
||||||
from kivy.uix.recycleview import RecycleView
|
from kivy.uix.recycleview import RecycleView
|
||||||
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
|
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
|
||||||
|
@ -299,6 +300,9 @@ class GameManager(App):
|
||||||
base_title: str = "Archipelago Client"
|
base_title: str = "Archipelago Client"
|
||||||
last_autofillable_command: str
|
last_autofillable_command: str
|
||||||
|
|
||||||
|
main_area_container: GridLayout
|
||||||
|
""" subclasses can add more columns beside the tabs """
|
||||||
|
|
||||||
def __init__(self, ctx: context_type):
|
def __init__(self, ctx: context_type):
|
||||||
self.title = self.base_title
|
self.title = self.base_title
|
||||||
self.ctx = ctx
|
self.ctx = ctx
|
||||||
|
@ -325,7 +329,7 @@ class GameManager(App):
|
||||||
|
|
||||||
super(GameManager, self).__init__()
|
super(GameManager, self).__init__()
|
||||||
|
|
||||||
def build(self):
|
def build(self) -> Layout:
|
||||||
self.container = ContainerLayout()
|
self.container = ContainerLayout()
|
||||||
|
|
||||||
self.grid = MainLayout()
|
self.grid = MainLayout()
|
||||||
|
@ -358,7 +362,10 @@ class GameManager(App):
|
||||||
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
|
self.log_panels[display_name] = panel.content = UILog(bridge_logger)
|
||||||
self.tabs.add_widget(panel)
|
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:
|
if len(self.logging_pairs) == 1:
|
||||||
# Hide Tab selection if only one tab
|
# Hide Tab selection if only one tab
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
class App:
|
||||||
|
async def async_run(self) -> None: ...
|
|
@ -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: ...
|
|
@ -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: ...
|
|
@ -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: ...
|
|
@ -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: ...
|
|
@ -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: ...
|
|
@ -1 +1,4 @@
|
||||||
|
import os
|
||||||
|
|
||||||
base_id = 8675309
|
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 |
Loading…
Reference in New Issue