From 5f8a8e6dade04007e729d36a329508e4a74f2eb4 Mon Sep 17 00:00:00 2001 From: jamesbrq Date: Sun, 9 Jun 2024 10:54:07 -0400 Subject: [PATCH 01/58] Update Rom.py (#3498) --- worlds/mlss/Rom.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/mlss/Rom.py b/worlds/mlss/Rom.py index 08921500dacb..7cbbe8875195 100644 --- a/worlds/mlss/Rom.py +++ b/worlds/mlss/Rom.py @@ -306,8 +306,7 @@ def write_tokens(world: "MLSSWorld", patch: MLSSProcedurePatch) -> None: if world.options.scale_stats: patch.write_token(APTokenTypes.WRITE, 0xD00002, bytes([0x1])) - if world.options.xp_multiplier: - patch.write_token(APTokenTypes.WRITE, 0xD00003, bytes([world.options.xp_multiplier.value])) + patch.write_token(APTokenTypes.WRITE, 0xD00003, bytes([world.options.xp_multiplier.value])) if world.options.tattle_hp: patch.write_token(APTokenTypes.WRITE, 0xD00000, bytes([0x1])) From 84a6d50ae7453f87af3dcc67a6cda32ea4767c9c Mon Sep 17 00:00:00 2001 From: Phaneros <31861583+MatthewMarinets@users.noreply.github.com> Date: Sun, 9 Jun 2024 07:55:05 -0700 Subject: [PATCH 02/58] sc2: Fixed sc2 client's /received command breaking after PR 1933 merged (#3497) --- worlds/sc2/Client.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/sc2/Client.py b/worlds/sc2/Client.py index c97902fbcb86..e6696b782da2 100644 --- a/worlds/sc2/Client.py +++ b/worlds/sc2/Client.py @@ -107,10 +107,10 @@ def __call__(self, text: str) -> 'ColouredMessage': def coloured(self, text: str, colour: str) -> 'ColouredMessage': add_json_text(self.parts, text, type="color", color=colour) return self - def location(self, location_id: int, player_id: int = 0) -> 'ColouredMessage': + def location(self, location_id: int, player_id: int) -> 'ColouredMessage': add_json_location(self.parts, location_id, player_id) return self - def item(self, item_id: int, player_id: int = 0, flags: int = 0) -> 'ColouredMessage': + def item(self, item_id: int, player_id: int, flags: int = 0) -> 'ColouredMessage': add_json_item(self.parts, item_id, player_id, flags) return self def player(self, player_id: int) -> 'ColouredMessage': @@ -256,7 +256,7 @@ def print_faction_title(): for item in received_items_of_this_type: print_faction_title() has_printed_faction_title = True - (ColouredMessage('* ').item(item.item, flags=item.flags) + (ColouredMessage('* ').item(item.item, self.ctx.slot, flags=item.flags) (" from ").location(item.location, self.ctx.slot) (" by ").player(item.player) ).send(self.ctx) @@ -277,7 +277,7 @@ def print_faction_title(): received_items_of_this_type = items_received.get(child_item, []) for item in received_items_of_this_type: filter_match_count += len(received_items_of_this_type) - (ColouredMessage(' * ').item(item.item, flags=item.flags) + (ColouredMessage(' * ').item(item.item, self.ctx.slot, flags=item.flags) (" from ").location(item.location, self.ctx.slot) (" by ").player(item.player) ).send(self.ctx) From 0a912808e37ea67064252dc4edb83340074731ba Mon Sep 17 00:00:00 2001 From: Phaneros <31861583+MatthewMarinets@users.noreply.github.com> Date: Sun, 9 Jun 2024 17:05:39 -0700 Subject: [PATCH 03/58] SC2: update inno_setup.iss to remove old sc2wol world folder (#3495) --- inno_setup.iss | 3 +++ 1 file changed, 3 insertions(+) diff --git a/inno_setup.iss b/inno_setup.iss index a0f4944d989f..f2e850e07f20 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -89,6 +89,9 @@ Type: files; Name: "{app}\ArchipelagoPokemonClient.exe" Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua" Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy" Type: dirifempty; Name: "{app}\lib\worlds\rogue-legacy" +Type: files; Name: "{app}\lib\worlds\sc2wol.apworld" +Type: filesandordirs; Name: "{app}\lib\worlds\sc2wol" +Type: dirifempty; Name: "{app}\lib\worlds\sc2wol" Type: filesandordirs; Name: "{app}\lib\worlds\bk_sudoku" Type: dirifempty; Name: "{app}\lib\worlds\bk_sudoku" Type: files; Name: "{app}\ArchipelagoLauncher(DEBUG).exe" From 35617bdac517149a2b8b4af3f1ae7e8381f2dc45 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Mon, 10 Jun 2024 02:28:28 -0500 Subject: [PATCH 04/58] Tests: Add checksum validation to the postgen datapackage test (#3456) * Tests: Add checksum validation to the postgen datapackage test * add a special case for the test world datapackage rather than hidden * add the test world to the datapackage instead of special casing around it --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- test/general/__init__.py | 5 +++++ test/general/test_ids.py | 2 ++ 2 files changed, 7 insertions(+) diff --git a/test/general/__init__.py b/test/general/__init__.py index 1d4fc80c3e55..8afd84976540 100644 --- a/test/general/__init__.py +++ b/test/general/__init__.py @@ -2,6 +2,7 @@ from typing import List, Optional, Tuple, Type, Union from BaseClasses import CollectionState, Item, ItemClassification, Location, MultiWorld, Region +from worlds import network_data_package from worlds.AutoWorld import World, call_all gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") @@ -60,6 +61,10 @@ class TestWorld(World): hidden = True +# add our test world to the data package, so we can test it later +network_data_package["games"][TestWorld.game] = TestWorld.get_data_package_data() + + def generate_test_multiworld(players: int = 1) -> MultiWorld: """ Generates a multiworld using a special Test Case World class, and seed of 0. diff --git a/test/general/test_ids.py b/test/general/test_ids.py index e4010af394f5..e51a070c1fd7 100644 --- a/test/general/test_ids.py +++ b/test/general/test_ids.py @@ -1,6 +1,7 @@ import unittest from Fill import distribute_items_restrictive +from worlds import network_data_package from worlds.AutoWorld import AutoWorldRegister, call_all from . import setup_solo_multiworld @@ -84,3 +85,4 @@ def test_postgen_datapackage(self): f"{loc_name} is not a valid item name for location_name_to_id") self.assertIsInstance(loc_id, int, f"{loc_id} for {loc_name} should be an int") + self.assertEqual(datapackage["checksum"], network_data_package["games"][gamename]["checksum"]) From 484082616f3aba18da31f579e965c0f2f1d5227e Mon Sep 17 00:00:00 2001 From: JusticePS <5125765+JusticePS@users.noreply.github.com> Date: Mon, 10 Jun 2024 15:42:01 -0700 Subject: [PATCH 05/58] Adventure: Update to use new options api (#3326) --- AdventureClient.py | 8 ++++---- worlds/adventure/Options.py | 39 ++++++++++++++++++------------------ worlds/adventure/Regions.py | 5 +++-- worlds/adventure/Rules.py | 4 ++-- worlds/adventure/__init__.py | 37 +++++++++++++++++----------------- 5 files changed, 48 insertions(+), 45 deletions(-) diff --git a/AdventureClient.py b/AdventureClient.py index 7bfbd5ef6bd3..206c55df9abd 100644 --- a/AdventureClient.py +++ b/AdventureClient.py @@ -80,7 +80,7 @@ def __init__(self, server_address, password): self.local_item_locations = {} self.dragon_speed_info = {} - options = Utils.get_options() + options = Utils.get_settings() self.display_msgs = options["adventure_options"]["display_msgs"] async def server_auth(self, password_requested: bool = False): @@ -102,7 +102,7 @@ def _set_message(self, msg: str, msg_id: int): def on_package(self, cmd: str, args: dict): if cmd == 'Connected': self.locations_array = None - if Utils.get_options()["adventure_options"].get("death_link", False): + if Utils.get_settings()["adventure_options"].get("death_link", False): self.set_deathlink = True async_start(self.get_freeincarnates_used()) elif cmd == "RoomInfo": @@ -415,8 +415,8 @@ async def atari_sync_task(ctx: AdventureContext): async def run_game(romfile): - auto_start = Utils.get_options()["adventure_options"].get("rom_start", True) - rom_args = Utils.get_options()["adventure_options"].get("rom_args") + auto_start = Utils.get_settings()["adventure_options"].get("rom_start", True) + rom_args = Utils.get_settings()["adventure_options"].get("rom_args") if auto_start is True: import webbrowser webbrowser.open(romfile) diff --git a/worlds/adventure/Options.py b/worlds/adventure/Options.py index 9e0cc9d686b8..e6a8e4c20200 100644 --- a/worlds/adventure/Options.py +++ b/worlds/adventure/Options.py @@ -2,7 +2,8 @@ from typing import Dict -from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle +from dataclasses import dataclass +from Options import Choice, Option, DefaultOnToggle, DeathLink, Range, Toggle, PerGameCommonOptions class FreeincarnateMax(Range): @@ -223,22 +224,22 @@ class StartCastle(Choice): option_white = 2 default = option_yellow +@dataclass +class AdventureOptions(PerGameCommonOptions): + dragon_slay_check: DragonSlayCheck + death_link: DeathLink + bat_logic: BatLogic + freeincarnate_max: FreeincarnateMax + dragon_rando_type: DragonRandoType + connector_multi_slot: ConnectorMultiSlot + yorgle_speed: YorgleStartingSpeed + yorgle_min_speed: YorgleMinimumSpeed + grundle_speed: GrundleStartingSpeed + grundle_min_speed: GrundleMinimumSpeed + rhindle_speed: RhindleStartingSpeed + rhindle_min_speed: RhindleMinimumSpeed + difficulty_switch_a: DifficultySwitchA + difficulty_switch_b: DifficultySwitchB + start_castle: StartCastle + -adventure_option_definitions: Dict[str, type(Option)] = { - "dragon_slay_check": DragonSlayCheck, - "death_link": DeathLink, - "bat_logic": BatLogic, - "freeincarnate_max": FreeincarnateMax, - "dragon_rando_type": DragonRandoType, - "connector_multi_slot": ConnectorMultiSlot, - "yorgle_speed": YorgleStartingSpeed, - "yorgle_min_speed": YorgleMinimumSpeed, - "grundle_speed": GrundleStartingSpeed, - "grundle_min_speed": GrundleMinimumSpeed, - "rhindle_speed": RhindleStartingSpeed, - "rhindle_min_speed": RhindleMinimumSpeed, - "difficulty_switch_a": DifficultySwitchA, - "difficulty_switch_b": DifficultySwitchB, - "start_castle": StartCastle, - -} diff --git a/worlds/adventure/Regions.py b/worlds/adventure/Regions.py index 00617b2f7164..e72806ca454f 100644 --- a/worlds/adventure/Regions.py +++ b/worlds/adventure/Regions.py @@ -1,4 +1,5 @@ from BaseClasses import MultiWorld, Region, Entrance, LocationProgressType +from Options import PerGameCommonOptions from .Locations import location_table, LocationData, AdventureLocation, dragon_room_to_region @@ -24,7 +25,7 @@ def connect(world: MultiWorld, player: int, source: str, target: str, rule: call connect(world, player, target, source, rule, True) -def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> None: +def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player: int, dragon_rooms: []) -> None: menu = Region("Menu", player, multiworld) @@ -74,7 +75,7 @@ def create_regions(multiworld: MultiWorld, player: int, dragon_rooms: []) -> Non credits_room_far_side.exits.append(Entrance(player, "CreditsFromFarSide", credits_room_far_side)) multiworld.regions.append(credits_room_far_side) - dragon_slay_check = multiworld.dragon_slay_check[player].value + dragon_slay_check = options.dragon_slay_check.value priority_locations = determine_priority_locations(multiworld, dragon_slay_check) for name, location_data in location_table.items(): diff --git a/worlds/adventure/Rules.py b/worlds/adventure/Rules.py index 6f4b53faa11b..930295301288 100644 --- a/worlds/adventure/Rules.py +++ b/worlds/adventure/Rules.py @@ -6,7 +6,7 @@ def set_rules(self) -> None: world = self.multiworld - use_bat_logic = world.bat_logic[self.player].value == BatLogic.option_use_logic + use_bat_logic = self.options.bat_logic.value == BatLogic.option_use_logic set_rule(world.get_entrance("YellowCastlePort", self.player), lambda state: state.has("Yellow Key", self.player)) @@ -28,7 +28,7 @@ def set_rules(self) -> None: lambda state: state.has("Bridge", self.player) or state.has("Magnet", self.player)) - dragon_slay_check = world.dragon_slay_check[self.player].value + dragon_slay_check = self.options.dragon_slay_check.value if dragon_slay_check: if self.difficulty_switch_b == DifficultySwitchB.option_hard_with_unlock_item: set_rule(world.get_location("Slay Yorgle", self.player), diff --git a/worlds/adventure/__init__.py b/worlds/adventure/__init__.py index 1c2583b3ed6e..ed5ebbd3dc56 100644 --- a/worlds/adventure/__init__.py +++ b/worlds/adventure/__init__.py @@ -15,7 +15,8 @@ from worlds.AutoWorld import WebWorld, World from Fill import fill_restrictive from worlds.generic.Rules import add_rule, set_rule -from .Options import adventure_option_definitions, DragonRandoType, DifficultySwitchA, DifficultySwitchB +from .Options import DragonRandoType, DifficultySwitchA, DifficultySwitchB, \ + AdventureOptions from .Rom import get_base_rom_bytes, get_base_rom_path, AdventureDeltaPatch, apply_basepatch, \ AdventureAutoCollectLocation from .Items import item_table, ItemData, nothing_item_id, event_table, AdventureItem, standard_item_max @@ -109,7 +110,7 @@ class AdventureWorld(World): game: ClassVar[str] = "Adventure" web: ClassVar[WebWorld] = AdventureWeb() - option_definitions: ClassVar[Dict[str, AssembleOptions]] = adventure_option_definitions + options_dataclass = AdventureOptions settings: ClassVar[AdventureSettings] item_name_to_id: ClassVar[Dict[str, int]] = {name: data.id for name, data in item_table.items()} location_name_to_id: ClassVar[Dict[str, int]] = {name: data.location_id for name, data in location_table.items()} @@ -149,18 +150,18 @@ def generate_early(self) -> None: bytearray(f"ADVENTURE{__version__.replace('.', '')[:3]}_{self.player}_{self.multiworld.seed}", "utf8")[:21] self.rom_name.extend([0] * (21 - len(self.rom_name))) - self.dragon_rando_type = self.multiworld.dragon_rando_type[self.player].value - self.dragon_slay_check = self.multiworld.dragon_slay_check[self.player].value - self.connector_multi_slot = self.multiworld.connector_multi_slot[self.player].value - self.yorgle_speed = self.multiworld.yorgle_speed[self.player].value - self.yorgle_min_speed = self.multiworld.yorgle_min_speed[self.player].value - self.grundle_speed = self.multiworld.grundle_speed[self.player].value - self.grundle_min_speed = self.multiworld.grundle_min_speed[self.player].value - self.rhindle_speed = self.multiworld.rhindle_speed[self.player].value - self.rhindle_min_speed = self.multiworld.rhindle_min_speed[self.player].value - self.difficulty_switch_a = self.multiworld.difficulty_switch_a[self.player].value - self.difficulty_switch_b = self.multiworld.difficulty_switch_b[self.player].value - self.start_castle = self.multiworld.start_castle[self.player].value + self.dragon_rando_type = self.options.dragon_rando_type.value + self.dragon_slay_check = self.options.dragon_slay_check.value + self.connector_multi_slot = self.options.connector_multi_slot.value + self.yorgle_speed = self.options.yorgle_speed.value + self.yorgle_min_speed = self.options.yorgle_min_speed.value + self.grundle_speed = self.options.grundle_speed.value + self.grundle_min_speed = self.options.grundle_min_speed.value + self.rhindle_speed = self.options.rhindle_speed.value + self.rhindle_min_speed = self.options.rhindle_min_speed.value + self.difficulty_switch_a = self.options.difficulty_switch_a.value + self.difficulty_switch_b = self.options.difficulty_switch_b.value + self.start_castle = self.options.start_castle.value self.created_items = 0 if self.dragon_slay_check == 0: @@ -227,7 +228,7 @@ def create_items(self) -> None: extra_filler_count = num_locations - self.created_items # traps would probably go here, if enabled - freeincarnate_max = self.multiworld.freeincarnate_max[self.player].value + freeincarnate_max = self.options.freeincarnate_max.value actual_freeincarnates = min(extra_filler_count, freeincarnate_max) self.multiworld.itempool += [self.create_item("Freeincarnate") for _ in range(actual_freeincarnates)] self.created_items += actual_freeincarnates @@ -247,7 +248,7 @@ def create_dragon_slow_items(self, min_speed: int, speed: int, item_name: str, m self.created_items += 1 def create_regions(self) -> None: - create_regions(self.multiworld, self.player, self.dragon_rooms) + create_regions(self.options, self.multiworld, self.player, self.dragon_rooms) set_rules = set_rules @@ -354,7 +355,7 @@ def generate_output(self, output_directory: str) -> None: auto_collect_locations: [AdventureAutoCollectLocation] = [] local_item_to_location: {int, int} = {} bat_no_touch_locs: [LocationData] = [] - bat_logic: int = self.multiworld.bat_logic[self.player].value + bat_logic: int = self.options.bat_logic.value try: rom_deltas: { int, int } = {} self.place_dragons(rom_deltas) @@ -421,7 +422,7 @@ def generate_output(self, output_directory: str) -> None: item_position_data_start = get_item_position_data_start(unplaced_item.table_index) rom_deltas[item_position_data_start] = 0xff - if self.multiworld.connector_multi_slot[self.player].value: + if self.options.connector_multi_slot.value: rom_deltas[connector_port_offset] = (self.player & 0xff) else: rom_deltas[connector_port_offset] = 0 From 75bef3ddb15334d22ca6d0bbe905f93c4752a5ea Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 11 Jun 2024 00:42:57 +0200 Subject: [PATCH 06/58] Various: fix absolute imports in worlds (#3489) --- worlds/messenger/options.py | 2 +- worlds/shorthike/Rules.py | 3 ++- worlds/yugioh06/client_bh.py | 2 +- worlds/yugioh06/opponents.py | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/worlds/messenger/options.py b/worlds/messenger/options.py index 73adf4ebdf0a..1f76dba4894a 100644 --- a/worlds/messenger/options.py +++ b/worlds/messenger/options.py @@ -5,7 +5,7 @@ from Options import Accessibility, Choice, DeathLinkMixin, DefaultOnToggle, OptionDict, PerGameCommonOptions, \ PlandoConnections, Range, StartInventoryPool, Toggle, Visibility -from worlds.messenger.portals import CHECKPOINTS, PORTALS, SHOP_POINTS +from .portals import CHECKPOINTS, PORTALS, SHOP_POINTS class MessengerAccessibility(Accessibility): diff --git a/worlds/shorthike/Rules.py b/worlds/shorthike/Rules.py index 4a71ebd3c80a..33741c6d80c6 100644 --- a/worlds/shorthike/Rules.py +++ b/worlds/shorthike/Rules.py @@ -1,5 +1,6 @@ from worlds.generic.Rules import forbid_items_for_player, add_rule -from worlds.shorthike.Options import Goal, GoldenFeatherProgression, MinShopCheckLogic, ShopCheckLogic +from .Options import Goal, GoldenFeatherProgression, MinShopCheckLogic, ShopCheckLogic + def create_rules(self, location_table): multiworld = self.multiworld diff --git a/worlds/yugioh06/client_bh.py b/worlds/yugioh06/client_bh.py index 910eba7c6a88..ecbe48110a6c 100644 --- a/worlds/yugioh06/client_bh.py +++ b/worlds/yugioh06/client_bh.py @@ -5,7 +5,7 @@ import worlds._bizhawk as bizhawk from worlds._bizhawk.client import BizHawkClient -from worlds.yugioh06 import item_to_index +from . import item_to_index if TYPE_CHECKING: from worlds._bizhawk.context import BizHawkClientContext diff --git a/worlds/yugioh06/opponents.py b/worlds/yugioh06/opponents.py index 1746b5652962..68d7c2880f03 100644 --- a/worlds/yugioh06/opponents.py +++ b/worlds/yugioh06/opponents.py @@ -3,8 +3,8 @@ from BaseClasses import MultiWorld from worlds.generic.Rules import CollectionRule -from worlds.yugioh06 import item_to_index, tier_1_opponents, yugioh06_difficulty -from worlds.yugioh06.locations import special +from . import item_to_index, tier_1_opponents, yugioh06_difficulty +from .locations import special class OpponentData(NamedTuple): From ccfffa11478981cdaa9317da88526bf75d27afdd Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Mon, 10 Jun 2024 18:55:02 -0500 Subject: [PATCH 07/58] CODEOWNERS: Replace @ThePhar with @qwint as Hollow Knight maintainer. (#3508) --- docs/CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CODEOWNERS b/docs/CODEOWNERS index 10b962d49970..9fbd4837960f 100644 --- a/docs/CODEOWNERS +++ b/docs/CODEOWNERS @@ -70,7 +70,7 @@ /worlds/heretic/ @Daivuk # Hollow Knight -/worlds/hk/ @BadMagic100 @ThePhar +/worlds/hk/ @BadMagic100 @qwint # Hylics 2 /worlds/hylics2/ @TRPG0 From 54531c6ebae4f3dab1ebb08a6513d51e96517e31 Mon Sep 17 00:00:00 2001 From: Justus Lind Date: Tue, 11 Jun 2024 11:11:19 +1000 Subject: [PATCH 08/58] Muse Dash: Remove regions for a decent speed gain in generating worlds (#3435) * Remove Muse Dash Regions. * Update comments. --- worlds/musedash/__init__.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/worlds/musedash/__init__.py b/worlds/musedash/__init__.py index a9eacbbcf82c..ab3a4819fc48 100644 --- a/worlds/musedash/__init__.py +++ b/worlds/musedash/__init__.py @@ -249,9 +249,7 @@ def create_items(self) -> None: def create_regions(self) -> None: menu_region = Region("Menu", self.player, self.multiworld) - song_select_region = Region("Song Select", self.player, self.multiworld) - self.multiworld.regions += [menu_region, song_select_region] - menu_region.connect(song_select_region) + self.multiworld.regions += [menu_region] # Make a collection of all songs available for this rando. # 1. All starting songs @@ -265,18 +263,16 @@ def create_regions(self) -> None: self.random.shuffle(included_song_copy) all_selected_locations.extend(included_song_copy) - # Make a region per song/album, then adds 1-2 item locations to them + # Adds 2 item locations per song/album to the menu region. for i in range(0, len(all_selected_locations)): name = all_selected_locations[i] - region = Region(name, self.player, self.multiworld) - self.multiworld.regions.append(region) - song_select_region.connect(region, name, lambda state, place=name: state.has(place, self.player)) - - # Muse Dash requires 2 locations per song to be *interesting*. Balanced out by filler. - region.add_locations({ - name + "-0": self.md_collection.song_locations[name + "-0"], - name + "-1": self.md_collection.song_locations[name + "-1"] - }, MuseDashLocation) + loc1 = MuseDashLocation(self.player, name + "-0", self.md_collection.song_locations[name + "-0"], menu_region) + loc1.access_rule = lambda state, place=name: state.has(place, self.player) + menu_region.locations.append(loc1) + + loc2 = MuseDashLocation(self.player, name + "-1", self.md_collection.song_locations[name + "-1"], menu_region) + loc2.access_rule = lambda state, place=name: state.has(place, self.player) + menu_region.locations.append(loc2) def set_rules(self) -> None: self.multiworld.completion_condition[self.player] = lambda state: \ From 87d24eb38a00b1eee26b61a27d3cd9f1fd264e6a Mon Sep 17 00:00:00 2001 From: Louis M Date: Tue, 11 Jun 2024 18:59:46 -0400 Subject: [PATCH 09/58] Aquaria: Add entrance rule and fix start_inventory_from_pool (#3473) --- worlds/aquaria/Locations.py | 6 +++--- worlds/aquaria/Regions.py | 7 ++++--- worlds/aquaria/__init__.py | 12 ++++-------- worlds/aquaria/test/__init__.py | 6 +++--- worlds/aquaria/test/test_beast_form_access.py | 4 ++-- .../test_no_progression_hard_hidden_locations.py | 2 +- .../test/test_progression_hard_hidden_locations.py | 2 +- worlds/aquaria/test/test_sun_form_access.py | 3 +++ 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/worlds/aquaria/Locations.py b/worlds/aquaria/Locations.py index 7360efde065e..33d165db411a 100644 --- a/worlds/aquaria/Locations.py +++ b/worlds/aquaria/Locations.py @@ -185,7 +185,7 @@ class AquariaLocations: "Mithalas City, second bulb at the end of the top path": 698040, "Mithalas City, bulb in the top path": 698036, "Mithalas City, Mithalas Pot": 698174, - "Mithalas City, urn in the Cathedral flower tube entrance": 698128, + "Mithalas City, urn in the Castle flower tube entrance": 698128, } locations_mithalas_city_fishpass = { @@ -246,7 +246,7 @@ class AquariaLocations: "Kelp Forest top left area, bulb in the bottom left clearing": 698044, "Kelp Forest top left area, bulb in the path down from the top left clearing": 698045, "Kelp Forest top left area, bulb in the top left clearing": 698046, - "Kelp Forest top left, Jelly Egg": 698185, + "Kelp Forest top left area, Jelly Egg": 698185, } locations_forest_tl_fp = { @@ -332,7 +332,7 @@ class AquariaLocations: } locations_veil_tr_l = { - "The Veil top right area, bulb in the top of the waterfall": 698080, + "The Veil top right area, bulb at the top of the waterfall": 698080, "The Veil top right area, Transturtle": 698210, } diff --git a/worlds/aquaria/Regions.py b/worlds/aquaria/Regions.py index f2f85749f3fb..28120259254c 100755 --- a/worlds/aquaria/Regions.py +++ b/worlds/aquaria/Regions.py @@ -771,6 +771,7 @@ def __connect_sunken_city_regions(self) -> None: self.__connect_regions("Sunken City left area", "Sunken City boss area", self.sunken_city_l, self.sunken_city_boss, lambda state: _has_beast_form(state, self.player) and + _has_sun_form(state, self.player) and _has_energy_form(state, self.player) and _has_bind_song(state, self.player)) @@ -983,7 +984,7 @@ def __adjusting_urns_rules(self) -> None: lambda state: _has_damaging_item(state, self.player)) add_rule(self.multiworld.get_location("Mithalas City, third urn in the city reserve", self.player), lambda state: _has_damaging_item(state, self.player)) - add_rule(self.multiworld.get_location("Mithalas City, urn in the Cathedral flower tube entrance", self.player), + add_rule(self.multiworld.get_location("Mithalas City, urn in the Castle flower tube entrance", self.player), lambda state: _has_damaging_item(state, self.player)) add_rule(self.multiworld.get_location("Mithalas City Castle, urn in the bedroom", self.player), lambda state: _has_damaging_item(state, self.player)) @@ -1023,7 +1024,7 @@ def __adjusting_soup_rules(self) -> None: lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) add_rule(self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player), lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) - add_rule(self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall", self.player), + add_rule(self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.player), lambda state: _has_hot_soup(state, self.player) and _has_beast_form(state, self.player)) def __adjusting_under_rock_location(self) -> None: @@ -1175,7 +1176,7 @@ def __no_progression_hard_or_hidden_location(self) -> None: self.multiworld.get_location("Sun Worm path, second cliff bulb", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression - self.multiworld.get_location("The Veil top right area, bulb in the top of the waterfall", + self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall", self.player).item_rule =\ lambda item: item.classification != ItemClassification.progression self.multiworld.get_location("Bubble Cave, bulb in the left cave wall", diff --git a/worlds/aquaria/__init__.py b/worlds/aquaria/__init__.py index 3c0cc3bdedca..ce46aeea75aa 100644 --- a/worlds/aquaria/__init__.py +++ b/worlds/aquaria/__init__.py @@ -167,14 +167,10 @@ def create_items(self) -> None: self.__pre_fill_item("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected) self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected) for name, data in item_table.items(): - if name in precollected: - precollected.remove(name) - self.multiworld.itempool.append(self.create_item(self.get_filler_item_name())) - else: - if name not in self.exclude: - for i in range(data.count): - item = self.create_item(name) - self.multiworld.itempool.append(item) + if name not in self.exclude: + for i in range(data.count): + item = self.create_item(name) + self.multiworld.itempool.append(item) def set_rules(self) -> None: """ diff --git a/worlds/aquaria/test/__init__.py b/worlds/aquaria/test/__init__.py index 198ccb0f628b..5c63c9bb2968 100644 --- a/worlds/aquaria/test/__init__.py +++ b/worlds/aquaria/test/__init__.py @@ -56,7 +56,7 @@ "Mithalas City, second bulb at the end of the top path", "Mithalas City, bulb in the top path", "Mithalas City, Mithalas Pot", - "Mithalas City, urn in the Cathedral flower tube entrance", + "Mithalas City, urn in the Castle flower tube entrance", "Mithalas City, Doll", "Mithalas City, urn inside a home fish pass", "Mithalas City Castle, bulb in the flesh hole", @@ -93,7 +93,7 @@ "Kelp Forest top left area, bulb in the bottom left clearing", "Kelp Forest top left area, bulb in the path down from the top left clearing", "Kelp Forest top left area, bulb in the top left clearing", - "Kelp Forest top left, Jelly Egg", + "Kelp Forest top left area, Jelly Egg", "Kelp Forest top left area, bulb close to the Verse Egg", "Kelp Forest top left area, Verse Egg", "Kelp Forest top right area, bulb under the rock in the right path", @@ -125,7 +125,7 @@ "Turtle cave, Urchin Costume", "The Veil top right area, bulb in the middle of the wall jump cliff", "The Veil top right area, Golden Starfish", - "The Veil top right area, bulb in the top of the waterfall", + "The Veil top right area, bulb at the top of the waterfall", "The Veil top right area, Transturtle", "The Veil bottom area, bulb in the left path", "The Veil bottom area, bulb in the spirit path", diff --git a/worlds/aquaria/test/test_beast_form_access.py b/worlds/aquaria/test/test_beast_form_access.py index c25070d470b5..4bb4d5656c01 100644 --- a/worlds/aquaria/test/test_beast_form_access.py +++ b/worlds/aquaria/test/test_beast_form_access.py @@ -20,14 +20,14 @@ def test_beast_form_location(self) -> None: "Mithalas City, second bulb at the end of the top path", "Mithalas City, bulb in the top path", "Mithalas City, Mithalas Pot", - "Mithalas City, urn in the Cathedral flower tube entrance", + "Mithalas City, urn in the Castle flower tube entrance", "Mermog cave, Piranha Egg", "Mithalas Cathedral, Mithalan Dress", "Turtle cave, bulb in Bubble Cliff", "Turtle cave, Urchin Costume", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "The Veil top right area, bulb in the top of the waterfall", + "The Veil top right area, bulb at the top of the waterfall", "Bubble Cave, bulb in the left cave wall", "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", "Bubble Cave, Verse Egg", diff --git a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py index 817b9547a892..b0d2b0d880fa 100644 --- a/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_no_progression_hard_hidden_locations.py @@ -30,7 +30,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): "Final Boss area, bulb in the boss third form room", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "The Veil top right area, bulb in the top of the waterfall", + "The Veil top right area, bulb at the top of the waterfall", "Bubble Cave, bulb in the left cave wall", "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", "Bubble Cave, Verse Egg", diff --git a/worlds/aquaria/test/test_progression_hard_hidden_locations.py b/worlds/aquaria/test/test_progression_hard_hidden_locations.py index 2b7c8ddac93a..390fc40b295d 100644 --- a/worlds/aquaria/test/test_progression_hard_hidden_locations.py +++ b/worlds/aquaria/test/test_progression_hard_hidden_locations.py @@ -30,7 +30,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase): "Final Boss area, bulb in the boss third form room", "Sun Worm path, first cliff bulb", "Sun Worm path, second cliff bulb", - "The Veil top right area, bulb in the top of the waterfall", + "The Veil top right area, bulb at the top of the waterfall", "Bubble Cave, bulb in the left cave wall", "Bubble Cave, bulb in the right cave wall (behind the ice crystal)", "Bubble Cave, Verse Egg", diff --git a/worlds/aquaria/test/test_sun_form_access.py b/worlds/aquaria/test/test_sun_form_access.py index dfd732ec910c..cbe8c08a52a7 100644 --- a/worlds/aquaria/test/test_sun_form_access.py +++ b/worlds/aquaria/test/test_sun_form_access.py @@ -18,6 +18,9 @@ def test_sun_form_location(self) -> None: "Abyss right area, bulb behind the rock in the whale room", "Octopus Cave, Dumbo Egg", "Beating Octopus Prime", + "Sunken City, bulb on top of the boss area", + "Beating the Golem", + "Sunken City cleared", "Final Boss area, bulb in the boss third form room", "Objective complete" ] From e755f1a0b5b588f51b26e8ac9eddd69fba044567 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 12 Jun 2024 02:14:30 +0200 Subject: [PATCH 10/58] SC2: don't close all SC2 instances when one quits (#3507) --- worlds/_sc2common/bot/sc2process.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/worlds/_sc2common/bot/sc2process.py b/worlds/_sc2common/bot/sc2process.py index e36632165979..f74ed9c18f9f 100644 --- a/worlds/_sc2common/bot/sc2process.py +++ b/worlds/_sc2common/bot/sc2process.py @@ -28,6 +28,11 @@ def add(cls, value): logger.debug("kill_switch: Add switch") cls._to_kill.append(value) + @classmethod + def kill(cls, value): + logger.info(f"kill_switch: Process cleanup for 1 process") + value._clean(verbose=False) + @classmethod def kill_all(cls): logger.info(f"kill_switch: Process cleanup for {len(cls._to_kill)} processes") @@ -116,7 +121,7 @@ def signal_handler(*_args): async def __aexit__(self, *args): logger.exception("async exit") await self._close_connection() - kill_switch.kill_all() + kill_switch.kill(self) signal.signal(signal.SIGINT, signal.SIG_DFL) @property From 7299891bdf86d5134692ae55fac92ad32ba17059 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 11 Jun 2024 18:22:14 -0700 Subject: [PATCH 11/58] Allow worlds to add options to prebuilt groups (#3509) Previously, this crashed because `typing.NamedTuple` fields such as `group.name` aren't assignable. Now it will only fail for group names that are actually incorrectly cased, and will fail with a better error message. --- worlds/AutoWorld.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 6e17f023f6fb..bed375cf080a 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -123,8 +123,8 @@ def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> Web assert group.options, "A custom defined Option Group must contain at least one Option." # catch incorrectly titled versions of the prebuilt groups so they don't create extra groups title_name = group.name.title() - if title_name in prebuilt_options: - group.name = title_name + assert title_name not in prebuilt_options or title_name == group.name, \ + f"Prebuilt group name \"{group.name}\" must be \"{title_name}\"" if group.name == "Item & Location Options": assert not any(option in item_and_loc_options for option in group.options), \ From b9e454ab4ec7903a66e71324805f93b0332bc286 Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Tue, 11 Jun 2024 20:23:46 -0500 Subject: [PATCH 12/58] TS: add indirect connections (#3490) --- worlds/timespinner/Regions.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/worlds/timespinner/Regions.py b/worlds/timespinner/Regions.py index 4f53f75eff7a..757a41c38821 100644 --- a/worlds/timespinner/Regions.py +++ b/worlds/timespinner/Regions.py @@ -70,7 +70,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w logic = TimespinnerLogic(world, player, precalculated_weights) connect(world, player, 'Lake desolation', 'Lower lake desolation', lambda state: flooded.flood_lake_desolation or logic.has_timestop(state) or state.has('Talaria Attachment', player)) - connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) + connect(world, player, 'Lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player), "Upper Lake Serene") connect(world, player, 'Lake desolation', 'Skeleton Shaft', lambda state: flooded.flood_lake_desolation or logic.has_doublejump(state)) connect(world, player, 'Lake desolation', 'Space time continuum', logic.has_teleport) connect(world, player, 'Upper lake desolation', 'Lake desolation') @@ -80,7 +80,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w connect(world, player, 'Eastern lake desolation', 'Space time continuum', logic.has_teleport) connect(world, player, 'Eastern lake desolation', 'Library') connect(world, player, 'Eastern lake desolation', 'Lower lake desolation') - connect(world, player, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player)) + connect(world, player, 'Eastern lake desolation', 'Upper lake desolation', lambda state: logic.has_fire(state) and state.can_reach('Upper Lake Serene', 'Region', player), "Upper Lake Serene") connect(world, player, 'Library', 'Eastern lake desolation') connect(world, player, 'Library', 'Library top', lambda state: logic.has_doublejump(state) or state.has('Talaria Attachment', player)) connect(world, player, 'Library', 'Varndagroth tower left', logic.has_keycard_D) @@ -185,7 +185,7 @@ def create_regions_and_locations(world: MultiWorld, player: int, precalculated_w if is_option_enabled(world, player, "GyreArchives"): connect(world, player, 'The lab (upper)', 'Ravenlord\'s Lair', lambda state: state.has('Merchant Crow', player)) connect(world, player, 'Ravenlord\'s Lair', 'The lab (upper)') - connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player)) + connect(world, player, 'Library top', 'Ifrit\'s Lair', lambda state: state.has('Kobo', player) and state.can_reach('Refugee Camp', 'Region', player), "Refugee Camp") connect(world, player, 'Ifrit\'s Lair', 'Library top') @@ -242,11 +242,19 @@ def connectStartingRegion(world: MultiWorld, player: int): def connect(world: MultiWorld, player: int, source: str, target: str, - rule: Optional[Callable[[CollectionState], bool]] = None): + rule: Optional[Callable[[CollectionState], bool]] = None, + indirect: str = ""): sourceRegion = world.get_region(source, player) targetRegion = world.get_region(target, player) - sourceRegion.connect(targetRegion, rule=rule) + entrance = sourceRegion.connect(targetRegion, rule=rule) + + if indirect: + indirectRegion = world.get_region(indirect, player) + if indirectRegion in world.indirect_connections: + world.indirect_connections[indirectRegion].add(entrance) + else: + world.indirect_connections[indirectRegion] = {entrance} def split_location_datas_per_region(locations: List[LocationData]) -> Dict[str, List[LocationData]]: From 3b9b9353b7e7588cb59c496fd295397dcbcdf9b6 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 12 Jun 2024 15:34:46 +0200 Subject: [PATCH 13/58] WebHost: delete old docs files (#3503) --- WebHost.py | 1 + 1 file changed, 1 insertion(+) diff --git a/WebHost.py b/WebHost.py index afacd6288ec2..08ef3c430795 100644 --- a/WebHost.py +++ b/WebHost.py @@ -58,6 +58,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] worlds[game] = world base_target_path = Utils.local_path("WebHostLib", "static", "generated", "docs") + shutil.rmtree(base_target_path, ignore_errors=True) for game, world in worlds.items(): # copy files from world's docs folder to the generated folder target_path = os.path.join(base_target_path, game) From 2daccded365258633bd8ac268c5a58ae9ee31a43 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 12 Jun 2024 15:35:51 +0200 Subject: [PATCH 14/58] Core: don't lock progression (#3501) --- Fill.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Fill.py b/Fill.py index d8147b2eac80..4967ff073601 100644 --- a/Fill.py +++ b/Fill.py @@ -483,15 +483,15 @@ def mark_for_locking(location: Location): if panic_method == "swap": fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=True, - on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + name="Progression", single_player_placement=multiworld.players == 1) elif panic_method == "raise": fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False, - on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + name="Progression", single_player_placement=multiworld.players == 1) elif panic_method == "start_inventory": fill_restrictive(multiworld, multiworld.state, defaultlocations, progitempool, swap=False, allow_partial=True, - on_place=mark_for_locking, name="Progression", single_player_placement=multiworld.players == 1) + name="Progression", single_player_placement=multiworld.players == 1) if progitempool: for item in progitempool: logging.debug(f"Moved {item} to start_inventory to prevent fill failure.") From acf85eb9abb3de4863fe9350a6cdddee71c9be44 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 12 Jun 2024 18:54:59 +0200 Subject: [PATCH 15/58] Speedups: remove dependency on c++ (#2796) * Speedups: remove dependency on c++ * Speedups: intset: handle malloc failing * Speedups: intset: fix corner case for int64 on 32bit systems original idea was to only use bucket->val if int(-1) # this is all 0xff... adding 1 results in 0, but it's not negative +# configure INTSET for player +cdef extern from *: + """ + #define INTSET_NAME ap_player_set + #define INTSET_TYPE uint32_t // has to match ap_player_t + """ + +# create INTSET for player +cdef extern from "intset.h": + """ + #undef INTSET_NAME + #undef INTSET_TYPE + """ + ctypedef struct ap_player_set: + pass + + ap_player_set* ap_player_set_new(size_t bucket_count) nogil + void ap_player_set_free(ap_player_set* set) nogil + bint ap_player_set_add(ap_player_set* set, ap_player_t val) nogil + bint ap_player_set_contains(ap_player_set* set, ap_player_t val) nogil + cdef struct LocationEntry: # layout is so that @@ -185,7 +206,7 @@ cdef class LocationStore: def find_item(self, slots: Set[int], seeked_item_id: int) -> Generator[Tuple[int, int, int, int, int], None, None]: cdef ap_id_t item = seeked_item_id cdef ap_player_t receiver - cdef std_set[ap_player_t] receivers + cdef ap_player_set* receivers cdef size_t slot_count = len(slots) if slot_count == 1: # specialized implementation for single slot @@ -197,13 +218,20 @@ cdef class LocationStore: yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags elif slot_count: # generic implementation with lookup in set - for receiver in slots: - receivers.insert(receiver) - with nogil: - for entry in self.entries[:self.entry_count]: - if entry.item == item and receivers.count(entry.receiver): - with gil: - yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags + receivers = ap_player_set_new(min(1023, slot_count)) # limit top level struct to 16KB + if not receivers: + raise MemoryError() + try: + for receiver in slots: + if not ap_player_set_add(receivers, receiver): + raise MemoryError() + with nogil: + for entry in self.entries[:self.entry_count]: + if entry.item == item and ap_player_set_contains(receivers, entry.receiver): + with gil: + yield entry.sender, entry.location, entry.item, entry.receiver, entry.flags + finally: + ap_player_set_free(receivers) def get_for_player(self, slot: int) -> Dict[int, Set[int]]: cdef ap_player_t receiver = slot diff --git a/_speedups.pyxbld b/_speedups.pyxbld index e1fe19b2efc6..974eaed03b6a 100644 --- a/_speedups.pyxbld +++ b/_speedups.pyxbld @@ -1,8 +1,10 @@ -# This file is required to get pyximport to work with C++. -# Switching from std::set to a pure C implementation is still on the table to simplify everything. +# This file is used when doing pyximport +import os def make_ext(modname, pyxfilename): from distutils.extension import Extension return Extension(name=modname, sources=[pyxfilename], - language='c++') + depends=["intset.h"], + include_dirs=[os.getcwd()], + language="c") diff --git a/intset.h b/intset.h new file mode 100644 index 000000000000..fac84fb6f890 --- /dev/null +++ b/intset.h @@ -0,0 +1,135 @@ +/* A specialized unordered_set implementation for literals, where bucket_count + * is defined at initialization rather than increased automatically. + */ +#include +#include +#include +#include + +#ifndef INTSET_NAME +#error "Please #define INTSET_NAME ... before including intset.h" +#endif + +#ifndef INTSET_TYPE +#error "Please #define INTSET_TYPE ... before including intset.h" +#endif + +/* macros to generate unique names from INTSET_NAME */ +#ifndef INTSET_CONCAT +#define INTSET_CONCAT_(a, b) a ## b +#define INTSET_CONCAT(a, b) INTSET_CONCAT_(a, b) +#define INTSET_FUNC_(a, b) INTSET_CONCAT(a, _ ## b) +#endif + +#define INTSET_FUNC(name) INTSET_FUNC_(INTSET_NAME, name) +#define INTSET_BUCKET INTSET_CONCAT(INTSET_NAME, Bucket) +#define INTSET_UNION INTSET_CONCAT(INTSET_NAME, Union) + +#if defined(_MSC_VER) +#pragma warning(push) +#pragma warning(disable : 4200) +#endif + + +typedef struct { + size_t count; + union INTSET_UNION { + INTSET_TYPE val; + INTSET_TYPE *data; + } v; +} INTSET_BUCKET; + +typedef struct { + size_t bucket_count; + INTSET_BUCKET buckets[]; +} INTSET_NAME; + +static INTSET_NAME *INTSET_FUNC(new)(size_t buckets) +{ + size_t i, size; + INTSET_NAME *set; + + if (buckets < 1) + buckets = 1; + if ((SIZE_MAX - sizeof(INTSET_NAME)) / sizeof(INTSET_BUCKET) < buckets) + return NULL; + size = sizeof(INTSET_NAME) + buckets * sizeof(INTSET_BUCKET); + set = (INTSET_NAME*)malloc(size); + if (!set) + return NULL; + memset(set, 0, size); /* gcc -fanalyzer does not understand this sets all buckets' count to 0 */ + for (i = 0; i < buckets; i++) { + set->buckets[i].count = 0; + } + set->bucket_count = buckets; + return set; +} + +static void INTSET_FUNC(free)(INTSET_NAME *set) +{ + size_t i; + if (!set) + return; + for (i = 0; i < set->bucket_count; i++) { + if (set->buckets[i].count > 1) + free(set->buckets[i].v.data); + } + free(set); +} + +static bool INTSET_FUNC(contains)(INTSET_NAME *set, INTSET_TYPE val) +{ + size_t i; + INTSET_BUCKET* bucket = &set->buckets[(size_t)val % set->bucket_count]; + if (bucket->count == 1) + return bucket->v.val == val; + for (i = 0; i < bucket->count; ++i) { + if (bucket->v.data[i] == val) + return true; + } + return false; +} + +static bool INTSET_FUNC(add)(INTSET_NAME *set, INTSET_TYPE val) +{ + INTSET_BUCKET* bucket; + + if (INTSET_FUNC(contains)(set, val)) + return true; /* ok */ + + bucket = &set->buckets[(size_t)val % set->bucket_count]; + if (bucket->count == 0) { + bucket->v.val = val; + bucket->count = 1; + } else if (bucket->count == 1) { + INTSET_TYPE old = bucket->v.val; + bucket->v.data = (INTSET_TYPE*)malloc(2 * sizeof(INTSET_TYPE)); + if (!bucket->v.data) { + bucket->v.val = old; + return false; /* error */ + } + bucket->v.data[0] = old; + bucket->v.data[1] = val; + bucket->count = 2; + } else { + size_t new_bucket_size; + INTSET_TYPE* new_bucket_data; + + new_bucket_size = (bucket->count + 1) * sizeof(INTSET_TYPE); + new_bucket_data = (INTSET_TYPE*)realloc(bucket->v.data, new_bucket_size); + if (!new_bucket_data) + return false; /* error */ + bucket->v.data = new_bucket_data; + bucket->v.data[bucket->count++] = val; + } + return true; /* success */ +} + + +#if defined(_MSC_VER) +#pragma warning(pop) +#endif + +#undef INTSET_FUNC +#undef INTSET_BUCKET +#undef INTSET_UNION diff --git a/test/cpp/CMakeLists.txt b/test/cpp/CMakeLists.txt new file mode 100644 index 000000000000..927b7494dac4 --- /dev/null +++ b/test/cpp/CMakeLists.txt @@ -0,0 +1,49 @@ +cmake_minimum_required(VERSION 3.5) +project(ap-cpp-tests) + +enable_testing() + +find_package(GTest REQUIRED) + +if (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + add_definitions("/source-charset:utf-8") + set(CMAKE_CXX_FLAGS_DEBUG "/MTd") + set(CMAKE_CXX_FLAGS_RELEASE "/MT") +elseif (CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + # enable static analysis for gcc + add_compile_options(-fanalyzer -Werror) + # disable stuff that gets triggered by googletest + add_compile_options(-Wno-analyzer-malloc-leak) + # enable asan for gcc + add_compile_options(-fsanitize=address) + add_link_options(-fsanitize=address) +endif () + +add_executable(test_default) + +target_include_directories(test_default + PRIVATE + ${GTEST_INCLUDE_DIRS} +) + +target_link_libraries(test_default + ${GTEST_BOTH_LIBRARIES} +) + +add_test( + NAME test_default + COMMAND test_default +) + +set_property( + TEST test_default + PROPERTY ENVIRONMENT "ASAN_OPTIONS=allocator_may_return_null=1" +) + +file(GLOB ITEMS *) +foreach(item ${ITEMS}) + if(IS_DIRECTORY ${item} AND EXISTS ${item}/CMakeLists.txt) + message(${item}) + add_subdirectory(${item}) + endif() +endforeach() diff --git a/test/cpp/README.md b/test/cpp/README.md new file mode 100644 index 000000000000..792b9be77e72 --- /dev/null +++ b/test/cpp/README.md @@ -0,0 +1,32 @@ +# C++ tests + +Test framework for C and C++ code in AP. + +## Adding a Test + +### GoogleTest + +Adding GoogleTests is as simple as creating a directory with +* one or more `test_*.cpp` files that define tests using + [GoogleTest API](https://google.github.io/googletest/) +* a `CMakeLists.txt` that adds the .cpp files to `test_default` target using + [target_sources](https://cmake.org/cmake/help/latest/command/target_sources.html) + +### CTest + +If either GoogleTest is not suitable for the test or the build flags / sources / libraries are incompatible, +you can add another CTest to the project using add_target and add_test, similar to how it's done for `test_default`. + +## Running Tests + +* Install [CMake](https://cmake.org/). +* Build and/or install GoogleTest and make sure + [CMake can find it](https://cmake.org/cmake/help/latest/module/FindGTest.html), or + [create a parent `CMakeLists.txt` that fetches GoogleTest](https://google.github.io/googletest/quickstart-cmake.html). +* Enter the directory with the top-most `CMakeLists.txt` and run + ```sh + mkdir build + cmake -S . -B build/ -DCMAKE_BUILD_TYPE=Release + cmake --build build/ --config Release && \ + ctest --test-dir build/ -C Release --output-on-failure + ``` diff --git a/test/cpp/intset/CMakeLists.txt b/test/cpp/intset/CMakeLists.txt new file mode 100644 index 000000000000..175e0bd0b9e8 --- /dev/null +++ b/test/cpp/intset/CMakeLists.txt @@ -0,0 +1,4 @@ +target_sources(test_default + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/test_intset.cpp +) diff --git a/test/cpp/intset/test_intset.cpp b/test/cpp/intset/test_intset.cpp new file mode 100644 index 000000000000..2f85bea960c4 --- /dev/null +++ b/test/cpp/intset/test_intset.cpp @@ -0,0 +1,105 @@ +#include +#include +#include + +// uint32Set +#define INTSET_NAME uint32Set +#define INTSET_TYPE uint32_t +#include "../../../intset.h" +#undef INTSET_NAME +#undef INTSET_TYPE + +// int64Set +#define INTSET_NAME int64Set +#define INTSET_TYPE int64_t +#include "../../../intset.h" + + +TEST(IntsetTest, ZeroBuckets) +{ + // trying to allocate with zero buckets has to either fail or be functioning + uint32Set *set = uint32Set_new(0); + if (!set) + return; // failed -> OK + + EXPECT_FALSE(uint32Set_contains(set, 1)); + EXPECT_TRUE(uint32Set_add(set, 1)); + EXPECT_TRUE(uint32Set_contains(set, 1)); + uint32Set_free(set); +} + +TEST(IntsetTest, Duplicate) +{ + // adding the same number again can't fail + uint32Set *set = uint32Set_new(2); + ASSERT_TRUE(set); + EXPECT_TRUE(uint32Set_add(set, 0)); + EXPECT_TRUE(uint32Set_add(set, 0)); + EXPECT_TRUE(uint32Set_contains(set, 0)); + uint32Set_free(set); +} + +TEST(IntsetTest, SetAllocFailure) +{ + // try to allocate 100TB of RAM, should fail and return NULL + if (sizeof(size_t) < 8) + GTEST_SKIP() << "Alloc error not testable on 32bit"; + int64Set *set = int64Set_new(6250000000000ULL); + EXPECT_FALSE(set); + int64Set_free(set); +} + +TEST(IntsetTest, SetAllocOverflow) +{ + // try to overflow argument passed to malloc + int64Set *set = int64Set_new(std::numeric_limits::max()); + EXPECT_FALSE(set); + int64Set_free(set); +} + +TEST(IntsetTest, NullFree) +{ + // free(NULL) should not try to free buckets + uint32Set_free(NULL); + int64Set_free(NULL); +} + +TEST(IntsetTest, BucketRealloc) +{ + // add a couple of values to the same bucket to test growing the bucket + uint32Set* set = uint32Set_new(1); + ASSERT_TRUE(set); + EXPECT_FALSE(uint32Set_contains(set, 0)); + EXPECT_TRUE(uint32Set_add(set, 0)); + EXPECT_TRUE(uint32Set_contains(set, 0)); + for (uint32_t i = 1; i < 32; ++i) { + EXPECT_TRUE(uint32Set_add(set, i)); + EXPECT_TRUE(uint32Set_contains(set, i - 1)); + EXPECT_TRUE(uint32Set_contains(set, i)); + EXPECT_FALSE(uint32Set_contains(set, i + 1)); + } + uint32Set_free(set); +} + +TEST(IntSet, Max) +{ + constexpr auto n = std::numeric_limits::max(); + uint32Set *set = uint32Set_new(1); + ASSERT_TRUE(set); + EXPECT_FALSE(uint32Set_contains(set, n)); + EXPECT_TRUE(uint32Set_add(set, n)); + EXPECT_TRUE(uint32Set_contains(set, n)); + uint32Set_free(set); +} + +TEST(InsetTest, Negative) +{ + constexpr auto n = std::numeric_limits::min(); + static_assert(n < 0, "n not negative"); + int64Set *set = int64Set_new(3); + ASSERT_TRUE(set); + EXPECT_FALSE(int64Set_contains(set, n)); + EXPECT_TRUE(int64Set_add(set, n)); + EXPECT_TRUE(int64Set_contains(set, n)); + int64Set_free(set); +} diff --git a/test/netutils/test_location_store.py b/test/netutils/test_location_store.py index a7f117255faa..f3e83989bea4 100644 --- a/test/netutils/test_location_store.py +++ b/test/netutils/test_location_store.py @@ -1,4 +1,5 @@ # Tests for _speedups.LocationStore and NetUtils._LocationStore +import os import typing import unittest import warnings @@ -7,6 +8,8 @@ State = typing.Dict[typing.Tuple[int, int], typing.Set[int]] RawLocations = typing.Dict[int, typing.Dict[int, typing.Tuple[int, int, int]]] +ci = bool(os.environ.get("CI")) # always set in GitHub actions + sample_data: RawLocations = { 1: { 11: (21, 2, 7), @@ -24,6 +27,9 @@ 3: { 9: (99, 4, 0), }, + 5: { + 9: (99, 5, 0), + } } empty_state: State = { @@ -45,14 +51,14 @@ class TestLocationStore(unittest.TestCase): store: typing.Union[LocationStore, _LocationStore] def test_len(self) -> None: - self.assertEqual(len(self.store), 4) + self.assertEqual(len(self.store), 5) self.assertEqual(len(self.store[1]), 3) def test_key_error(self) -> None: with self.assertRaises(KeyError): _ = self.store[0] with self.assertRaises(KeyError): - _ = self.store[5] + _ = self.store[6] locations = self.store[1] # no Exception with self.assertRaises(KeyError): _ = locations[7] @@ -71,7 +77,7 @@ def test_get(self) -> None: self.assertEqual(self.store[1].get(10, (None, None, None)), (None, None, None)) def test_iter(self) -> None: - self.assertEqual(sorted(self.store), [1, 2, 3, 4]) + self.assertEqual(sorted(self.store), [1, 2, 3, 4, 5]) self.assertEqual(len(self.store), len(sample_data)) self.assertEqual(list(self.store[1]), [11, 12, 13]) self.assertEqual(len(self.store[1]), len(sample_data[1])) @@ -85,13 +91,26 @@ def test_items(self) -> None: self.assertEqual(sorted(self.store[1].items())[0][1], self.store[1][11]) def test_find_item(self) -> None: + # empty player set self.assertEqual(sorted(self.store.find_item(set(), 99)), []) + # no such player, single + self.assertEqual(sorted(self.store.find_item({6}, 99)), []) + # no such player, set + self.assertEqual(sorted(self.store.find_item({7, 8, 9}, 99)), []) + # no such item self.assertEqual(sorted(self.store.find_item({3}, 1)), []) - self.assertEqual(sorted(self.store.find_item({5}, 99)), []) + # valid matches self.assertEqual(sorted(self.store.find_item({3}, 99)), [(4, 9, 99, 3, 0)]) self.assertEqual(sorted(self.store.find_item({3, 4}, 99)), [(3, 9, 99, 4, 0), (4, 9, 99, 3, 0)]) + self.assertEqual(sorted(self.store.find_item({2, 3, 4}, 99)), + [(3, 9, 99, 4, 0), (4, 9, 99, 3, 0)]) + # test hash collision in set + self.assertEqual(sorted(self.store.find_item({3, 5}, 99)), + [(4, 9, 99, 3, 0), (5, 9, 99, 5, 0)]) + self.assertEqual(sorted(self.store.find_item(set(range(2048)), 13)), + [(1, 13, 13, 1, 0)]) def test_get_for_player(self) -> None: self.assertEqual(self.store.get_for_player(3), {4: {9}}) @@ -196,18 +215,20 @@ def setUp(self) -> None: super().setUp() -@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available") +@unittest.skipIf(LocationStore is _LocationStore and not ci, "_speedups not available") class TestSpeedupsLocationStore(Base.TestLocationStore): """Run base method tests for cython implementation.""" def setUp(self) -> None: + self.assertFalse(LocationStore is _LocationStore, "Failed to load _speedups") self.store = LocationStore(sample_data) super().setUp() -@unittest.skipIf(LocationStore is _LocationStore, "_speedups not available") +@unittest.skipIf(LocationStore is _LocationStore and not ci, "_speedups not available") class TestSpeedupsLocationStoreConstructor(Base.TestLocationStoreConstructor): """Run base constructor tests and tests the additional constraints for cython implementation.""" def setUp(self) -> None: + self.assertFalse(LocationStore is _LocationStore, "Failed to load _speedups") self.type = LocationStore super().setUp() From c108845d1ff26979ddb4a5168faec82b1206292e Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 12 Jun 2024 18:55:48 +0200 Subject: [PATCH 16/58] CI: more checks in build and rework compression (#3336) * CI: build: fail fast if setup.py fails on windows * CI: build: fail for missing uploads, rework compression Upload-artifact allows setting compression level now. The change speeds up both upload and extraction. * CI: match build gz in release * CI: build: verify worlds all load * CI: build: generate a game * Generate: move worlds loaded exception to allow settings to init from worlds * CI: build: build setup before running tests --- .github/workflows/build.yml | 60 +++++++++++++++++++++++++++++++---- .github/workflows/release.yml | 2 +- Generate.py | 8 +++-- 3 files changed, 59 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 80aaf70c215e..dd88d8d7d7bf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -40,6 +40,10 @@ jobs: run: | python -m pip install --upgrade pip python setup.py build_exe --yes + if ( $? -eq $false ) { + Write-Error "setup.py failed!" + exit 1 + } $NAME="$(ls build | Select-String -Pattern 'exe')".Split('.',2)[1] $ZIP_NAME="Archipelago_$NAME.7z" echo "$NAME -> $ZIP_NAME" @@ -49,12 +53,6 @@ jobs: Rename-Item "exe.$NAME" Archipelago 7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago Rename-Item Archipelago "exe.$NAME" # inno_setup.iss expects the original name - - name: Store 7z - uses: actions/upload-artifact@v4 - with: - name: ${{ env.ZIP_NAME }} - path: dist/${{ env.ZIP_NAME }} - retention-days: 7 # keep for 7 days, should be enough - name: Build Setup run: | & "${env:ProgramFiles(x86)}\Inno Setup 6\iscc.exe" inno_setup.iss /DNO_SIGNTOOL @@ -65,11 +63,38 @@ jobs: $contents = Get-ChildItem -Path setups/*.exe -Force -Recurse $SETUP_NAME=$contents[0].Name echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV + - name: Check build loads expected worlds + shell: bash + run: | + cd build/exe* + mv Players/Templates/meta.yaml . + ls -1 Players/Templates | sort > setup-player-templates.txt + rm -R Players/Templates + timeout 30 ./ArchipelagoLauncher "Generate Template Options" || true + ls -1 Players/Templates | sort > generated-player-templates.txt + cmp setup-player-templates.txt generated-player-templates.txt \ + || diff setup-player-templates.txt generated-player-templates.txt + mv meta.yaml Players/Templates/ + - name: Test Generate + shell: bash + run: | + cd build/exe* + cp Players/Templates/Clique.yaml Players/ + timeout 30 ./ArchipelagoGenerate + - name: Store 7z + uses: actions/upload-artifact@v4 + with: + name: ${{ env.ZIP_NAME }} + path: dist/${{ env.ZIP_NAME }} + compression-level: 0 # .7z is incompressible by zip + if-no-files-found: error + retention-days: 7 # keep for 7 days, should be enough - name: Store Setup uses: actions/upload-artifact@v4 with: name: ${{ env.SETUP_NAME }} path: setups/${{ env.SETUP_NAME }} + if-no-files-found: error retention-days: 7 # keep for 7 days, should be enough build-ubuntu2004: @@ -110,7 +135,7 @@ jobs: echo -e "setup.py dist output:\n `ls dist`" cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd .. export TAR_NAME="${APPIMAGE_NAME%.AppImage}.tar.gz" - (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME") + (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -cv Archipelago | gzip -8 > ../dist/$TAR_NAME && mv Archipelago "$DIR_NAME") echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV # - copy code above to release.yml - @@ -118,15 +143,36 @@ jobs: run: | source venv/bin/activate python setup.py build_exe --yes + - name: Check build loads expected worlds + shell: bash + run: | + cd build/exe* + mv Players/Templates/meta.yaml . + ls -1 Players/Templates | sort > setup-player-templates.txt + rm -R Players/Templates + timeout 30 ./ArchipelagoLauncher "Generate Template Options" || true + ls -1 Players/Templates | sort > generated-player-templates.txt + cmp setup-player-templates.txt generated-player-templates.txt \ + || diff setup-player-templates.txt generated-player-templates.txt + mv meta.yaml Players/Templates/ + - name: Test Generate + shell: bash + run: | + cd build/exe* + cp Players/Templates/Clique.yaml Players/ + timeout 30 ./ArchipelagoGenerate - name: Store AppImage uses: actions/upload-artifact@v4 with: name: ${{ env.APPIMAGE_NAME }} path: dist/${{ env.APPIMAGE_NAME }} + if-no-files-found: error retention-days: 7 - name: Store .tar.gz uses: actions/upload-artifact@v4 with: name: ${{ env.TAR_NAME }} path: dist/${{ env.TAR_NAME }} + compression-level: 0 # .gz is incompressible by zip + if-no-files-found: error retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d7f1253b760..3f8651d408e7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -69,7 +69,7 @@ jobs: echo -e "setup.py dist output:\n `ls dist`" cd dist && export APPIMAGE_NAME="`ls *.AppImage`" && cd .. export TAR_NAME="${APPIMAGE_NAME%.AppImage}.tar.gz" - (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME") + (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -cv Archipelago | gzip -8 > ../dist/$TAR_NAME && mv Archipelago "$DIR_NAME") echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV # - code above copied from build.yml - diff --git a/Generate.py b/Generate.py index 0cef081120e6..1fbb9e76a483 100644 --- a/Generate.py +++ b/Generate.py @@ -66,13 +66,15 @@ def get_seed_name(random_source) -> str: def main(args=None): + # __name__ == "__main__" check so unittests that already imported worlds don't trip this. + if __name__ == "__main__" and "worlds" in sys.modules: + raise Exception("Worlds system should not be loaded before logging init.") + if not args: args = mystery_argparse() seed = get_seed(args.seed) - # __name__ == "__main__" check so unittests that already imported worlds don't trip this. - if __name__ == "__main__" and "worlds" in sys.modules: - raise Exception("Worlds system should not be loaded before logging init.") + Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level) random.seed(seed) seed_name = get_seed_name(random) From da34800f43a445470820778e3a7e0c5be5b7d4d3 Mon Sep 17 00:00:00 2001 From: JoshuaEagles Date: Thu, 13 Jun 2024 00:53:01 -0400 Subject: [PATCH 17/58] Fix Incorrect Link Syntax in SA2B Linux Setup (#3524) --- worlds/sa2b/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sa2b/docs/setup_en.md b/worlds/sa2b/docs/setup_en.md index 354ef4bbe986..f32001a67827 100644 --- a/worlds/sa2b/docs/setup_en.md +++ b/worlds/sa2b/docs/setup_en.md @@ -48,7 +48,7 @@ 7. Install protontricks, on the Steam Deck this can be done via the Discover store, on other distros instructions vary, [see its github page](https://github.com/Matoking/protontricks). -8. Download the [.NET 7 Desktop Runtime for x64 Windows](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-7.0.17-windows-x64-installer}. If this link does not work, the download can be found on [this page](https://dotnet.microsoft.com/en-us/download/dotnet/7.0). +8. Download the [.NET 7 Desktop Runtime for x64 Windows](https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-desktop-7.0.17-windows-x64-installer). If this link does not work, the download can be found on [this page](https://dotnet.microsoft.com/en-us/download/dotnet/7.0). 9. Right click the .NET 7 Desktop Runtime exe, and assuming protontricks was installed correctly, the option to "Open with Protontricks Launcher" should be available. Click that, and in the popup window that opens, select SAModManager.exe. Follow the prompts after this to install the .NET 7 Desktop Runtime for SAModManager. Once it is done, you should be able to successfully launch SAModManager to steam. From f6e3113af6805c38e1e5c322c5747b89465722f1 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 13 Jun 2024 10:39:16 +0200 Subject: [PATCH 18/58] WebHost: Fix "Add" button for custom option values causing a weird redirect (#3518) * WebHost: Fix "Add" button for Progression Balancing causing a weird redirect This "add" button is part of a form, which causes it to submit the form, because the default type for a button is "submit". This PR changes the type of the button to "button", which causes it to not submit the form and just execute its normal effect. (An alternative would be `event.preventDefault()` but that seems less clean to me, but also I'm not a HTML/JS dev) * There's also multiple. --- WebHostLib/templates/weightedOptions/macros.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html index 55a56e32851d..c7a5d4174b64 100644 --- a/WebHostLib/templates/weightedOptions/macros.html +++ b/WebHostLib/templates/weightedOptions/macros.html @@ -47,7 +47,7 @@ {% endif %}
- +
@@ -72,7 +72,7 @@ This option allows custom values only. Please enter your desired values below.
- +
@@ -89,7 +89,7 @@ Custom values are also allowed for this option. To create one, enter it into the input box below.
- +
From 2ae51364d9052731ea3fae2b25aa61d5a7e5a8ae Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 13 Jun 2024 18:24:56 +0200 Subject: [PATCH 19/58] WebHost: Fix default values that are 2 or more words in Weighted Options (#3519) * WeightedOptions: Fix default values that are 2 or more words * So much simpler --- WebHostLib/templates/weightedOptions/macros.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WebHostLib/templates/weightedOptions/macros.html b/WebHostLib/templates/weightedOptions/macros.html index c7a5d4174b64..4d9e7ca4d338 100644 --- a/WebHostLib/templates/weightedOptions/macros.html +++ b/WebHostLib/templates/weightedOptions/macros.html @@ -19,7 +19,7 @@ {% for id, name in option.name_lookup.items() %} {% if name != 'random' %} {% if option.default != 'random' %} - {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }} + {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.default == id else None) }} {% else %} {{ RangeRow(option_name, option, option.get_option_name(id), name) }} {% endif %} @@ -97,7 +97,7 @@ {% for id, name in option.name_lookup.items() %} {% if name != 'random' %} {% if option.default != 'random' %} - {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.get_option_name(option.default)|lower == name else None) }} + {{ RangeRow(option_name, option, option.get_option_name(id), name, False, name if option.default == id else None) }} {% else %} {{ RangeRow(option_name, option, option.get_option_name(id), name) }} {% endif %} From 533395d336073081bcf8a7cba461f7529a49d8ff Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 13 Jun 2024 23:29:39 +0200 Subject: [PATCH 20/58] WebHost: Fix Named Range displays on Player Options page (#3521) * Player Options: Fix Named Range displays * Also add validation to the NamedRange class itself * Don't break Stardew * Comment * Do replace first so title works correctly * Bring change to Weighted Options as well --- Options.py | 6 ++++++ WebHostLib/templates/playerOptions/macros.html | 4 ++-- WebHostLib/templates/weightedOptions/macros.html | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/Options.py b/Options.py index 40a6996d325a..2d3ef99d6491 100644 --- a/Options.py +++ b/Options.py @@ -735,6 +735,12 @@ def __init__(self, value: int) -> None: elif value > self.range_end and value not in self.special_range_names.values(): raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " + f"and is also not one of the supported named special values: {self.special_range_names}") + + # See docstring + for key in self.special_range_names: + if key != key.lower(): + raise Exception(f"{self.__class__.__name__} has an invalid special_range_names key: {key}. " + f"NamedRange keys must use only lowercase letters, and ideally should be snake_case.") self.value = value @classmethod diff --git a/WebHostLib/templates/playerOptions/macros.html b/WebHostLib/templates/playerOptions/macros.html index b34ac79a029e..2187ffe913e5 100644 --- a/WebHostLib/templates/playerOptions/macros.html +++ b/WebHostLib/templates/playerOptions/macros.html @@ -57,9 +57,9 @@