Compare commits

...

9 Commits

Author SHA1 Message Date
NewSoupVi
f6ffff674c Merge branch 'main' into NewSoupVi-patch-20 2025-04-05 03:42:23 +02:00
Richard Snider
507e051a5a Core: Handle integer arguments in player names gracefully (#4151) 2025-04-05 03:36:20 +02:00
Scipio Wright
b5bf9ed1d7 TUNIC: Error message in the spot that UT errors at if you have an old APWorld #4788
Schnice and Shrimple
2025-04-05 00:53:13 +02:00
Fabian Dill
215eb7e473 core: increment version (#4808) 2025-04-04 23:25:37 +02:00
qwint
f42233699a Core: make accessibility_corrections only state.remove if the location was collected 2025-04-04 23:20:45 +02:00
massimilianodelliubaldini
1bec68df4d WebHost: Standardize some 404 redirects (#4642) 2025-04-04 23:11:45 +02:00
NewSoupVi
7b21121df1 Update AutoWorld.py 2024-09-21 23:00:46 +02:00
NewSoupVi
0fdc481082 Verbose af 2024-09-21 18:12:35 +02:00
NewSoupVi
92ca11b729 Core: Prevent people from using LogicMixin incorrectly
There's a world that ran into some issues because it defined its custom LogicMixin variables at the class level.

This caused "instance bleed" when new CollectionState objects were created.

I don't think there is ever a reason to have a non-function class variable on LogicMixin without also having `init_mixin`, so this asserts that this is the case.

Tested:
Doesn't fail any current worlds
Correctly fails the world in question

Also, not gonna call out that world because it was literally my fault for explaining it to them wrong :D
2024-09-21 18:02:24 +02:00
16 changed files with 60 additions and 23 deletions

View File

@@ -348,10 +348,10 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
if (location.item is not None and location.item.advancement and location.address is not None and not if (location.item is not None and location.item.advancement and location.address is not None and not
location.locked and location.item.player not in minimal_players): location.locked and location.item.player not in minimal_players):
pool.append(location.item) pool.append(location.item)
state.remove(location.item)
location.item = None location.item = None
if location in state.advancements: if location in state.advancements:
state.advancements.remove(location) state.advancements.remove(location)
state.remove(location.item)
locations.append(location) locations.append(location)
if pool and locations: if pool and locations:
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY) locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)

View File

@@ -279,22 +279,30 @@ def get_choice(option, root, value=None) -> Any:
raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.") raise RuntimeError(f"All options specified in \"{option}\" are weighted as zero.")
class SafeDict(dict): class SafeFormatter(string.Formatter):
def __missing__(self, key): def get_value(self, key, args, kwargs):
return '{' + key + '}' if isinstance(key, int):
if key < len(args):
return args[key]
else:
return "{" + str(key) + "}"
else:
return kwargs.get(key, "{" + key + "}")
def handle_name(name: str, player: int, name_counter: Counter): def handle_name(name: str, player: int, name_counter: Counter):
name_counter[name.lower()] += 1 name_counter[name.lower()] += 1
number = name_counter[name.lower()] number = name_counter[name.lower()]
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")]) new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
new_name = string.Formatter().vformat(new_name, (), SafeDict(number=number,
NUMBER=(number if number > 1 else ''), new_name = SafeFormatter().vformat(new_name, (), {"number": number,
player=player, "NUMBER": (number if number > 1 else ''),
PLAYER=(player if player > 1 else ''))) "player": player,
"PLAYER": (player if player > 1 else '')})
# Run .strip twice for edge case where after the initial .slice new_name has a leading whitespace. # Run .strip twice for edge case where after the initial .slice new_name has a leading whitespace.
# Could cause issues for some clients that cannot handle the additional whitespace. # Could cause issues for some clients that cannot handle the additional whitespace.
new_name = new_name.strip()[:16].strip() new_name = new_name.strip()[:16].strip()
if new_name == "Archipelago": if new_name == "Archipelago":
raise Exception(f"You cannot name yourself \"{new_name}\"") raise Exception(f"You cannot name yourself \"{new_name}\"")
return new_name return new_name

