Skip to content

Commit 1bbcaa5

Browse files
authored
Merge branch 'ArchipelagoMW:main' into StardewValley/5.x.x
2 parents 5b8f9ae + 37a871e commit 1bbcaa5

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+837
-473
lines changed

BaseClasses.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -717,14 +717,23 @@ def can_reach(self,
717717
assert isinstance(player, int), "can_reach: player is required if spot is str"
718718
# try to resolve a name
719719
if resolution_hint == 'Location':
720-
spot = self.multiworld.get_location(spot, player)
720+
return self.can_reach_location(spot, player)
721721
elif resolution_hint == 'Entrance':
722-
spot = self.multiworld.get_entrance(spot, player)
722+
return self.can_reach_entrance(spot, player)
723723
else:
724724
# default to Region
725-
spot = self.multiworld.get_region(spot, player)
725+
return self.can_reach_region(spot, player)
726726
return spot.can_reach(self)
727727

728+
def can_reach_location(self, spot: str, player: int) -> bool:
729+
return self.multiworld.get_location(spot, player).can_reach(self)
730+
731+
def can_reach_entrance(self, spot: str, player: int) -> bool:
732+
return self.multiworld.get_entrance(spot, player).can_reach(self)
733+
734+
def can_reach_region(self, spot: str, player: int) -> bool:
735+
return self.multiworld.get_region(spot, player).can_reach(self)
736+
728737
def sweep_for_events(self, key_only: bool = False, locations: Optional[Iterable[Location]] = None) -> None:
729738
if locations is None:
730739
locations = self.multiworld.get_filled_locations()

Launcher.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ def launch(exe, in_terminal=False):
161161

162162

163163
def run_gui():
164-
from kvui import App, ContainerLayout, GridLayout, Button, Label
164+
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget
165165
from kivy.uix.image import AsyncImage
166166
from kivy.uix.relativelayout import RelativeLayout
167167

@@ -185,11 +185,16 @@ def build(self):
185185
self.container = ContainerLayout()
186186
self.grid = GridLayout(cols=2)
187187
self.container.add_widget(self.grid)
188-
self.grid.add_widget(Label(text="General"))
189-
self.grid.add_widget(Label(text="Clients"))
190-
button_layout = self.grid # make buttons fill the window
191-
192-
def build_button(component: Component):
188+
self.grid.add_widget(Label(text="General", size_hint_y=None, height=40))
189+
self.grid.add_widget(Label(text="Clients", size_hint_y=None, height=40))
190+
tool_layout = ScrollBox()
191+
tool_layout.layout.orientation = "vertical"
192+
self.grid.add_widget(tool_layout)
193+
client_layout = ScrollBox()
194+
client_layout.layout.orientation = "vertical"
195+
self.grid.add_widget(client_layout)
196+
197+
def build_button(component: Component) -> Widget:
193198
"""
194199
Builds a button widget for a given component.
195200
@@ -200,31 +205,26 @@ def build_button(component: Component):
200205
None. The button is added to the parent grid layout.
201206
202207
"""
203-
button = Button(text=component.display_name)
208+
button = Button(text=component.display_name, size_hint_y=None, height=40)
204209
button.component = component
205210
button.bind(on_release=self.component_action)
206211
if component.icon != "icon":
207212
image = AsyncImage(source=icon_paths[component.icon],
208213
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
209-
box_layout = RelativeLayout()
214+
box_layout = RelativeLayout(size_hint_y=None, height=40)
210215
box_layout.add_widget(button)
211216
box_layout.add_widget(image)
212-
button_layout.add_widget(box_layout)
213-
else:
214-
button_layout.add_widget(button)
217+
return box_layout
218+
return button
215219

216220
for (tool, client) in itertools.zip_longest(itertools.chain(
217221
self._tools.items(), self._miscs.items(), self._adjusters.items()), self._clients.items()):
218222
# column 1
219223
if tool:
220-
build_button(tool[1])
221-
else:
222-
button_layout.add_widget(Label())
224+
tool_layout.layout.add_widget(build_button(tool[1]))
223225
# column 2
224226
if client:
225-
build_button(client[1])
226-
else:
227-
button_layout.add_widget(Label())
227+
client_layout.layout.add_widget(build_button(client[1]))
228228

229229
return self.container
230230

MultiServer.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,8 @@ def get_aliased_name(self, team: int, slot: int):
656656
else:
657657
return self.player_names[team, slot]
658658

659-
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False):
659+
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False,
660+
recipients: typing.Sequence[int] = None):
660661
"""Send and remember hints."""
661662
if only_new:
662663
hints = [hint for hint in hints if hint not in self.hints[team, hint.finding_player]]
@@ -685,12 +686,13 @@ def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: b
685686
for slot in new_hint_events:
686687
self.on_new_hint(team, slot)
687688
for slot, hint_data in concerns.items():
688-
clients = self.clients[team].get(slot)
689-
if not clients:
690-
continue
691-
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)]
692-
for client in clients:
693-
async_start(self.send_msgs(client, client_hints))
689+
if recipients is None or slot in recipients:
690+
clients = self.clients[team].get(slot)
691+
if not clients:
692+
continue
693+
client_hints = [datum[1] for datum in sorted(hint_data, key=lambda x: x[0].finding_player == slot)]
694+
for client in clients:
695+
async_start(self.send_msgs(client, client_hints))
694696

