Skip to content

Commit 6ddd2e2

Browse files
committed
Merge remote-tracking branch 'upstream/main' into instruction_patch_clean
2 parents abdb90d + e33ea01 commit 6ddd2e2

File tree

123 files changed

+11395
-974
lines changed

Some content is hidden

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

123 files changed

+11395
-974
lines changed

Diff for: BaseClasses.py

+12-3
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()

Diff for: Generate.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,9 @@ def handle_name(name: str, player: int, name_counter: Counter):
302302
NUMBER=(number if number > 1 else ''),
303303
player=player,
304304
PLAYER=(player if player > 1 else '')))
305-
new_name = new_name.strip()[:16]
305+
# Run .strip twice for edge case where after the initial .slice new_name has a leading whitespace.
306+
# Could cause issues for some clients that cannot handle the additional whitespace.
307+
new_name = new_name.strip()[:16].strip()
306308
if new_name == "Archipelago":
307309
raise Exception(f"You cannot name yourself \"{new_name}\"")
308310
return new_name

Diff for: Launcher.py

+17-17
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

Diff for: MultiServer.py

+14-8
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():

Diff for: Options.py

+19-13
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]
@@ -727,7 +728,7 @@ def __new__(cls, value: int) -> SpecialRange:
727728
"Consider switching to NamedRange, which supports all use-cases of SpecialRange, and more. In "
728729
"NamedRange, range_start specifies the lower end of the regular range, while special values can be "
729730
"placed anywhere (below, inside, or above the regular range).")
730-
return super().__new__(cls, value)
731+
return super().__new__(cls)
731732

732733
@classmethod
733734
def weighted_range(cls, text) -> Range:
@@ -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

Diff for: README.md

+2
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ Currently, the following games are supported:
5959
* Landstalker: The Treasures of King Nole
6060
* Final Fantasy Mystic Quest
6161
* TUNIC
62+
* Kirby's Dream Land 3
63+
* Celeste 64
6264

6365
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
6466
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

Diff for: Utils.py

+11
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)

Diff for: data/options.yaml

+2-2
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.

Diff for: docs/CODEOWNERS

+6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
# Bumper Stickers
2929
/worlds/bumpstik/ @FelicitusNeko
3030

31+
# Celeste 64
32+
/worlds/celeste64/ @PoryGone
33+
3134
# ChecksFinder
3235
/worlds/checksfinder/ @jonloveslegos
3336

@@ -67,6 +70,9 @@
6770
# Hylics 2
6871
/worlds/hylics2/ @TRPG0
6972

73+
# Kirby's Dream Land 3
74+
/worlds/kdl3/ @Silvris
75+
7076
# Kingdom Hearts 2
7177
/worlds/kh2/ @JaredWeakStrike
7278

Diff for: docs/contributing.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,17 @@ It is recommended that automated github actions are turned on in your fork to ha
1717
You can turn them on here:
1818
![Github actions example](./img/github-actions-example.png)
1919

20-
Other than these requests, we tend to judge code on a case by case basis.
20+
* **When reviewing PRs, please leave a message about what was done.**
21+
We don't have full test coverage, so manual testing can help.
22+
For code changes that could affect multiple worlds or that could have changes in unexpected code paths, manual testing
23+
or checking if all code paths are covered by automated tests is desired. The original author may not have been able
24+
to test all possibly affected worlds, or didn't know it would affect another world. In such cases, it is helpful to
25+
state which games or settings were rolled, if any.
26+
Please also tell us if you looked at code, just did functional testing, did both, or did neither.
27+
If testing the PR depends on other PRs, please state what you merged into what for testing.
28+
We cannot determine what "LGTM" means without additional context, so that should not be the norm.
29+
30+
Other than these requests, we tend to judge code on a case-by-case basis.
2131

2232
For contribution to the website, please refer to the [WebHost README](/WebHostLib/README.md).
2333

Diff for: docs/network protocol.md

+1-1
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.

Diff for: docs/settings api.md

+4
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.

Diff for: docs/world api.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -738,8 +738,9 @@ def generate_output(self, output_directory: str) -> None:
738738

739739
If the game client needs to know information about the generated seed, a preferred method of transferring the data
740740
is through the slot data. This is filled with the `fill_slot_data` method of your world by returning
741-
a `Dict[str, Any]`, but, to not waste resources, should be limited to data that is absolutely necessary. Slot data is
742-
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).
743744
If you need to know information about locations in your world, instead of propagating the slot data, it is preferable
744745
to use [LocationScouts](network%20protocol.md#locationscouts), since that data already exists on the server. The most
745746
common usage of slot data is sending option results that the client needs to be aware of.

Diff for: inno_setup.iss

+5
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,11 @@ Root: HKCR; Subkey: "{#MyAppName}l2acpatch"; ValueData: "Arc
131131
Root: HKCR; Subkey: "{#MyAppName}l2acpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
132132
Root: HKCR; Subkey: "{#MyAppName}l2acpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
133133

134+
Root: HKCR; Subkey: ".apkdl3"; ValueData: "{#MyAppName}kdl3patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: ""; Components: client/sni
135+
Root: HKCR; Subkey: "{#MyAppName}kdl3patch"; ValueData: "Archipelago Kirby's Dream Land 3 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: ""; Components: client/sni
136+
Root: HKCR; Subkey: "{#MyAppName}kdl3patch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: ""; Components: client/sni
137+
Root: HKCR; Subkey: "{#MyAppName}kdl3patch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: ""; Components: client/sni
138+
134139
Root: HKCR; Subkey: ".apmc"; ValueData: "{#MyAppName}mcdata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
135140
Root: HKCR; Subkey: "{#MyAppName}mcdata"; ValueData: "Archipelago Minecraft Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
136141
Root: HKCR; Subkey: "{#MyAppName}mcdata\DefaultIcon"; ValueData: "{app}\ArchipelagoMinecraftClient.exe,0"; ValueType: string; ValueName: "";

0 commit comments

Comments
 (0)