Skip to content

Commit

Permalink
Merge remote-tracking branch 'upstream/main' into progression_power_star
Browse files Browse the repository at this point in the history
  • Loading branch information
Kappatechy committed Mar 5, 2024
2 parents a1a5d2a + b9d561a commit 2bcc676
Show file tree
Hide file tree
Showing 42 changed files with 749 additions and 470 deletions.
15 changes: 12 additions & 3 deletions BaseClasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -717,14 +717,23 @@ def can_reach(self,
assert isinstance(player, int), "can_reach: player is required if spot is str"
# try to resolve a name
if resolution_hint == 'Location':
spot = self.multiworld.get_location(spot, player)
return self.can_reach_location(spot, player)
elif resolution_hint == 'Entrance':
spot = self.multiworld.get_entrance(spot, player)
return self.can_reach_entrance(spot, player)
else:
# default to Region
spot = self.multiworld.get_region(spot, player)
return self.can_reach_region(spot, player)
return spot.can_reach(self)

def can_reach_location(self, spot: str, player: int) -> bool:
return self.multiworld.get_location(spot, player).can_reach(self)

def can_reach_entrance(self, spot: str, player: int) -> bool:
return self.multiworld.get_entrance(spot, player).can_reach(self)

def can_reach_region(self, spot: str, player: int) -> bool:
return self.multiworld.get_region(spot, player).can_reach(self)

def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None:
if locations is None:
locations = self.multiworld.get_filled_locations()
Expand Down
34 changes: 17 additions & 17 deletions Launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def launch(exe, in_terminal=False):


def run_gui():
from kvui import App, ContainerLayout, GridLayout, Button, Label
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget
from kivy.uix.image import AsyncImage
from kivy.uix.relativelayout import RelativeLayout

Expand All @@ -185,11 +185,16 @@ def build(self):
self.container = ContainerLayout()
self.grid = GridLayout(cols=2)
self.container.add_widget(self.grid)
self.grid.add_widget(Label(text="General"))
self.grid.add_widget(Label(text="Clients"))
button_layout = self.grid # make buttons fill the window

def build_button(component: Component):
self.grid.add_widget(Label(text="General", size_hint_y=None, height=40))
self.grid.add_widget(Label(text="Clients", size_hint_y=None, height=40))
tool_layout = ScrollBox()
tool_layout.layout.orientation = "vertical"
self.grid.add_widget(tool_layout)
client_layout = ScrollBox()
client_layout.layout.orientation = "vertical"
self.grid.add_widget(client_layout)

def build_button(component: Component) -> Widget:
"""
Builds a button widget for a given component.
Expand All @@ -200,31 +205,26 @@ def build_button(component: Component):
None. The button is added to the parent grid layout.
"""
button = Button(text=component.display_name)
button = Button(text=component.display_name, size_hint_y=None, height=40)
button.component = component
button.bind(on_release=self.component_action)
if component.icon != "icon":
image = AsyncImage(source=icon_paths[component.icon],
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
box_layout = RelativeLayout()
box_layout = RelativeLayout(size_hint_y=None, height=40)
box_layout.add_widget(button)
box_layout.add_widget(image)
button_layout.add_widget(box_layout)
else:
button_layout.add_widget(button)
return box_layout
return button

for (tool, client) in itertools.zip_longest(itertools.chain(
self._tools.items(), self._miscs.items(), self._adjusters.items()), self._clients.items()):
# column 1
if tool:
build_button(tool[1])
else:
button_layout.add_widget(Label())
tool_layout.layout.add_widget(build_button(tool[1]))
# column 2
if client:
build_button(client[1])
else:
button_layout.add_widget(Label())
client_layout.layout.add_widget(build_button(client[1]))

return self.container

Expand Down
22 changes: 14 additions & 8 deletions MultiServer.py
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,8 @@ def get_aliased_name(self, team: int, slot: int):
else:
return self.player_names[team, slot]

def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False,
recipients: typing.Sequence[int] = None):
"""Send and remember hints."""
if only_new:
hints = [hint for hint in hints if hint not in self.hints[team, hint.finding_player]]
Expand Down Expand Up @@ -685,12 +686,13 @@ def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: b
for slot in new_hint_events:
self.on_new_hint(team, slot)
for slot, hint_data in concerns.items():
clients = self.clients[team].get(slot)
if not clients:
continue
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)]
for client in clients:
async_start(self.send_msgs(client, client_hints))
if recipients is None or slot in recipients:
clients = self.clients[team].get(slot)
if not clients:
continue
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)]
for client in clients:
async_start(self.send_msgs(client, client_hints))