695697
# "events"
696698

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

14371443
elif input_text.isnumeric():

Options.py

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
from __future__ import annotations
22

33
import abc
4-
import logging
5-
from copy import deepcopy
6-
from dataclasses import dataclass
74
import functools
5+
import logging
86
import math
97
import numbers
108
import random
119
import typing
1210
from copy import deepcopy
11+
from dataclasses import dataclass
1312

1413
from schema import And, Optional, Or, Schema
1514

16-
from Utils import get_fuzzy_results
15+
from Utils import get_fuzzy_results, is_iterable_of_str
1716

1817
if typing.TYPE_CHECKING:
1918
from BaseClasses import PlandoOptions
@@ -59,6 +58,7 @@ def __new__(mcs, name, bases, attrs):
5958
def verify(self, *args, **kwargs) -> None:
6059
for f in verifiers:
6160
f(self, *args, **kwargs)
61+
6262
attrs["verify"] = verify
6363
else:
6464
assert verifiers, "class Option is supposed to implement def verify"
@@ -183,6 +183,7 @@ def get_option_name(cls, value: str) -> str:
183183

184184
class NumericOption(Option[int], numbers.Integral, abc.ABC):
185185
default = 0
186+
186187
# note: some of the `typing.Any`` here is a result of unresolved issue in python standards
187188
# `int` is not a `numbers.Integral` according to the official typestubs
188189
# (even though isinstance(5, numbers.Integral) == True)
@@ -598,7 +599,7 @@ def verify(self, world: typing.Type[World], player_name: str, plando_options: "P
598599
if isinstance(self.value, int):
599600
return
600601
from BaseClasses import PlandoOptions
601-
if not(PlandoOptions.bosses & plando_options):
602+
if not (PlandoOptions.bosses & plando_options):
602603
# plando is disabled but plando options were given so pull the option and change it to an int
603604
option = self.value.split(";")[-1]
604605
self.value = self.options[option]
@@ -765,7 +766,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
765766
value: typing.Any
766767

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

846-
default: typing.List[typing.Any] = []
847+
default: typing.Union[typing.List[typing.Any], typing.Tuple[typing.Any, ...]] = ()
847848
supports_weighting = False
848849

849-
def __init__(self, value: typing.List[typing.Any]):
850-
self.value = deepcopy(value)
850+
def __init__(self, value: typing.Iterable[str]):
851+
self.value = list(deepcopy(value))
851852
super(OptionList, self).__init__()
852853

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

857858
@classmethod
858859
def from_any(cls, data: typing.Any):
859-
if type(data) == list:
860+
if is_iterable_of_str(data):
860861
cls.verify_keys(data)
861862
return cls(data)
862863
return cls.from_text(str(data))
@@ -882,7 +883,7 @@ def from_text(cls, text: str):
882883

883884
@classmethod
884885
def from_any(cls, data: typing.Any):
885-
if isinstance(data, (list, set, frozenset)):
886+
if is_iterable_of_str(data):
886887
cls.verify_keys(data)
887888
return cls(data)
888889
return cls.from_text(str(data))
@@ -932,7 +933,7 @@ def __new__(mcs,
932933
bases: typing.Tuple[type, ...],
933934
attrs: typing.Dict[str, typing.Any]) -> "OptionsMetaProperty":
934935
for attr_type in attrs.values():
935-
assert not isinstance(attr_type, AssembleOptions),\
936+
assert not isinstance(attr_type, AssembleOptions), \
936937
f"Options for {name} should be type hinted on the class, not assigned"
937938
return super().__new__(mcs, name, bases, attrs)
938939

@@ -1110,6 +1111,11 @@ class PerGameCommonOptions(CommonOptions):
11101111
item_links: ItemLinks
11111112

11121113

1114+
@dataclass
1115+
class DeathLinkMixin:
1116+
death_link: DeathLink
1117+
1118+
11131119
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True):
11141120
import os
11151121

Utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from argparse import Namespace
2020
from settings import Settings, get_settings
2121
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
22+
from typing_extensions import TypeGuard
2223
from yaml import load, load_all, dump
2324

2425
try:
@@ -966,3 +967,13 @@ def __bool__(self):
966967

967968
def __len__(self):
968969
return sum(len(iterable) for iterable in self.iterable)
970+
971+
972+
def is_iterable_of_str(obj: object) -> TypeGuard[typing.Iterable[str]]:
973+
""" but not a `str` (because technically, `str` is `Iterable[str]`) """
974+
if isinstance(obj, str):
975+
return False
976+
if not isinstance(obj, typing.Iterable):
977+
return False
978+
obj_it: typing.Iterable[object] = obj
979+
return all(isinstance(v, str) for v in obj_it)

