Skip to content

Commit ceea0ed

Browse files
Merge branch 'ArchipelagoMW:main' into main
2 parents 9f5663f + aa3614a commit ceea0ed

File tree

147 files changed

+55895
-3197
lines changed

Some content is hidden

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

147 files changed

+55895
-3197
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Output Logs/
6363
/installdelete.iss
6464
/data/user.kv
6565
/datapackage
66+
/datapackage_export.json
6667
/custom_worlds
6768

6869
# Byte-compiled / optimized / DLL files
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<component name="ProjectRunConfigurationManager">
2-
<configuration default="false" name="Build APWorld" type="PythonConfigurationType" factoryName="Python">
2+
<configuration default="false" name="Build APWorlds" type="PythonConfigurationType" factoryName="Python">
33
<module name="Archipelago" />
44
<option name="ENV_FILES" value="" />
55
<option name="INTERPRETER_OPTIONS" value="" />

Generate.py

Lines changed: 86 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,9 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
119119
else:
120120
meta_weights = None
121121

122-
123-
player_id = 1
124-
player_files = {}
122+
player_id: int = 1
123+
player_files: dict[int, str] = {}
124+
player_errors: list[str] = []
125125
for file in os.scandir(args.player_files_path):
126126
fname = file.name
127127
if file.is_file() and not fname.startswith(".") and not fname.lower().endswith(".ini") and \
@@ -137,7 +137,11 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
137137
weights_cache[fname] = tuple(weights_for_file)
138138

139139
except Exception as e:
140-
raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e
140+
logging.exception(f"Exception reading weights in file {fname}")
141+
player_errors.append(
142+
f"{len(player_errors) + 1}. "
143+
f"File {fname} is invalid. Please fix your yaml.\n{Utils.get_all_causes(e)}"
144+
)
141145

142146
# sort dict for consistent results across platforms:
143147
weights_cache = {key: value for key, value in sorted(weights_cache.items(), key=lambda k: k[0].casefold())}
@@ -152,6 +156,10 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
152156
args.multi = max(player_id - 1, args.multi)
153157

154158
if args.multi == 0:
159+
if player_errors:
160+
errors = "\n\n".join(player_errors)
161+
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
162+
f"See logs for full tracebacks.\n\n{errors}")
155163
raise ValueError(
156164
"No individual player files found and number of players is 0. "
157165
"Provide individual player files or specify the number of players via host.yaml or --multi."
@@ -161,6 +169,10 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
161169
f"{seed_name} Seed {seed} with plando: {args.plando}")
162170

163171
if not weights_cache:
172+
if player_errors:
173+
errors = "\n\n".join(player_errors)
174+
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
175+
f"See logs for full tracebacks.\n\n{errors}")
164176
raise Exception(f"No weights found. "
165177
f"Provide a general weights file ({args.weights_file_path}) or individual player files. "
166178
f"A mix is also permitted.")
@@ -171,10 +183,6 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
171183
args.sprite_pool = dict.fromkeys(range(1, args.multi+1), None)
172184
args.name = {}
173185

174-
settings_cache: dict[str, tuple[argparse.Namespace, ...]] = \
175-
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
176-
for fname, yamls in weights_cache.items()}
177-
178186
if meta_weights:
179187
for category_name, category_dict in meta_weights.items():
180188
for key in category_dict:
@@ -197,7 +205,24 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
197205
else:
198206
yaml[category_name][key] = option
199207