# "events"

Expand Down Expand Up @@ -1429,9 +1431,13 @@ def get_hints(self, input_text: str, for_location: bool = False) -> bool:
hints = {hint.re_check(self.ctx, self.client.team) for hint in
self.ctx.hints[self.client.team, self.client.slot]}
self.ctx.hints[self.client.team, self.client.slot] = hints
self.ctx.notify_hints(self.client.team, list(hints))
self.ctx.notify_hints(self.client.team, list(hints), recipients=(self.client.slot,))
self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. "
f"You have {points_available} points.")
if hints and Utils.version_tuple < (0, 5, 0):
self.output("It was recently changed, so that the above hints are only shown to you. "
"If you meant to alert another player of an above hint, "
"please let them know of the content or to run !hint themselves.")
return True

elif input_text.isnumeric():
Expand Down
30 changes: 18 additions & 12 deletions Options.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
from __future__ import annotations

import abc
import logging
from copy import deepcopy
from dataclasses import dataclass
import functools
import logging
import math
import numbers
import random
import typing
from copy import deepcopy
from dataclasses import dataclass

from schema import And, Optional, Or, Schema

from Utils import get_fuzzy_results
from Utils import get_fuzzy_results, is_iterable_of_str

if typing.TYPE_CHECKING:
from BaseClasses import PlandoOptions
Expand Down Expand Up @@ -59,6 +58,7 @@ def __new__(mcs, name, bases, attrs):
def verify(self, *args, **kwargs) -> None:
for f in verifiers:
f(self, *args, **kwargs)

attrs["verify"] = verify
else:
assert verifiers, "class Option is supposed to implement def verify"
Expand Down Expand Up @@ -183,6 +183,7 @@ def get_option_name(cls, value: str) -> str:

class NumericOption(Option[int], numbers.Integral, abc.ABC):
default = 0

# note: some of the `typing.Any`` here is a result of unresolved issue in python standards
# `int` is not a `numbers.Integral` according to the official typestubs
# (even though isinstance(5, numbers.Integral) == True)
Expand Down Expand Up @@ -598,7 +599,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P
if isinstance(self.value, int):
return
from BaseClasses import PlandoOptions
if not(PlandoOptions.bosses & plando_options):
if not (PlandoOptions.bosses & plando_options):
# plando is disabled but plando options were given so pull the option and change it to an int
option = self.value.split(";")[-1]
self.value = self.options[option]
Expand Down Expand Up @@ -765,7 +766,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
value: typing.Any

@classmethod
def verify_keys(cls, data: typing.List[str]):
def verify_keys(cls, data: typing.Iterable[str]) -> None:
if cls.valid_keys:
data = set(data)
dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data)
Expand Down Expand Up @@ -843,11 +844,11 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
# If only unique entries are needed and input order of elements does not matter, OptionSet should be used instead.
# Not a docstring so it doesn't get grabbed by the options system.

default: typing.List[typing.Any] = []
default: typing.Union[typing.List[typing.Any], typing.Tuple[typing.Any, ...]] = ()
supports_weighting = False

def __init__(self, value: typing.List[typing.Any]):
self.value = deepcopy(value)
def __init__(self, value: typing.Iterable[str]):
self.value = list(deepcopy(value))
super(OptionList, self).__init__()

@classmethod
Expand All @@ -856,7 +857,7 @@ def from_text(cls, text: str):

@classmethod
def from_any(cls, data: typing.Any):
if type(data) == list:
if is_iterable_of_str(data):
cls.verify_keys(data)
return cls(data)
return cls.from_text(str(data))
Expand All @@ -882,7 +883,7 @@ def from_text(cls, text: str):

@classmethod
def from_any(cls, data: typing.Any):
if isinstance(data, (list, set, frozenset)):
if is_iterable_of_str(data):
cls.verify_keys(data)
return cls(data)
return cls.from_text(str(data))
Expand Down Expand Up @@ -932,7 +933,7 @@ def __new__(mcs,
bases: typing.Tuple[type, ...],
attrs: typing.Dict[str, typing.Any]) -> "OptionsMetaProperty":
for attr_type in attrs.values():
assert not isinstance(attr_type, AssembleOptions),\
assert not isinstance(attr_type, AssembleOptions), \
f"Options for {name} should be type hinted on the class, not assigned"
return super().__new__(mcs, name, bases, attrs)