View File

@@ -47,7 +47,7 @@ class Version(typing.NamedTuple):
return ".".join(str(item) for item in self) return ".".join(str(item) for item in self)
__version__ = "0.6.1" __version__ = "0.6.2"
version_tuple = tuplize_version(__version__) version_tuple = tuplize_version(__version__)
is_linux = sys.platform.startswith("linux") is_linux = sys.platform.startswith("linux")

View File

@@ -35,6 +35,12 @@ def start_playing():
@app.route('/games/<string:game>/info/<string:lang>') @app.route('/games/<string:game>/info/<string:lang>')
@cache.cached() @cache.cached()
def game_info(game, lang): def game_info(game, lang):
try:
world = AutoWorldRegister.world_types[game]
if lang not in world.web.game_info_languages:
raise KeyError("Sorry, this game's info page is not available in that language yet.")
except KeyError:
return abort(404)
return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game)) return render_template('gameInfo.html', game=game, lang=lang, theme=get_world_theme(game))
@@ -52,6 +58,12 @@ def games():
@app.route('/tutorial/<string:game>/<string:file>/<string:lang>') @app.route('/tutorial/<string:game>/<string:file>/<string:lang>')
@cache.cached() @cache.cached()
def tutorial(game, file, lang): def tutorial(game, file, lang):
try:
world = AutoWorldRegister.world_types[game]
if lang not in [tut.link.split("/")[1] for tut in world.web.tutorials]:
raise KeyError("Sorry, the tutorial is not available in that language yet.")
except KeyError:
return abort(404)
return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game)) return render_template("tutorial.html", game=game, file=file, lang=lang, theme=get_world_theme(game))

View File

@@ -6,7 +6,7 @@ from typing import Dict, Union
from docutils.core import publish_parts from docutils.core import publish_parts
import yaml import yaml
from flask import redirect, render_template, request, Response from flask import redirect, render_template, request, Response, abort
import Options import Options
from Utils import local_path from Utils import local_path
@@ -142,7 +142,10 @@ def weighted_options_old():
@app.route("/games/<string:game>/weighted-options") @app.route("/games/<string:game>/weighted-options")
@cache.cached() @cache.cached()
def weighted_options(game: str): def weighted_options(game: str):
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True) try:
return render_options_page("weightedOptions/weightedOptions.html", game, is_complex=True)
except KeyError:
return abort(404)
@app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"]) @app.route("/games/<string:game>/generate-weighted-yaml", methods=["POST"])
@@ -197,7 +200,10 @@ def generate_weighted_yaml(game: str):
@app.route("/games/<string:game>/player-options") @app.route("/games/<string:game>/player-options")
@cache.cached() @cache.cached()
def player_options(game: str): def player_options(game: str):
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False) try:
return render_options_page("playerOptions/playerOptions.html", game, is_complex=False)
except KeyError:
return abort(404)
# YAML generator for player-options # YAML generator for player-options

View File

@@ -42,10 +42,5 @@ window.addEventListener('load', () => {
scrollTarget?.scrollIntoView(); scrollTarget?.scrollIntoView();
} }
}); });
}).catch((error) => {
console.error(error);
gameInfo.innerHTML =
`<h2>This page is out of logic!</h2>
<h3>Click <a href="${window.location.origin}">here</a> to return to safety.</h3>`;
}); });
}); });

View File

@@ -49,10 +49,5 @@ window.addEventListener('load', () => {
scrollTarget?.scrollIntoView(); scrollTarget?.scrollIntoView();
} }
}); });
}).catch((error) => {
console.error(error);
tutorialWrapper.innerHTML =
`<h2>This page is out of logic!</h2>
<h3>Click <a href="${window.location.origin}/tutorial">here</a> to return to safety.</h3>`;
}); });
}); });

