From 394633558fae17683bb9baaa5f4d4da01fd0b57c Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 15 Dec 2023 14:39:09 -0500 Subject: [PATCH 001/134] ALTTP: Restore allow_excluded (#2607) Restores allow_excluded to the dungeon fill_restrictive call, which was apparently removed by mistake during merge conflict resolution --- worlds/alttp/Dungeons.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index a68acf7288..b456174f39 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -264,7 +264,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld): if loc in all_state_base.events: all_state_base.events.remove(loc) - fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, + fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, True, True, allow_excluded=True, name="LttP Dungeon Items") From b500cf600c329b7c6e598f928be3ff173ceea020 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:16:13 -0500 Subject: [PATCH 002/134] FFMQ: Actually fix the spellbook option (#2594) --- worlds/ffmq/Options.py | 4 ++-- worlds/ffmq/data/settings.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/ffmq/Options.py b/worlds/ffmq/Options.py index eaf3097494..4b9f4a4a88 100644 --- a/worlds/ffmq/Options.py +++ b/worlds/ffmq/Options.py @@ -267,11 +267,11 @@ class CompanionLevelingType(Choice): class CompanionSpellbookType(Choice): """Update companions' spellbook. Standard: Original game spellbooks. - Standard Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard. + Extended: Add some extra spells. Tristam gains Exit and Quake and Reuben gets Blizzard. Random Balanced: Randomize the spellbooks with an appropriate mix of spells. Random Chaos: Randomize the spellbooks in total free-for-all.""" option_standard = 0 - option_standard_extended = 1 + option_extended = 1 option_random_balanced = 2 option_random_chaos = 3 default = 0 diff --git a/worlds/ffmq/data/settings.yaml b/worlds/ffmq/data/settings.yaml index ff03ed26e6..826a8c744d 100644 --- a/worlds/ffmq/data/settings.yaml +++ b/worlds/ffmq/data/settings.yaml @@ -85,7 +85,7 @@ Final Fantasy Mystic Quest: Normal: 0 OneAndHalf: 0 Double: 0 - DoubleHalf: 0 + DoubleAndHalf: 0 Triple: 0 Quadruple: 0 companion_leveling_type: @@ -98,7 +98,7 @@ Final Fantasy Mystic Quest: BenPlus10: 0 companion_spellbook_type: Standard: 0 - StandardExtended: 0 + Extended: 0 RandomBalanced: 0 RandomChaos: 0 starting_companion: From 6c4fdc985df5d1fabe1bfeb3da594bda1b9baa5d Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:16:36 -0500 Subject: [PATCH 003/134] SA2B: Fix Weapons Bed - Omochao 2 Logic (#2605) --- worlds/sa2b/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sa2b/Rules.py b/worlds/sa2b/Rules.py index 6b7ad69cd1..afd0dcb182 100644 --- a/worlds/sa2b/Rules.py +++ b/worlds/sa2b/Rules.py @@ -638,7 +638,7 @@ def set_mission_upgrade_rules_standard(multiworld: MultiWorld, world: World, pla add_rule(multiworld.get_location(LocationName.radical_highway_omo_2, player), lambda state: state.has(ItemName.shadow_air_shoes, player)) add_rule(multiworld.get_location(LocationName.weapons_bed_omo_2, player), - lambda state: state.has(ItemName.eggman_jet_engine, player)) + lambda state: state.has(ItemName.eggman_large_cannon, player)) add_rule(multiworld.get_location(LocationName.mission_street_omo_3, player), lambda state: state.has(ItemName.tails_booster, player)) From c56cbd047488d09e3055d1247abb124269199502 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Fri, 15 Dec 2023 22:28:54 -0500 Subject: [PATCH 004/134] SM: item link replacement fix (#2597) --- worlds/sm/variaRandomizer/rando/GraphBuilder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/sm/variaRandomizer/rando/GraphBuilder.py b/worlds/sm/variaRandomizer/rando/GraphBuilder.py index 7bee33ec82..88b539e7f0 100644 --- a/worlds/sm/variaRandomizer/rando/GraphBuilder.py +++ b/worlds/sm/variaRandomizer/rando/GraphBuilder.py @@ -170,7 +170,8 @@ class GraphBuilder(object): ap = "Landing Site" # dummy value it'll be overwritten at first collection while len(itemLocs) > 0 and not (sm.canPassG4() and graph.canAccess(sm, ap, "Landing Site", maxDiff)): il = itemLocs.pop(0) - if il.Location.restricted or il.Item.Type == "ArchipelagoItem": + # can happen with item links replacement items that its not in the container's itemPool + if il.Location.restricted or il.Item.Type == "ArchipelagoItem" or il.Item not in container.itemPool: continue self.log.debug("collecting " + getItemLocStr(il)) container.collect(il) From 7dff09dc1ad0d760621684c847169042ed78643f Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Sat, 16 Dec 2023 15:21:05 -0600 Subject: [PATCH 005/134] Options: set old options api before the world is created (#2378) --- BaseClasses.py | 15 ++++++++++----- Utils.py | 19 +++++++++++++++++++ worlds/AutoWorld.py | 4 ++++ 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index c0a77708c0..dddcfc3a6f 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -252,15 +252,20 @@ class MultiWorld(): range(1, self.players + 1)} def set_options(self, args: Namespace) -> None: + # TODO - remove this section once all worlds use options dataclasses + all_keys: Set[str] = {key for player in self.player_ids for key in + AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints} + for option_key in all_keys: + option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. " + f"Please use `self.options.{option_key}` instead.") + option.update(getattr(args, option_key, {})) + setattr(self, option_key, option) + for player in self.player_ids: world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]] self.worlds[player] = world_type(self, player) self.worlds[player].random = self.per_slot_randoms[player] - for option_key in world_type.options_dataclass.type_hints: - option_values = getattr(args, option_key, {}) - setattr(self, option_key, option_values) - # TODO - remove this loop once all worlds use options dataclasses - options_dataclass: typing.Type[Options.PerGameCommonOptions] = self.worlds[player].options_dataclass + options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player] for option_key in options_dataclass.type_hints}) diff --git a/Utils.py b/Utils.py index 5955e92432..f6e4a9ab60 100644 --- a/Utils.py +++ b/Utils.py @@ -779,6 +779,25 @@ def deprecate(message: str): import warnings warnings.warn(message) + +class DeprecateDict(dict): + log_message: str + should_error: bool + + def __init__(self, message, error: bool = False) -> None: + self.log_message = message + self.should_error = error + super().__init__() + + def __getitem__(self, item: Any) -> Any: + if self.should_error: + deprecate(self.log_message) + elif __debug__: + import warnings + warnings.warn(self.log_message) + return super().__getitem__(item) + + def _extend_freeze_support() -> None: """Extend multiprocessing.freeze_support() to also work on Non-Windows for spawn.""" # upstream issue: https://github.com/python/cpython/issues/76327 diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 5d0533e068..f56c39f690 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -77,6 +77,10 @@ class AutoWorldRegister(type): # create missing options_dataclass from legacy option_definitions # TODO - remove this once all worlds use options dataclasses if "options_dataclass" not in dct and "option_definitions" in dct: + # TODO - switch to deprecate after a version + if __debug__: + from warnings import warn + warn("Assigning options through option_definitions is now deprecated. Use options_dataclass instead.") dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(), bases=(PerGameCommonOptions,)) From f958af4067c9f0be0faf5349e19cb5a92b190edf Mon Sep 17 00:00:00 2001 From: Silvris <58583688+Silvris@users.noreply.github.com> Date: Sat, 16 Dec 2023 15:22:51 -0600 Subject: [PATCH 006/134] Adventure: Fix KeyError on Retrieved (#2560) --- AdventureClient.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/AdventureClient.py b/AdventureClient.py index d2f4e734ac..06e4d60dad 100644 --- a/AdventureClient.py +++ b/AdventureClient.py @@ -115,11 +115,12 @@ class AdventureContext(CommonContext): msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}" self._set_message(msg, SYSTEM_MESSAGE_ID) elif cmd == "Retrieved": - self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"] - if self.freeincarnates_used is None: - self.freeincarnates_used = 0 - self.freeincarnates_used += self.freeincarnate_pending - self.send_pending_freeincarnates() + if f"adventure_{self.auth}_freeincarnates_used" in args["keys"]: + self.freeincarnates_used = args["keys"][f"adventure_{self.auth}_freeincarnates_used"] + if self.freeincarnates_used is None: + self.freeincarnates_used = 0 + self.freeincarnates_used += self.freeincarnate_pending + self.send_pending_freeincarnates() elif cmd == "SetReply": if args["key"] == f"adventure_{self.auth}_freeincarnates_used": self.freeincarnates_used = args["value"] From 4979314825c4498bbaa16409ce1a4275c80bbf4b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 17 Dec 2023 06:08:40 +0100 Subject: [PATCH 007/134] Webhost: open graph support for /room (#2580) * WebHost: add Open Graph metadata to /room * WebHost: Open Graph cleanup --- WebHostLib/templates/hostRoom.html | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index ba15d64aca..2981c41452 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -3,6 +3,16 @@ {% block head %} Multiworld {{ room.id|suuid }} {% if should_refresh %}{% endif %} + + + + {% if room.seed.slots|length < 2 %} + + {% else %} + + {% endif %} {% endblock %} From a549af8304de03c3a6814c53d589a7b2f4a9c3f4 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sun, 17 Dec 2023 10:11:40 -0600 Subject: [PATCH 008/134] Hollow Knight: Add additional DeathLink option and add ExtraPlatforms option. (#2545) --- worlds/hk/Options.py | 47 ++++++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index fcc938474d..ef7fbd0dfe 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -2,7 +2,7 @@ import typing from .ExtractedData import logic_options, starts, pool_options from .Rules import cost_terms -from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange +from Options import Option, DefaultOnToggle, Toggle, Choice, Range, OptionDict, NamedRange, DeathLink from .Charms import vanilla_costs, names as charm_names if typing.TYPE_CHECKING: @@ -402,22 +402,34 @@ class WhitePalace(Choice): default = 0 -class DeathLink(Choice): +class ExtraPlatforms(DefaultOnToggle): + """Places additional platforms to make traveling throughout Hallownest more convenient.""" + + +class DeathLinkShade(Choice): + """Sets whether to create a shade when you are killed by a DeathLink and how to handle your existing shade, if any. + + vanilla: DeathLink deaths function like any other death and overrides your existing shade (including geo), if any. + shadeless: DeathLink deaths do not spawn shades. Your existing shade (including geo), if any, is untouched. + shade: DeathLink deaths spawn a shade if you do not have an existing shade. Otherwise, it acts like shadeless. + + * This option has no effect if DeathLink is disabled. + ** Self-death shade behavior is not changed; if a self-death normally creates a shade in vanilla, it will override + your existing shade, if any. """ - When you die, everyone dies. Of course the reverse is true too. - When enabled, choose how incoming deathlinks are handled: - vanilla: DeathLink kills you and is just like any other death. RIP your previous shade and geo. - shadeless: DeathLink kills you, but no shade spawns and no geo is lost. Your previous shade, if any, is untouched. - shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise. - """ - option_off = 0 - alias_no = 0 - alias_true = 1 - alias_on = 1 - alias_yes = 1 + option_vanilla = 0 option_shadeless = 1 - option_vanilla = 2 - option_shade = 3 + option_shade = 2 + default = 2 + + +class DeathLinkBreaksFragileCharms(Toggle): + """Sets if fragile charms break when you are killed by a DeathLink. + + * This option has no effect if DeathLink is disabled. + ** Self-death fragile charm behavior is not changed; if a self-death normally breaks fragile charms in vanilla, it + will continue to do so. + """ class StartingGeo(Range): @@ -476,7 +488,8 @@ hollow_knight_options: typing.Dict[str, type(Option)] = { **{ option.__name__: option for option in ( - StartLocation, Goal, WhitePalace, StartingGeo, DeathLink, + StartLocation, Goal, WhitePalace, ExtraPlatforms, StartingGeo, + DeathLink, DeathLinkShade, DeathLinkBreaksFragileCharms, MinimumGeoPrice, MaximumGeoPrice, MinimumGrubPrice, MaximumGrubPrice, MinimumEssencePrice, MaximumEssencePrice, @@ -488,7 +501,7 @@ hollow_knight_options: typing.Dict[str, type(Option)] = { LegEaterShopSlots, GrubfatherRewardSlots, SeerRewardSlots, ExtraShopSlots, SplitCrystalHeart, SplitMothwingCloak, SplitMantisClaw, - CostSanity, CostSanityHybridChance, + CostSanity, CostSanityHybridChance ) }, **cost_sanity_weights From c8adadb08bbd23d1d11c114333fdb95477eb4fbf Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 18 Dec 2023 10:39:04 -0500 Subject: [PATCH 009/134] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Fix=20Flash=20lea?= =?UTF-8?q?rnable=20logic=20(#2615)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- worlds/pokemon_rb/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index d9bd6dde76..ee0f0052e1 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -414,7 +414,7 @@ class PokemonRedBlueWorld(World): > 7) or (self.multiworld.door_shuffle[self.player] not in ("off", "simple")))): intervene_move = "Cut" elif ((not logic.can_learn_hm(test_state, "Flash", self.player)) and self.multiworld.dark_rock_tunnel_logic[self.player] - and (((self.multiworld.accessibility[self.player] != "minimal" and + and (((self.multiworld.accessibility[self.player] != "minimal" or (self.multiworld.trainersanity[self.player] or self.multiworld.extra_key_items[self.player])) or self.multiworld.door_shuffle[self.player]))): intervene_move = "Flash" From 817197c14dabf8a90cf81464ce4c7cc1fb477493 Mon Sep 17 00:00:00 2001 From: Star Rauchenberger Date: Mon, 18 Dec 2023 10:46:24 -0500 Subject: [PATCH 010/134] Lingo: Tests no longer disable forced good item (#2602) The static class with the "disable forced good item" field is gone. Now, certain tests that want to check for specific access progression can run a method that removes the forced good item and adds it back to the pool. Tests that don't care about this will collect the forced good item like normal. This should prevent the intermittent fill failures on complex doors unit tests, since the forced good item should provide enough locations to fill in. --- worlds/lingo/__init__.py | 1 - worlds/lingo/player_logic.py | 3 +-- worlds/lingo/test/TestDoors.py | 10 ++++++++++ worlds/lingo/test/TestOrangeTower.py | 4 ++++ worlds/lingo/test/TestProgressive.py | 6 ++++++ worlds/lingo/test/__init__.py | 8 ++++++-- worlds/lingo/testing.py | 2 -- 7 files changed, 27 insertions(+), 7 deletions(-) delete mode 100644 worlds/lingo/testing.py diff --git a/worlds/lingo/__init__.py b/worlds/lingo/__init__.py index f22d344c8f..0889674450 100644 --- a/worlds/lingo/__init__.py +++ b/worlds/lingo/__init__.py @@ -11,7 +11,6 @@ from .options import LingoOptions from .player_logic import LingoPlayerLogic from .regions import create_regions from .static_logic import Room, RoomEntrance -from .testing import LingoTestOptions class LingoWebWorld(WebWorld): diff --git a/worlds/lingo/player_logic.py b/worlds/lingo/player_logic.py index b046f1cfe3..fa497c59bd 100644 --- a/worlds/lingo/player_logic.py +++ b/worlds/lingo/player_logic.py @@ -6,7 +6,6 @@ from .options import LocationChecks, ShuffleDoors, VictoryCondition from .static_logic import DOORS_BY_ROOM, Door, PAINTINGS, PAINTINGS_BY_ROOM, PAINTING_ENTRANCES, PAINTING_EXITS, \ PANELS_BY_ROOM, PROGRESSION_BY_ROOM, REQUIRED_PAINTING_ROOMS, REQUIRED_PAINTING_WHEN_NO_DOORS_ROOMS, RoomAndDoor, \ RoomAndPanel -from .testing import LingoTestOptions if TYPE_CHECKING: from . import LingoWorld @@ -224,7 +223,7 @@ class LingoPlayerLogic: "kind of logic error.") if door_shuffle != ShuffleDoors.option_none and location_classification != LocationClassification.insanity \ - and not early_color_hallways and LingoTestOptions.disable_forced_good_item is False: + and not early_color_hallways is False: # If shuffle doors is on, force a useful item onto the HI panel. This may not necessarily get you out of BK, # but the goal is to allow you to reach at least one more check. The non-painting ones are hardcoded right # now. We only allow the entrance to the Pilgrim Room if color shuffle is off, because otherwise there are diff --git a/worlds/lingo/test/TestDoors.py b/worlds/lingo/test/TestDoors.py index f496c5f578..49a0f9c490 100644 --- a/worlds/lingo/test/TestDoors.py +++ b/worlds/lingo/test/TestDoors.py @@ -8,6 +8,8 @@ class TestRequiredRoomLogic(LingoTestBase): } def test_pilgrim_first(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Pilgrim Antechamber", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) @@ -28,6 +30,8 @@ class TestRequiredRoomLogic(LingoTestBase): self.assertTrue(self.can_reach_location("The Seeker - Achievement")) def test_hidden_first(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("The Seeker", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Pilgrim Room", "Region", self.player)) self.assertFalse(self.can_reach_location("The Seeker - Achievement")) @@ -55,6 +59,8 @@ class TestRequiredDoorLogic(LingoTestBase): } def test_through_rhyme(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) self.collect_by_name("Starting Room - Rhyme Room Entrance") @@ -64,6 +70,8 @@ class TestRequiredDoorLogic(LingoTestBase): self.assertTrue(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) def test_through_hidden(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.can_reach_location("Rhyme Room - Circle/Looped Square Wall")) self.collect_by_name("Starting Room - Rhyme Room Entrance") @@ -83,6 +91,8 @@ class TestSimpleDoors(LingoTestBase): } def test_requirement(self): + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Outside The Wanderer", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) diff --git a/worlds/lingo/test/TestOrangeTower.py b/worlds/lingo/test/TestOrangeTower.py index 7b0c3bb525..9170de108a 100644 --- a/worlds/lingo/test/TestOrangeTower.py +++ b/worlds/lingo/test/TestOrangeTower.py @@ -8,6 +8,8 @@ class TestProgressiveOrangeTower(LingoTestBase): } def test_from_welcome_back(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) @@ -83,6 +85,8 @@ class TestProgressiveOrangeTower(LingoTestBase): self.assertTrue(self.multiworld.state.can_reach("Orange Tower Seventh Floor", "Region", self.player)) def test_from_hub_room(self) -> None: + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Orange Tower First Floor", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Second Floor", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Orange Tower Third Floor", "Region", self.player)) diff --git a/worlds/lingo/test/TestProgressive.py b/worlds/lingo/test/TestProgressive.py index 917c6e7e89..8edc7ce6cc 100644 --- a/worlds/lingo/test/TestProgressive.py +++ b/worlds/lingo/test/TestProgressive.py @@ -7,6 +7,8 @@ class TestComplexProgressiveHallwayRoom(LingoTestBase): } def test_item(self): + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) @@ -58,6 +60,8 @@ class TestSimpleHallwayRoom(LingoTestBase): } def test_item(self): + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Outside The Agreeable", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Hallway Room (2)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Hallway Room (3)", "Region", self.player)) @@ -86,6 +90,8 @@ class TestProgressiveArtGallery(LingoTestBase): } def test_item(self): + self.remove_forced_good_item() + self.assertFalse(self.multiworld.state.can_reach("Art Gallery", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Second Floor)", "Region", self.player)) self.assertFalse(self.multiworld.state.can_reach("Art Gallery (Third Floor)", "Region", self.player)) diff --git a/worlds/lingo/test/__init__.py b/worlds/lingo/test/__init__.py index ffbf9032b6..7ff456d8fc 100644 --- a/worlds/lingo/test/__init__.py +++ b/worlds/lingo/test/__init__.py @@ -1,7 +1,6 @@ from typing import ClassVar from test.bases import WorldTestBase -from .. import LingoTestOptions class LingoTestBase(WorldTestBase): @@ -9,5 +8,10 @@ class LingoTestBase(WorldTestBase): player: ClassVar[int] = 1 def world_setup(self, *args, **kwargs): - LingoTestOptions.disable_forced_good_item = True super().world_setup(*args, **kwargs) + + def remove_forced_good_item(self): + location = self.multiworld.get_location("Second Room - Good Luck", self.player) + self.remove(location.item) + self.multiworld.itempool.append(location.item) + self.multiworld.state.events.add(location) diff --git a/worlds/lingo/testing.py b/worlds/lingo/testing.py deleted file mode 100644 index 22fafea0fc..0000000000 --- a/worlds/lingo/testing.py +++ /dev/null @@ -1,2 +0,0 @@ -class LingoTestOptions: - disable_forced_good_item: bool = False From 8842f5d5c722529bab2fb3aa8187c3752dfe0c63 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 21 Dec 2023 04:11:11 +0100 Subject: [PATCH 011/134] Core: make update_reachable_regions local variables more wordy (#2522) --- BaseClasses.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index dddcfc3a6f..855e69c5d4 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -651,34 +651,34 @@ class CollectionState(): def update_reachable_regions(self, player: int): self.stale[player] = False - rrp = self.reachable_regions[player] - bc = self.blocked_connections[player] + reachable_regions = self.reachable_regions[player] + blocked_connections = self.blocked_connections[player] queue = deque(self.blocked_connections[player]) - start = self.multiworld.get_region('Menu', player) + start = self.multiworld.get_region("Menu", player) # init on first call - this can't be done on construction since the regions don't exist yet - if start not in rrp: - rrp.add(start) - bc.update(start.exits) + if start not in reachable_regions: + reachable_regions.add(start) + blocked_connections.update(start.exits) queue.extend(start.exits) # run BFS on all connections, and keep track of those blocked by missing items while queue: connection = queue.popleft() new_region = connection.connected_region - if new_region in rrp: - bc.remove(connection) + if new_region in reachable_regions: + blocked_connections.remove(connection) elif connection.can_reach(self): assert new_region, f"tried to search through an Entrance \"{connection}\" with no Region" - rrp.add(new_region) - bc.remove(connection) - bc.update(new_region.exits) + reachable_regions.add(new_region) + blocked_connections.remove(connection) + blocked_connections.update(new_region.exits) queue.extend(new_region.exits) self.path[new_region] = (new_region.name, self.path.get(connection, None)) # Retry connections if the new region can unblock them for new_entrance in self.multiworld.indirect_connections.get(new_region, set()): - if new_entrance in bc and new_entrance not in queue: + if new_entrance in blocked_connections and new_entrance not in queue: queue.append(new_entrance) def copy(self) -> CollectionState: From 0d929b81e8cfa19297e3acad16d0a220adefa487 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 21 Dec 2023 04:26:41 +0100 Subject: [PATCH 012/134] Factorio: fix files from mod base directory not being grabbed correctly in non-apworld (#2603) --- worlds/factorio/Mod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 21a8c684f9..d7b3d4b1eb 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -177,7 +177,7 @@ def generate_mod(world: "Factorio", output_directory: str): else: basepath = os.path.join(os.path.dirname(__file__), "data", "mod") for dirpath, dirnames, filenames in os.walk(basepath): - base_arc_path = versioned_mod_name+"/"+os.path.relpath(dirpath, basepath) + base_arc_path = (versioned_mod_name+"/"+os.path.relpath(dirpath, basepath)).rstrip("/.\\") for filename in filenames: mod.writing_tasks.append(lambda arcpath=base_arc_path+"/"+filename, file_path=os.path.join(dirpath, filename): From bb0a0f2acaf04ab5919284a88f5cf613576d97d3 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 22 Dec 2023 20:02:49 -0800 Subject: [PATCH 013/134] Factorio: Fix unbeatable seeds where a science pack needs chemical plant (#2613) --- worlds/factorio/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index eb078720c6..17f3163e90 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -246,7 +246,8 @@ class Factorio(World): location.access_rule = lambda state, ingredient=ingredient, custom_recipe=custom_recipe: \ (ingredient not in technology_table or state.has(ingredient, player)) and \ all(state.has(technology.name, player) for sub_ingredient in custom_recipe.ingredients - for technology in required_technologies[sub_ingredient]) + for technology in required_technologies[sub_ingredient]) and \ + all(state.has(technology.name, player) for technology in required_technologies[custom_recipe.crafting_machine]) else: location.access_rule = lambda state, ingredient=ingredient: \ all(state.has(technology.name, player) for technology in required_technologies[ingredient]) From 2512eb750109d83fe1003bf25d18002f09f39763 Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Wed, 27 Dec 2023 22:25:41 -0700 Subject: [PATCH 014/134] Hylics 2: Add missing logic (#2638) --- worlds/hylics2/Rules.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/worlds/hylics2/Rules.py b/worlds/hylics2/Rules.py index 6c55c8745b..ff9544e0e8 100644 --- a/worlds/hylics2/Rules.py +++ b/worlds/hylics2/Rules.py @@ -444,6 +444,8 @@ def set_rules(hylics2world): lambda state: paddle(state, player)) add_rule(world.get_location("Arcade 1: Alcove Medallion", player), lambda state: paddle(state, player)) + add_rule(world.get_location("Arcade 1: Lava Medallion", player), + lambda state: paddle(state, player)) add_rule(world.get_location("Foglast: Under Lair Medallion", player), lambda state: bridge_key(state, player)) add_rule(world.get_location("Foglast: Mid-Air Medallion", player), From 7c70b87f2971528730254638d2a30d61b9a8f851 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Thu, 28 Dec 2023 08:01:48 +0100 Subject: [PATCH 015/134] sm64ex: Fix randomizing Courses and Secrets separately (#2637) Backported from #2569 --- worlds/sm64ex/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index c428f85543..fedd5b7a6e 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -21,7 +21,7 @@ def fix_reg(entrance_map: dict, entrance: SM64Levels, invalid_regions: set, def set_rules(world, player: int, area_connections: dict): randomized_level_to_paintings = sm64_level_to_paintings.copy() randomized_level_to_secrets = sm64_level_to_secrets.copy() - if world.AreaRandomizer[player].value == 1: # Some randomization is happening, randomize Courses + if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses randomized_level_to_paintings = shuffle_dict_keys(world,sm64_level_to_paintings) if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well randomized_level_to_secrets = shuffle_dict_keys(world,sm64_level_to_secrets) From b99c7349549b1a9597d13dcc25c264ab7ca3138c Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Thu, 28 Dec 2023 06:14:13 -0500 Subject: [PATCH 016/134] SM: strict rom validation fix (#2632) added a more robust ROM tag validation to free oher games to use tag starting with "SM" followed by another letter (SMW, SMZ3, SMRPG, SMMR,...) --- worlds/sm/Client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sm/Client.py b/worlds/sm/Client.py index df73ae47a0..756fd4bf36 100644 --- a/worlds/sm/Client.py +++ b/worlds/sm/Client.py @@ -61,7 +61,7 @@ class SMSNIClient(SNIClient): from SNIClient import snes_buffered_write, snes_flush_writes, snes_read rom_name = await snes_read(ctx, SM_ROMNAME_START, ROMNAME_SIZE) - if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"SM" or rom_name[:3] == b"SMW": + if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"SM" or rom_name[2] not in b"1234567890": return False ctx.game = self.game From 576c7051068b3c99d13ff1b8afd7e7640ae4e51c Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Thu, 28 Dec 2023 06:15:48 -0500 Subject: [PATCH 017/134] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Badge=20plando=20?= =?UTF-8?q?fix=20(#2628)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only attempt to place badges in badge locations if they are empty. Return unplaced badges to the item pool if fewer than 8 locations are being filled. This should fix errors that occur when items are placed into badge locations via plando, or whatever other worlds may do. --- worlds/pokemon_rb/__init__.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index ee0f0052e1..5a94a8b5ff 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -281,18 +281,20 @@ class PokemonRedBlueWorld(World): self.multiworld.itempool.remove(badge) progitempool.remove(badge) for _ in range(5): - badgelocs = [self.multiworld.get_location(loc, self.player) for loc in [ - "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize", - "Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize", - "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize", - "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize"]] + badgelocs = [ + self.multiworld.get_location(loc, self.player) for loc in [ + "Pewter Gym - Brock Prize", "Cerulean Gym - Misty Prize", + "Vermilion Gym - Lt. Surge Prize", "Celadon Gym - Erika Prize", + "Fuchsia Gym - Koga Prize", "Saffron Gym - Sabrina Prize", + "Cinnabar Gym - Blaine Prize", "Viridian Gym - Giovanni Prize" + ] if self.multiworld.get_location(loc, self.player).item is None] state = self.multiworld.get_all_state(False) self.multiworld.random.shuffle(badges) self.multiworld.random.shuffle(badgelocs) badgelocs_copy = badgelocs.copy() # allow_partial so that unplaced badges aren't lost, for debugging purposes fill_restrictive(self.multiworld, state, badgelocs_copy, badges, True, True, allow_partial=True) - if badges: + if len(badges) > 8 - len(badgelocs): for location in badgelocs: if location.item: badges.append(location.item) @@ -302,6 +304,7 @@ class PokemonRedBlueWorld(World): for location in badgelocs: if location.item: fill_locations.remove(location) + progitempool += badges break else: raise FillError(f"Failed to place badges for player {self.player}") From 70eb2b58f55c277433d4f453ea2f5daa51462fbf Mon Sep 17 00:00:00 2001 From: Rosalie-A <61372066+Rosalie-A@users.noreply.github.com> Date: Thu, 28 Dec 2023 06:16:38 -0500 Subject: [PATCH 018/134] [TLOZ] Fix bug with item drops in non-expanded item pool (#2623) There was a bug in non-expanded item pool where due to the base patch changes to accommodate more items in dungeons, some items were transformed into glitch items that removed bombs (this also happened in expanded item pool, but the item placement would overwrite the results of this bug so it didn't appear as frequently). Being a Zelda game, losing bombs is bad. This PR fixes the base patch process to avoid this bug, by properly carrying the value of a variable through a procedure. --- worlds/tloz/__init__.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 20ab003ead..6e8927c4e7 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -200,15 +200,17 @@ class TLoZWorld(World): for i in range(0, 0x7F): item = rom_data[first_quest_dungeon_items_early + i] if item & 0b00100000: - rom_data[first_quest_dungeon_items_early + i] = item & 0b11011111 - rom_data[first_quest_dungeon_items_early + i] = item | 0b01000000 + item = item & 0b11011111 + item = item | 0b01000000 + rom_data[first_quest_dungeon_items_early + i] = item if item & 0b00011111 == 0b00000011: # Change all Item 03s to Item 3F, the proper "nothing" rom_data[first_quest_dungeon_items_early + i] = item | 0b00111111 item = rom_data[first_quest_dungeon_items_late + i] if item & 0b00100000: - rom_data[first_quest_dungeon_items_late + i] = item & 0b11011111 - rom_data[first_quest_dungeon_items_late + i] = item | 0b01000000 + item = item & 0b11011111 + item = item | 0b01000000 + rom_data[first_quest_dungeon_items_late + i] = item if item & 0b00011111 == 0b00000011: rom_data[first_quest_dungeon_items_late + i] = item | 0b00111111 return rom_data From 04d194db741ee190d87348c282195e299dccfc74 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Thu, 28 Dec 2023 04:33:30 -0800 Subject: [PATCH 019/134] Pokemon Emerald: Change "settings" to "options" in docs (#2517) * Pokemon Emerald: Change "settings" to "options" in docs * Pokemon Emerald: Fix two more usages of "setting" instead of "option" * Pokemon Emerald: Minor rephrase in docs Co-authored-by: Aaron Wagener --------- Co-authored-by: Aaron Wagener --- worlds/pokemon_emerald/docs/en_Pokemon Emerald.md | 10 +++++----- worlds/pokemon_emerald/docs/setup_en.md | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md b/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md index 5d50c37ea9..8b09b51b38 100644 --- a/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md +++ b/worlds/pokemon_emerald/docs/en_Pokemon Emerald.md @@ -1,15 +1,15 @@ # Pokémon Emerald -## Where is the settings page? +## Where is the options page? -You can read through all the settings and generate a YAML [here](../player-settings). +You can read through all the options and generate a YAML [here](../player-options). ## What does randomization do to this game? This randomizer handles both item randomization and pokémon randomization. Badges, HMs, gifts from NPCs, and items on the ground can all be randomized. There are also many options for randomizing wild pokémon, starters, opponent pokémon, abilities, types, etc… You can even change a percentage of single battles into double battles. Check the -[settings page](../player-settings) for a more comprehensive list of what can be changed. +[options page](../player-options) for a more comprehensive list of what can be changed. ## What items and locations get randomized? @@ -28,7 +28,7 @@ randomizer. Here are some of the more important ones: - You can have both bikes simultaneously - You can run or bike (almost) anywhere - The Wally catching tutorial is skipped -- All text is instant, and with a setting it can be automatically progressed by holding A +- All text is instant and, with an option, can be automatically progressed by holding A - When a Repel runs out, you will be prompted to use another - Many more minor improvements… @@ -44,7 +44,7 @@ your inventory. ## When the player receives an item, what happens? You will only receive items while in the overworld and not during battles. Depending on your `Receive Item Messages` -setting, the received item will either be silently added to your bag or you will be shown a text box with the item's +option, the received item will either be silently added to your bag or you will be shown a text box with the item's name and the item will be added to your bag while a fanfare plays. ## Can I play offline? diff --git a/worlds/pokemon_emerald/docs/setup_en.md b/worlds/pokemon_emerald/docs/setup_en.md index 3c5c8c193a..e3f6d3c301 100644 --- a/worlds/pokemon_emerald/docs/setup_en.md +++ b/worlds/pokemon_emerald/docs/setup_en.md @@ -26,8 +26,8 @@ clear it. ## Generating and Patching a Game -1. Create your settings file (YAML). You can make one on the -[Pokémon Emerald settings page](../../../games/Pokemon%20Emerald/player-settings). +1. Create your options file (YAML). You can make one on the +[Pokémon Emerald options page](../../../games/Pokemon%20Emerald/player-options). 2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game). This will generate an output file for you. Your patch file will have the `.apemerald` file extension. 3. Open `ArchipelagoLauncher.exe` From af1f6e9113a80442dd91fcef0b22935c216633c6 Mon Sep 17 00:00:00 2001 From: TheLynk <44308308+TheLynk@users.noreply.github.com> Date: Thu, 28 Dec 2023 13:43:42 +0100 Subject: [PATCH 020/134] Oot : Update setup fr (#2394) * add new translation * Add translation for OOT Setup in french * Update setup_fr.md * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/minecraft/docs/minecraft_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update worlds/oot/docs/setup_fr.md Co-authored-by: Ludovic Marechal * Update setup_fr.md Fix treu to true * Update worlds/oot/docs/setup_fr.md Co-authored-by: Marech * Update OOT Init and Update Minecraft Init * Fix formatting errors * Fix wrong link in stardew valley randomizer setup guide Fix wrong link in stardew valley randomizer setup guide * Add new translation for Adventure and Archipidle in french Add new translation for Adventure and Archipidle in french * Add more store in setup page subnautica for more fairness Add more store in setup page subnautica for more fairness * tweak update merge #1685 for lua file tweak update merge #1685 for lua file * fix text fix text * fix wrong translation fix wrong translation * Yes it's better Yes it's better Co-authored-by: Fabian Dill * Update OOT Setup FR Update OOT Setup FR * Tweak Text Tweak Text --------- Co-authored-by: Ludovic Marechal Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: Fabian Dill --- worlds/oot/docs/setup_fr.md | 426 ++++-------------------------------- 1 file changed, 37 insertions(+), 389 deletions(-) diff --git a/worlds/oot/docs/setup_fr.md b/worlds/oot/docs/setup_fr.md index 57099cdf2e..f5915e1878 100644 --- a/worlds/oot/docs/setup_fr.md +++ b/worlds/oot/docs/setup_fr.md @@ -1,422 +1,70 @@ -# Guide d'installation Archipelago pour Ocarina of Time +# Guide de configuration pour Ocarina of Time Archipelago ## Important -Comme nous utilisons BizHawk, ce guide ne s'applique qu'aux systèmes Windows et Linux. +Comme nous utilisons BizHawk, ce guide s'applique uniquement aux systèmes Windows et Linux. ## Logiciel requis -- BizHawk : [BizHawk sort de TASVideos] (https://tasvideos.org/BizHawk/ReleaseHistory) - - Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour la stabilité. +- BizHawk : [Sorties BizHawk de TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory) + - Les versions 2.3.1 et ultérieures sont prises en charge. La version 2.7 est recommandée pour des raisons de stabilité. - Des instructions d'installation détaillées pour BizHawk peuvent être trouvées sur le lien ci-dessus. - - Les utilisateurs Windows doivent d'abord exécuter le programme d'installation prereq, qui peut également être trouvé sur le lien ci-dessus. + - Les utilisateurs Windows doivent d'abord exécuter le programme d'installation des prérequis, qui peut également être trouvé sur le lien ci-dessus. - Le client Archipelago intégré, qui peut être installé [ici](https://github.com/ArchipelagoMW/Archipelago/releases) - (sélectionnez `Ocarina of Time Client` lors de l'installation). + (sélectionnez « Ocarina of Time Client » lors de l'installation). - Une ROM Ocarina of Time v1.0. ## Configuration de BizHawk -Une fois BizHawk installé, ouvrez BizHawk et modifiez les paramètres suivants : +Une fois BizHawk installé, ouvrez EmuHawk et modifiez les paramètres suivants : -- Allez dans Config > Personnaliser. Basculez vers l'onglet Avancé, puis basculez le Lua Core de "NLua+KopiLua" vers - "Interface Lua+Lua". Redémarrez ensuite BizHawk. Ceci est nécessaire pour que le script Lua fonctionne correctement. - **REMARQUE : Même si "Lua+LuaInterface" est déjà sélectionné, basculez entre les deux options et resélectionnez-le. Nouvelles installations** - ** des versions plus récentes de BizHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais se chargent toujours ** - **"NLua+KopiLua" jusqu'à ce que cette étape soit terminée.** -- Sous Config > Personnaliser > Avancé, assurez-vous que la case pour AutoSaveRAM est cochée et cliquez sur le bouton 5s. - Cela réduit la possibilité de perdre des données de sauvegarde en cas de plantage de l'émulateur. -- Sous Config > Personnaliser, cochez les cases "Exécuter en arrière-plan" et "Accepter la saisie en arrière-plan". Cela vous permettra de - continuer à jouer en arrière-plan, même si une autre fenêtre est sélectionnée. -- Sous Config> Raccourcis clavier, de nombreux raccourcis clavier sont répertoriés, dont beaucoup sont liés aux touches communes du clavier. Vous voudrez probablement - désactiver la plupart d'entre eux, ce que vous pouvez faire rapidement en utilisant `Esc`. -- Si vous jouez avec une manette, lorsque vous liez les commandes, désactivez "P1 A Up", "P1 A Down", "P1 A Left" et "P1 A Right" - car ceux-ci interfèrent avec la visée s'ils sont liés. Définissez l'entrée directionnelle à l'aide de l'onglet Analogique à la place. -- Sous N64, activez "Utiliser l'emplacement d'extension". Ceci est nécessaire pour que les sauvegardes fonctionnent. +- (≤ 2,8) Allez dans Config > Personnaliser. Passez à l'onglet Avancé, puis faites passer le Lua Core de "NLua+KopiLua" à + "Lua+LuaInterface". Puis redémarrez EmuHawk. Ceci est nécessaire pour que le script Lua fonctionne correctement. + **REMARQUE : Même si « Lua+LuaInterface » est déjà sélectionné, basculez entre les deux options et resélectionnez-la. Nouvelles installations** + **des versions plus récentes d'EmuHawk ont tendance à afficher "Lua+LuaInterface" comme option sélectionnée par défaut mais ce pendant refait l'épate juste au dessus par précautions** +- Sous Config > Personnaliser > Avancé, assurez-vous que la case AutoSaveRAM est cochée et cliquez sur le bouton 5s. + Cela réduit la possibilité de perdre des données de sauvegarde en cas de crash de l'émulateur. +- Sous Config > Personnaliser, cochez les cases « Exécuter en arrière-plan » et « Accepter la saisie en arrière-plan ». Cela vous permettra continuez à jouer en arrière-plan, même si une autre fenêtre est sélectionnée. +- Sous Config > Hotkeys, de nombreux raccourcis clavier sont répertoriés, dont beaucoup sont liés aux touches communes du clavier. Vous voudrez probablement pour désactiver la plupart d'entre eux, ce que vous pouvez faire rapidement en utilisant « Esc ». +- Si vous jouez avec une manette, lorsque vous associez des commandes, désactivez "P1 A Up", "P1 A Down", "P1 A Left" et "P1 A Right". + car ceux-ci interfèrent avec la visée s’ils sont liés. Définissez plutôt l'entrée directionnelle à l'aide de l'onglet Analogique. +- Sous N64, activez "Utiliser le connecteur d'extension". Ceci est nécessaire pour que les états de sauvegarde fonctionnent. (Le menu N64 n'apparaît qu'après le chargement d'une ROM.) -Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) au BizHawk que nous venons d'installer. -Pour ce faire, nous devons simplement rechercher n'importe quelle rom N64 que nous possédons, faire un clic droit et sélectionner "Ouvrir avec ...", dépliez -la liste qui apparaît et sélectionnez l'option du bas "Rechercher une autre application", puis naviguez jusqu'au dossier BizHawk -et sélectionnez EmuHawk.exe. +Il est fortement recommandé d'associer les extensions de rom N64 (\*.n64, \*.z64) à l'EmuHawk que nous venons d'installer. +Pour ce faire, vous devez simplement rechercher n'importe quelle rom N64 que vous possédez, faire un clic droit et sélectionner "Ouvrir avec...", déplier la liste qui apparaît et sélectionnez l'option du bas "Rechercher une autre application", puis accédez au dossier BizHawk et sélectionnez EmuHawk.exe. -Un guide de configuration BizHawk alternatif ainsi que divers conseils de dépannage peuvent être trouvés +Un guide de configuration BizHawk alternatif ainsi que divers conseils de dépannage sont disponibles [ici](https://wiki.ootrandomizer.com/index.php?title=Bizhawk). -## Configuration de votre fichier YAML +## Créer un fichier de configuration (.yaml) -### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ? +### Qu'est-ce qu'un fichier de configuration et pourquoi en ai-je besoin ? -Votre fichier YAML contient un ensemble d'options de configuration qui fournissent au générateur des informations sur la façon dont il doit -générer votre jeu. Chaque joueur d'un multimonde fournira son propre fichier YAML. Cette configuration permet à chaque joueur de profiter -d'une expérience personnalisée à leur goût, et différents joueurs dans le même multimonde peuvent tous avoir des options différentes. +Consultez le guide sur la configuration d'un YAML de base lors de la configuration de l'archipel. +guide : [Guide de configuration de base de Multiworld](/tutorial/Archipelago/setup/en) -### Où puis-je obtenir un fichier YAML ? +### Où puis-je obtenir un fichier de configuration (.yaml) ? -Un yaml OoT de base ressemblera à ceci. Il y a beaucoup d'options cosmétiques qui ont été supprimées pour le plaisir de ce -tutoriel, si vous voulez voir une liste complète, téléchargez Archipelago depuis -la [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) et recherchez l'exemple de fichier dans -le dossier "Lecteurs". +La page Paramètres du lecteur sur le site Web vous permet de configurer vos paramètres personnels et d'exporter un fichier de configuration depuis eux. Page des paramètres du joueur : [Page des paramètres du joueur d'Ocarina of Time](/games/Ocarina%20of%20Time/player-settings) -``` yaml -description: Modèle par défaut d'Ocarina of Time # Utilisé pour décrire votre yaml. Utile si vous avez plusieurs fichiers -# Votre nom dans le jeu. Les espaces seront remplacés par des underscores et il y a une limite de 16 caractères -name: VotreNom -game: - Ocarina of Time: 1 -requires: - version: 0.1.7 # Version d'Archipelago requise pour que ce yaml fonctionne comme prévu. -# Options partagées prises en charge par tous les jeux : -accessibility: - items: 0 # Garantit que vous pourrez acquérir tous les articles, mais vous ne pourrez peut-être pas accéder à tous les emplacements - locations: 50 # Garantit que vous pourrez accéder à tous les emplacements, et donc à tous les articles - none: 0 # Garantit seulement que le jeu est battable. Vous ne pourrez peut-être pas accéder à tous les emplacements ou acquérir tous les objets -progression_balancing: # Un système pour réduire le BK, comme dans les périodes où vous ne pouvez rien faire, en déplaçant vos éléments dans une sphère d'accès antérieure - 0: 0 # Choisissez un nombre inférieur si cela ne vous dérange pas d'avoir un multimonde plus long, ou si vous pouvez glitch / faire du hors logique. - 25: 0 - 50: 50 # Faites en sorte que vous ayez probablement des choses à faire. - 99: 0 # Obtenez les éléments importants tôt et restez en tête de la progression. -Ocarina of Time: - logic_rules: # définit la logique utilisée pour le générateur. - glitchless: 50 - glitched: 0 - no_logic: 0 - logic_no_night_tokens_without_suns_song: # Les skulltulas nocturnes nécessiteront logiquement le Chant du soleil. - false: 50 - true: 0 - open_forest: # Définissez l'état de la forêt de Kokiri et du chemin vers l'arbre Mojo. - open: 50 - closed_deku: 0 - closed: 0 - open_kakariko: # Définit l'état de la porte du village de Kakariko. - open: 50 - zelda: 0 - closed: 0 - open_door_of_time: # Ouvre la Porte du Temps par défaut, sans le Chant du Temps. - false: 0 - true: 50 - zora_fountain: # Définit l'état du roi Zora, bloquant le chemin vers la fontaine de Zora. - open: 0 - adult: 0 - closed: 50 - gerudo_fortress: # Définit les conditions d'accès à la forteresse Gerudo. - normal: 0 - fast: 50 - open: 0 - bridge: # Définit les exigences pour le pont arc-en-ciel. - open: 0 - vanilla: 0 - stones: 0 - medallions: 50 - dungeons: 0 - tokens: 0 - trials: # Définit le nombre d'épreuves requises dans le Château de Ganon. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 50 # valeur minimale - 6: 0 # valeur maximale - random: 0 - random-low: 0 - random-higt: 0 - starting_age: # Choisissez l'âge auquel Link commencera. - child: 50 - adult: 0 - triforce_hunt: # Rassemblez des morceaux de la Triforce dispersés dans le monde entier pour terminer le jeu. - false: 50 - true: 0 - triforce_goal: # Nombre de pièces Triforce nécessaires pour terminer le jeu. Nombre total placé déterminé par le paramètre Item Pool. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 1: 0 # valeur minimale - 50: 0 # valeur maximale - random: 0 - random-low: 0 - random-higt: 0 - 20: 50 - bombchus_in_logic: # Les Bombchus sont correctement pris en compte dans la logique. Le premier pack trouvé aura 20 chus ; Kokiri Shop et Bazaar vendent des recharges ; bombchus ouvre Bombchu Bowling. - false: 50 - true: 0 - bridge_stones: # Définissez le nombre de pierres spirituelles requises pour le pont arc-en-ciel. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 3: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - bridge_medallions: # Définissez le nombre de médaillons requis pour le pont arc-en-ciel. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 6: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - bridge_rewards: # Définissez le nombre de récompenses de donjon requises pour le pont arc-en-ciel. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 9: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - bridge_tokens: # Définissez le nombre de jetons Gold Skulltula requis pour le pont arc-en-ciel. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 100: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - shuffle_mapcompass: # Contrôle où mélanger les cartes et boussoles des donjons. - remove: 0 - startwith: 50 - vanilla: 0 - dungeon: 0 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_smallkeys: # Contrôle où mélanger les petites clés de donjon. - remove: 0 - vanilla: 0 - dungeon: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_hideoutkeys: # Contrôle où mélanger les petites clés de la Forteresse Gerudo. - vanilla: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_bosskeys: # Contrôle où mélanger les clés du boss, à l'exception de la clé du boss du château de Ganon. - remove: 0 - vanilla: 0 - dungeon: 50 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - shuffle_ganon_bosskey: # Contrôle où mélanger la clé du patron du château de Ganon. - remove: 50 - vanilla: 0 - dungeon: 0 - overworld: 0 - any_dungeon: 0 - keysanity: 0 - on_lacs: 0 - enhance_map_compass: # La carte indique si un donjon est vanille ou MQ. La boussole indique quelle est la récompense du donjon. - false: 50 - true: 0 - lacs_condition: # Définissez les exigences pour la cinématique de la Flèche lumineuse dans le Temple du temps. - vanilla: 50 - stones: 0 - medallions: 0 - dungeons: 0 - tokens: 0 - lacs_stones: # Définissez le nombre de pierres spirituelles requises pour le LACS. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 3: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - lacs_medallions: # Définissez le nombre de médaillons requis pour LACS. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 6: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - lacs_rewards: # Définissez le nombre de récompenses de donjon requises pour LACS. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 9: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - lacs_tokens: # Définissez le nombre de jetons Gold Skulltula requis pour le LACS. - # vous pouvez ajouter des valeurs supplémentaires entre minimum et maximum - 0: 0 # valeur minimale - 100: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - shuffle_song_items: # Définit où les chansons peuvent apparaître. - song: 50 - dungeon: 0 - any: 0 - shopsanity: # Randomise le contenu de la boutique. Réglez sur "off" pour ne pas mélanger les magasins ; "0" mélange les magasins mais ne n'autorise pas les articles multimonde dans les magasins. - 0: 0 - 1: 0 - 2: 0 - 3: 0 - 4: 0 - random_value: 0 - off: 50 - tokensanity : # les récompenses en jetons des Skulltulas dorées sont mélangées dans la réserve. - off: 50 - dungeons: 0 - overworld: 0 - all: 0 - shuffle_scrubs: # Mélangez les articles vendus par Business Scrubs et fixez les prix. - off: 50 - low: 0 - regular: 0 - random_prices: 0 - shuffle_cows: # les vaches donnent des objets lorsque la chanson d'Epona est jouée. - false: 50 - true: 0 - shuffle_kokiri_sword: # Mélangez l'épée Kokiri dans la réserve d'objets. - false: 50 - true: 0 - shuffle_ocarinas: # Mélangez l'Ocarina des fées et l'Ocarina du temps dans la réserve d'objets. - false: 50 - true: 0 - shuffle_weird_egg: # Mélangez l'œuf bizarre de Malon au château d'Hyrule. - false: 50 - true: 0 - shuffle_gerudo_card: # Mélangez la carte de membre Gerudo dans la réserve d'objets. - false: 50 - true: 0 - shuffle_beans: # Ajoute un paquet de 10 haricots au pool d'objets et change le vendeur de haricots pour qu'il vende un objet pour 60 roupies. - false: 50 - true: 0 - shuffle_medigoron_carpet_salesman: # Mélangez les objets vendus par Medigoron et le vendeur de tapis Haunted Wasteland. - false: 50 - true: 0 - skip_child_zelda: # le jeu commence avec la lettre de Zelda, l'objet de la berceuse de Zelda et les événements pertinents déjà terminés. - false: 50 - true: 0 - no_escape_sequence: # Ignore la séquence d'effondrement de la tour entre les combats de Ganondorf et de Ganon. - false: 50 - true: 0 - no_guard_stealth: # Le vide sanitaire du château d'Hyrule passe directement à Zelda. - false: 50 - true: 0 - no_epona_race: # Epona peut toujours être invoquée avec Epona's Song. - false: 50 - true: 0 - skip_some_minigame_phases: # Dampe Race et Horseback Archery donnent les deux récompenses si la deuxième condition est remplie lors de la première tentative. - false: 50 - true: 0 - complete_mask_quest: # Tous les masques sont immédiatement disponibles à l'emprunt dans la boutique Happy Mask. - false: 50 - true: 0 - useful_cutscenes: # Réactive la cinématique Poe dans le Temple de la forêt, Darunia dans le Temple du feu et l'introduction de Twinrova. Surtout utile pour les pépins. - false: 50 - true: 0 - fast_chests: # Toutes les animations des coffres sont rapides. Si désactivé, les éléments principaux ont une animation lente. - false: 50 - true: 0 - free_scarecrow: # Sortir l'ocarina près d'un point d'épouvantail fait apparaître Pierre sans avoir besoin de la chanson. - false: 50 - true: 0 - fast_bunny_hood: # Bunny Hood vous permet de vous déplacer 1,5 fois plus vite comme dans Majora's Mask. - false: 50 - true: 0 - chicken_count: # Contrôle le nombre de Cuccos pour qu'Anju donne un objet en tant qu'enfant. - \# vous pouvez ajouter des valeurs supplémentaires entre le minimum et le maximum - 0: 0 # valeur minimale - 7: 50 # valeur maximale - random: 0 - random-low: 0 - random-high: 0 - hints: # les pierres à potins peuvent donner des indices sur l'emplacement des objets. - none: 0 - mask: 0 - agony: 0 - always: 50 - hint_dist: # Choisissez la distribution d'astuces à utiliser. Affecte la fréquence des indices forts, quels éléments sont toujours indiqués, etc. - balanced: 50 - ddr: 0 - league: 0 - mw2: 0 - scrubs: 0 - strong: 0 - tournament: 0 - useless: 0 - very_strong: 0 - text_shuffle: # Randomise le texte dans le jeu pour un effet comique. - none: 50 - except_hints: 0 - complete: 0 - damage_multiplier: # contrôle la quantité de dégâts subis par Link. - half: 0 - normal: 50 - double: 0 - quadruple: 0 - ohko: 0 - no_collectible_hearts: # les cœurs ne tomberont pas des ennemis ou des objets. - false: 50 - true: 0 - starting_tod: # Changer l'heure de début de la journée. - default: 50 - sunrise: 0 - morning: 0 - noon: 0 - afternoon: 0 - sunset: 0 - evening: 0 - midnight: 0 - witching_hour: 0 - start_with_consumables: # Démarrez le jeu avec des Deku Sticks et des Deku Nuts pleins. - false: 50 - true: 0 - start_with_rupees: # Commencez avec un portefeuille plein. Les mises à niveau de portefeuille rempliront également votre portefeuille. - false: 50 - true: 0 - item_pool_value: # modifie le nombre d'objets disponibles dans le jeu. - plentiful: 0 - balanced: 50 - scarce: 0 - minimal: 0 - junk_ice_traps: # Ajoute des pièges à glace au pool d'objets. - off: 0 - normal: 50 - on: 0 - mayhem: 0 - onslaught: 0 - ice_trap_appearance: # modifie l'apparence des pièges à glace en tant qu'éléments autonomes. - major_only: 50 - junk_only: 0 - anything: 0 - logic_earliest_adult_trade: # premier élément pouvant apparaître dans la séquence d'échange pour adultes. - pocket_egg: 0 - pocket_cucco: 0 - cojiro: 0 - odd_mushroom: 0 - poachers_saw: 0 - broken_sword: 0 - prescription: 50 - eyeball_frog: 0 - eyedrops: 0 - claim_check: 0 - logic_latest_adult_trade: # Dernier élément pouvant apparaître dans la séquence d'échange pour adultes. - pocket_egg: 0 - pocket_cucco: 0 - cojiro: 0 - odd_mushroom: 0 - poachers_saw: 0 - broken_sword: 0 - prescription: 0 - eyeball_frog: 0 - eyedrops: 0 - claim_check: 50 +### Vérification de votre fichier de configuration -``` +Si vous souhaitez valider votre fichier de configuration pour vous assurer qu'il fonctionne, vous pouvez le faire sur la page YAML Validator. +YAML page du validateur : [page de validation YAML](/mysterycheck) -## Rejoindre une partie MultiWorld +## Rejoindre un jeu multimonde -### Obtenez votre fichier de correctif OOT +### Obtenez votre fichier OOT modifié -Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à l'hébergeur. Une fois que c'est Fini, -l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun -des dossiers. Votre fichier de données doit avoir une extension `.apz5`. +Lorsque vous rejoignez un jeu multimonde, il vous sera demandé de fournir votre fichier YAML à celui qui l'héberge. Une fois cela fait, l'hébergeur vous fournira soit un lien pour télécharger votre fichier de données, soit un fichier zip contenant les données de chacun des dossiers. Votre fichier de données doit avoir une extension « .apz5 ». -Double-cliquez sur votre fichier `.apz5` pour démarrer votre client et démarrer le processus de patch ROM. Une fois le processus terminé -(cela peut prendre un certain temps), le client et l'émulateur seront lancés automatiquement (si vous avez associé l'extension -à l'émulateur comme recommandé). +Double-cliquez sur votre fichier « .apz5 » pour démarrer votre client et démarrer le processus de correctif ROM. Une fois le processus terminé (cela peut prendre un certain temps), le client et l'émulateur seront automatiquement démarrés (si vous avez associé l'extension à l'émulateur comme recommandé). ### Connectez-vous au multiserveur -Une fois le client et l'émulateur démarrés, vous devez les connecter. Dans l'émulateur, cliquez sur "Outils" -menu et sélectionnez "Console Lua". Cliquez sur le bouton du dossier ou appuyez sur Ctrl+O pour ouvrir un script Lua. +Une fois le client et l'émulateur démarrés, vous devez les connecter. Accédez à votre dossier d'installation Archipelago, puis vers `data/lua`, et faites glisser et déposez le script `connector_oot.lua` sur la fenêtre principale d'EmuHawk. (Vous pourrez plutôt ouvrir depuis la console Lua manuellement, cliquez sur `Script` 〉 `Open Script` et accédez à `connector_oot.lua` avec le sélecteur de fichiers.) -Accédez à votre dossier d'installation Archipelago et ouvrez `data/lua/connector_oot.lua`. +Pour connecter le client au multiserveur, mettez simplement `:` dans le champ de texte en haut et appuyez sur Entrée (si le serveur utilise un mot de passe, tapez dans le champ de texte inférieur `/connect : [mot de passe]`) -Pour connecter le client au multiserveur, mettez simplement `:` dans le champ de texte en haut et appuyez sur Entrée (si le -le serveur utilise un mot de passe, saisissez dans le champ de texte inférieur `/connect : [mot de passe]`) - -Vous êtes maintenant prêt à commencer votre aventure à Hyrule. +Vous êtes maintenant prêt à commencer votre aventure dans Hyrule. \ No newline at end of file From 7af654e619bb39ee45544484fa14046264238e30 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 28 Dec 2023 13:57:41 +0100 Subject: [PATCH 021/134] WebHost: validate uploaded datapackage and calculate own checksum (#2639) --- WebHostLib/upload.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 8f01294eac..af4ed264aa 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -11,11 +11,14 @@ from flask import request, flash, redirect, url_for, session, render_template from markupsafe import Markup from pony.orm import commit, flush, select, rollback from pony.orm.core import TransactionIntegrityError +import schema import MultiServer from NetUtils import SlotType from Utils import VersionException, __version__ +from worlds import GamesPackage from worlds.Files import AutoPatchRegister +from worlds.AutoWorld import data_package_checksum from . import app from .models import Seed, Room, Slot, GameDataPackage @@ -23,6 +26,15 @@ banned_extensions = (".sfc", ".z64", ".n64", ".nes", ".smc", ".sms", ".gb", ".gb allowed_options_extensions = (".yaml", ".json", ".yml", ".txt", ".zip") allowed_generation_extensions = (".archipelago", ".zip") +games_package_schema = schema.Schema({ + "item_name_groups": {str: [str]}, + "item_name_to_id": {str: int}, + "location_name_groups": {str: [str]}, + "location_name_to_id": {str: int}, + schema.Optional("checksum"): str, + schema.Optional("version"): int, +}) + def allowed_options(filename: str) -> bool: return filename.endswith(allowed_options_extensions) @@ -37,6 +49,8 @@ def banned_file(filename: str) -> bool: def process_multidata(compressed_multidata, files={}): + game_data: GamesPackage + decompressed_multidata = MultiServer.Context.decompress(compressed_multidata) slots: typing.Set[Slot] = set() @@ -45,11 +59,19 @@ def process_multidata(compressed_multidata, files={}): game_data_packages: typing.List[GameDataPackage] = [] for game, game_data in decompressed_multidata["datapackage"].items(): if game_data.get("checksum"): + original_checksum = game_data.pop("checksum") + game_data = games_package_schema.validate(game_data) + game_data = {key: value for key, value in sorted(game_data.items())} + game_data["checksum"] = data_package_checksum(game_data) game_data_package = GameDataPackage(checksum=game_data["checksum"], data=pickle.dumps(game_data)) + if original_checksum != game_data["checksum"]: + raise Exception(f"Original checksum {original_checksum} != " + f"calculated checksum {game_data['checksum']} " + f"for game {game}.") decompressed_multidata["datapackage"][game] = { "version": game_data.get("version", 0), - "checksum": game_data["checksum"] + "checksum": game_data["checksum"], } try: commit() # commit game data package @@ -64,14 +86,15 @@ def process_multidata(compressed_multidata, files={}): if slot_info.type == SlotType.group: continue slots.add(Slot(data=files.get(slot, None), - player_name=slot_info.name, - player_id=slot, - game=slot_info.game)) + player_name=slot_info.name, + player_id=slot, + game=slot_info.game)) flush() # commit slots compressed_multidata = compressed_multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9) return slots, compressed_multidata + def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, sid=None): if not owner: owner = session["_id"] From 8e708f829d143fb32771282c0de57bb2e7955dff Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Thu, 28 Dec 2023 14:12:37 +0100 Subject: [PATCH 022/134] The Witness: Fix an instance of multiworld.random being used (#2630) o_o --- worlds/witness/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index c2d2311c15..6360c33aef 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -143,7 +143,7 @@ class WitnessWorld(World): # Pick an early item to place on the tutorial gate. early_items = [item for item in self.items.get_early_items() if item in self.items.get_mandatory_items()] if early_items: - random_early_item = self.multiworld.random.choice(early_items) + random_early_item = self.random.choice(early_items) if self.options.puzzle_randomization == 1: # In Expert, only tag the item as early, rather than forcing it onto the gate. self.multiworld.local_early_items[self.player][random_early_item] = 1 From c7617f92dd299f0907872c7ceb167d194b5dec93 Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Thu, 28 Dec 2023 08:17:23 -0500 Subject: [PATCH 023/134] TLOZ: Try accounting for non_local_items with the pool of starting weapons (#2620) It was brought up that if you attempt to non_local any of the starting weapons, there is still a chance for it to get chosen as your starting weapon if you are on a StartingPosition value lower than very_dangerous. This fix will attempt to build the starting weapons list accounting for non_local items, but if all possible weapons have been set to non_local, force one of them to be your starting weapon anyway since the player is still expecting a starting weapon in their world if they have chosen one of the lower StartingPosition values. --- worlds/tloz/ItemPool.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/worlds/tloz/ItemPool.py b/worlds/tloz/ItemPool.py index 7773accd8d..456598edec 100644 --- a/worlds/tloz/ItemPool.py +++ b/worlds/tloz/ItemPool.py @@ -93,7 +93,11 @@ def get_pool_core(world): # Starting Weapon start_weapon_locations = starting_weapon_locations.copy() - starting_weapon = random.choice(starting_weapons) + final_starting_weapons = [weapon for weapon in starting_weapons + if weapon not in world.multiworld.non_local_items[world.player]] + if not final_starting_weapons: + final_starting_weapons = starting_weapons + starting_weapon = random.choice(final_starting_weapons) if world.multiworld.StartingPosition[world.player] == StartingPosition.option_safe: placed_items[start_weapon_locations[0]] = starting_weapon elif world.multiworld.StartingPosition[world.player] in \ From 901201f675794d76d8f08232e1799246f7c9ea50 Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Thu, 28 Dec 2023 08:21:54 -0500 Subject: [PATCH 024/134] Noita: Don't allow impossible slot names (#2608) * Noita: Add note about allowable slot names * Update character list * Update init to raise an exception if a yaml has bad characters * Slightly adjust exception message --- worlds/noita/__init__.py | 4 ++++ worlds/noita/docs/setup_en.md | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/worlds/noita/__init__.py b/worlds/noita/__init__.py index 499d202a64..792b90e3f5 100644 --- a/worlds/noita/__init__.py +++ b/worlds/noita/__init__.py @@ -35,6 +35,10 @@ class NoitaWorld(World): web = NoitaWeb() + def generate_early(self): + if not self.multiworld.get_player_name(self.player).isascii(): + raise Exception("Noita yaml's slot name has invalid character(s).") + # Returned items will be sent over to the client def fill_slot_data(self): return {name: getattr(self.multiworld, name)[self.player].value for name in self.option_definitions} diff --git a/worlds/noita/docs/setup_en.md b/worlds/noita/docs/setup_en.md index bd6a151432..b67e840bb9 100644 --- a/worlds/noita/docs/setup_en.md +++ b/worlds/noita/docs/setup_en.md @@ -40,6 +40,8 @@ or try restarting your game. ### What is a YAML and why do I need one? You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) here on the Archipelago website to learn about why Archipelago uses YAML files and what they're for. +Please note that Noita only allows you to type certain characters for your slot name. +These characters are: `` !#$%&'()+,-.0123456789;=@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`abcdefghijklmnopqrstuvwxyz{}~<>|\/`` ### Where do I get a YAML? You can use the [game settings page for Noita](/games/Noita/player-settings) here on the Archipelago website to @@ -54,4 +56,4 @@ Place the unzipped pack in the `packs` folder. Then, open Poptracker and open th Click on the "AP" symbol at the top, then enter the desired address, slot name, and password. That's all you need for it. It will provide you with a quick reference to see which checks you've done and -which checks you still have left. \ No newline at end of file +which checks you still have left. From 24ac3de12586ce1cfb47e371b9fa7a91e3f3b933 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 28 Dec 2023 14:30:10 +0100 Subject: [PATCH 025/134] Factorio: "improve" default start items (#2588) Makes it less likely that people kill themselves via pollution and gives them some healing items they may not even know about. --- worlds/factorio/Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index b72d57ad9b..3429ebbd42 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -210,7 +210,7 @@ class RecipeIngredientsOffset(Range): class FactorioStartItems(OptionDict): """Mapping of Factorio internal item-name to amount granted on start.""" display_name = "Starting Items" - default = {"burner-mining-drill": 19, "stone-furnace": 19} + default = {"burner-mining-drill": 4, "stone-furnace": 4, "raw-fish": 50} class FactorioFreeSampleBlacklist(OptionSet): From d1a17a350d94428ce345da9f23032bb91cc76e3f Mon Sep 17 00:00:00 2001 From: Jarno Date: Thu, 28 Dec 2023 14:41:24 +0100 Subject: [PATCH 026/134] Docs: Add missing Get location_name_groups_* to network protocol (#2550) --- docs/network protocol.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index 199f96f481..274b6e3716 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -380,12 +380,13 @@ Additional arguments sent in this package will also be added to the [Retrieved]( Some special keys exist with specific return data, all of them have the prefix `_read_`, so `hints_{team}_{slot}` is `_read_hints_{team}_{slot}`. -| Name | Type | Notes | -|------------------------------|-------------------------------|---------------------------------------------------| -| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. | -| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. | -| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. | -| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. | +| Name | Type | Notes | +|----------------------------------|-------------------------------|-------------------------------------------------------| +| hints_{team}_{slot} | list\[[Hint](#Hint)\] | All Hints belonging to the requested Player. | +| slot_data_{slot} | dict\[str, any\] | slot_data belonging to the requested slot. | +| item_name_groups_{game_name} | dict\[str, list\[str\]\] | item_name_groups belonging to the requested game. | +| location_name_groups_{game_name} | dict\[str, list\[str\]\] | location_name_groups belonging to the requested game. | +| client_status_{team}_{slot} | [ClientStatus](#ClientStatus) | The current game status of the requested player. | ### Set Used to write data to the server's data storage, that data can then be shared across worlds or just saved for later. Values for keys in the data storage can be retrieved with a [Get](#Get) package, or monitored with a [SetNotify](#SetNotify) package. From e674e37e08fed77fa724021a29eeaa7d92b25bfa Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Thu, 28 Dec 2023 17:43:16 -0500 Subject: [PATCH 027/134] SMZ3: optimized message queues (#2611) --- worlds/smz3/Client.py | 22 +++++++++++----------- worlds/smz3/__init__.py | 3 ++- worlds/smz3/data/zsm.ips | Bin 1470841 -> 1470841 bytes 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/worlds/smz3/Client.py b/worlds/smz3/Client.py index 687a43b00f..859cf234eb 100644 --- a/worlds/smz3/Client.py +++ b/worlds/smz3/Client.py @@ -69,7 +69,7 @@ class SMZ3SNIClient(SNIClient): ctx.finished_game = True return - data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, 4) + data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, 4) if data is None: return @@ -77,14 +77,14 @@ class SMZ3SNIClient(SNIClient): recv_item = data[2] | (data[3] << 8) while (recv_index < recv_item): - item_address = recv_index * 8 - message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x700 + item_address, 8) - is_z3_item = ((message[5] & 0x80) != 0) - masked_part = (message[5] & 0x7F) if is_z3_item else message[5] - item_index = ((message[4] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0) + item_address = recv_index * 2 + message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xDA0 + item_address, 2) + is_z3_item = ((message[1] & 0x80) != 0) + masked_part = (message[1] & 0x7F) if is_z3_item else message[1] + item_index = ((message[0] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0) recv_index += 1 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) + snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) from .TotalSMZ3.Location import locations_start_id from . import convertLocSMZ3IDToAPID @@ -95,7 +95,7 @@ class SMZ3SNIClient(SNIClient): snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) - data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x600, 4) + data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD36, 4) if data is None: return @@ -106,10 +106,10 @@ class SMZ3SNIClient(SNIClient): item = ctx.items_received[item_out_ptr] item_id = item.item - items_start_id - player_id = item.player if item.player <= SMZ3_ROM_PLAYER_LIMIT else 0 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 4, bytes([player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, (item_id >> 8) & 0xFF])) + player_id = item.player if item.player < SMZ3_ROM_PLAYER_LIMIT else 0 + snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 2, bytes([player_id, item_id])) item_out_ptr += 1 - snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x602, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) + snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD38, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'), ctx.location_names[item.location], item_out_ptr, len(ctx.items_received))) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 2cc2ac97d9..39aa42c07a 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -80,7 +80,8 @@ class SMZ3World(World): locationNamesGT: Set[str] = {loc.Name for loc in GanonsTower(None, None).Locations} # first added for 0.2.6 - required_client_version = (0, 2, 6) + # optimized message queues for 0.4.4 + required_client_version = (0, 4, 4) def __init__(self, world: MultiWorld, player: int): self.rom_name_available_event = threading.Event() diff --git a/worlds/smz3/data/zsm.ips b/worlds/smz3/data/zsm.ips index fff36d95d15c96a9cc756c4fd0aa10ccda3e1751..87a4f924f1933fcf59493753c034192ef03a328c 100644 GIT binary patch delta 508 zcmezQDDvl{$O+3CKWtnvO`6Fece8^`77OFb&8ro5T;lp7_~TYgayzHem*(8h?YW;B zftU%1nSq!Eh*`Jier9WyVpQM$k(d2ABcuBCo&4;ojArfk`PqS(1Bf|+m}~oee(u$e z7+t4#f8ss^v}AhZXKq7phulRkUOWh3xbRV<>Q_d#$F22txr?|~&B(}@zaV!Jh$Gbx zB(gm?4jHUB%U#rOmAgm-s1C#hGW&r-K(2)VgJ|9M`=7au80$9(g4CV2$X)cI=#j$u zPm&B5QrA9Vx{$E;J@bX=rLiX&KJcAn`M`Bj@B`aP@efQ5?n?zuvIE(iK(_T#!3*YV z|4Du@vg-XO2~^6|pnFIGWFXh7{R|0}CyxW8(DYbNo@#BF z%ast;9fnzVP(cYC1Q6@4Z2!l}qn!#2XRUmm?OOS~-_#kk+ZCtt0x{oq#p(PkeT+NX h)l>zxtEmcJ?O+sY|Fc#Qh(WY45Q}X8vsSeBFaS*}+?xOZ delta 513 zcmezQDDvl{$O+3CUvFG7O`2&z+GYotEEdKkn^!CBxWx5d@W-u~LNMw6(95PtXkhZ9wC2f%gPzQ($WcCAvfLsd!2GPpxS3h$bG1jjZ1gSgE zl(y(Y?jwcupClPBB&>bHbRl}}d*%yaOJh$meBe9D@`3B5;0Lyo;vbkAoR$ilWCyZ2 zfo!v-f)|X|{*(NmYt{Qt5~!4^LF14D$Uvau_A?|bYOUMP1SD$|PJykF0y&_8btM;2 zHOMPU4AZ;6a&PB$6aJz3pW%bZs{PBR`+nor=axefX#d9T!?t From 3d1be0c468d0717fb9781886b006b498fa7751a4 Mon Sep 17 00:00:00 2001 From: wildham <64616385+wildham0@users.noreply.github.com> Date: Mon, 1 Jan 2024 12:13:35 -0500 Subject: [PATCH 028/134] FF1: Fix terminated_event access_rule not getting set (#2648) --- worlds/ff1/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index 16905cc6da..4ff361c072 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -74,6 +74,7 @@ class FF1World(World): items = get_options(self.multiworld, 'items', self.player) goal_rule = generate_rule([[name for name in items.keys() if name in FF1_PROGRESSION_LIST and name != "Shard"]], self.player) + terminated_event.access_rule = goal_rule if "Shard" in items.keys(): def goal_rule_and_shards(state): return goal_rule(state) and state.has("Shard", self.player, 32) From c104e81145d6a5e89a25fb90c402abe2585ce007 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Mon, 1 Jan 2024 11:42:41 -0800 Subject: [PATCH 029/134] Zillion: move client to worlds/zillion (#2649) --- ZillionClient.py | 505 +-------------------------------------- worlds/zillion/client.py | 501 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 506 insertions(+), 500 deletions(-) create mode 100644 worlds/zillion/client.py diff --git a/ZillionClient.py b/ZillionClient.py index 5f3cbb943f..ef96edab04 100644 --- a/ZillionClient.py +++ b/ZillionClient.py @@ -1,505 +1,10 @@ -import asyncio -import base64 -import platform -from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast +import ModuleUpdate +ModuleUpdate.update() -# CommonClient import first to trigger ModuleUpdater -from CommonClient import CommonContext, server_loop, gui_enabled, \ - ClientCommandProcessor, logger, get_base_parser -from NetUtils import ClientStatus -import Utils -from Utils import async_start - -import colorama - -from zilliandomizer.zri.memory import Memory -from zilliandomizer.zri import events -from zilliandomizer.utils.loc_name_maps import id_to_loc -from zilliandomizer.options import Chars -from zilliandomizer.patch import RescueInfo - -from worlds.zillion.id_maps import make_id_to_others -from worlds.zillion.config import base_id, zillion_map - - -class ZillionCommandProcessor(ClientCommandProcessor): - ctx: "ZillionContext" - - def _cmd_sms(self) -> None: - """ Tell the client that Zillion is running in RetroArch. """ - logger.info("ready to look for game") - self.ctx.look_for_retroarch.set() - - def _cmd_map(self) -> None: - """ Toggle view of the map tracker. """ - self.ctx.ui_toggle_map() - - -class ToggleCallback(Protocol): - def __call__(self) -> None: ... - - -class SetRoomCallback(Protocol): - def __call__(self, rooms: List[List[int]]) -> None: ... - - -class ZillionContext(CommonContext): - game = "Zillion" - command_processor = ZillionCommandProcessor - items_handling = 1 # receive items from other players - - known_name: Optional[str] - """ This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """ - - from_game: "asyncio.Queue[events.EventFromGame]" - to_game: "asyncio.Queue[events.EventToGame]" - ap_local_count: int - """ local checks watched by server """ - next_item: int - """ index in `items_received` """ - ap_id_to_name: Dict[int, str] - ap_id_to_zz_id: Dict[int, int] - start_char: Chars = "JJ" - rescues: Dict[int, RescueInfo] = {} - loc_mem_to_id: Dict[int, int] = {} - got_room_info: asyncio.Event - """ flag for connected to server """ - got_slot_data: asyncio.Event - """ serves as a flag for whether I am logged in to the server """ - - look_for_retroarch: asyncio.Event - """ - There is a bug in Python in Windows - https://github.com/python/cpython/issues/91227 - that makes it so if I look for RetroArch before it's ready, - it breaks the asyncio udp transport system. - - As a workaround, we don't look for RetroArch until this event is set. - """ - - ui_toggle_map: ToggleCallback - ui_set_rooms: SetRoomCallback - """ parameter is y 16 x 8 numbers to show in each room """ - - def __init__(self, - server_address: str, - password: str) -> None: - super().__init__(server_address, password) - self.known_name = None - self.from_game = asyncio.Queue() - self.to_game = asyncio.Queue() - self.got_room_info = asyncio.Event() - self.got_slot_data = asyncio.Event() - self.ui_toggle_map = lambda: None - self.ui_set_rooms = lambda rooms: None - - self.look_for_retroarch = asyncio.Event() - if platform.system() != "Windows": - # asyncio udp bug is only on Windows - self.look_for_retroarch.set() - - self.reset_game_state() - - def reset_game_state(self) -> None: - for _ in range(self.from_game.qsize()): - self.from_game.get_nowait() - for _ in range(self.to_game.qsize()): - self.to_game.get_nowait() - self.got_slot_data.clear() - - self.ap_local_count = 0 - self.next_item = 0 - self.ap_id_to_name = {} - self.ap_id_to_zz_id = {} - self.rescues = {} - self.loc_mem_to_id = {} - - self.locations_checked.clear() - self.missing_locations.clear() - self.checked_locations.clear() - self.finished_game = False - self.items_received.clear() - - # override - def on_deathlink(self, data: Dict[str, Any]) -> None: - self.to_game.put_nowait(events.DeathEventToGame()) - return super().on_deathlink(data) - - # override - async def server_auth(self, password_requested: bool = False) -> None: - if password_requested and not self.password: - await super().server_auth(password_requested) - if not self.auth: - logger.info('waiting for connection to game...') - return - logger.info("logging in to server...") - await self.send_connect() - - # override - def run_gui(self) -> None: - from kvui import GameManager - from kivy.core.text import Label as CoreLabel - from kivy.graphics import Ellipse, Color, Rectangle - from kivy.uix.layout import Layout - from kivy.uix.widget import Widget - - class ZillionManager(GameManager): - logging_pairs = [ - ("Client", "Archipelago") - ] - base_title = "Archipelago Zillion Client" - - class MapPanel(Widget): - MAP_WIDTH: ClassVar[int] = 281 - - _number_textures: List[Any] = [] - rooms: List[List[int]] = [] - - def __init__(self, **kwargs: Any) -> None: - super().__init__(**kwargs) - - self.rooms = [[0 for _ in range(8)] for _ in range(16)] - - self._make_numbers() - self.update_map() - - self.bind(pos=self.update_map) - # self.bind(size=self.update_bg) - - def _make_numbers(self) -> None: - self._number_textures = [] - for n in range(10): - label = CoreLabel(text=str(n), font_size=22, color=(0.1, 0.9, 0, 1)) - label.refresh() - self._number_textures.append(label.texture) - - def update_map(self, *args: Any) -> None: - self.canvas.clear() - - with self.canvas: - Color(1, 1, 1, 1) - Rectangle(source=zillion_map, - pos=self.pos, - size=(ZillionManager.MapPanel.MAP_WIDTH, - int(ZillionManager.MapPanel.MAP_WIDTH * 1.456))) # aspect ratio of that image - for y in range(16): - for x in range(8): - num = self.rooms[15 - y][x] - if num > 0: - Color(0, 0, 0, 0.4) - pos = [self.pos[0] + 17 + x * 32, self.pos[1] + 14 + y * 24] - Ellipse(size=[22, 22], pos=pos) - Color(1, 1, 1, 1) - pos = [self.pos[0] + 22 + x * 32, self.pos[1] + 12 + y * 24] - num_texture = self._number_textures[num] - Rectangle(texture=num_texture, size=num_texture.size, pos=pos) - - def build(self) -> Layout: - container = super().build() - self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=0) - self.main_area_container.add_widget(self.map_widget) - return container - - def toggle_map_width(self) -> None: - if self.map_widget.width == 0: - self.map_widget.width = ZillionManager.MapPanel.MAP_WIDTH - else: - self.map_widget.width = 0 - self.container.do_layout() - - def set_rooms(self, rooms: List[List[int]]) -> None: - self.map_widget.rooms = rooms - self.map_widget.update_map() - - self.ui = ZillionManager(self) - self.ui_toggle_map = lambda: self.ui.toggle_map_width() - self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms) - run_co: Coroutine[Any, Any, None] = self.ui.async_run() - self.ui_task = asyncio.create_task(run_co, name="UI") - - def on_package(self, cmd: str, args: Dict[str, Any]) -> None: - self.room_item_numbers_to_ui() - if cmd == "Connected": - logger.info("logged in to Archipelago server") - if "slot_data" not in args: - logger.warn("`Connected` packet missing `slot_data`") - return - slot_data = args["slot_data"] - - if "start_char" not in slot_data: - logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`") - return - self.start_char = slot_data['start_char'] - if self.start_char not in {"Apple", "Champ", "JJ"}: - logger.warn("invalid Zillion `Connected` packet, " - f"`slot_data` `start_char` has invalid value: {self.start_char}") - - if "rescues" not in slot_data: - logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`") - return - rescues = slot_data["rescues"] - self.rescues = {} - for rescue_id, json_info in rescues.items(): - assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}" - # TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch? - assert json_info["start_char"] == self.start_char, \ - f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}' - ri = RescueInfo(json_info["start_char"], - json_info["room_code"], - json_info["mask"]) - self.rescues[0 if rescue_id == "0" else 1] = ri - - if "loc_mem_to_id" not in slot_data: - logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`") - return - loc_mem_to_id = slot_data["loc_mem_to_id"] - self.loc_mem_to_id = {} - for mem_str, id_str in loc_mem_to_id.items(): - mem = int(mem_str) - id_ = int(id_str) - room_i = mem // 256 - assert 0 <= room_i < 74 - assert id_ in id_to_loc - self.loc_mem_to_id[mem] = id_ - - if len(self.loc_mem_to_id) != 394: - logger.warn("invalid Zillion `Connected` packet, " - f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}") - - self.got_slot_data.set() - - payload = { - "cmd": "Get", - "keys": [f"zillion-{self.auth}-doors"] - } - async_start(self.send_msgs([payload])) - elif cmd == "Retrieved": - if "keys" not in args: - logger.warning(f"invalid Retrieved packet to ZillionClient: {args}") - return - keys = cast(Dict[str, Optional[str]], args["keys"]) - doors_b64 = keys.get(f"zillion-{self.auth}-doors", None) - if doors_b64: - logger.info("received door data from server") - doors = base64.b64decode(doors_b64) - self.to_game.put_nowait(events.DoorEventToGame(doors)) - elif cmd == "RoomInfo": - self.seed_name = args["seed_name"] - self.got_room_info.set() - - def room_item_numbers_to_ui(self) -> None: - rooms = [[0 for _ in range(8)] for _ in range(16)] - for loc_id in self.missing_locations: - loc_id_small = loc_id - base_id - loc_name = id_to_loc[loc_id_small] - y = ord(loc_name[0]) - 65 - x = ord(loc_name[2]) - 49 - if y == 9 and x == 5: - # don't show main computer in numbers - continue - assert (0 <= y < 16) and (0 <= x < 8), f"invalid index from location name {loc_name}" - rooms[y][x] += 1 - # TODO: also add locations with locals lost from loading save state or reset - self.ui_set_rooms(rooms) - - def process_from_game_queue(self) -> None: - if self.from_game.qsize(): - event_from_game = self.from_game.get_nowait() - if isinstance(event_from_game, events.AcquireLocationEventFromGame): - server_id = event_from_game.id + base_id - loc_name = id_to_loc[event_from_game.id] - self.locations_checked.add(server_id) - if server_id in self.missing_locations: - self.ap_local_count += 1 - n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win - logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})') - async_start(self.send_msgs([ - {"cmd": 'LocationChecks', "locations": [server_id]} - ])) - else: - # This will happen a lot in Zillion, - # because all the key words are local and unwatched by the server. - logger.debug(f"DEBUG: {loc_name} not in missing") - elif isinstance(event_from_game, events.DeathEventFromGame): - async_start(self.send_death()) - elif isinstance(event_from_game, events.WinEventFromGame): - if not self.finished_game: - async_start(self.send_msgs([ - {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL} - ])) - self.finished_game = True - elif isinstance(event_from_game, events.DoorEventFromGame): - if self.auth: - doors_b64 = base64.b64encode(event_from_game.doors).decode() - payload = { - "cmd": "Set", - "key": f"zillion-{self.auth}-doors", - "operations": [{"operation": "replace", "value": doors_b64}] - } - async_start(self.send_msgs([payload])) - else: - logger.warning(f"WARNING: unhandled event from game {event_from_game}") - - def process_items_received(self) -> None: - if len(self.items_received) > self.next_item: - zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received] - for index in range(self.next_item, len(self.items_received)): - ap_id = self.items_received[index].item - from_name = self.player_names[self.items_received[index].player] - # TODO: colors in this text, like sni client? - logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}') - self.to_game.put_nowait( - events.ItemEventToGame(zz_item_ids) - ) - self.next_item = len(self.items_received) - - -def name_seed_from_ram(data: bytes) -> Tuple[str, str]: - """ returns player name, and end of seed string """ - if len(data) == 0: - # no connection to game - return "", "xxx" - null_index = data.find(b'\x00') - if null_index == -1: - logger.warning(f"invalid game id in rom {repr(data)}") - null_index = len(data) - name = data[:null_index].decode() - null_index_2 = data.find(b'\x00', null_index + 1) - if null_index_2 == -1: - null_index_2 = len(data) - seed_name = data[null_index + 1:null_index_2].decode() - - return name, seed_name - - -async def zillion_sync_task(ctx: ZillionContext) -> None: - logger.info("started zillion sync task") - - # to work around the Python bug where we can't check for RetroArch - if not ctx.look_for_retroarch.is_set(): - logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.") - await asyncio.wait(( - asyncio.create_task(ctx.look_for_retroarch.wait()), - asyncio.create_task(ctx.exit_event.wait()) - ), return_when=asyncio.FIRST_COMPLETED) - - last_log = "" - - def log_no_spam(msg: str) -> None: - nonlocal last_log - if msg != last_log: - last_log = msg - logger.info(msg) - - # to only show this message once per client run - help_message_shown = False - - with Memory(ctx.from_game, ctx.to_game) as memory: - while not ctx.exit_event.is_set(): - ram = await memory.read() - game_id = memory.get_rom_to_ram_data(ram) - name, seed_end = name_seed_from_ram(game_id) - if len(name): - if name == ctx.known_name: - ctx.auth = name - # this is the name we know - if ctx.server and ctx.server.socket: # type: ignore - if ctx.got_room_info.is_set(): - if ctx.seed_name and ctx.seed_name.endswith(seed_end): - # correct seed - if memory.have_generation_info(): - log_no_spam("everything connected") - await memory.process_ram(ram) - ctx.process_from_game_queue() - ctx.process_items_received() - else: # no generation info - if ctx.got_slot_data.is_set(): - memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id) - ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \ - make_id_to_others(ctx.start_char) - ctx.next_item = 0 - ctx.ap_local_count = len(ctx.checked_locations) - else: # no slot data yet - async_start(ctx.send_connect()) - log_no_spam("logging in to server...") - await asyncio.wait(( - asyncio.create_task(ctx.got_slot_data.wait()), - asyncio.create_task(ctx.exit_event.wait()), - asyncio.create_task(asyncio.sleep(6)) - ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets - else: # not correct seed name - log_no_spam("incorrect seed - did you mix up roms?") - else: # no room info - # If we get here, it looks like `RoomInfo` packet got lost - log_no_spam("waiting for room info from server...") - else: # server not connected - log_no_spam("waiting for server connection...") - else: # new game - log_no_spam("connected to new game") - await ctx.disconnect() - ctx.reset_server_state() - ctx.seed_name = None - ctx.got_room_info.clear() - ctx.reset_game_state() - memory.reset_game_state() - - ctx.auth = name - ctx.known_name = name - async_start(ctx.connect()) - await asyncio.wait(( - asyncio.create_task(ctx.got_room_info.wait()), - asyncio.create_task(ctx.exit_event.wait()), - asyncio.create_task(asyncio.sleep(6)) - ), return_when=asyncio.FIRST_COMPLETED) - else: # no name found in game - if not help_message_shown: - logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.') - help_message_shown = True - log_no_spam("looking for connection to game...") - await asyncio.sleep(0.3) - - await asyncio.sleep(0.09375) - logger.info("zillion sync task ending") - - -async def main() -> None: - parser = get_base_parser() - parser.add_argument('diff_file', default="", type=str, nargs="?", - help='Path to a .apzl Archipelago Binary Patch file') - # SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) - args = parser.parse_args() - print(args) - - if args.diff_file: - import Patch - logger.info("patch file was supplied - creating sms rom...") - meta, rom_file = Patch.create_rom_file(args.diff_file) - if "server" in meta: - args.connect = meta["server"] - logger.info(f"wrote rom file to {rom_file}") - - ctx = ZillionContext(args.connect, args.password) - if ctx.server_task is None: - ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") - - if gui_enabled: - ctx.run_gui() - ctx.run_cli() - - sync_task = asyncio.create_task(zillion_sync_task(ctx)) - - await ctx.exit_event.wait() - - ctx.server_address = None - logger.debug("waiting for sync task to end") - await sync_task - logger.debug("sync task ended") - await ctx.shutdown() +import Utils # noqa: E402 +from worlds.zillion.client import launch # noqa: E402 if __name__ == "__main__": Utils.init_logging("ZillionClient", exception_logger="Client") - - colorama.init() - asyncio.run(main()) - colorama.deinit() + launch() diff --git a/worlds/zillion/client.py b/worlds/zillion/client.py new file mode 100644 index 0000000000..ac73f6db50 --- /dev/null +++ b/worlds/zillion/client.py @@ -0,0 +1,501 @@ +import asyncio +import base64 +import platform +from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast + +from CommonClient import CommonContext, server_loop, gui_enabled, \ + ClientCommandProcessor, logger, get_base_parser +from NetUtils import ClientStatus +from Utils import async_start + +import colorama + +from zilliandomizer.zri.memory import Memory +from zilliandomizer.zri import events +from zilliandomizer.utils.loc_name_maps import id_to_loc +from zilliandomizer.options import Chars +from zilliandomizer.patch import RescueInfo + +from .id_maps import make_id_to_others +from .config import base_id, zillion_map + + +class ZillionCommandProcessor(ClientCommandProcessor): + ctx: "ZillionContext" + + def _cmd_sms(self) -> None: + """ Tell the client that Zillion is running in RetroArch. """ + logger.info("ready to look for game") + self.ctx.look_for_retroarch.set() + + def _cmd_map(self) -> None: + """ Toggle view of the map tracker. """ + self.ctx.ui_toggle_map() + + +class ToggleCallback(Protocol): + def __call__(self) -> None: ... + + +class SetRoomCallback(Protocol): + def __call__(self, rooms: List[List[int]]) -> None: ... + + +class ZillionContext(CommonContext): + game = "Zillion" + command_processor = ZillionCommandProcessor + items_handling = 1 # receive items from other players + + known_name: Optional[str] + """ This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """ + + from_game: "asyncio.Queue[events.EventFromGame]" + to_game: "asyncio.Queue[events.EventToGame]" + ap_local_count: int + """ local checks watched by server """ + next_item: int + """ index in `items_received` """ + ap_id_to_name: Dict[int, str] + ap_id_to_zz_id: Dict[int, int] + start_char: Chars = "JJ" + rescues: Dict[int, RescueInfo] = {} + loc_mem_to_id: Dict[int, int] = {} + got_room_info: asyncio.Event + """ flag for connected to server """ + got_slot_data: asyncio.Event + """ serves as a flag for whether I am logged in to the server """ + + look_for_retroarch: asyncio.Event + """ + There is a bug in Python in Windows + https://github.com/python/cpython/issues/91227 + that makes it so if I look for RetroArch before it's ready, + it breaks the asyncio udp transport system. + + As a workaround, we don't look for RetroArch until this event is set. + """ + + ui_toggle_map: ToggleCallback + ui_set_rooms: SetRoomCallback + """ parameter is y 16 x 8 numbers to show in each room """ + + def __init__(self, + server_address: str, + password: str) -> None: + super().__init__(server_address, password) + self.known_name = None + self.from_game = asyncio.Queue() + self.to_game = asyncio.Queue() + self.got_room_info = asyncio.Event() + self.got_slot_data = asyncio.Event() + self.ui_toggle_map = lambda: None + self.ui_set_rooms = lambda rooms: None + + self.look_for_retroarch = asyncio.Event() + if platform.system() != "Windows": + # asyncio udp bug is only on Windows + self.look_for_retroarch.set() + + self.reset_game_state() + + def reset_game_state(self) -> None: + for _ in range(self.from_game.qsize()): + self.from_game.get_nowait() + for _ in range(self.to_game.qsize()): + self.to_game.get_nowait() + self.got_slot_data.clear() + + self.ap_local_count = 0 + self.next_item = 0 + self.ap_id_to_name = {} + self.ap_id_to_zz_id = {} + self.rescues = {} + self.loc_mem_to_id = {} + + self.locations_checked.clear() + self.missing_locations.clear() + self.checked_locations.clear() + self.finished_game = False + self.items_received.clear() + + # override + def on_deathlink(self, data: Dict[str, Any]) -> None: + self.to_game.put_nowait(events.DeathEventToGame()) + return super().on_deathlink(data) + + # override + async def server_auth(self, password_requested: bool = False) -> None: + if password_requested and not self.password: + await super().server_auth(password_requested) + if not self.auth: + logger.info('waiting for connection to game...') + return + logger.info("logging in to server...") + await self.send_connect() + + # override + def run_gui(self) -> None: + from kvui import GameManager + from kivy.core.text import Label as CoreLabel + from kivy.graphics import Ellipse, Color, Rectangle + from kivy.uix.layout import Layout + from kivy.uix.widget import Widget + + class ZillionManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago Zillion Client" + + class MapPanel(Widget): + MAP_WIDTH: ClassVar[int] = 281 + + _number_textures: List[Any] = [] + rooms: List[List[int]] = [] + + def __init__(self, **kwargs: Any) -> None: + super().__init__(**kwargs) + + self.rooms = [[0 for _ in range(8)] for _ in range(16)] + + self._make_numbers() + self.update_map() + + self.bind(pos=self.update_map) + # self.bind(size=self.update_bg) + + def _make_numbers(self) -> None: + self._number_textures = [] + for n in range(10): + label = CoreLabel(text=str(n), font_size=22, color=(0.1, 0.9, 0, 1)) + label.refresh() + self._number_textures.append(label.texture) + + def update_map(self, *args: Any) -> None: + self.canvas.clear() + + with self.canvas: + Color(1, 1, 1, 1) + Rectangle(source=zillion_map, + pos=self.pos, + size=(ZillionManager.MapPanel.MAP_WIDTH, + int(ZillionManager.MapPanel.MAP_WIDTH * 1.456))) # aspect ratio of that image + for y in range(16): + for x in range(8): + num = self.rooms[15 - y][x] + if num > 0: + Color(0, 0, 0, 0.4) + pos = [self.pos[0] + 17 + x * 32, self.pos[1] + 14 + y * 24] + Ellipse(size=[22, 22], pos=pos) + Color(1, 1, 1, 1) + pos = [self.pos[0] + 22 + x * 32, self.pos[1] + 12 + y * 24] + num_texture = self._number_textures[num] + Rectangle(texture=num_texture, size=num_texture.size, pos=pos) + + def build(self) -> Layout: + container = super().build() + self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=0) + self.main_area_container.add_widget(self.map_widget) + return container + + def toggle_map_width(self) -> None: + if self.map_widget.width == 0: + self.map_widget.width = ZillionManager.MapPanel.MAP_WIDTH + else: + self.map_widget.width = 0 + self.container.do_layout() + + def set_rooms(self, rooms: List[List[int]]) -> None: + self.map_widget.rooms = rooms + self.map_widget.update_map() + + self.ui = ZillionManager(self) + self.ui_toggle_map = lambda: self.ui.toggle_map_width() + self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms) + run_co: Coroutine[Any, Any, None] = self.ui.async_run() + self.ui_task = asyncio.create_task(run_co, name="UI") + + def on_package(self, cmd: str, args: Dict[str, Any]) -> None: + self.room_item_numbers_to_ui() + if cmd == "Connected": + logger.info("logged in to Archipelago server") + if "slot_data" not in args: + logger.warn("`Connected` packet missing `slot_data`") + return + slot_data = args["slot_data"] + + if "start_char" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `start_char`") + return + self.start_char = slot_data['start_char'] + if self.start_char not in {"Apple", "Champ", "JJ"}: + logger.warn("invalid Zillion `Connected` packet, " + f"`slot_data` `start_char` has invalid value: {self.start_char}") + + if "rescues" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `rescues`") + return + rescues = slot_data["rescues"] + self.rescues = {} + for rescue_id, json_info in rescues.items(): + assert rescue_id in ("0", "1"), f"invalid rescue_id in Zillion slot_data: {rescue_id}" + # TODO: just take start_char out of the RescueInfo so there's no opportunity for a mismatch? + assert json_info["start_char"] == self.start_char, \ + f'mismatch in Zillion slot data: {json_info["start_char"]} {self.start_char}' + ri = RescueInfo(json_info["start_char"], + json_info["room_code"], + json_info["mask"]) + self.rescues[0 if rescue_id == "0" else 1] = ri + + if "loc_mem_to_id" not in slot_data: + logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`") + return + loc_mem_to_id = slot_data["loc_mem_to_id"] + self.loc_mem_to_id = {} + for mem_str, id_str in loc_mem_to_id.items(): + mem = int(mem_str) + id_ = int(id_str) + room_i = mem // 256 + assert 0 <= room_i < 74 + assert id_ in id_to_loc + self.loc_mem_to_id[mem] = id_ + + if len(self.loc_mem_to_id) != 394: + logger.warn("invalid Zillion `Connected` packet, " + f"`slot_data` missing locations in `loc_mem_to_id` - len {len(self.loc_mem_to_id)}") + + self.got_slot_data.set() + + payload = { + "cmd": "Get", + "keys": [f"zillion-{self.auth}-doors"] + } + async_start(self.send_msgs([payload])) + elif cmd == "Retrieved": + if "keys" not in args: + logger.warning(f"invalid Retrieved packet to ZillionClient: {args}") + return + keys = cast(Dict[str, Optional[str]], args["keys"]) + doors_b64 = keys.get(f"zillion-{self.auth}-doors", None) + if doors_b64: + logger.info("received door data from server") + doors = base64.b64decode(doors_b64) + self.to_game.put_nowait(events.DoorEventToGame(doors)) + elif cmd == "RoomInfo": + self.seed_name = args["seed_name"] + self.got_room_info.set() + + def room_item_numbers_to_ui(self) -> None: + rooms = [[0 for _ in range(8)] for _ in range(16)] + for loc_id in self.missing_locations: + loc_id_small = loc_id - base_id + loc_name = id_to_loc[loc_id_small] + y = ord(loc_name[0]) - 65 + x = ord(loc_name[2]) - 49 + if y == 9 and x == 5: + # don't show main computer in numbers + continue + assert (0 <= y < 16) and (0 <= x < 8), f"invalid index from location name {loc_name}" + rooms[y][x] += 1 + # TODO: also add locations with locals lost from loading save state or reset + self.ui_set_rooms(rooms) + + def process_from_game_queue(self) -> None: + if self.from_game.qsize(): + event_from_game = self.from_game.get_nowait() + if isinstance(event_from_game, events.AcquireLocationEventFromGame): + server_id = event_from_game.id + base_id + loc_name = id_to_loc[event_from_game.id] + self.locations_checked.add(server_id) + if server_id in self.missing_locations: + self.ap_local_count += 1 + n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win + logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})') + async_start(self.send_msgs([ + {"cmd": 'LocationChecks', "locations": [server_id]} + ])) + else: + # This will happen a lot in Zillion, + # because all the key words are local and unwatched by the server. + logger.debug(f"DEBUG: {loc_name} not in missing") + elif isinstance(event_from_game, events.DeathEventFromGame): + async_start(self.send_death()) + elif isinstance(event_from_game, events.WinEventFromGame): + if not self.finished_game: + async_start(self.send_msgs([ + {"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL} + ])) + self.finished_game = True + elif isinstance(event_from_game, events.DoorEventFromGame): + if self.auth: + doors_b64 = base64.b64encode(event_from_game.doors).decode() + payload = { + "cmd": "Set", + "key": f"zillion-{self.auth}-doors", + "operations": [{"operation": "replace", "value": doors_b64}] + } + async_start(self.send_msgs([payload])) + else: + logger.warning(f"WARNING: unhandled event from game {event_from_game}") + + def process_items_received(self) -> None: + if len(self.items_received) > self.next_item: + zz_item_ids = [self.ap_id_to_zz_id[item.item] for item in self.items_received] + for index in range(self.next_item, len(self.items_received)): + ap_id = self.items_received[index].item + from_name = self.player_names[self.items_received[index].player] + # TODO: colors in this text, like sni client? + logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}') + self.to_game.put_nowait( + events.ItemEventToGame(zz_item_ids) + ) + self.next_item = len(self.items_received) + + +def name_seed_from_ram(data: bytes) -> Tuple[str, str]: + """ returns player name, and end of seed string """ + if len(data) == 0: + # no connection to game + return "", "xxx" + null_index = data.find(b'\x00') + if null_index == -1: + logger.warning(f"invalid game id in rom {repr(data)}") + null_index = len(data) + name = data[:null_index].decode() + null_index_2 = data.find(b'\x00', null_index + 1) + if null_index_2 == -1: + null_index_2 = len(data) + seed_name = data[null_index + 1:null_index_2].decode() + + return name, seed_name + + +async def zillion_sync_task(ctx: ZillionContext) -> None: + logger.info("started zillion sync task") + + # to work around the Python bug where we can't check for RetroArch + if not ctx.look_for_retroarch.is_set(): + logger.info("Start Zillion in RetroArch, then use the /sms command to connect to it.") + await asyncio.wait(( + asyncio.create_task(ctx.look_for_retroarch.wait()), + asyncio.create_task(ctx.exit_event.wait()) + ), return_when=asyncio.FIRST_COMPLETED) + + last_log = "" + + def log_no_spam(msg: str) -> None: + nonlocal last_log + if msg != last_log: + last_log = msg + logger.info(msg) + + # to only show this message once per client run + help_message_shown = False + + with Memory(ctx.from_game, ctx.to_game) as memory: + while not ctx.exit_event.is_set(): + ram = await memory.read() + game_id = memory.get_rom_to_ram_data(ram) + name, seed_end = name_seed_from_ram(game_id) + if len(name): + if name == ctx.known_name: + ctx.auth = name + # this is the name we know + if ctx.server and ctx.server.socket: # type: ignore + if ctx.got_room_info.is_set(): + if ctx.seed_name and ctx.seed_name.endswith(seed_end): + # correct seed + if memory.have_generation_info(): + log_no_spam("everything connected") + await memory.process_ram(ram) + ctx.process_from_game_queue() + ctx.process_items_received() + else: # no generation info + if ctx.got_slot_data.is_set(): + memory.set_generation_info(ctx.rescues, ctx.loc_mem_to_id) + ctx.ap_id_to_name, ctx.ap_id_to_zz_id, _ap_id_to_zz_item = \ + make_id_to_others(ctx.start_char) + ctx.next_item = 0 + ctx.ap_local_count = len(ctx.checked_locations) + else: # no slot data yet + async_start(ctx.send_connect()) + log_no_spam("logging in to server...") + await asyncio.wait(( + asyncio.create_task(ctx.got_slot_data.wait()), + asyncio.create_task(ctx.exit_event.wait()), + asyncio.create_task(asyncio.sleep(6)) + ), return_when=asyncio.FIRST_COMPLETED) # to not spam connect packets + else: # not correct seed name + log_no_spam("incorrect seed - did you mix up roms?") + else: # no room info + # If we get here, it looks like `RoomInfo` packet got lost + log_no_spam("waiting for room info from server...") + else: # server not connected + log_no_spam("waiting for server connection...") + else: # new game + log_no_spam("connected to new game") + await ctx.disconnect() + ctx.reset_server_state() + ctx.seed_name = None + ctx.got_room_info.clear() + ctx.reset_game_state() + memory.reset_game_state() + + ctx.auth = name + ctx.known_name = name + async_start(ctx.connect()) + await asyncio.wait(( + asyncio.create_task(ctx.got_room_info.wait()), + asyncio.create_task(ctx.exit_event.wait()), + asyncio.create_task(asyncio.sleep(6)) + ), return_when=asyncio.FIRST_COMPLETED) + else: # no name found in game + if not help_message_shown: + logger.info('In RetroArch, make sure "Settings > Network > Network Commands" is on.') + help_message_shown = True + log_no_spam("looking for connection to game...") + await asyncio.sleep(0.3) + + await asyncio.sleep(0.09375) + logger.info("zillion sync task ending") + + +async def main() -> None: + parser = get_base_parser() + parser.add_argument('diff_file', default="", type=str, nargs="?", + help='Path to a .apzl Archipelago Binary Patch file') + # SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical']) + args = parser.parse_args() + print(args) + + if args.diff_file: + import Patch + logger.info("patch file was supplied - creating sms rom...") + meta, rom_file = Patch.create_rom_file(args.diff_file) + if "server" in meta: + args.connect = meta["server"] + logger.info(f"wrote rom file to {rom_file}") + + ctx = ZillionContext(args.connect, args.password) + if ctx.server_task is None: + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + sync_task = asyncio.create_task(zillion_sync_task(ctx)) + + await ctx.exit_event.wait() + + ctx.server_address = None + logger.debug("waiting for sync task to end") + await sync_task + logger.debug("sync task ended") + await ctx.shutdown() + + +def launch() -> None: + colorama.init() + asyncio.run(main()) + colorama.deinit() From 88c7484b3a105cea8adbb8ade5c2afd80fff4c4c Mon Sep 17 00:00:00 2001 From: GodlFire <46984098+GodlFire@users.noreply.github.com> Date: Tue, 2 Jan 2024 03:16:45 -0700 Subject: [PATCH 030/134] Shivers: Fixes rule logic for location 'puzzle solved three floor elevator' (#2657) Fixes rule logic for location 'puzzle solved three floor elevator'. Missing a parenthesis caused only the key requirement to be checked for the blue maze region. --- worlds/shivers/Rules.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/shivers/Rules.py b/worlds/shivers/Rules.py index fdd260ca91..4e1058fecf 100644 --- a/worlds/shivers/Rules.py +++ b/worlds/shivers/Rules.py @@ -157,7 +157,7 @@ def get_rules_lookup(player: int): "Puzzle Solved Underground Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player) and state.has("Key for Office Elevator", player))), "Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)), - "Puzzle Solved Three Floor Elevator": lambda state: ((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player) + "Puzzle Solved Three Floor Elevator": lambda state: (((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player)) and state.has("Key for Three Floor Elevator", player))) }, "lightning": { From e5c739ee31c43450dd5845768fa459f98e917dce Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Tue, 2 Jan 2024 05:19:57 -0500 Subject: [PATCH 031/134] KH2: Ability dupe fix and stat increase fix (#2621) Makes the client make sure the player has the correct amount of stat increase instead of letting the goa mod (apcompanion) do it abilities: checks the slot where abilities could dupe unless that slot is being used for an actual abiliity given to the player --- worlds/kh2/Client.py | 95 +++++++++++++++++++++++++++++++++++--------- worlds/kh2/Rules.py | 5 +-- 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/worlds/kh2/Client.py b/worlds/kh2/Client.py index a5be06c7fb..544e710741 100644 --- a/worlds/kh2/Client.py +++ b/worlds/kh2/Client.py @@ -80,11 +80,6 @@ class KH2Context(CommonContext): }, }, } - self.front_of_inventory = { - "Sora": 0x2546, - "Donald": 0x2658, - "Goofy": 0x276C, - } self.kh2seedname = None self.kh2slotdata = None self.itemamount = {} @@ -169,6 +164,14 @@ class KH2Context(CommonContext): self.ability_code_list = None self.master_growth = {"High Jump", "Quick Run", "Dodge Roll", "Aerial Dodge", "Glide"} + self.base_hp = 20 + self.base_mp = 100 + self.base_drive = 5 + self.base_accessory_slots = 1 + self.base_armor_slots = 1 + self.base_item_slots = 3 + self.front_ability_slots = [0x2546, 0x2658, 0x276C, 0x2548, 0x254A, 0x254C, 0x265A, 0x265C, 0x265E, 0x276E, 0x2770, 0x2772] + async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: await super(KH2Context, self).server_auth(password_requested) @@ -219,6 +222,12 @@ class KH2Context(CommonContext): def kh2_read_byte(self, address): return int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + address, 1), "big") + def kh2_read_int(self, address): + return self.kh2.read_int(self.kh2.base_address + address) + + def kh2_write_int(self, address, value): + self.kh2.write_int(self.kh2.base_address + address, value) + def on_package(self, cmd: str, args: dict): if cmd in {"RoomInfo"}: self.kh2seedname = args['seed_name'] @@ -476,7 +485,7 @@ class KH2Context(CommonContext): async def give_item(self, item, location): try: - # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites + # todo: ripout all the itemtype stuff and just have one dictionary. the only thing that needs to be tracked from the server/local is abilites itemname = self.lookup_id_to_item[item] itemdata = self.item_name_to_data[itemname] # itemcode = self.kh2_item_name_to_id[itemname] @@ -507,6 +516,8 @@ class KH2Context(CommonContext): ability_slot = self.kh2_seed_save_cache["GoofyInvo"][1] self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["GoofyInvo"][1] -= 2 + if ability_slot in self.front_ability_slots: + self.front_ability_slots.remove(ability_slot) elif len(self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname]) < \ self.AbilityQuantityDict[itemname]: @@ -518,11 +529,14 @@ class KH2Context(CommonContext): ability_slot = self.kh2_seed_save_cache["DonaldInvo"][0] self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["DonaldInvo"][0] -= 2 - elif itemname in self.goofy_ability_set: + else: ability_slot = self.kh2_seed_save_cache["GoofyInvo"][0] self.kh2_seed_save_cache["AmountInvo"]["Ability"][itemname].append(ability_slot) self.kh2_seed_save_cache["GoofyInvo"][0] -= 2 + if ability_slot in self.front_ability_slots: + self.front_ability_slots.remove(ability_slot) + elif itemdata.memaddr in {0x36C4, 0x36C5, 0x36C6, 0x36C0, 0x36CA}: # if memaddr is in a bitmask location in memory if itemname not in self.kh2_seed_save_cache["AmountInvo"]["Bitmask"]: @@ -615,7 +629,7 @@ class KH2Context(CommonContext): master_sell = master_equipment | master_staff | master_shield await asyncio.create_task(self.IsInShop(master_sell)) - + # print(self.kh2_seed_save_cache["AmountInvo"]["Ability"]) for item_name in master_amount: item_data = self.item_name_to_data[item_name] amount_of_items = 0 @@ -673,10 +687,10 @@ class KH2Context(CommonContext): self.kh2_write_short(self.Save + slot, item_data.memaddr) # removes the duped ability if client gave faster than the game. - for charInvo in {"Sora", "Donald", "Goofy"}: - if self.kh2_read_short(self.Save + self.front_of_inventory[charInvo]) != 0: - print(f"removed {self.Save + self.front_of_inventory[charInvo]} from {charInvo}") - self.kh2_write_short(self.Save + self.front_of_inventory[charInvo], 0) + for ability in self.front_ability_slots: + if self.kh2_read_short(self.Save + ability) != 0: + print(f"removed {self.Save + ability} from {ability}") + self.kh2_write_short(self.Save + ability, 0) # remove the dummy level 1 growths if they are in these invo slots. for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}: @@ -740,15 +754,60 @@ class KH2Context(CommonContext): self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) for item_name in master_stat: - item_data = self.item_name_to_data[item_name] amount_of_items = 0 amount_of_items += self.kh2_seed_save_cache["AmountInvo"]["StatIncrease"][item_name] + if self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5: + if item_name == ItemName.MaxHPUp: + if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical + Bonus = 5 + else: # Critical + Bonus = 2 + if self.kh2_read_int(self.Slot1 + 0x004) != self.base_hp + (Bonus * amount_of_items): + self.kh2_write_int(self.Slot1 + 0x004, self.base_hp + (Bonus * amount_of_items)) + + elif item_name == ItemName.MaxMPUp: + if self.kh2_read_byte(self.Save + 0x2498) < 3: # Non-Critical + Bonus = 10 + else: # Critical + Bonus = 5 + if self.kh2_read_int(self.Slot1 + 0x184) != self.base_mp + (Bonus * amount_of_items): + self.kh2_write_int(self.Slot1 + 0x184, self.base_mp + (Bonus * amount_of_items)) + + elif item_name == ItemName.DriveGaugeUp: + current_max_drive = self.kh2_read_byte(self.Slot1 + 0x1B2) + # change when max drive is changed from 6 to 4 + if current_max_drive < 9 and current_max_drive != self.base_drive + amount_of_items: + self.kh2_write_byte(self.Slot1 + 0x1B2, self.base_drive + amount_of_items) + + elif item_name == ItemName.AccessorySlotUp: + current_accessory = self.kh2_read_byte(self.Save + 0x2501) + if current_accessory != self.base_accessory_slots + amount_of_items: + if 4 > current_accessory < self.base_accessory_slots + amount_of_items: + self.kh2_write_byte(self.Save + 0x2501, current_accessory + 1) + elif self.base_accessory_slots + amount_of_items < 4: + self.kh2_write_byte(self.Save + 0x2501, self.base_accessory_slots + amount_of_items) + + elif item_name == ItemName.ArmorSlotUp: + current_armor_slots = self.kh2_read_byte(self.Save + 0x2500) + if current_armor_slots != self.base_armor_slots + amount_of_items: + if 4 > current_armor_slots < self.base_armor_slots + amount_of_items: + self.kh2_write_byte(self.Save + 0x2500, current_armor_slots + 1) + elif self.base_armor_slots + amount_of_items < 4: + self.kh2_write_byte(self.Save + 0x2500, self.base_armor_slots + amount_of_items) + + elif item_name == ItemName.ItemSlotUp: + current_item_slots = self.kh2_read_byte(self.Save + 0x2502) + if current_item_slots != self.base_item_slots + amount_of_items: + if 8 > current_item_slots < self.base_item_slots + amount_of_items: + self.kh2_write_byte(self.Save + 0x2502, current_item_slots + 1) + elif self.base_item_slots + amount_of_items < 8: + self.kh2_write_byte(self.Save + 0x2502, self.base_item_slots + amount_of_items) + + # if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \ + # and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \ + # self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}: + # self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) - # if slot1 has 5 drive gauge and goa lost illusion is checked and they are not in a cutscene - if self.kh2_read_byte(self.Save + item_data.memaddr) != amount_of_items \ - and self.kh2_read_byte(self.Slot1 + 0x1B2) >= 5 and \ - self.kh2_read_byte(self.Save + 0x23DF) & 0x1 << 3 > 0 and self.kh2_read_byte(0x741320) in {10, 8}: - self.kh2_write_byte(self.Save + item_data.memaddr, amount_of_items) if "PoptrackerVersionCheck" in self.kh2slotdata: if self.kh2slotdata["PoptrackerVersionCheck"] > 4.2 and self.kh2_read_byte(self.Save + 0x3607) != 1: # telling the goa they are on version 4.3 self.kh2_write_byte(self.Save + 0x3607, 1) diff --git a/worlds/kh2/Rules.py b/worlds/kh2/Rules.py index 41207c6cb3..7c5551dbd5 100644 --- a/worlds/kh2/Rules.py +++ b/worlds/kh2/Rules.py @@ -268,7 +268,6 @@ class KH2WorldRules(KH2Rules): add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys()) def set_kh2_goal(self): - final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player) if self.multiworld.Goal[self.player] == "three_proofs": final_xemnas_location.access_rule = lambda state: self.kh2_has_all(three_proofs, state) @@ -291,8 +290,8 @@ class KH2WorldRules(KH2Rules): else: self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) else: - final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and\ - state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) + final_xemnas_location.access_rule = lambda state: state.has(ItemName.Bounty, self.player, self.multiworld.BountyRequired[self.player].value) and \ + state.has(ItemName.LuckyEmblem, self.player, self.multiworld.LuckyEmblemsRequired[self.player].value) if self.multiworld.FinalXemnas[self.player]: self.multiworld.completion_condition[self.player] = lambda state: state.has(ItemName.Victory, self.player, 1) else: From bf17582c5534d3dc35bdb597bcb2da097228e275 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Tue, 2 Jan 2024 03:32:03 -0700 Subject: [PATCH 032/134] BizHawkClient: Add some handling for non-string errors (#2656) --- data/lua/connector_bizhawk_generic.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/data/lua/connector_bizhawk_generic.lua b/data/lua/connector_bizhawk_generic.lua index eff400cb03..47af6e003d 100644 --- a/data/lua/connector_bizhawk_generic.lua +++ b/data/lua/connector_bizhawk_generic.lua @@ -456,6 +456,7 @@ function send_receive () failed_guard_response = response end else + if type(response) ~= "string" then response = "Unknown error" end res[i] = {type = "ERROR", err = response} end end From 0df0955415cd4523531ab091f05090831fb5016d Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 2 Jan 2024 08:03:39 -0600 Subject: [PATCH 033/134] Core: check if a location is an event before excluding it (#2653) * Core: check if a location is an event before excluding it * log a warning * put the warning in the right spot --- worlds/generic/Rules.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index 520ad22525..ac5e1aa507 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -1,4 +1,5 @@ import collections +import logging import typing from BaseClasses import LocationProgressType, MultiWorld, Location, Region, Entrance @@ -81,15 +82,18 @@ def locality_rules(world: MultiWorld): i.name not in sending_blockers[i.player] and old_rule(i) -def exclusion_rules(world: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None: +def exclusion_rules(multiworld: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None: for loc_name in exclude_locations: try: - location = world.get_location(loc_name, player) + location = multiworld.get_location(loc_name, player) except KeyError as e: # failed to find the given location. Check if it's a legitimate location - if loc_name not in world.worlds[player].location_name_to_id: + if loc_name not in multiworld.worlds[player].location_name_to_id: raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e else: - location.progress_type = LocationProgressType.EXCLUDED + if not location.event: + location.progress_type = LocationProgressType.EXCLUDED + else: + logging.warning(f"Unable to exclude location {loc_name} in player {player}'s world.") def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule): From 7406a1e512b51748cc173a18a52ec747135f054a Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Wed, 3 Jan 2024 18:43:41 -0600 Subject: [PATCH 034/134] WebHost: Copyright update time. (#2660) --- WebHostLib/templates/islandFooter.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/templates/islandFooter.html b/WebHostLib/templates/islandFooter.html index 7b89c4a9e0..08cf227990 100644 --- a/WebHostLib/templates/islandFooter.html +++ b/WebHostLib/templates/islandFooter.html @@ -1,6 +1,6 @@ {% block footer %}