Expand Down Expand Up @@ -1110,6 +1111,11 @@ class PerGameCommonOptions(CommonOptions):
item_links: ItemLinks


@dataclass
class DeathLinkMixin:
death_link: DeathLink


def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True):
import os

Expand Down
11 changes: 11 additions & 0 deletions Utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from argparse import Namespace
from settings import Settings, get_settings
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
from typing_extensions import TypeGuard
from yaml import load, load_all, dump

try:
Expand Down Expand Up @@ -966,3 +967,13 @@ def __bool__(self):

def __len__(self):
return sum(len(iterable) for iterable in self.iterable)


def is_iterable_of_str(obj: object) -> TypeGuard[typing.Iterable[str]]:
""" but not a `str` (because technically, `str` is `Iterable[str]`) """
if isinstance(obj, str):
return False
if not isinstance(obj, typing.Iterable):
return False
obj_it: typing.Iterable[object] = obj
return all(isinstance(v, str) for v in obj_it)
4 changes: 4 additions & 0 deletions docs/settings api.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,10 @@ Path to a single file. Automatically resolves as user_path:
Source folder or AP install path on Windows. ~/Archipelago for the AppImage.
Will open a file browser if the file is missing when in GUI mode.

If the file is used in the world's `generate_output`, make sure to add a `stage_assert_generate` that checks if the
file is available, otherwise generation may fail at the very end.
See also [world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md#generation).

#### class method validate(cls, path: str)

Override this and raise ValueError if validation fails.
Expand Down
13 changes: 13 additions & 0 deletions kvui.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@
from kivy.factory import Factory
from kivy.properties import BooleanProperty, ObjectProperty
from kivy.metrics import dp
from kivy.effects.scroll import ScrollEffect
from kivy.uix.widget import Widget
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.scrollview import ScrollView
from kivy.uix.recycleview import RecycleView
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
from kivy.uix.boxlayout import BoxLayout
Expand Down Expand Up @@ -118,6 +120,17 @@ class ServerToolTip(ToolTip):
pass


class ScrollBox(ScrollView):
layout: BoxLayout

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.layout = BoxLayout(size_hint_y=None)
self.layout.bind(minimum_height=self.layout.setter("height"))
self.add_widget(self.layout)
self.effect_cls = ScrollEffect


class HovererableLabel(HoverBehavior, Label):
pass

Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ certifi>=2023.11.17
cython>=3.0.8
cymem>=2.0.8
orjson>=3.9.10
typing-extensions>=4.7.0
1 change: 0 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,6 @@
"Super Mario 64",
"VVVVVV",
"Wargroove",
"Zillion",
}

# LogicMixin is broken before 3.10 import revamp
Expand Down
2 changes: 1 addition & 1 deletion typings/kivy/graphics/texture.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ class FillType_Drawable:


class Texture:
pass
size: FillType_Vec
17 changes: 16 additions & 1 deletion worlds/Files.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ def get_handler(file: str) -> Optional[AutoPatchRegister]:
current_patch_version: int = 5


class InvalidDataError(Exception):
"""
Since games can override `read_contents` in APContainer,
this is to report problems in that process.
"""


class APContainer:
"""A zipfile containing at least archipelago.json"""
version: int = current_patch_version
Expand Down Expand Up @@ -89,7 +96,15 @@ def read(self, file: Optional[Union[str, BinaryIO]] = None) -> None:
with zipfile.ZipFile(zip_file, "r") as zf:
if file:
self.path = zf.filename
self.read_contents(zf)
try:
self.read_contents(zf)
except Exception as e:
message = ""
if len(e.args):
arg0 = e.args[0]
if isinstance(arg0, str):
message = f"{arg0} - "
raise InvalidDataError(f"{message}This might be the incorrect world version for this file") from e

def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
with opened_zipfile.open("archipelago.json", "r") as f:
Expand Down
4 changes: 2 additions & 2 deletions worlds/alttp/Options.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,9 @@ class OpenPyramid(Choice):

def to_bool(self, world: MultiWorld, player: int) -> bool:
if self.value == self.option_goal:
return world.goal[player] in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
return world.goal[player].current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
elif self.value == self.option_auto:
return world.goal[player] in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
return world.goal[player].current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
and (world.entrance_shuffle[player] in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
world.shuffle_ganon)
elif self.value == self.option_open:
Expand Down
Loading

0 comments on commit 2bcc676

Please sign in to comment.