200-
player_path_cache = {}
208+
settings_cache: dict[str, tuple[argparse.Namespace, ...]] = {fname: None for fname in weights_cache}
209+
if args.sameoptions:
210+
for fname, yamls in weights_cache.items():
211+
try:
212+
settings_cache[fname] = tuple(roll_settings(yaml, args.plando) for yaml in yamls)
213+
except Exception as e:
214+
logging.exception(f"Exception reading settings in file {fname}")
215+
player_errors.append(
216+
f"{len(player_errors) + 1}. "
217+
f"File {fname} is invalid. Please fix your yaml.\n{Utils.get_all_causes(e)}"
218+
)
219+
# Exit early here to avoid throwing the same errors again later
220+
if player_errors:
221+
errors = "\n\n".join(player_errors)
222+
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
223+
f"See logs for full tracebacks.\n\n{errors}")
224+
225+
player_path_cache: dict[int, str] = {}
201226
for player in range(1, args.multi + 1):
202227
player_path_cache[player] = player_files.get(player, args.weights_file_path)
203228
name_counter = Counter()
@@ -206,38 +231,62 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
206231
player = 1
207232
while player <= args.multi:
208233
path = player_path_cache[player]
209-
if path:
234+
if not path:
235+
player_errors.append(f'No weights specified for player {player}')
236+
player += 1
237+
continue
238+
239+
for doc_index, yaml in enumerate(weights_cache[path]):
240+
name = yaml.get("name")
210241
try:
211-
settings: tuple[argparse.Namespace, ...] = settings_cache[path] if settings_cache[path] else \
212-
tuple(roll_settings(yaml, args.plando) for yaml in weights_cache[path])
213-
for settingsObject in settings:
214-
for k, v in vars(settingsObject).items():
215-
if v is not None:
216-
try:
217-
getattr(args, k)[player] = v
218-
except AttributeError:
219-
setattr(args, k, {player: v})
220-
except Exception as e:
221-
raise Exception(f"Error setting {k} to {v} for player {player}") from e
222-
223-
# name was not specified
224-
if player not in args.name:
225-
if path == args.weights_file_path:
226-
# weights file, so we need to make the name unique
227-
args.name[player] = f"Player{player}"
228-
else:
229-
# use the filename
230-
args.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
231-
args.name[player] = handle_name(args.name[player], player, name_counter)
232-
233-
player += 1
242+
# Use the cached settings object if it exists, otherwise roll settings within the try-catch
243+
# Invariant: settings_cache[path] and weights_cache[path] have the same length
244+
settingsObject: argparse.Namespace = (
245+
settings_cache[path][doc_index]
246+
if settings_cache[path]
247+
else roll_settings(yaml, args.plando)
248+
)
249+
250+
for k, v in vars(settingsObject).items():
251+
if v is not None:
252+
try:
253+
getattr(args, k)[player] = v
254+
except AttributeError:
255+
setattr(args, k, {player: v})
256+
except Exception as e:
257+
raise Exception(f"Error setting {k} to {v} for player {player}") from e
258+
259+
# name was not specified
260+
if player not in args.name:
261+
if path == args.weights_file_path:
262+
# weights file, so we need to make the name unique
263+
args.name[player] = f"Player{player}"
264+
else:
265+
# use the filename
266+
args.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
267+
args.name[player] = handle_name(args.name[player], player, name_counter)
268+
234269
except Exception as e:
235-
raise ValueError(f"File {path} is invalid. Please fix your yaml.") from e
236-
else:
237-
raise RuntimeError(f'No weights specified for player {player}')
270+
logging.exception(f"Exception reading settings in file {path} document #{doc_index + 1} "
271+
f"(name: {args.name.get(player, name)})")
272+
player_errors.append(
273+
f"{len(player_errors) + 1}. "
274+
f"File {path} document #{doc_index + 1} (name: {args.name.get(player, name)}) is invalid. "
275+
f"Please fix your yaml.\n{Utils.get_all_causes(e)}")
276+
277+
# increment for each yaml document in the file
278+
player += 1
238279

239280
if len(set(name.lower() for name in args.name.values())) != len(args.name):
240-
raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in args.name.values())}")
281+
player_errors.append(
282+
f"{len(player_errors) + 1}. "
283+
f"Names have to be unique. Names: {Counter(name.lower() for name in args.name.values())}"
284+
)
285+
286+
if player_errors:
287+
errors = "\n\n".join(player_errors)
288+
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
289+
f"See logs for full tracebacks.\n\n{errors}")
241290

242291
return args, seed
243292

MultiServer.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ def remove_from_list(container, value):
6969

7070