data/options.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@
1717
# A. This is a .yaml file. You are allowed to use most characters.
1818
# To test if your yaml is valid or not, you can use this website:
1919
# http://www.yamllint.com/
20-
# You can also verify your Archipelago settings are valid at this site:
20+
# You can also verify that your Archipelago options are valid at this site:
2121
# https://archipelago.gg/check
2222

23-
# Your name in-game. Spaces will be replaced with underscores and there is a 16-character limit.
23+
# Your name in-game, limited to 16 characters.
2424
# {player} will be replaced with the player's slot number.
2525
# {PLAYER} will be replaced with the player's slot number, if that slot number is greater than 1.
2626
# {number} will be replaced with the counter value of the name.

docs/network protocol.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ Sent to the server to update on the sender's status. Examples include readiness
345345
#### Arguments
346346
| Name | Type | Notes |
347347
| ---- | ---- | ----- |
348-
| status | ClientStatus\[int\] | One of [Client States](#Client-States). Send as int. Follow the link for more information. |
348+
| status | ClientStatus\[int\] | One of [Client States](#ClientStatus). Send as int. Follow the link for more information. |
349349

350350
### Say
351351
Basic chat command which sends text to the server to be distributed to other clients.

docs/settings api.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@ Path to a single file. Automatically resolves as user_path:
121121
Source folder or AP install path on Windows. ~/Archipelago for the AppImage.
122122
Will open a file browser if the file is missing when in GUI mode.
123123

124+
If the file is used in the world's `generate_output`, make sure to add a `stage_assert_generate` that checks if the
125+
file is available, otherwise generation may fail at the very end.
126+
See also [world api.md](https://github.com/ArchipelagoMW/Archipelago/blob/main/docs/world%20api.md#generation).
127+
124128
#### class method validate(cls, path: str)
125129

126130
Override this and raise ValueError if validation fails.

docs/world api.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ could also be progress in a research tree, or even something more abstract like
170170
Each location has a `name` and an `address` (hereafter referred to as an `id`), is placed in a Region, has access rules,
171171
and has a classification. The name needs to be unique within each game and must not be numeric (must contain least 1
172172
letter or symbol). The ID needs to be unique across all games, and is best kept in the same range as the item IDs.
173+
Locations and items can share IDs, so typically a game's locations and items start at the same ID.
173174

174175
World-specific IDs must be in the range 1 to 2<sup>53</sup>-1; IDs ≤ 0 are global and reserved.
175176

@@ -737,8 +738,9 @@ def generate_output(self, output_directory: str) -> None:
737738

738739
If the game client needs to know information about the generated seed, a preferred method of transferring the data
739740
is through the slot data. This is filled with the `fill_slot_data` method of your world by returning
740-
a `Dict[str, Any]`, but, to not waste resources, should be limited to data that is absolutely necessary. Slot data is
741-
sent to your client once it has successfully [connected](network%20protocol.md#connected).
741+
a `dict` with `str` keys that can be serialized with json.
742+
But, to not waste resources, it should be limited to data that is absolutely necessary. Slot data is sent to your client
743+
once it has successfully [connected](network%20protocol.md#connected).
742744
If you need to know information about locations in your world, instead of propagating the slot data, it is preferable
743745
to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. The most
744746
common usage of slot data is sending option results that the client needs to be aware of.

kvui.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,13 @@
3838
from kivy.factory import Factory
3939
from kivy.properties import BooleanProperty, ObjectProperty
4040
from kivy.metrics import dp
41+
from kivy.effects.scroll import ScrollEffect
4142
from kivy.uix.widget import Widget
4243
from kivy.uix.button import Button
4344
from kivy.uix.gridlayout import GridLayout
4445
from kivy.uix.layout import Layout
4546
from kivy.uix.textinput import TextInput
47+
from kivy.uix.scrollview import ScrollView
4648
from kivy.uix.recycleview import RecycleView
4749
from kivy.uix.tabbedpanel import TabbedPanel, TabbedPanelItem
4850
from kivy.uix.boxlayout import BoxLayout
@@ -118,6 +120,17 @@ class ServerToolTip(ToolTip):
118120
pass
119121

120122

123+
class ScrollBox(ScrollView):
124+
layout: BoxLayout
125+
126+
def __init__(self, *args, **kwargs):
127+
super().__init__(*args, **kwargs)
128+
self.layout = BoxLayout(size_hint_y=None)
129+
self.layout.bind(minimum_height=self.layout.setter("height"))
130+
self.add_widget(self.layout)
131+
self.effect_cls = ScrollEffect
132+
133+
121134
class HovererableLabel(HoverBehavior, Label):
122135
pass
123136

0 commit comments

Comments
 (0)