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