View File

@@ -110,6 +110,16 @@ class AutoLogicRegister(type):
elif not item_name.startswith("__"): elif not item_name.startswith("__"):
if hasattr(CollectionState, item_name): if hasattr(CollectionState, item_name):
raise Exception(f"Name conflict on Logic Mixin {name} trying to overwrite {item_name}") raise Exception(f"Name conflict on Logic Mixin {name} trying to overwrite {item_name}")
assert callable(function) or "init_mixin" in dct, (
f"{name} defined class variable {item_name} without also having init_mixin.\n\n"
"Explanation:\n"
"Class variables that will be mutated need to be inintialized as instance variables in init_mixin.\n"
"If your LogicMixin variables aren't actually mutable / you don't intend to mutate them, "
"there is no point in using LogixMixin.\n"
"LogicMixin exists to track custom state variables that change when items are collected/removed."
)
setattr(CollectionState, item_name, function) setattr(CollectionState, item_name, function)
return new_class return new_class

View File

@@ -121,6 +121,7 @@ class ALTTPWeb(WebWorld):
) )
tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound] tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound]
game_info_languages = ["en", "fr"]
class ALTTPWorld(World): class ALTTPWorld(World):

View File

@@ -41,6 +41,7 @@ class AquariaWeb(WebWorld):
) )
tutorials = [setup, setup_fr] tutorials = [setup, setup_fr]
game_info_languages = ["en", "fr"]
class AquariaWorld(World): class AquariaWorld(World):

View File

@@ -31,6 +31,7 @@ class CliqueWebWorld(WebWorld):
) )
tutorials = [setup_en, setup_de] tutorials = [setup_en, setup_de]
game_info_languages = ["en", "de"]
class CliqueWorld(World): class CliqueWorld(World):

View File

@@ -34,6 +34,7 @@ class DLCqwebworld(WebWorld):
["Deoxis"] ["Deoxis"]
) )
tutorials = [setup_en, setup_fr] tutorials = [setup_en, setup_fr]
game_info_languages = ["en", "fr"]
class DLCqworld(World): class DLCqworld(World):

View File

@@ -43,6 +43,7 @@ class FFMQWebWorld(WebWorld):
) )
tutorials = [setup_en, setup_fr] tutorials = [setup_en, setup_fr]
game_info_languages = ["en", "fr"]
class FFMQWorld(World): class FFMQWorld(World):

View File

@@ -130,6 +130,7 @@ class OOTWeb(WebWorld):
tutorials = [setup, setup_fr, setup_de] tutorials = [setup, setup_fr, setup_de]
option_groups = oot_option_groups option_groups = oot_option_groups
game_info_languages = ["en", "de"]
class OOTWorld(World): class OOTWorld(World):

View File

@@ -41,6 +41,7 @@ class Starcraft2WebWorld(WebWorld):
) )
tutorials = [setup_en, setup_fr] tutorials = [setup_en, setup_fr]
game_info_languages = ["en", "fr"]
class SC2World(World): class SC2World(World):

View File

@@ -330,7 +330,11 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
else: else:
if not portal2: if not portal2:
raise Exception(f"Could not find entrance named {p_exit} for " raise Exception(f"Could not find entrance named {p_exit} for "
f"plando connections in {player_name}'s YAML.") f"plando connections in {player_name}'s YAML.\n"
f"If you are using Universal Tracker, the most likely reason for this error "
f"is that the host generated with a newer version of the APWorld.\n"
f"Please check the TUNIC Randomizer Github and place the newest APWorld in your "
f"custom_worlds folder, and remove the one in lib/worlds if there is one there.")
dead_ends.remove(portal2) dead_ends.remove(portal2)
# update the traversal chart to say you can get from portal1's region to portal2's and vice versa # update the traversal chart to say you can get from portal1's region to portal2's and vice versa