7171
def pop_from_container(container, value):
72+
if isinstance(container, list) and isinstance(value, int) and len(container) <= value:
73+
return container
74+
75+
if isinstance(container, dict) and value not in container:
76+
return container
77+
7278
try:
7379
container.pop(value)
7480
except ValueError:
@@ -911,12 +917,6 @@ async def server(websocket: "ServerConnection", path: str = "/", ctx: Context =
911917

912918

913919
async def on_client_connected(ctx: Context, client: Client):
914-
players = []
915-
for team, clients in ctx.clients.items():
916-
for slot, connected_clients in clients.items():
917-
if connected_clients:
918-
name = ctx.player_names[team, slot]
919-
players.append(NetworkPlayer(team, slot, ctx.name_aliases.get((team, slot), name), name))
920920
games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)}
921921
games.add("Archipelago")
922922
await ctx.send_msgs(client, [{
@@ -1364,7 +1364,10 @@ def get_help_text(self) -> str:
13641364
argname += "=" + parameter.default
13651365
argtext += argname
13661366
argtext += " "
1367-
doctext = '\n '.join(inspect.getdoc(method).split('\n'))
1367+
method_doc = inspect.getdoc(method)
1368+
if method_doc is None:
1369+
method_doc = "(missing help text)"
1370+
doctext = "\n ".join(method_doc.split("\n"))
13681371
s += f"{self.marker}{command} {argtext}\n {doctext}\n"
13691372
return s
13701373

OptionsCreator.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -509,8 +509,10 @@ def randomize_option(instance: Widget, value: str):
509509
self.options[name] = "random-" + str(self.options[name])
510510
else:
511511
self.options[name] = self.options[name].replace("random-", "")
512-
if self.options[name].isnumeric() or self.options[name] in ("True", "False"):
513-
self.options[name] = eval(self.options[name])
512+
if self.options[name].isnumeric():
513+
self.options[name] = int(self.options[name])
514+
elif self.options[name] in ("True", "False"):
515+
self.options[name] = self.options[name] == "True"
514516

515517
base_object = instance.parent.parent
516518
label_object = instance.parent
@@ -632,7 +634,7 @@ def world_button_action(world_btn: WorldButton):
632634
self.create_options_panel(world_btn)
633635

634636
for world, cls in sorted(AutoWorldRegister.world_types.items(), key=lambda x: x[0]):
635-
if world == "Archipelago":
637+
if cls.hidden:
636638
continue
637639
world_text = MDButtonText(text=world, size_hint_y=None, width=dp(150),
638640
pos_hint={"x": 0.03, "center_y": 0.5})

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ Currently, the following games are supported:
8383
* Celeste (Open World)
8484
* Choo-Choo Charles
8585
* APQuest
86+
* Satisfactory
87+
* EarthBound
8688

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

Utils.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from time import sleep
2323
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
2424
from yaml import load, load_all, dump
25+
from pathspec import PathSpec, GitIgnoreSpec
2526

2627
try:
2728
from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper
@@ -48,7 +49,7 @@ def as_simple_string(self) -> str:
4849
return ".".join(str(item) for item in self)
4950

5051

51-
__version__ = "0.6.5"
52+
__version__ = "0.6.7"
5253
version_tuple = tuplize_version(__version__)
5354

5455
is_linux = sys.platform.startswith("linux")
@@ -387,6 +388,14 @@ def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> N
387388
logging.debug(f"Could not store data package: {e}")
388389

389390

391+
def read_apignore(filename: str | pathlib.Path) -> PathSpec | None:
392+
try:
393+
with open(filename) as ignore_file:
394+
return GitIgnoreSpec.from_lines(ignore_file)
395+
except FileNotFoundError:
396+
return None
397+
398+
390399
def get_default_adjuster_settings(game_name: str) -> Namespace:
391400
import LttPAdjuster
392401
adjuster_settings = Namespace()
@@ -1222,3 +1231,35 @@ def weakref_cb(_, q=self._work_queue):
12221231
t.start()
12231232
self._threads.add(t)
12241233
# NOTE: don't add to _threads_queues so we don't block on shutdown
1234+
1235+
1236+
def get_full_typename(t: type) -> str:
1237+
"""Returns the full qualified name of a type, including its module (if not builtins)."""
1238+
module = t.__module__
1239+
if module and module != "builtins":
1240+
return f"{module}.{t.__qualname__}"
1241+
return t.__qualname__
1242+
1243+
1244+
def get_all_causes(ex: Exception) -> str:
1245+
"""Return a string describing the recursive causes of this exception.
1246+
1247+
:param ex: The exception to be described.
1248+
:return A multiline string starting with the initial exception on the first line and each resulting exception
1249+
on subsequent lines with progressive indentation.
1250+
1251+
For example:
1252+
1253+
```
1254+
Exception: Invalid value 'bad'.
1255+
Which caused: Options.OptionError: Error generating option
1256+
Which caused: ValueError: File bad.yaml is invalid.
1257+
```
1258+
"""
1259+
cause = ex
1260+
causes = [f"{get_full_typename(type(ex))}: {ex}"]
1261+
while cause := cause.__cause__:
1262+
causes.append(f"{get_full_typename(type(cause))}: {cause}")
1263+
top = causes[-1]
1264+
others = "".join(f"\n{' ' * (i + 1)}Which caused: {c}" for i, c in enumerate(reversed(causes[:-1])))
1265+
return f"{top}{others}"

WebHostLib/__init__.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,30 @@
2323
app.jinja_env.filters['all'] = all
2424
app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name
2525

26+
# overwrites of flask default config
27+
app.config["DEBUG"] = False
28+
app.config["PORT"] = 80
29+
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
30+
app.config["MAX_CONTENT_LENGTH"] = 64 * 1024 * 1024 # 64 megabyte limit
31+
# if you want to deploy, make sure you have a non-guessable secret key
32+
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
33+
app.config["SESSION_PERMANENT"] = True
34+
app.config["MAX_FORM_MEMORY_SIZE"] = 2 * 1024 * 1024 # 2 MB, needed for large option pages such as SC2
35+
36+
# custom config
2637
app.config["SELFHOST"] = True # application process is in charge of running the websites
2738
app.config["GENERATORS"] = 8 # maximum concurrent world gens
2839
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
2940
app.config["SELFLAUNCH"] = True # application process is in charge of launching Rooms.
3041
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
3142
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
3243
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
33-
app.config["DEBUG"] = False
34-
app.config["PORT"] = 80
35-
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
36-
app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit
37-
# if you want to deploy, make sure you have a non-guessable secret key
38-
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
3944
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
4045
app.config["JOB_THRESHOLD"] = 1
4146
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
4247
app.config["JOB_TIME"] = 600
4348
# memory limit for generator processes in bytes
4449
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
45-
app.config['SESSION_PERMANENT'] = True
4650

4751
# waitress uses one thread for I/O, these are for processing of views that then get sent
4852
# archipelago.gg uses gunicorn + nginx; ignoring this option

WebHostLib/customserver.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ async def start_room(room_id):
325325

326326
except (KeyboardInterrupt, SystemExit):
327327
if ctx.saving:
328-
ctx._save()
328+
ctx._save(True)
329329
setattr(asyncio.current_task(), "save", None)
330330
except Exception as e:
331331
with db_session:
@@ -336,7 +336,7 @@ async def start_room(room_id):
336336
raise
337337
else:
338338
if ctx.saving:
339-
ctx._save()
339+
ctx._save(True)
340340
setattr(asyncio.current_task(), "save", None)
341341
finally:
342342
try:

WebHostLib/misc.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,13 @@ def tutorial_landing():
128128
"authors": tutorial.authors,
129129
"language": tutorial.language
130130
}
131-
tutorials = {world_name: tutorials for world_name, tutorials in title_sorted(
132-
tutorials.items(), key=lambda element: "\x00" if element[0] == "Archipelago" else worlds[element[0]].game)}
131+
132+
worlds = dict(
133+
title_sorted(
134+
worlds.items(), key=lambda element: "\x00" if element[0] == "Archipelago" else worlds[element[0]].game
135+
)
136+
)
137+
133138
return render_template("tutorialLanding.html", worlds=worlds, tutorials=tutorials)
134139

135140

0 commit comments

Comments
 (0)