From eef8f7af1a4f6fea4722e2e28ade4d5cafc033b8 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 6 Apr 2023 03:48:30 -0500 Subject: [PATCH 001/489] The Messenger: Add Mega Time Shards and Quest 1 boss locations (#1661) * implement mega shards * create the option and locations, add to slot data and tests * add boss refights as locations * remove barma'thazel. it's apparently impossible to get to him * remove barma'thazel again * up max shard count to 85 * increment version * dynamically alter the power seal pool * revert host.yaml change * two mega shards were missing from the maps * add new checks to the info page * add some more rules to skylands * forgot to update my tests * explicit imports, remove unnecessary typing, lower required client ver * use generators for shard and seal creation --- worlds/messenger/Constants.py | 8 ++++ worlds/messenger/Options.py | 12 ++++-- worlds/messenger/Regions.py | 32 ++++++++++++--- worlds/messenger/Rules.py | 24 +++++++++-- worlds/messenger/SubClasses.py | 12 +++--- worlds/messenger/__init__.py | 50 ++++++++++++++++------- worlds/messenger/docs/en_The Messenger.md | 4 +- worlds/messenger/test/TestAccess.py | 38 ++++++++++++----- worlds/messenger/test/TestLogic.py | 8 ++-- worlds/messenger/test/TestShopChest.py | 30 ++++++++++++++ 10 files changed, 169 insertions(+), 49 deletions(-) diff --git a/worlds/messenger/Constants.py b/worlds/messenger/Constants.py index f967fec8cb..643c3f8327 100644 --- a/worlds/messenger/Constants.py +++ b/worlds/messenger/Constants.py @@ -1,5 +1,6 @@ # items # listing individual groups first for easy lookup + NOTES = [ "Key of Hope", "Key of Chaos", @@ -151,3 +152,10 @@ SEALS = [ "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", ] + +BOSS_LOCATIONS = [ + "Leaf Golem", + "Ruxxtin", + "Emerald Golem", + "Queen of Quills", +] diff --git a/worlds/messenger/Options.py b/worlds/messenger/Options.py index 47ebf66f28..636b60549b 100644 --- a/worlds/messenger/Options.py +++ b/worlds/messenger/Options.py @@ -1,4 +1,4 @@ -from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice +from Options import DefaultOnToggle, DeathLink, Range, Accessibility, Choice, Toggle class MessengerAccessibility(Accessibility): @@ -27,6 +27,11 @@ class PowerSeals(DefaultOnToggle): display_name = "Shuffle Seals" +class MegaShards(Toggle): + """Whether mega shards should be item locations.""" + display_name = "Shuffle Mega Time Shards" + + class Goal(Choice): """Requirement to finish the game. Power Seal Hunt will force power seal locations to be shuffled.""" display_name = "Goal" @@ -51,8 +56,8 @@ class AmountSeals(Range): """Number of power seals that exist in the item pool when power seal hunt is the goal.""" display_name = "Total Power Seals" range_start = 1 - range_end = 45 - default = range_end + range_end = 85 + default = 45 class RequiredSeals(Range): @@ -67,6 +72,7 @@ messenger_options = { "accessibility": MessengerAccessibility, "logic_level": Logic, "shuffle_seals": PowerSeals, + "shuffle_shards": MegaShards, "goal": Goal, "music_box": MusicBox, "notes_needed": NotesNeeded, diff --git a/worlds/messenger/Regions.py b/worlds/messenger/Regions.py index ab84f0b3ce..8a9cfffb56 100644 --- a/worlds/messenger/Regions.py +++ b/worlds/messenger/Regions.py @@ -6,16 +6,17 @@ REGIONS: Dict[str, List[str]] = { "The Shop": [], "Tower of Time": [], "Ninja Village": ["Candle", "Astral Seed"], - "Autumn Hills": ["Climbing Claws", "Key of Hope"], + "Autumn Hills": ["Climbing Claws", "Key of Hope", "Leaf Golem"], "Forlorn Temple": ["Demon King Crown"], - "Catacombs": ["Necro", "Ruxxtin's Amulet"], + "Catacombs": ["Necro", "Ruxxtin's Amulet", "Ruxxtin"], "Bamboo Creek": ["Claustro"], - "Howling Grotto": ["Wingsuit"], - "Quillshroom Marsh": ["Seashell"], + "Howling Grotto": ["Wingsuit", "Emerald Golem"], + "Quillshroom Marsh": ["Seashell", "Queen of Quills"], "Searing Crags": ["Rope Dart"], "Searing Crags Upper": ["Power Thistle", "Key of Strength", "Astral Tea Leaves"], "Glacial Peak": [], - "Cloud Ruins": ["Acro"], + "Cloud Ruins": [], + "Cloud Ruins Right": ["Acro"], "Underworld": ["Pyro", "Key of Chaos"], "Dark Cave": [], "Riviere Turquoise": ["Fairy Bottle"], @@ -26,6 +27,24 @@ REGIONS: Dict[str, List[str]] = { } """seal locations have the region in their name and may not need to be created so skip them here""" +MEGA_SHARDS: Dict[str, List[str]] = { + "Autumn Hills": ["Autumn Hills Mega Shard", "Hidden Entrance Mega Shard"], + "Catacombs": ["Catacombs Mega Shard"], + "Bamboo Creek": ["Above Entrance Mega Shard", "Abandoned Mega Shard", "Time Loop Mega Shard"], + "Howling Grotto": ["Bottom Left Mega Shard", "Near Portal Mega Shard", "Pie in the Sky Mega Shard"], + "Quillshroom Marsh": ["Quillshroom Marsh Mega Shard"], + "Searing Crags Upper": ["Searing Crags Mega Shard"], + "Glacial Peak": ["Glacial Peak Mega Shard"], + "Tower of Time": [], + "Cloud Ruins": ["Cloud Entrance Mega Shard", "Time Warp Mega Shard"], + "Cloud Ruins Right": ["Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2"], + "Underworld": ["Under Entrance Mega Shard", "Hot Tub Mega Shard", "Projectile Pit Mega Shard"], + "Forlorn Temple": ["Sunny Day Mega Shard", "Down Under Mega Shard"], + "Sunken Shrine": ["Mega Shard of the Moon", "Beginner's Mega Shard", "Mega Shard of the Stars", "Mega Shard of the Sun"], + "Riviere Turquoise": ["Waterfall Mega Shard", "Quick Restock Mega Shard 1", "Quick Restock Mega Shard 2"], + "Elemental Skylands": ["Earth Mega Shard", "Water Mega Shard"], +} + REGION_CONNECTIONS: Dict[str, Set[str]] = { "Menu": {"Tower HQ"}, @@ -42,7 +61,8 @@ REGION_CONNECTIONS: Dict[str, Set[str]] = { "Searing Crags": {"Searing Crags Upper", "Quillshroom Marsh", "Underworld"}, "Searing Crags Upper": {"Searing Crags", "Glacial Peak"}, "Glacial Peak": {"Searing Crags Upper", "Tower HQ", "Cloud Ruins", "Elemental Skylands"}, - "Cloud Ruins": {"Underworld"}, + "Cloud Ruins": {"Cloud Ruins Right"}, + "Cloud Ruins Right": {"Underworld"}, "Underworld": set(), "Dark Cave": {"Catacombs", "Riviere Turquoise"}, "Riviere Turquoise": set(), diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py index a459fdb7d0..c171eddfdd 100644 --- a/worlds/messenger/Rules.py +++ b/worlds/messenger/Rules.py @@ -27,7 +27,8 @@ class MessengerRules: "Catacombs": self.has_wingsuit, "Bamboo Creek": self.has_wingsuit, "Searing Crags Upper": self.has_vertical, - "Cloud Ruins": lambda state: self.has_wingsuit(state) and state.has("Ruxxtin's Amulet", self.player), + "Cloud Ruins": lambda state: self.has_vertical(state) and state.has("Ruxxtin's Amulet", self.player), + "Cloud Ruins Right": self.has_wingsuit, "Underworld": self.has_tabi, "Forlorn Temple": lambda state: state.has_all({"Wingsuit", *PHOBEKINS}, self.player), "Glacial Peak": self.has_vertical, @@ -43,6 +44,7 @@ class MessengerRules: # howling grotto "Howling Grotto Seal - Windy Saws and Balls": self.has_wingsuit, "Howling Grotto Seal - Crushing Pits": lambda state: self.has_wingsuit(state) and self.has_dart(state), + "Emerald Golem": self.has_wingsuit, # searing crags "Astral Tea Leaves": lambda state: state.can_reach("Astral Seed", "Location", self.player), "Key of Strength": lambda state: state.has("Power Thistle", self.player), @@ -64,14 +66,20 @@ class MessengerRules: "Key of Love": lambda state: state.has_all({"Sun Crest", "Moon Crest"}, self.player), "Sunken Shrine Seal - Waterfall Paradise": self.has_tabi, "Sunken Shrine Seal - Tabi Gauntlet": self.has_tabi, + "Mega Shard of the Moon": self.has_tabi, + "Mega Shard of the Sun": self.has_tabi, # riviere turquoise "Fairy Bottle": self.has_vertical, "Riviere Turquoise Seal - Flower Power": self.has_vertical, + "Quick Restock Mega Shard 1": self.has_vertical, + "Quick Restock Mega Shard 2": self.has_vertical, # elemental skylands "Key of Symbiosis": self.has_dart, "Elemental Skylands Seal - Air": self.has_wingsuit, "Elemental Skylands Seal - Water": self.has_dart, "Elemental Skylands Seal - Fire": self.has_dart, + "Earth Mega Shard": self.has_dart, + "Water Mega Shard": self.has_dart, # corrupted future "Key of Courage": lambda state: state.has_all({"Demon King Crown", "Fairy Bottle"}, self.player), # the shop @@ -130,12 +138,17 @@ class MessengerHardRules(MessengerRules): "Forlorn Temple": lambda state: self.has_vertical(state) and state.has_all(set(PHOBEKINS), self.player), "Searing Crags Upper": self.true, "Glacial Peak": self.true, + "Elemental Skylands": lambda state: state.has("Fairy Bottle", self.player) or self.has_windmill(state), }) self.location_rules.update({ "Howling Grotto Seal - Windy Saws and Balls": self.true, "Glacial Peak Seal - Projectile Spike Pit": self.true, "Claustro": self.has_wingsuit, + "Elemental Skylands Seal - Water": self.true, + "Elemental Skylands Seal - Fire": self.true, + "Earth Mega Shard": self.true, + "Water Mega Shard": self.true, }) self.extra_rules = { @@ -156,6 +169,8 @@ class MessengerHardRules(MessengerRules): for loc, rule in self.extra_rules.items(): if not self.world.multiworld.shuffle_seals[self.player] and "Seal" in loc: continue + if not self.world.multiworld.shuffle_shards[self.player] and "Shard" in loc: + continue add_rule(self.world.multiworld.get_location(loc, self.player), rule, "or") @@ -166,7 +181,8 @@ class MessengerChallengeRules(MessengerHardRules): self.region_rules.update({ "Forlorn Temple": lambda state: (self.has_vertical(state) and state.has_all(set(PHOBEKINS), self.player)) or state.has_all({"Wingsuit", "Windmill Shuriken"}, self.player), - "Elemental Skylands": lambda state: self.has_wingsuit(state) or state.has("Fairy Bottle", self.player), + "Elemental Skylands": lambda state: self.has_wingsuit(state) or state.has("Fairy Bottle", self.player) + or self.has_windmill(state), }) self.location_rules.update({ @@ -220,6 +236,6 @@ def set_self_locking_items(multiworld: MultiWorld, player: int) -> None: allow_self_locking_items(multiworld.get_location("Key of Courage", player), "Demon King Crown") # add these locations when seals aren't shuffled - if not multiworld.shuffle_seals[player]: - allow_self_locking_items(multiworld.get_region("Cloud Ruins", player), "Ruxxtin's Amulet") + if not multiworld.shuffle_seals[player] and not multiworld.shuffle_shards[player]: + allow_self_locking_items(multiworld.get_region("Cloud Ruins Right", player), "Ruxxtin's Amulet") allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS) diff --git a/worlds/messenger/SubClasses.py b/worlds/messenger/SubClasses.py index 1f26a4265b..3daf183e0b 100644 --- a/worlds/messenger/SubClasses.py +++ b/worlds/messenger/SubClasses.py @@ -3,7 +3,7 @@ from typing import Set, TYPE_CHECKING, Optional, Dict from BaseClasses import Region, Location, Item, ItemClassification, Entrance from .Constants import SEALS, NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS from .Options import Goal -from .Regions import REGIONS +from .Regions import REGIONS, MEGA_SHARDS if TYPE_CHECKING: from . import MessengerWorld @@ -23,10 +23,12 @@ class MessengerRegion(Region): if self.name == "The Shop" and self.multiworld.goal[self.player] > Goal.option_open_music_box: self.locations.append(MessengerLocation("Shop Chest", self, name_to_id.get("Shop Chest", None))) # putting some dumb special case for searing crags and ToT so i can split them into 2 regions - if self.multiworld.shuffle_seals[self.player] and self.name not in {"Searing Crags", "Tower HQ"}: - for seal_loc in SEALS: - if seal_loc.startswith(self.name.split(" ")[0]): - self.locations.append(MessengerLocation(seal_loc, self, name_to_id.get(seal_loc, None))) + if self.multiworld.shuffle_seals[self.player] and self.name not in {"Searing Crags", "Tower HQ", "Cloud Ruins"}: + self.locations += [MessengerLocation(seal_loc, self, name_to_id.get(seal_loc, None)) + for seal_loc in SEALS if seal_loc.startswith(self.name.split(" ")[0])] + if self.multiworld.shuffle_shards[self.player] and self.name in MEGA_SHARDS: + self.locations += [MessengerLocation(shard, self, name_to_id.get(shard, None)) + for shard in MEGA_SHARDS[self.name]] def add_exits(self, exits: Set[str]) -> None: for exit in exits: diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index d23f4da34f..5d304edd1d 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -1,10 +1,11 @@ -from typing import Dict, Any, List, Optional +import logging +from typing import Dict, Any, Optional, List from BaseClasses import Tutorial, ItemClassification from worlds.AutoWorld import World, WebWorld -from .Constants import NOTES, PROG_ITEMS, PHOBEKINS, USEFUL_ITEMS, ALWAYS_LOCATIONS, SEALS, ALL_ITEMS +from .Constants import NOTES, PHOBEKINS, ALL_ITEMS, ALWAYS_LOCATIONS, SEALS, BOSS_LOCATIONS from .Options import messenger_options, NotesNeeded, Goal, PowerSeals, Logic -from .Regions import REGIONS, REGION_CONNECTIONS +from .Regions import REGIONS, REGION_CONNECTIONS, MEGA_SHARDS from .SubClasses import MessengerRegion, MessengerItem from . import Rules @@ -48,21 +49,28 @@ class MessengerWorld(World): base_offset = 0xADD_000 item_name_to_id = {item: item_id for item_id, item in enumerate(ALL_ITEMS, base_offset)} + mega_shard_locs = [shard for region in MEGA_SHARDS for shard in MEGA_SHARDS[region]] location_name_to_id = {location: location_id - for location_id, location in enumerate([*ALWAYS_LOCATIONS, *SEALS], base_offset)} + for location_id, location in + enumerate([ + *ALWAYS_LOCATIONS, + *SEALS, + *mega_shard_locs, + *BOSS_LOCATIONS, + ], base_offset)} - data_version = 1 + data_version = 2 + required_client_version = (0, 3, 9) web = MessengerWeb() - total_seals: Optional[int] = None - required_seals: Optional[int] = None + total_seals: int = 0 + required_seals: int = 0 def generate_early(self) -> None: if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt: self.multiworld.shuffle_seals[self.player].value = PowerSeals.option_true self.total_seals = self.multiworld.total_seals[self.player].value - self.required_seals = int(self.multiworld.percent_seals_required[self.player].value / 100 * self.total_seals) def create_regions(self) -> None: for region in [MessengerRegion(reg_name, self) for reg_name in REGIONS]: @@ -71,12 +79,7 @@ class MessengerWorld(World): def create_items(self) -> None: itempool: List[MessengerItem] = [] - if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt: - seals = [self.create_item("Power Seal") for _ in range(self.total_seals)] - for i in range(self.required_seals): - seals[i].classification = ItemClassification.progression_skip_balancing - itempool += seals - else: + if self.multiworld.goal[self.player] == Goal.option_open_music_box: notes = self.multiworld.random.sample(NOTES, k=len(NOTES)) precollected_notes_amount = NotesNeeded.range_end - self.multiworld.notes_needed[self.player] if precollected_notes_amount: @@ -94,6 +97,20 @@ class MessengerWorld(World): # if we get in a position where this can have duplicates of items that aren't Power Seals # or Time shards, this will need to be redone. }] + + if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt: + total_seals = min(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool), + self.multiworld.total_seals[self.player].value) + if total_seals < self.total_seals: + logging.warning(f"Not enough locations for total seals setting. Adjusting to {total_seals}") + self.total_seals = total_seals + self.required_seals = int(self.multiworld.percent_seals_required[self.player].value / 100 * self.total_seals) + + seals = [self.create_item("Power Seal") for _ in range(self.total_seals)] + for i in range(self.required_seals): + seals[i].classification = ItemClassification.progression_skip_balancing + itempool += seals + itempool += [self.create_filler() for _ in range(len(self.multiworld.get_unfilled_locations(self.player)) - len(itempool))] @@ -122,7 +139,10 @@ class MessengerWorld(World): "music_box": self.multiworld.music_box[self.player].value, "required_seals": self.required_seals, "locations": locations, - "settings": {"Difficulty": "Basic" if not self.multiworld.shuffle_seals[self.player] else "Advanced"}, + "settings": { + "Difficulty": "Basic" if not self.multiworld.shuffle_seals[self.player] else "Advanced", + "Mega Shards": self.multiworld.shuffle_shards[self.player].value + }, "logic": self.multiworld.logic_level[self.player].current_key, } diff --git a/worlds/messenger/docs/en_The Messenger.md b/worlds/messenger/docs/en_The Messenger.md index e25be4b907..64b0dd73ae 100644 --- a/worlds/messenger/docs/en_The Messenger.md +++ b/worlds/messenger/docs/en_The Messenger.md @@ -32,7 +32,9 @@ You can find items wherever items can be picked up in the original game. This in * Quest Item pickups * Music Box notes * Phobekins +* Bosses * Power seals +* Mega Time Shards ## What are the item name groups? @@ -64,8 +66,6 @@ for it. The groups you can use for The Messenger are: * Ruxxtin Coffin cutscene will sometimes not play correctly, but will still reward the item * If you receive the Fairy Bottle while in Quillshroom Marsh, The De-curse Queen cutscene will not play. You can exit to Searing Crags and re-enter to get it to play correctly. -* If you defeat Barma'thazël, the cutscene afterward will not play correctly since that is what normally transitions - you to 2nd quest. The game will not kill you if you fall here, so you can teleport to HQ at any point after defeating him. * Sometimes upon teleporting back to HQ, Ninja will run left and enter a different portal than the one entered by the player. This may also cause a softlock. * Text entry menus don't accept controller input diff --git a/worlds/messenger/test/TestAccess.py b/worlds/messenger/test/TestAccess.py index 84b29406c2..87bf55f7c9 100644 --- a/worlds/messenger/test/TestAccess.py +++ b/worlds/messenger/test/TestAccess.py @@ -3,12 +3,17 @@ from ..Constants import NOTES, PHOBEKINS class AccessTest(MessengerTestBase): + options = { + "shuffle_shards": "true", + } def testTabi(self) -> None: """locations that hard require the Ninja Tabi""" locations = ["Pyro", "Key of Chaos", "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Spike Wall", "Underworld Seal - Fireball Wave", "Underworld Seal - Rising Fanta", "Sun Crest", "Moon Crest", - "Sunken Shrine Seal - Waterfall Paradise", "Sunken Shrine Seal - Tabi Gauntlet"] + "Sunken Shrine Seal - Waterfall Paradise", "Sunken Shrine Seal - Tabi Gauntlet", + "Mega Shard of the Moon", "Mega Shard of the Sun", "Under Entrance Mega Shard", + "Hot Tub Mega Shard", "Projectile Pit Mega Shard"] items = [["Ninja Tabi"]] self.assertAccessDependency(locations, items) @@ -17,7 +22,8 @@ class AccessTest(MessengerTestBase): locations = ["Ninja Village Seal - Tree House", "Key of Hope", "Howling Grotto Seal - Crushing Pits", "Glacial Peak Seal - Ice Climbers", "Tower of Time Seal - Time Waster Seal", "Tower of Time Seal - Arcane Orbs", "Underworld Seal - Rising Fanta", "Key of Symbiosis", - "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire"] + "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", "Earth Mega Shard", + "Water Mega Shard"] items = [["Rope Dart"]] self.assertAccessDependency(locations, items) @@ -35,7 +41,11 @@ class AccessTest(MessengerTestBase): "Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs", "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave", "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", - "Forlorn Temple Seal - Rocket Sunset", "Astral Seed", "Astral Tea Leaves"] + "Forlorn Temple Seal - Rocket Sunset", "Astral Seed", "Astral Tea Leaves", + "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard", "Sunny Day Mega Shard", + "Down Under Mega Shard", "Catacombs Mega Shard", "Above Entrance Mega Shard", + "Abandoned Mega Shard", "Time Loop Mega Shard", "Money Farm Room Mega Shard 1", + "Money Farm Room Mega Shard 2", "Leaf Golem", "Ruxxtin", "Emerald Golem"] items = [["Wingsuit"]] self.assertAccessDependency(locations, items) @@ -56,18 +66,26 @@ class AccessTest(MessengerTestBase): "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", "Tower of Time Seal - Lantern Climb", "Tower of Time Seal - Arcane Orbs", "Underworld Seal - Sharp and Windy Climb", "Underworld Seal - Fireball Wave", - "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset", - "Power Thistle", "Key of Strength", "Glacial Peak Seal - Projectile Spike Pit", - "Glacial Peak Seal - Glacial Air Swag", "Fairy Bottle", "Riviere Turquoise Seal - Flower Power", - "Searing Crags Seal - Triple Ball Spinner", "Searing Crags Seal - Raining Rocks", - "Searing Crags Seal - Rhythm Rocks", "Astral Seed", "Astral Tea Leaves", "Rescue Phantom"] + "Elemental Skylands Seal - Air", "Forlorn Temple Seal - Rocket Maze", + "Forlorn Temple Seal - Rocket Sunset", "Power Thistle", "Key of Strength", + "Glacial Peak Seal - Projectile Spike Pit", "Glacial Peak Seal - Glacial Air Swag", + "Fairy Bottle", "Riviere Turquoise Seal - Flower Power", "Searing Crags Seal - Triple Ball Spinner", + "Searing Crags Seal - Raining Rocks", "Searing Crags Seal - Rhythm Rocks", "Astral Seed", + "Astral Tea Leaves", "Rescue Phantom", "Autumn Hills Mega Shard", "Hidden Entrance Mega Shard", + "Sunny Day Mega Shard", "Down Under Mega Shard", "Catacombs Mega Shard", + "Above Entrance Mega Shard", "Abandoned Mega Shard", "Time Loop Mega Shard", + "Searing Crags Mega Shard", "Glacial Peak Mega Shard", "Cloud Entrance Mega Shard", + "Time Warp Mega Shard", "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2", + "Quick Restock Mega Shard 1", "Quick Restock Mega Shard 2", "Earth Mega Shard", "Water Mega Shard", + "Leaf Golem", "Ruxxtin", "Emerald Golem"] items = [["Wingsuit", "Rope Dart"]] self.assertAccessDependency(locations, items) def testAmulet(self) -> None: """Locations that require Ruxxtin's Amulet""" locations = ["Acro", "Cloud Ruins Seal - Ghost Pit", "Cloud Ruins Seal - Toothbrush Alley", - "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room"] + "Cloud Ruins Seal - Saw Pit", "Cloud Ruins Seal - Money Farm Room", "Cloud Entrance Mega Shard", + "Time Warp Mega Shard", "Money Farm Room Mega Shard 1", "Money Farm Room Mega Shard 2"] # Cloud Ruins requires Ruxxtin's Amulet items = [["Ruxxtin's Amulet"]] self.assertAccessDependency(locations, items) @@ -75,7 +93,7 @@ class AccessTest(MessengerTestBase): def testBottle(self) -> None: """Elemental Skylands and Corrupted Future require the Fairy Bottle""" locations = ["Key of Symbiosis", "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Fire", - "Elemental Skylands Seal - Water", "Key of Courage"] + "Elemental Skylands Seal - Water", "Key of Courage", "Earth Mega Shard", "Water Mega Shard"] items = [["Fairy Bottle"]] self.assertAccessDependency(locations, items) diff --git a/worlds/messenger/test/TestLogic.py b/worlds/messenger/test/TestLogic.py index 2fd8111030..8b3d58541f 100644 --- a/worlds/messenger/test/TestLogic.py +++ b/worlds/messenger/test/TestLogic.py @@ -16,21 +16,21 @@ class HardLogicTest(MessengerTestBase): # ninja village "Candle", "Astral Seed", "Ninja Village Seal - Tree House", "Astral Tea Leaves", # autumn hills - "Climbing Claws", "Key of Hope", + "Climbing Claws", "Key of Hope", "Leaf Golem", "Autumn Hills Seal - Trip Saws", "Autumn Hills Seal - Double Swing Saws", "Autumn Hills Seal - Spike Ball Swing", "Autumn Hills Seal - Spike Ball Darts", # forlorn temple "Demon King Crown", "Forlorn Temple Seal - Rocket Maze", "Forlorn Temple Seal - Rocket Sunset", # catacombs - "Necro", "Ruxxtin's Amulet", + "Necro", "Ruxxtin's Amulet", "Ruxxtin", "Catacombs Seal - Triple Spike Crushers", "Catacombs Seal - Crusher Gauntlet", "Catacombs Seal - Dirty Pond", # bamboo creek "Claustro", "Bamboo Creek Seal - Spike Crushers and Doors", "Bamboo Creek Seal - Spike Ball Pits", "Bamboo Creek Seal - Spike Crushers and Doors v2", # howling grotto - "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Crushing Pits", + "Emerald Golem", "Howling Grotto Seal - Crushing Pits", "Howling Grotto Seal - Crushing Pits", # glacial peak "Glacial Peak Seal - Ice Climbers", # cloud ruins @@ -41,7 +41,7 @@ class HardLogicTest(MessengerTestBase): # riviere turquoise "Fairy Bottle", "Riviere Turquoise Seal - Flower Power", # elemental skylands - "Elemental Skylands Seal - Air", "Elemental Skylands Seal - Water", "Elemental Skylands Seal - Fire", + "Elemental Skylands Seal - Air", # phantom "Rescue Phantom", ] diff --git a/worlds/messenger/test/TestShopChest.py b/worlds/messenger/test/TestShopChest.py index 9289ec9970..fea49de80b 100644 --- a/worlds/messenger/test/TestShopChest.py +++ b/worlds/messenger/test/TestShopChest.py @@ -77,3 +77,33 @@ class ThirtyThirtySeals(MessengerTestBase): required_seals = [seal for seal in total_seals if seal.classification == ItemClassification.progression_skip_balancing] self.assertEqual(len(total_seals), 30) self.assertEqual(len(required_seals), 10) + + +class MaxSealsNoShards(MessengerTestBase): + options = { + "goal": "power_seal_hunt", + "total_seals": 85, + } + + def testSealsAmount(self) -> None: + """Should set total seals to 57 since shards aren't shuffled.""" + self.assertEqual(self.multiworld.total_seals[self.player], 85) + self.assertEqual(self.multiworld.worlds[self.player].total_seals, 57) + + +class MaxSealsWithShards(MessengerTestBase): + options = { + "goal": "power_seal_hunt", + "total_seals": 85, + "shuffle_shards": "true", + } + + def testSealsAmount(self) -> None: + """Should have 85 seals in the pool with all required and be a valid seed.""" + self.assertEqual(self.multiworld.total_seals[self.player], 85) + self.assertEqual(self.multiworld.worlds[self.player].total_seals, 85) + self.assertEqual(self.multiworld.worlds[self.player].required_seals, 85) + total_seals = [seal for seal in self.multiworld.itempool if seal.name == "Power Seal"] + required_seals = [seal for seal in total_seals if seal.classification == ItemClassification.progression_skip_balancing] + self.assertEqual(len(total_seals), 85) + self.assertEqual(len(required_seals), 85) From ece6598b09c754f9475414b0bc56d063a963fef5 Mon Sep 17 00:00:00 2001 From: zig-for Date: Thu, 6 Apr 2023 11:06:34 -0700 Subject: [PATCH 002/489] LADX: apworld (#1665) --- setup.py | 1 + worlds/ladx/LADXR/assembler.py | 4 ++-- worlds/ladx/LADXR/patches/bank34.py | 5 +++-- worlds/ladx/LADXR/patches/bank3e.py | 19 +++++++++++-------- worlds/ladx/LADXR/patches/endscreen.py | 8 ++++---- worlds/ladx/LADXR/rom.py | 2 +- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/setup.py b/setup.py index e455c04bad..5447ed8b61 100644 --- a/setup.py +++ b/setup.py @@ -71,6 +71,7 @@ apworlds: set = { "Timespinner", "Minecraft", "The Messenger", + "Links Awakening DX", } diff --git a/worlds/ladx/LADXR/assembler.py b/worlds/ladx/LADXR/assembler.py index 07fcfde566..6c35fac4b3 100644 --- a/worlds/ladx/LADXR/assembler.py +++ b/worlds/ladx/LADXR/assembler.py @@ -115,8 +115,8 @@ class Tokenizer: assert kind is not None value: Union[str, int] = mo.group() if kind == 'MISMATCH': - print(code.split("\n")[line_num-1]) - raise RuntimeError("Syntax error on line: %d: %s\n%s", line_num, value) + line = code.split("\n")[line_num - 1] + raise RuntimeError(f"Syntax error on line: {line_num}: {kind}:`{line}`") elif kind == 'SKIP': pass elif kind == 'COMMENT': diff --git a/worlds/ladx/LADXR/patches/bank34.py b/worlds/ladx/LADXR/patches/bank34.py index 22abd48b39..31b9ca1244 100644 --- a/worlds/ladx/LADXR/patches/bank34.py +++ b/worlds/ladx/LADXR/patches/bank34.py @@ -1,5 +1,7 @@ import os import binascii +import pkgutil + from ..assembler import ASM from ..utils import formatText @@ -13,7 +15,6 @@ ItemNameStringBufferStart = ItemNameLookupTable + \ def addBank34(rom, item_list): - my_path = os.path.dirname(__file__) rom.patch(0x34, 0x0000, ItemNameLookupTable, ASM(""" ; Get the pointer in the lookup table, doubled as it's two bytes ld hl, $2080 @@ -74,7 +75,7 @@ def addBank34(rom, item_list): .notCavesA: add hl, de ret - """ + open(os.path.join(my_path, "bank3e.asm/message.asm"), "rt").read(), 0x4000), fill_nop=True) + """ + pkgutil.get_data(__name__, os.path.join("bank3e.asm", "message.asm")).decode().replace("\r", ""), 0x4000), fill_nop=True) nextItemLookup = ItemNameStringBufferStart nameLookup = { diff --git a/worlds/ladx/LADXR/patches/bank3e.py b/worlds/ladx/LADXR/patches/bank3e.py index d2b31adf91..7e690349a3 100644 --- a/worlds/ladx/LADXR/patches/bank3e.py +++ b/worlds/ladx/LADXR/patches/bank3e.py @@ -3,6 +3,7 @@ import binascii from ..assembler import ASM from ..utils import formatText +import pkgutil def hasBank3E(rom): return rom.banks[0x3E][0] != 0x00 @@ -54,7 +55,9 @@ def addBank3E(rom, seed, player_id, player_name_list): ret """)) - my_path = os.path.dirname(__file__) + def get_asm(name): + return pkgutil.get_data(__name__, os.path.join("bank3e.asm", name)).decode().replace("\r", "") + rom.patch(0x3E, 0x0000, 0x2F00, ASM(""" call MainJumpTable pop af @@ -204,15 +207,15 @@ LocalOnlyItemAndMessage: call GiveItemFromChest call ItemMessage ret - """ + open(os.path.join(my_path, "bank3e.asm/multiworld.asm"), "rt").read() - + open(os.path.join(my_path, "bank3e.asm/link.asm"), "rt").read() - + open(os.path.join(my_path, "bank3e.asm/chest.asm"), "rt").read() - + open(os.path.join(my_path, "bank3e.asm/bowwow.asm"), "rt").read() - + open(os.path.join(my_path, "bank3e.asm/message.asm"), "rt").read() - + open(os.path.join(my_path, "bank3e.asm/itemnames.asm"), "rt").read() + """ + get_asm("multiworld.asm") + + get_asm("link.asm") + + get_asm("chest.asm") + + get_asm("bowwow.asm") + + get_asm("message.asm") + + get_asm("itemnames.asm") + "".join(generate_name(["The Server"] + player_name_list, i ) for i in range(100)) # allocate + 'db "another world", $ff\n' - + open(os.path.join(my_path, "bank3e.asm/owl.asm"), "rt").read(), 0x4000), fill_nop=True) + + get_asm("owl.asm"), 0x4000), fill_nop=True) # 3E:3300-3616: Multiworld flags per room (for both chests and dropped keys) # 3E:3800-3B16: DroppedKey item types # 3E:3B16-3E2C: Owl statue or trade quest items diff --git a/worlds/ladx/LADXR/patches/endscreen.py b/worlds/ladx/LADXR/patches/endscreen.py index 843120f1c0..3a8b4c2df7 100644 --- a/worlds/ladx/LADXR/patches/endscreen.py +++ b/worlds/ladx/LADXR/patches/endscreen.py @@ -1,6 +1,6 @@ from ..assembler import ASM import os - +import pkgutil def updateEndScreen(rom): # Call our custom data loader in bank 3F @@ -134,6 +134,6 @@ loadLoop2: """)) addr = 0x1000 - for c in open(os.path.join(os.path.dirname(__file__), "nyan.bin"), "rb").read(): - rom.banks[0x3F][addr] = c - addr += 1 + data = pkgutil.get_data(__name__, "nyan.bin") + rom.banks[0x3F][addr : addr + len(data)] = data + diff --git a/worlds/ladx/LADXR/rom.py b/worlds/ladx/LADXR/rom.py index ea4f14089f..baab443057 100644 --- a/worlds/ladx/LADXR/rom.py +++ b/worlds/ladx/LADXR/rom.py @@ -7,7 +7,7 @@ h2b = binascii.unhexlify class ROM: def __init__(self, filename): - data = open(Utils.local_path(filename), "rb").read() + data = open(Utils.user_path(filename), "rb").read() #assert len(data) == 1024 * 1024 self.banks = [] for n in range(0x40): From a86c0aa37daf2ead073c1e77e38b51e9ff925d9a Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Thu, 6 Apr 2023 14:07:09 -0400 Subject: [PATCH 003/489] Stardew: Fix link in Setup Guide (#1672) --- worlds/stardew_valley/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index 30d5f3da2d..1b2ed15643 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -23,7 +23,7 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) ### Where do I get a YAML file? -You can customize your settings by visiting the [Stardew Valley Player Settings Page](../player-settings) +You can customize your settings by visiting the [Stardew Valley Player Settings Page](/games/Stardew%20Valley/player-settings) ## Joining a MultiWorld Game From ccb89dd65cda061c39fda8276a1c71e474dfdabc Mon Sep 17 00:00:00 2001 From: zig-for Date: Thu, 6 Apr 2023 15:24:03 -0700 Subject: [PATCH 004/489] WebHost: Fix upload of .archipelago file (#1657) * Fix upload * simple fix for slot data * remove extra patch.data check * remove extra parens * Update WebHostLib/upload.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update WebHostLib/upload.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * parse -> process * Update WebHostLib/upload.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --------- Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- WebHostLib/templates/macros.html | 52 +++++++++++---------- WebHostLib/upload.py | 78 ++++++++++++++++---------------- 2 files changed, 67 insertions(+), 63 deletions(-) diff --git a/WebHostLib/templates/macros.html b/WebHostLib/templates/macros.html index b2a0c73344..746399da74 100644 --- a/WebHostLib/templates/macros.html +++ b/WebHostLib/templates/macros.html @@ -25,30 +25,34 @@ {{ patch.player_name }} {{ patch.game }} - {% if patch.game == "Minecraft" %} - - Download APMC File... - {% elif patch.game == "Factorio" %} - - Download Factorio Mod... - {% elif patch.game == "Kingdom Hearts 2" %} - - Download Kingdom Hearts 2 Mod... - {% elif patch.game == "Ocarina of Time" %} - - Download APZ5 File... - {% elif patch.game == "VVVVVV" and room.seed.slots|length == 1 %} - - Download APV6 File... - {% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %} - - Download APSM64EX File... - {% elif patch.game | supports_apdeltapatch %} - - Download Patch File... - {% elif patch.game == "Dark Souls III" and patch.data %} - - Download JSON File... + {% if patch.data %} + {% if patch.game == "Minecraft" %} + + Download APMC File... + {% elif patch.game == "Factorio" %} + + Download Factorio Mod... + {% elif patch.game == "Kingdom Hearts 2" %} + + Download Kingdom Hearts 2 Mod... + {% elif patch.game == "Ocarina of Time" %} + + Download APZ5 File... + {% elif patch.game == "VVVVVV" and room.seed.slots|length == 1 %} + + Download APV6 File... + {% elif patch.game == "Super Mario 64" and room.seed.slots|length == 1 %} + + Download APSM64EX File... + {% elif patch.game | supports_apdeltapatch %} + + Download Patch File... + {% elif patch.game == "Dark Souls III" %} + + Download JSON File... + {% else %} + No file to download for this game. + {% endif %} {% else %} No file to download for this game. {% endif %} diff --git a/WebHostLib/upload.py b/WebHostLib/upload.py index 0314d64ab1..04ee5b8756 100644 --- a/WebHostLib/upload.py +++ b/WebHostLib/upload.py @@ -20,6 +20,41 @@ from .models import Seed, Room, Slot, GameDataPackage banned_zip_contents = (".sfc", ".z64", ".n64", ".sms", ".gb") +def process_multidata(compressed_multidata, files={}): + decompressed_multidata = MultiServer.Context.decompress(compressed_multidata) + + slots: typing.Set[Slot] = set() + if "datapackage" in decompressed_multidata: + # strip datapackage from multidata, leaving only the checksums + game_data_packages: typing.List[GameDataPackage] = [] + for game, game_data in decompressed_multidata["datapackage"].items(): + if game_data.get("checksum"): + game_data_package = GameDataPackage(checksum=game_data["checksum"], + data=pickle.dumps(game_data)) + decompressed_multidata["datapackage"][game] = { + "version": game_data.get("version", 0), + "checksum": game_data["checksum"] + } + try: + commit() # commit game data package + game_data_packages.append(game_data_package) + except TransactionIntegrityError: + del game_data_package + rollback() + + if "slot_info" in decompressed_multidata: + for slot, slot_info in decompressed_multidata["slot_info"].items(): + # Ignore Player Groups (e.g. item links) + 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)) + 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: @@ -29,7 +64,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s flash(Markup("Error: Your .zip file only contains .yaml files. " 'Did you mean to generate a game?')) return - slots: typing.Set[Slot] = set() + spoiler = "" files = {} multidata = None @@ -80,42 +115,7 @@ def upload_zip_to_db(zfile: zipfile.ZipFile, owner=None, meta={"race": False}, s # Load multi data. if multidata: - decompressed_multidata = MultiServer.Context.decompress(multidata) - recompress = False - - if "datapackage" in decompressed_multidata: - # strip datapackage from multidata, leaving only the checksums - game_data_packages: typing.List[GameDataPackage] = [] - for game, game_data in decompressed_multidata["datapackage"].items(): - if game_data.get("checksum"): - game_data_package = GameDataPackage(checksum=game_data["checksum"], - data=pickle.dumps(game_data)) - decompressed_multidata["datapackage"][game] = { - "version": game_data.get("version", 0), - "checksum": game_data["checksum"] - } - recompress = True - try: - commit() # commit game data package - game_data_packages.append(game_data_package) - except TransactionIntegrityError: - del game_data_package - rollback() - - if "slot_info" in decompressed_multidata: - for slot, slot_info in decompressed_multidata["slot_info"].items(): - # Ignore Player Groups (e.g. item links) - 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)) - - flush() # commit slots - - if recompress: - multidata = multidata[0:1] + zlib.compress(pickle.dumps(decompressed_multidata), 9) + slots, multidata = process_multidata(multidata, files) seed = Seed(multidata=multidata, spoiler=spoiler, slots=slots, owner=owner, meta=json.dumps(meta), id=sid if sid else uuid.uuid4()) @@ -156,11 +156,11 @@ def uploads(): # noinspection PyBroadException try: multidata = file.read() - MultiServer.Context.decompress(multidata) + slots, multidata = process_multidata(multidata) except Exception as e: flash(f"Could not load multidata. File may be corrupted or incompatible. ({e})") else: - seed = Seed(multidata=multidata, owner=session["_id"]) + seed = Seed(multidata=multidata, slots=slots, owner=session["_id"]) flush() # place into DB and generate ids return redirect(url_for("view_seed", seed=seed.id)) else: From aa6ad5d34ff31229ad994042e026ccf4756754b0 Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Fri, 7 Apr 2023 12:01:26 -0600 Subject: [PATCH 005/489] Hylics 2: Update item creation (#1671) --- worlds/hylics2/__init__.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/worlds/hylics2/__init__.py b/worlds/hylics2/__init__.py index b12beaaa3a..f7b9a26343 100644 --- a/worlds/hylics2/__init__.py +++ b/worlds/hylics2/__init__.py @@ -1,5 +1,4 @@ -import random -from typing import Dict, Any +from typing import Dict, List, Any from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification from worlds.generic.Rules import set_rule from . import Exits, Items, Locations, Options, Rules @@ -73,7 +72,7 @@ class Hylics2World(World): elif i == 3: self.start_location = "Shield Facility" - def generate_basic(self): + def create_items(self): # create item pool pool = [] @@ -88,6 +87,22 @@ class Hylics2World(World): for i, data in Items.party_item_table.items(): pool.append(self.add_item(data["name"], data["classification"], i)) + # handle gesture shuffle + if not self.multiworld.gesture_shuffle[self.player]: # add gestures to pool like normal + for i, data in Items.gesture_item_table.items(): + pool.append(self.add_item(data["name"], data["classification"], i)) + + # add '10 Bones' items if medallion shuffle is enabled + if self.multiworld.medallion_shuffle[self.player]: + for i, data in Items.medallion_item_table.items(): + for j in range(data["count"]): + pool.append(self.add_item(data["name"], data["classification"], i)) + + # add to world's pool + self.multiworld.itempool += pool + + + def pre_fill(self): # handle gesture shuffle options if self.multiworld.gesture_shuffle[self.player] == 2: # vanilla locations gestures = Items.gesture_item_table @@ -135,19 +150,6 @@ class Hylics2World(World): gestures.remove(gest) tvs.remove(tv) - else: # add gestures to pool like normal - for i, data in Items.gesture_item_table.items(): - pool.append(self.add_item(data["name"], data["classification"], i)) - - # add '10 Bones' items if medallion shuffle is enabled - if self.multiworld.medallion_shuffle[self.player]: - for i, data in Items.medallion_item_table.items(): - for j in range(data["count"]): - pool.append(self.add_item(data["name"], data["classification"], i)) - - # add to world's pool - self.multiworld.itempool += pool - def fill_slot_data(self) -> Dict[str, Any]: slot_data: Dict[str, Any] = { From 8fc50510a094f1ec25cb20daa2e0ba32c61a08e1 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Sat, 8 Apr 2023 02:03:28 +0200 Subject: [PATCH 006/489] sm64ex,v6: Use create_items for itempool modification (#1674) --- worlds/sm64ex/__init__.py | 2 +- worlds/v6/__init__.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index f9cae84dc8..6a7a3bd272 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -71,7 +71,7 @@ class SM64World(World): return item - def generate_basic(self): + def create_items(self): starcount = self.multiworld.AmountOfStars[self.player].value if (not self.multiworld.EnableCoinStars[self.player].value): starcount = max(35,self.multiworld.AmountOfStars[self.player].value-15) diff --git a/worlds/v6/__init__.py b/worlds/v6/__init__.py index 5434ceec46..6ff7fba60c 100644 --- a/worlds/v6/__init__.py +++ b/worlds/v6/__init__.py @@ -54,10 +54,11 @@ class V6World(World): def create_item(self, name: str) -> Item: return V6Item(name, ItemClassification.progression, item_table[name], self.player) - def generate_basic(self): + def create_items(self): trinkets = [self.create_item("Trinket " + str(i+1).zfill(2)) for i in range(0,20)] self.multiworld.itempool += trinkets + def generate_basic(self): musiclist_o = [1,2,3,4,9,12] musiclist_s = musiclist_o.copy() if self.multiworld.MusicRandomizer[self.player].value: From 8e6ec85532a7b0e4d8168b15a11e41ff1d4ca647 Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Fri, 7 Apr 2023 18:04:34 -0600 Subject: [PATCH 007/489] Blasphemous: Update docs, item creation (#1670) * Blasphemous: Update docs, item creation * Blasphemous: Remove get_pre_fill_items --- worlds/blasphemous/Rules.py | 2 +- worlds/blasphemous/__init__.py | 4 ++-- worlds/blasphemous/docs/setup_en.md | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py index 01d9643542..ee99ecde63 100644 --- a/worlds/blasphemous/Rules.py +++ b/worlds/blasphemous/Rules.py @@ -143,7 +143,7 @@ class BlasphemousLogic(LogicMixin): return self.has_any({"Debla of the Lights", "Taranto to my Sister", "Cante Jondo of the Three Sisters", \ "Verdiales of the Forsaken Hamlet", "Cloistered Ruby"}, player) or \ (self.has("Tirana of the Celestial Bastion", player) and \ - self.has("Fervour Upgrade"), player, 2) + self.has("Fervour Upgrade", player, 2)) def _blasphemous_cherub_22_23_31_32(self, player): return self.has_any({"Debla of the Lights", "Taranto to my Sister", "Cloistered Ruby"}, player) diff --git a/worlds/blasphemous/__init__.py b/worlds/blasphemous/__init__.py index a7a86826c3..ac6d9b7038 100644 --- a/worlds/blasphemous/__init__.py +++ b/worlds/blasphemous/__init__.py @@ -1,4 +1,4 @@ -from typing import Dict, Set, Any +from typing import Dict, Set, List, Any from collections import Counter from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification from ..AutoWorld import World, WebWorld @@ -61,7 +61,7 @@ class BlasphemousWorld(World): return self.multiworld.random.choice(tears_set) - def generate_basic(self): + def create_items(self): placed_items = [] placed_items.extend(Vanilla.unrandomized_dict.values()) diff --git a/worlds/blasphemous/docs/setup_en.md b/worlds/blasphemous/docs/setup_en.md index 35b8670f6d..999765bb69 100644 --- a/worlds/blasphemous/docs/setup_en.md +++ b/worlds/blasphemous/docs/setup_en.md @@ -6,6 +6,7 @@ - Blasphemous Modding API from: [GitHub](https://github.com/BrandenEK/Blasphemous-Modding-API) - Blasphemous Randomizer from: [GitHub](https://github.com/BrandenEK/Blasphemous-Randomizer) - Blasphemous Multiworld from: [GitHub](https://github.com/BrandenEK/Blasphemous-Multiworld) +- (*Optional*) PopTracker Pack from: [GitHub](https://github.com/sassyvania/Blasphemous-Randomizer-Maptracker) ## Instructions (Windows) @@ -15,6 +16,8 @@ 3. Start Blasphemous. To verfy that the mods are working, look for a version number for both the Randomizer and Multiworld on the title screen. +4. (*Optional*) Add the Blasphemous pack to PopTracker. In game, open the console by pressing backslash `\` and type `randomizer autotracker on` to automatically connect the game to PopTracker. + ## Connecting To connect to an Archipelago server, open the in-game console by pressing backslash `\` and use the command `multiworld connect [address:port] [name] [password]`. The port and password are both optional - if no port is provided then the default port of 38281 is used. From 67a22b8b43768e009b7af285b0e8eb25639388ec Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Fri, 7 Apr 2023 19:05:16 -0500 Subject: [PATCH 008/489] Clique: Force priority location for "final location" and other minor tweaks. (#1583) * Clique: The greatest game of all time * Fix failing test * Increment data_version for backwards compat * Clique: Tweaked names and forced progression for "The Button" * Clique: Just location/item definitions to class * Clique: More tweaks. * Clique: Fix derp moment. * Clique: Fix derp moment, part dos. * Clique: Suggested changes. * Clique: Update domain * Clique: Create derived classes for Item and Location * Clique: simplify line --- worlds/clique/Options.py | 2 +- worlds/clique/__init__.py | 84 ++++++++++++++------------------- worlds/clique/docs/en_Clique.md | 3 +- worlds/clique/docs/guide_en.md | 2 +- 4 files changed, 39 insertions(+), 52 deletions(-) diff --git a/worlds/clique/Options.py b/worlds/clique/Options.py index 1d74d2c5a5..6636d2a0c5 100644 --- a/worlds/clique/Options.py +++ b/worlds/clique/Options.py @@ -4,7 +4,7 @@ from Options import Option, Toggle class HardMode(Toggle): - """Only for masochists: requires 2 presses!""" + """Only for the most masochistically inclined... Requires button activation!""" display_name = "Hard Mode" diff --git a/worlds/clique/__init__.py b/worlds/clique/__init__.py index c73d0437bf..449399c941 100644 --- a/worlds/clique/__init__.py +++ b/worlds/clique/__init__.py @@ -3,15 +3,13 @@ from worlds.AutoWorld import WebWorld, World from worlds.generic.Rules import set_rule from .Options import clique_options -item_table = { - "The feeling of satisfaction.": 69696969, - "Button Key": 69696968, -} -location_table = { - "The Button": 69696969, - "The Desk": 69696968, -} +class CliqueItem(Item): + game = "Clique" + + +class CliqueLocation(Location): + game = "Clique" class CliqueWebWorld(WebWorld): @@ -32,78 +30,68 @@ class CliqueWorld(World): """The greatest game ever designed. Full of exciting gameplay!""" game = "Clique" - topology_present = False - data_version = 1 + data_version = 2 web = CliqueWebWorld() option_definitions = clique_options - location_name_to_id = location_table - item_name_to_id = item_table + # Yes, I'm like 12 for this. + location_name_to_id = { + "The Big Red Button": 69696969, + "The Item on the Desk": 69696968, + } - def create_item(self, name: str) -> "Item": - return Item(name, ItemClassification.progression, self.item_name_to_id[name], self.player) + item_name_to_id = { + "Feeling of Satisfaction": 69696969, + "Button Activation": 69696968, + } - def get_setting(self, name: str): - return getattr(self.multiworld, name)[self.player] + def create_item(self, name: str) -> CliqueItem: + return CliqueItem(name, ItemClassification.progression, self.item_name_to_id[name], self.player) - def fill_slot_data(self) -> dict: - return {option_name: self.get_setting(option_name).value for option_name in self.option_definitions} - - def generate_basic(self) -> None: - self.multiworld.itempool.append(self.create_item("The feeling of satisfaction.")) + def create_items(self) -> None: + self.multiworld.itempool.append(self.create_item("Feeling of Satisfaction")) + self.multiworld.priority_locations[self.player].value.add("The Big Red Button") if self.multiworld.hard_mode[self.player]: - self.multiworld.itempool.append(self.create_item("Button Key")) + self.multiworld.itempool.append(self.create_item("Button Activation")) def create_regions(self) -> None: if self.multiworld.hard_mode[self.player]: self.multiworld.regions += [ - create_region(self.multiworld, self.player, "Menu", None, ["Entrance to THE BUTTON"]), - create_region(self.multiworld, self.player, "THE BUTTON", self.location_name_to_id) + create_region(self.multiworld, self.player, "Menu", None, ["The entrance to the button."]), + create_region(self.multiworld, self.player, "The realm of the button.", self.location_name_to_id) ] else: self.multiworld.regions += [ - create_region(self.multiworld, self.player, "Menu", None, ["Entrance to THE BUTTON"]), - create_region(self.multiworld, self.player, "THE BUTTON", {"The Button": 69696969}) - ] + create_region(self.multiworld, self.player, "Menu", None, ["The entrance to the button."]), + create_region(self.multiworld, self.player, "The realm of the button.", { + "The Big Red Button": 69696969 + })] - self.multiworld.get_entrance("Entrance to THE BUTTON", self.player)\ - .connect(self.multiworld.get_region("THE BUTTON", self.player)) + self.multiworld.get_entrance("The entrance to the button.", self.player) \ + .connect(self.multiworld.get_region("The realm of the button.", self.player)) def get_filler_item_name(self) -> str: - return self.multiworld.random.choice(item_table) + return self.multiworld.random.choice(self.item_name_to_id) def set_rules(self) -> None: if self.multiworld.hard_mode[self.player]: set_rule( - self.multiworld.get_location("The Button", self.player), - lambda state: state.has("Button Key", self.player) - ) + self.multiworld.get_location("The Big Red Button", self.player), + lambda state: state.has("Button Activation", self.player)) - self.multiworld.completion_condition[self.player] = lambda state: \ - state.has("Button Key", self.player) - else: - self.multiworld.completion_condition[self.player] = lambda state: \ - state.has("The feeling of satisfaction.", self.player) + self.multiworld.completion_condition[self.player] = lambda state: \ + state.has("Feeling of Satisfaction", self.player) def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): region = Region(name, player, world) if locations: for location_name in locations.keys(): - location = CliqueLocation(player, location_name, locations[location_name], region) - region.locations.append(location) + region.locations.append(CliqueLocation(player, location_name, locations[location_name], region)) if exits: for _exit in exits: region.exits.append(Entrance(player, _exit, region)) return region - - -class CliqueItem(Item): - game = "Clique" - - -class CliqueLocation(Location): - game: str = "Clique" diff --git a/worlds/clique/docs/en_Clique.md b/worlds/clique/docs/en_Clique.md index bf0562e2ba..afcf48c5d0 100644 --- a/worlds/clique/docs/en_Clique.md +++ b/worlds/clique/docs/en_Clique.md @@ -2,10 +2,9 @@ ## What is this game? -Even I don't know. +Yes. ## Where is the settings page? The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file. - diff --git a/worlds/clique/docs/guide_en.md b/worlds/clique/docs/guide_en.md index 7b4b0f1c21..a1e07093a6 100644 --- a/worlds/clique/docs/guide_en.md +++ b/worlds/clique/docs/guide_en.md @@ -1,6 +1,6 @@ # Clique Start Guide -Go to the [Clique Game](http://clique.darkshare.site.nfoservers.com/) and enter the hostname:ip address, +Go to the [Clique Game](http://clique.pharware.com/) and enter the hostname:ip address, then your slot name. Enjoy. \ No newline at end of file From bbf85468672ca936e5e1c1558d6bfc239c2e3911 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 8 Apr 2023 08:20:59 +0200 Subject: [PATCH 009/489] MultiServer: Flag for saving on datastore, create_as_hint scout and client state change --- MultiServer.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/MultiServer.py b/MultiServer.py index ea055b662e..95290ccfc6 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1742,6 +1742,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location)) locs.append(NetworkItem(target_item, location, target_player, flags)) ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2) + if locs and create_as_hint: + ctx.save() await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}]) elif cmd == 'StatusUpdate': @@ -1800,6 +1802,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): targets.add(client) if targets: ctx.broadcast(targets, [args]) + ctx.save() elif cmd == "SetNotify": if "keys" not in args or type(args["keys"]) != list: @@ -1817,6 +1820,7 @@ def update_client_status(ctx: Context, client: Client, new_status: ClientStatus) ctx.on_goal_achieved(client) ctx.client_game_state[client.team, client.slot] = new_status + ctx.save() class ServerCommandProcessor(CommonCommandProcessor): From f4035b8621a27f85924cbe23041cafabafc81482 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 2 Apr 2023 22:49:40 +0200 Subject: [PATCH 010/489] MultiServer: remove remaining forfeit compat from network layer --- MultiServer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 95290ccfc6..974886a9ca 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -544,7 +544,7 @@ class Context: "stored_data": self.stored_data, "game_options": {"hint_cost": self.hint_cost, "location_check_points": self.location_check_points, "server_password": self.server_password, "password": self.password, - "forfeit_mode": self.release_mode, "release_mode": self.release_mode, # TODO remove forfeit_mode around 0.4 + "release_mode": self.release_mode, "remaining_mode": self.remaining_mode, "collect_mode": self.collect_mode, "item_cheat": self.item_cheat, "compatibility": self.compatibility} @@ -769,7 +769,6 @@ async def on_client_connected(ctx: Context, client: Client): def get_permissions(ctx) -> typing.Dict[str, Permission]: return { - "forfeit": Permission.from_text(ctx.release_mode), # TODO remove around 0.4 "release": Permission.from_text(ctx.release_mode), "remaining": Permission.from_text(ctx.remaining_mode), "collect": Permission.from_text(ctx.collect_mode) From 84402a1b55fa516850631ea066851661bb9f59b2 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sat, 8 Apr 2023 16:52:34 -0400 Subject: [PATCH 011/489] SM and SMZ3 apworld support (#1677) --- setup.py | 2 + worlds/sm/Client.py | 6 +- worlds/sm/Regions.py | 4 +- worlds/sm/Rom.py | 3 +- worlds/sm/Rules.py | 6 +- worlds/sm/__init__.py | 43 ++++----- worlds/sm/variaRandomizer/graph/graph.py | 8 +- .../sm/variaRandomizer/graph/graph_utils.py | 10 +-- worlds/sm/variaRandomizer/graph/location.py | 2 +- .../graph/vanilla/graph_access.py | 12 +-- .../graph/vanilla/graph_helpers.py | 12 +-- .../graph/vanilla/graph_locations.py | 10 +-- worlds/sm/variaRandomizer/logic/helpers.py | 10 +-- worlds/sm/variaRandomizer/logic/logic.py | 16 ++-- .../sm/variaRandomizer/logic/smboolmanager.py | 12 +-- .../sm/variaRandomizer/patches/patchaccess.py | 21 ++--- worlds/sm/variaRandomizer/rando/Choice.py | 6 +- worlds/sm/variaRandomizer/rando/Filler.py | 18 ++-- .../sm/variaRandomizer/rando/GraphBuilder.py | 8 +- .../variaRandomizer/rando/ItemLocContainer.py | 6 +- worlds/sm/variaRandomizer/rando/Items.py | 4 +- worlds/sm/variaRandomizer/rando/RandoExec.py | 22 ++--- .../sm/variaRandomizer/rando/RandoServices.py | 8 +- .../sm/variaRandomizer/rando/RandoSettings.py | 6 +- worlds/sm/variaRandomizer/rando/RandoSetup.py | 20 ++--- .../sm/variaRandomizer/rando/Restrictions.py | 6 +- worlds/sm/variaRandomizer/randomizer.py | 30 +++---- worlds/sm/variaRandomizer/rom/ips.py | 4 +- worlds/sm/variaRandomizer/rom/rom.py | 2 +- worlds/sm/variaRandomizer/rom/rom_patches.py | 2 +- worlds/sm/variaRandomizer/rom/rompatcher.py | 18 ++-- .../sm/variaRandomizer/utils/doorsmanager.py | 6 +- worlds/sm/variaRandomizer/utils/parameters.py | 4 +- worlds/sm/variaRandomizer/utils/utils.py | 61 +++++++++++-- worlds/smz3/Client.py | 6 +- worlds/smz3/TotalSMZ3/Item.py | 8 +- worlds/smz3/TotalSMZ3/Location.py | 6 +- worlds/smz3/TotalSMZ3/Patch.py | 48 +++++----- worlds/smz3/TotalSMZ3/Region.py | 5 +- .../Regions/SuperMetroid/Brinstar/Blue.py | 6 +- .../Regions/SuperMetroid/Brinstar/Green.py | 8 +- .../Regions/SuperMetroid/Brinstar/Kraid.py | 8 +- .../Regions/SuperMetroid/Brinstar/Pink.py | 8 +- .../Regions/SuperMetroid/Brinstar/Red.py | 8 +- .../Regions/SuperMetroid/Crateria/Central.py | 6 +- .../Regions/SuperMetroid/Crateria/East.py | 8 +- .../Regions/SuperMetroid/Crateria/West.py | 8 +- .../Regions/SuperMetroid/Maridia/Inner.py | 8 +- .../Regions/SuperMetroid/Maridia/Outer.py | 8 +- .../Regions/SuperMetroid/NorfairLower/East.py | 8 +- .../Regions/SuperMetroid/NorfairLower/West.py | 8 +- .../SuperMetroid/NorfairUpper/Crocomire.py | 8 +- .../Regions/SuperMetroid/NorfairUpper/East.py | 8 +- .../Regions/SuperMetroid/NorfairUpper/West.py | 8 +- .../Regions/SuperMetroid/WreckedShip.py | 8 +- .../TotalSMZ3/Regions/Zelda/CastleTower.py | 8 +- .../Zelda/DarkWorld/DeathMountain/East.py | 8 +- .../Zelda/DarkWorld/DeathMountain/West.py | 6 +- .../TotalSMZ3/Regions/Zelda/DarkWorld/Mire.py | 8 +- .../Regions/Zelda/DarkWorld/NorthEast.py | 8 +- .../Regions/Zelda/DarkWorld/NorthWest.py | 8 +- .../Regions/Zelda/DarkWorld/South.py | 8 +- .../TotalSMZ3/Regions/Zelda/DesertPalace.py | 8 +- .../TotalSMZ3/Regions/Zelda/EasternPalace.py | 8 +- .../TotalSMZ3/Regions/Zelda/GanonsTower.py | 8 +- .../TotalSMZ3/Regions/Zelda/HyruleCastle.py | 8 +- .../smz3/TotalSMZ3/Regions/Zelda/IcePalace.py | 8 +- .../Zelda/LightWorld/DeathMountain/East.py | 8 +- .../Zelda/LightWorld/DeathMountain/West.py | 8 +- .../Regions/Zelda/LightWorld/NorthEast.py | 6 +- .../Regions/Zelda/LightWorld/NorthWest.py | 6 +- .../Regions/Zelda/LightWorld/South.py | 6 +- .../TotalSMZ3/Regions/Zelda/MiseryMire.py | 10 +-- .../Regions/Zelda/PalaceOfDarkness.py | 8 +- .../TotalSMZ3/Regions/Zelda/SkullWoods.py | 8 +- .../TotalSMZ3/Regions/Zelda/SwampPalace.py | 8 +- .../TotalSMZ3/Regions/Zelda/ThievesTown.py | 8 +- .../TotalSMZ3/Regions/Zelda/TowerOfHera.py | 8 +- .../TotalSMZ3/Regions/Zelda/TurtleRock.py | 10 +-- worlds/smz3/TotalSMZ3/Text/Dialog.py | 2 +- worlds/smz3/TotalSMZ3/Text/StringTable.py | 8 +- worlds/smz3/TotalSMZ3/Text/Texts.py | 43 ++++++--- worlds/smz3/TotalSMZ3/World.py | 88 +++++++++---------- worlds/smz3/TotalSMZ3/WorldState.py | 6 +- worlds/smz3/__init__.py | 23 ++--- worlds/smz3/ips.py | 4 +- 86 files changed, 522 insertions(+), 445 deletions(-) diff --git a/setup.py b/setup.py index 5447ed8b61..4b0c39545c 100644 --- a/setup.py +++ b/setup.py @@ -72,6 +72,8 @@ apworlds: set = { "Minecraft", "The Messenger", "Links Awakening DX", + "Super Metroid", + "SMZ3", } diff --git a/worlds/sm/Client.py b/worlds/sm/Client.py index 16aea935c0..df73ae47a0 100644 --- a/worlds/sm/Client.py +++ b/worlds/sm/Client.py @@ -118,7 +118,7 @@ class SMSNIClient(SNIClient): snes_buffered_write(ctx, SM_SEND_QUEUE_RCOUNT, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) - from worlds.sm import locations_start_id + from . import locations_start_id location_id = locations_start_id + item_index ctx.locations_checked.add(location_id) @@ -133,8 +133,8 @@ class SMSNIClient(SNIClient): item_out_ptr = data[0] | (data[1] << 8) - from worlds.sm import items_start_id - from worlds.sm import locations_start_id + from . import items_start_id + from . import locations_start_id if item_out_ptr < len(ctx.items_received): item = ctx.items_received[item_out_ptr] item_id = item.item - items_start_id diff --git a/worlds/sm/Regions.py b/worlds/sm/Regions.py index f8ef908889..ee6af4082d 100644 --- a/worlds/sm/Regions.py +++ b/worlds/sm/Regions.py @@ -1,8 +1,8 @@ def create_regions(self, world, player: int): from . import create_region from BaseClasses import Entrance - from worlds.sm.variaRandomizer.logic.logic import Logic - from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import locationsDict + from .variaRandomizer.logic.logic import Logic + from .variaRandomizer.graph.vanilla.graph_locations import locationsDict regions = [] for accessPoint in Logic.accessPoints: diff --git a/worlds/sm/Rom.py b/worlds/sm/Rom.py index 67c0780dbf..ac516ae48b 100644 --- a/worlds/sm/Rom.py +++ b/worlds/sm/Rom.py @@ -5,6 +5,7 @@ import json import Utils from Utils import read_snes_rom from worlds.Files import APDeltaPatch +from .variaRandomizer.utils.utils import openFile SMJUHASH = '21f3e98df4780ee1c667b84e57d88675' SM_ROM_MAX_PLAYERID = 65535 @@ -43,7 +44,7 @@ def get_base_rom_path(file_name: str = "") -> str: return file_name def get_sm_symbols(sym_json_path) -> dict: - with open(sym_json_path, "r") as stream: + with openFile(sym_json_path, "r") as stream: symbols = json.load(stream) symboltable = {} for name, sixdigitaddr in symbols.items(): diff --git a/worlds/sm/Rules.py b/worlds/sm/Rules.py index bce9247342..15706987ff 100644 --- a/worlds/sm/Rules.py +++ b/worlds/sm/Rules.py @@ -1,7 +1,7 @@ -from ..generic.Rules import set_rule, add_rule +from worlds.generic.Rules import set_rule, add_rule -from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import locationsDict -from worlds.sm.variaRandomizer.logic.logic import Logic +from .variaRandomizer.graph.vanilla.graph_locations import locationsDict +from .variaRandomizer.logic.logic import Logic def evalSMBool(smbool, maxDiff): return smbool.bool == True and smbool.difficulty <= maxDiff diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 255551c267..e8e4c56a90 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -7,6 +7,9 @@ import threading import base64 from typing import Any, Dict, Iterable, List, Set, TextIO, TypedDict +from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, CollectionState, Tutorial +from worlds.AutoWorld import World, AutoLogicRegister, WebWorld + logger = logging.getLogger("Super Metroid") from .Regions import create_regions @@ -16,20 +19,18 @@ from .Client import SMSNIClient from .Rom import get_base_rom_path, SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMDeltaPatch, get_sm_symbols import Utils -from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, CollectionState, Tutorial -from ..AutoWorld import World, AutoLogicRegister, WebWorld - -from worlds.sm.variaRandomizer.logic.smboolmanager import SMBoolManager -from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import locationsDict -from worlds.sm.variaRandomizer.graph.graph_utils import getAccessPoint -from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation -from worlds.sm.variaRandomizer.rando.Items import ItemManager -from worlds.sm.variaRandomizer.utils.parameters import * -from worlds.sm.variaRandomizer.logic.logic import Logic -from worlds.sm.variaRandomizer.randomizer import VariaRandomizer -from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager -from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches -from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils +from .variaRandomizer.logic.smboolmanager import SMBoolManager +from .variaRandomizer.graph.vanilla.graph_locations import locationsDict +from .variaRandomizer.graph.graph_utils import getAccessPoint +from .variaRandomizer.rando.ItemLocContainer import ItemLocation +from .variaRandomizer.rando.Items import ItemManager +from .variaRandomizer.utils.parameters import * +from .variaRandomizer.utils.utils import openFile +from .variaRandomizer.logic.logic import Logic +from .variaRandomizer.randomizer import VariaRandomizer +from .variaRandomizer.utils.doorsmanager import DoorsManager +from .variaRandomizer.rom.rom_patches import RomPatches +from .variaRandomizer.graph.graph_utils import GraphUtils class SMCollectionState(metaclass=AutoLogicRegister): @@ -279,14 +280,14 @@ class SMWorld(World): # first apply the sm multiworld code patch named 'basepatch' (also has empty tables that we'll overwrite), # + apply some patches from varia that we want to be always-on. # basepatch and variapatches are both generated from https://github.com/lordlou/SMBasepatch - romPatcher.applyIPSPatch(os.path.join(os.path.dirname(__file__), - "data", "SMBasepatch_prebuilt", "multiworld-basepatch.ips")) - romPatcher.applyIPSPatch(os.path.join(os.path.dirname(__file__), - "data", "SMBasepatch_prebuilt", "variapatches.ips")) + romPatcher.applyIPSPatch("/".join((os.path.dirname(self.__file__), + "data", "SMBasepatch_prebuilt", "multiworld-basepatch.ips"))) + romPatcher.applyIPSPatch("/".join((os.path.dirname(self.__file__), + "data", "SMBasepatch_prebuilt", "variapatches.ips"))) def APPostPatchRom(self, romPatcher): - symbols = get_sm_symbols(os.path.join(os.path.dirname(__file__), - "data", "SMBasepatch_prebuilt", "sm-basepatch-symbols.json")) + symbols = get_sm_symbols("/".join((os.path.dirname(self.__file__), + "data", "SMBasepatch_prebuilt", "sm-basepatch-symbols.json"))) # gather all player ids and names relevant to this rom, then write player name and player id data tables playerIdSet: Set[int] = {0} # 0 is for "Archipelago" server @@ -376,7 +377,7 @@ class SMWorld(World): idx = 0 offworldSprites: List[ByteEdit] = [] for itemSprite in itemSprites: - with open(os.path.join(os.path.dirname(__file__), "data", "custom_sprite", itemSprite["fileName"]), 'rb') as stream: + with openFile("/".join((os.path.dirname(self.__file__), "data", "custom_sprite", itemSprite["fileName"])), 'rb') as stream: buffer = bytearray(stream.read()) offworldSprites.append({"sym": symbols[itemSprite["paletteSymbolName"]], "offset": 0, diff --git a/worlds/sm/variaRandomizer/graph/graph.py b/worlds/sm/variaRandomizer/graph/graph.py index bf9af48dc9..3e29650061 100644 --- a/worlds/sm/variaRandomizer/graph/graph.py +++ b/worlds/sm/variaRandomizer/graph/graph.py @@ -1,9 +1,9 @@ import copy, logging from operator import attrgetter -from worlds.sm.variaRandomizer.utils import log -from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse -from worlds.sm.variaRandomizer.utils.parameters import infinity -from worlds.sm.variaRandomizer.logic.helpers import Bosses +from ..utils import log +from ..logic.smbool import SMBool, smboolFalse +from ..utils.parameters import infinity +from ..logic.helpers import Bosses class Path(object): __slots__ = ( 'path', 'pdiff', 'distance' ) diff --git a/worlds/sm/variaRandomizer/graph/graph_utils.py b/worlds/sm/variaRandomizer/graph/graph_utils.py index e147da5e57..ccff42a454 100644 --- a/worlds/sm/variaRandomizer/graph/graph_utils.py +++ b/worlds/sm/variaRandomizer/graph/graph_utils.py @@ -1,10 +1,10 @@ import copy import random -from worlds.sm.variaRandomizer.logic.logic import Logic -from worlds.sm.variaRandomizer.utils.parameters import Knows -from worlds.sm.variaRandomizer.graph.location import locationsDict -from worlds.sm.variaRandomizer.rom.rom import snes_to_pc -from worlds.sm.variaRandomizer.utils import log +from ..logic.logic import Logic +from ..utils.parameters import Knows +from ..graph.location import locationsDict +from ..rom.rom import snes_to_pc +from ..utils import log # order expected by ROM patches graphAreas = [ diff --git a/worlds/sm/variaRandomizer/graph/location.py b/worlds/sm/variaRandomizer/graph/location.py index 92eb1ccbca..fb20585332 100644 --- a/worlds/sm/variaRandomizer/graph/location.py +++ b/worlds/sm/variaRandomizer/graph/location.py @@ -1,4 +1,4 @@ -from worlds.sm.variaRandomizer.utils.parameters import infinity +from ..utils.parameters import infinity import copy class Location: diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py index 279f249e86..501dab8f26 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py @@ -1,9 +1,9 @@ -from worlds.sm.variaRandomizer.graph.graph import AccessPoint -from worlds.sm.variaRandomizer.utils.parameters import Settings -from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches -from worlds.sm.variaRandomizer.logic.smbool import SMBool -from worlds.sm.variaRandomizer.logic.helpers import Bosses -from worlds.sm.variaRandomizer.logic.cache import Cache +from ...graph.graph import AccessPoint +from ...utils.parameters import Settings +from ...rom.rom_patches import RomPatches +from ...logic.smbool import SMBool +from ...logic.helpers import Bosses +from ...logic.cache import Cache # all access points and traverse functions accessPoints = [ diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py index 41ffe51192..de18ab86d6 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py @@ -1,11 +1,11 @@ from math import ceil -from worlds.sm.variaRandomizer.logic.smbool import SMBool -from worlds.sm.variaRandomizer.logic.helpers import Helpers, Bosses -from worlds.sm.variaRandomizer.logic.cache import Cache -from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches -from worlds.sm.variaRandomizer.graph.graph_utils import getAccessPoint -from worlds.sm.variaRandomizer.utils.parameters import Settings +from ...logic.smbool import SMBool +from ...logic.helpers import Helpers, Bosses +from ...logic.cache import Cache +from ...rom.rom_patches import RomPatches +from ...graph.graph_utils import getAccessPoint +from ...utils.parameters import Settings class HelpersGraph(Helpers): def __init__(self, smbm): diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py index 62eaf3c0fe..5bf3c0e6c3 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py @@ -1,8 +1,8 @@ -from worlds.sm.variaRandomizer.logic.helpers import Bosses -from worlds.sm.variaRandomizer.utils.parameters import Settings -from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches -from worlds.sm.variaRandomizer.logic.smbool import SMBool -from worlds.sm.variaRandomizer.graph.location import locationsDict +from ...logic.helpers import Bosses +from ...utils.parameters import Settings +from ...rom.rom_patches import RomPatches +from ...logic.smbool import SMBool +from ...graph.location import locationsDict locationsDict["Energy Tank, Gauntlet"].AccessFrom = { 'Landing Site': lambda sm: SMBool(True) diff --git a/worlds/sm/variaRandomizer/logic/helpers.py b/worlds/sm/variaRandomizer/logic/helpers.py index 3f8720d84f..717561e7cd 100644 --- a/worlds/sm/variaRandomizer/logic/helpers.py +++ b/worlds/sm/variaRandomizer/logic/helpers.py @@ -1,11 +1,11 @@ import math -from worlds.sm.variaRandomizer.logic.cache import Cache -from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse -from worlds.sm.variaRandomizer.utils.parameters import Settings, easy, medium, diff2text -from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches -from worlds.sm.variaRandomizer.utils.utils import normalizeRounding +from ..logic.cache import Cache +from ..logic.smbool import SMBool, smboolFalse +from ..utils.parameters import Settings, easy, medium, diff2text +from ..rom.rom_patches import RomPatches +from ..utils.utils import normalizeRounding class Helpers(object): diff --git a/worlds/sm/variaRandomizer/logic/logic.py b/worlds/sm/variaRandomizer/logic/logic.py index 6ce20406b9..d58d09807d 100644 --- a/worlds/sm/variaRandomizer/logic/logic.py +++ b/worlds/sm/variaRandomizer/logic/logic.py @@ -4,20 +4,20 @@ class Logic(object): @staticmethod def factory(implementation): if implementation == 'vanilla': - from worlds.sm.variaRandomizer.graph.vanilla.graph_helpers import HelpersGraph - from worlds.sm.variaRandomizer.graph.vanilla.graph_access import accessPoints - from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import locations - from worlds.sm.variaRandomizer.graph.vanilla.graph_locations import LocationsHelper + from ..graph.vanilla.graph_helpers import HelpersGraph + from ..graph.vanilla.graph_access import accessPoints + from ..graph.vanilla.graph_locations import locations + from ..graph.vanilla.graph_locations import LocationsHelper Logic.locations = locations Logic.accessPoints = accessPoints Logic.HelpersGraph = HelpersGraph Logic.patches = implementation Logic.LocationsHelper = LocationsHelper elif implementation == 'rotation': - from worlds.sm.variaRandomizer.graph.rotation.graph_helpers import HelpersGraph - from worlds.sm.variaRandomizer.graph.rotation.graph_access import accessPoints - from worlds.sm.variaRandomizer.graph.rotation.graph_locations import locations - from worlds.sm.variaRandomizer.graph.rotation.graph_locations import LocationsHelper + from ..graph.rotation.graph_helpers import HelpersGraph + from ..graph.rotation.graph_access import accessPoints + from ..graph.rotation.graph_locations import locations + from ..graph.rotation.graph_locations import LocationsHelper Logic.locations = locations Logic.accessPoints = accessPoints Logic.HelpersGraph = HelpersGraph diff --git a/worlds/sm/variaRandomizer/logic/smboolmanager.py b/worlds/sm/variaRandomizer/logic/smboolmanager.py index a351e163fa..d4de6dae93 100644 --- a/worlds/sm/variaRandomizer/logic/smboolmanager.py +++ b/worlds/sm/variaRandomizer/logic/smboolmanager.py @@ -1,11 +1,11 @@ # object to handle the smbools and optimize them -from worlds.sm.variaRandomizer.logic.cache import Cache -from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse -from worlds.sm.variaRandomizer.logic.helpers import Bosses -from worlds.sm.variaRandomizer.logic.logic import Logic -from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager -from worlds.sm.variaRandomizer.utils.parameters import Knows, isKnows +from ..logic.cache import Cache +from ..logic.smbool import SMBool, smboolFalse +from ..logic.helpers import Bosses +from ..logic.logic import Logic +from ..utils.doorsmanager import DoorsManager +from ..utils.parameters import Knows, isKnows import logging import sys diff --git a/worlds/sm/variaRandomizer/patches/patchaccess.py b/worlds/sm/variaRandomizer/patches/patchaccess.py index 9ed8317294..f7f8060a66 100644 --- a/worlds/sm/variaRandomizer/patches/patchaccess.py +++ b/worlds/sm/variaRandomizer/patches/patchaccess.py @@ -1,17 +1,18 @@ -import os, importlib -from worlds.sm.variaRandomizer.logic.logic import Logic -from worlds.sm.variaRandomizer.patches.common.patches import patches, additional_PLMs -from worlds.sm.variaRandomizer.utils.parameters import appDir +import importlib +from ..logic.logic import Logic +from ..patches.common.patches import patches, additional_PLMs +from ..utils.parameters import appDir +from ..utils.utils import listDir, exists class PatchAccess(object): def __init__(self): # load all ips patches self.patchesPath = {} - commonDir = os.path.join(appDir, 'worlds/sm/variaRandomizer/patches/common/ips/') - for patch in os.listdir(commonDir): + commonDir = "/".join((appDir, 'worlds/sm/variaRandomizer/patches/common/ips')) + for patch in listDir(commonDir): self.patchesPath[patch] = commonDir - logicDir = os.path.join(appDir, 'worlds/sm/variaRandomizer/patches/{}/ips/'.format(Logic.patches)) - for patch in os.listdir(logicDir): + logicDir = "/".join((appDir, 'worlds/sm/variaRandomizer/patches/{}/ips'.format(Logic.patches))) + for patch in listDir(logicDir): self.patchesPath[patch] = logicDir # load dict patches @@ -27,10 +28,10 @@ class PatchAccess(object): def getPatchPath(self, patch): # is patch preloaded if patch in self.patchesPath: - return os.path.join(self.patchesPath[patch], patch) + return "/".join((self.patchesPath[patch], patch)) else: # patchs from varia_repository used by the customizer for permalinks - if os.path.exists(patch): + if exists(patch): return patch else: raise Exception("unknown patch: {}".format(patch)) diff --git a/worlds/sm/variaRandomizer/rando/Choice.py b/worlds/sm/variaRandomizer/rando/Choice.py index e200448b43..b4f4166f76 100644 --- a/worlds/sm/variaRandomizer/rando/Choice.py +++ b/worlds/sm/variaRandomizer/rando/Choice.py @@ -1,7 +1,7 @@ import random -from worlds.sm.variaRandomizer.utils import log -from worlds.sm.variaRandomizer.utils.utils import getRangeDict, chooseFromRange -from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation +from ..utils import log +from ..utils.utils import getRangeDict, chooseFromRange +from ..rando.ItemLocContainer import ItemLocation # helper object to choose item/loc class Choice(object): diff --git a/worlds/sm/variaRandomizer/rando/Filler.py b/worlds/sm/variaRandomizer/rando/Filler.py index 733e7cdbbb..00caa7e630 100644 --- a/worlds/sm/variaRandomizer/rando/Filler.py +++ b/worlds/sm/variaRandomizer/rando/Filler.py @@ -1,14 +1,14 @@ import copy, time, random -from worlds.sm.variaRandomizer.utils import log -from worlds.sm.variaRandomizer.logic.cache import RequestCache -from worlds.sm.variaRandomizer.rando.RandoServices import RandoServices -from worlds.sm.variaRandomizer.rando.Choice import ItemThenLocChoice -from worlds.sm.variaRandomizer.rando.RandoServices import ComebackCheckType -from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation, getItemLocationsStr -from worlds.sm.variaRandomizer.utils.parameters import infinity -from worlds.sm.variaRandomizer.logic.helpers import diffValue2txt -from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils +from ..utils import log +from ..logic.cache import RequestCache +from ..rando.RandoServices import RandoServices +from ..rando.Choice import ItemThenLocChoice +from ..rando.RandoServices import ComebackCheckType +from ..rando.ItemLocContainer import ItemLocation, getItemLocationsStr +from ..utils.parameters import infinity +from ..logic.helpers import diffValue2txt +from ..graph.graph_utils import GraphUtils # base class for fillers. a filler responsibility is to fill a given # ItemLocContainer while a certain condition is fulfilled (usually diff --git a/worlds/sm/variaRandomizer/rando/GraphBuilder.py b/worlds/sm/variaRandomizer/rando/GraphBuilder.py index 6eeb1d865c..9cde420644 100644 --- a/worlds/sm/variaRandomizer/rando/GraphBuilder.py +++ b/worlds/sm/variaRandomizer/rando/GraphBuilder.py @@ -1,9 +1,9 @@ import random, copy -from worlds.sm.variaRandomizer.utils import log -from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils, vanillaTransitions, vanillaBossesTransitions, escapeSource, escapeTargets -from worlds.sm.variaRandomizer.logic.logic import Logic -from worlds.sm.variaRandomizer.graph.graph import AccessGraphRando as AccessGraph +from ..utils import log +from ..graph.graph_utils import GraphUtils, vanillaTransitions, vanillaBossesTransitions, escapeSource, escapeTargets +from ..logic.logic import Logic +from ..graph.graph import AccessGraphRando as AccessGraph # creates graph and handles randomized escape class GraphBuilder(object): diff --git a/worlds/sm/variaRandomizer/rando/ItemLocContainer.py b/worlds/sm/variaRandomizer/rando/ItemLocContainer.py index 859fe5503f..524e31907c 100644 --- a/worlds/sm/variaRandomizer/rando/ItemLocContainer.py +++ b/worlds/sm/variaRandomizer/rando/ItemLocContainer.py @@ -1,8 +1,8 @@ import copy -from worlds.sm.variaRandomizer.utils import log -from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse -from worlds.sm.variaRandomizer.logic.smboolmanager import SMBoolManager +from ..utils import log +from ..logic.smbool import SMBool, smboolFalse +from ..logic.smboolmanager import SMBoolManager from collections import Counter class ItemLocation(object): diff --git a/worlds/sm/variaRandomizer/rando/Items.py b/worlds/sm/variaRandomizer/rando/Items.py index 6c4d35119e..6bfa8530ac 100644 --- a/worlds/sm/variaRandomizer/rando/Items.py +++ b/worlds/sm/variaRandomizer/rando/Items.py @@ -1,5 +1,5 @@ -from worlds.sm.variaRandomizer.utils.utils import randGaussBounds, getRangeDict, chooseFromRange -from worlds.sm.variaRandomizer.utils import log +from ..utils.utils import randGaussBounds, getRangeDict, chooseFromRange +from ..utils import log import logging, copy, random class Item: diff --git a/worlds/sm/variaRandomizer/rando/RandoExec.py b/worlds/sm/variaRandomizer/rando/RandoExec.py index 9ff6fc2d99..3505b180b2 100644 --- a/worlds/sm/variaRandomizer/rando/RandoExec.py +++ b/worlds/sm/variaRandomizer/rando/RandoExec.py @@ -1,16 +1,16 @@ import sys, random, time -from worlds.sm.variaRandomizer.utils import log -from worlds.sm.variaRandomizer.logic.logic import Logic -from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils, getAccessPoint -from worlds.sm.variaRandomizer.rando.Restrictions import Restrictions -from worlds.sm.variaRandomizer.rando.RandoServices import RandoServices -from worlds.sm.variaRandomizer.rando.GraphBuilder import GraphBuilder -from worlds.sm.variaRandomizer.rando.RandoSetup import RandoSetup -from worlds.sm.variaRandomizer.rando.Items import ItemManager -from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation -from worlds.sm.variaRandomizer.utils.vcr import VCR -from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager +from ..utils import log +from ..logic.logic import Logic +from ..graph.graph_utils import GraphUtils, getAccessPoint +from ..rando.Restrictions import Restrictions +from ..rando.RandoServices import RandoServices +from ..rando.GraphBuilder import GraphBuilder +from ..rando.RandoSetup import RandoSetup +from ..rando.Items import ItemManager +from ..rando.ItemLocContainer import ItemLocation +from ..utils.vcr import VCR +from ..utils.doorsmanager import DoorsManager # entry point for rando execution ("randomize" method) class RandoExec(object): diff --git a/worlds/sm/variaRandomizer/rando/RandoServices.py b/worlds/sm/variaRandomizer/rando/RandoServices.py index 6ea86c9e4a..60f9df74e9 100644 --- a/worlds/sm/variaRandomizer/rando/RandoServices.py +++ b/worlds/sm/variaRandomizer/rando/RandoServices.py @@ -1,10 +1,10 @@ import copy, random, sys, logging, os from enum import Enum, unique -from worlds.sm.variaRandomizer.utils import log -from worlds.sm.variaRandomizer.utils.parameters import infinity -from worlds.sm.variaRandomizer.rando.ItemLocContainer import getLocListStr, getItemListStr, getItemLocStr, ItemLocation -from worlds.sm.variaRandomizer.logic.helpers import Bosses +from ..utils import log +from ..utils.parameters import infinity +from ..rando.ItemLocContainer import getLocListStr, getItemListStr, getItemLocStr, ItemLocation +from ..logic.helpers import Bosses # used to specify whether we want to come back from locations @unique diff --git a/worlds/sm/variaRandomizer/rando/RandoSettings.py b/worlds/sm/variaRandomizer/rando/RandoSettings.py index 030b14fff2..6895b59716 100644 --- a/worlds/sm/variaRandomizer/rando/RandoSettings.py +++ b/worlds/sm/variaRandomizer/rando/RandoSettings.py @@ -1,9 +1,9 @@ import sys, random from collections import defaultdict -from worlds.sm.variaRandomizer.rando.Items import ItemManager -from worlds.sm.variaRandomizer.utils.utils import getRangeDict, chooseFromRange -from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocation +from ..rando.Items import ItemManager +from ..utils.utils import getRangeDict, chooseFromRange +from ..rando.ItemLocContainer import ItemLocation # Holder for settings and a few utility functions related to them # (especially for plando/rando). diff --git a/worlds/sm/variaRandomizer/rando/RandoSetup.py b/worlds/sm/variaRandomizer/rando/RandoSetup.py index c82802f8c1..637a5fed39 100644 --- a/worlds/sm/variaRandomizer/rando/RandoSetup.py +++ b/worlds/sm/variaRandomizer/rando/RandoSetup.py @@ -1,15 +1,15 @@ import copy, random -from worlds.sm.variaRandomizer.utils import log -from worlds.sm.variaRandomizer.utils.utils import randGaussBounds -from worlds.sm.variaRandomizer.logic.smbool import SMBool, smboolFalse -from worlds.sm.variaRandomizer.logic.smboolmanager import SMBoolManager -from worlds.sm.variaRandomizer.logic.helpers import Bosses -from worlds.sm.variaRandomizer.graph.graph_utils import getAccessPoint, GraphUtils -from worlds.sm.variaRandomizer.rando.Filler import FrontFiller -from worlds.sm.variaRandomizer.rando.ItemLocContainer import ItemLocContainer, getLocListStr, ItemLocation, getItemListStr -from worlds.sm.variaRandomizer.rando.Restrictions import Restrictions -from worlds.sm.variaRandomizer.utils.parameters import infinity +from ..utils import log +from ..utils.utils import randGaussBounds +from ..logic.smbool import SMBool, smboolFalse +from ..logic.smboolmanager import SMBoolManager +from ..logic.helpers import Bosses +from ..graph.graph_utils import getAccessPoint, GraphUtils +from ..rando.Filler import FrontFiller +from ..rando.ItemLocContainer import ItemLocContainer, getLocListStr, ItemLocation, getItemListStr +from ..rando.Restrictions import Restrictions +from ..utils.parameters import infinity # checks init conditions for the randomizer: processes super fun settings, graph, start location, special restrictions # the entry point is createItemLocContainer diff --git a/worlds/sm/variaRandomizer/rando/Restrictions.py b/worlds/sm/variaRandomizer/rando/Restrictions.py index 953eb2ef06..6880123197 100644 --- a/worlds/sm/variaRandomizer/rando/Restrictions.py +++ b/worlds/sm/variaRandomizer/rando/Restrictions.py @@ -1,7 +1,7 @@ import copy, random -from worlds.sm.variaRandomizer.utils import log -from worlds.sm.variaRandomizer.graph.graph_utils import getAccessPoint -from worlds.sm.variaRandomizer.rando.ItemLocContainer import getLocListStr +from ..utils import log +from ..graph.graph_utils import getAccessPoint +from ..rando.ItemLocContainer import getLocListStr # Holds settings related to item placement restrictions. # canPlaceAtLocation is the main entry point here diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index 6a2c33ca4e..332c333c03 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -3,19 +3,19 @@ from Utils import output_path import argparse, os.path, json, sys, shutil, random, copy, requests -from worlds.sm.variaRandomizer.rando.RandoSettings import RandoSettings, GraphSettings -from worlds.sm.variaRandomizer.rando.RandoExec import RandoExec -from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils, getAccessPoint -from worlds.sm.variaRandomizer.utils.parameters import Controller, easy, medium, hard, harder, hardcore, mania, infinity, text2diff, appDir -from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches -from worlds.sm.variaRandomizer.rom.rompatcher import RomPatcher -from worlds.sm.variaRandomizer.utils.utils import PresetLoader, loadRandoPreset, getDefaultMultiValues, getPresetDir -from worlds.sm.variaRandomizer.utils.version import displayedVersion -from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager -from worlds.sm.variaRandomizer.logic.logic import Logic +from .rando.RandoSettings import RandoSettings, GraphSettings +from .rando.RandoExec import RandoExec +from .graph.graph_utils import GraphUtils, getAccessPoint +from .utils.parameters import Controller, easy, medium, hard, harder, hardcore, mania, infinity, text2diff, appDir +from .rom.rom_patches import RomPatches +from .rom.rompatcher import RomPatcher +from .utils.utils import PresetLoader, loadRandoPreset, getDefaultMultiValues, getPresetDir +from .utils.version import displayedVersion +from .utils.doorsmanager import DoorsManager +from .logic.logic import Logic -from worlds.sm.variaRandomizer.utils import log -from worlds.sm.Options import StartLocation +from .utils import log +from ..Options import StartLocation # we need to know the logic before doing anything else def getLogic(): @@ -327,8 +327,8 @@ class VariaRandomizer: preset = loadRandoPreset(world, self.player, args) # use the skill preset from the rando preset if preset is not None and preset != 'custom' and preset != 'varia_custom' and args.paramsFileName is None: - args.paramsFileName = os.path.join(appDir, getPresetDir(preset), preset+".json") - + args.paramsFileName = "/".join((appDir, getPresetDir(preset), preset+".json")) + # if diff preset given, load it if args.paramsFileName is not None: PresetLoader.factory(args.paramsFileName).load(self.player) @@ -353,7 +353,7 @@ class VariaRandomizer: raise Exception("Got error {} {} {} from trying to fetch varia custom preset named {}".format(response.status_code, response.reason, response.text, preset_name)) else: preset = 'default' - PresetLoader.factory(os.path.join(appDir, getPresetDir('casual'), 'casual.json')).load(self.player) + PresetLoader.factory("/".join((appDir, getPresetDir('casual'), 'casual.json'))).load(self.player) diff --git a/worlds/sm/variaRandomizer/rom/ips.py b/worlds/sm/variaRandomizer/rom/ips.py index 34a41e2ecf..23c5281372 100644 --- a/worlds/sm/variaRandomizer/rom/ips.py +++ b/worlds/sm/variaRandomizer/rom/ips.py @@ -1,6 +1,6 @@ import itertools -from worlds.sm.variaRandomizer.utils.utils import range_union +from ..utils.utils import range_union, openFile # adapted from ips-util for python 3.2 (https://pypi.org/project/ips-util/) class IPS_Patch(object): @@ -25,7 +25,7 @@ class IPS_Patch(object): @staticmethod def load(filename): loaded_patch = IPS_Patch() - with open(filename, 'rb') as file: + with openFile(filename, 'rb') as file: header = file.read(5) if header != b'PATCH': raise Exception('Not a valid IPS patch file!') diff --git a/worlds/sm/variaRandomizer/rom/rom.py b/worlds/sm/variaRandomizer/rom/rom.py index 7b1cf06ffc..d173f7ab54 100644 --- a/worlds/sm/variaRandomizer/rom/rom.py +++ b/worlds/sm/variaRandomizer/rom/rom.py @@ -1,6 +1,6 @@ import base64 -from worlds.sm.variaRandomizer.rom.ips import IPS_Patch +from ..rom.ips import IPS_Patch def pc_to_snes(pcaddress): snesaddress=(((pcaddress<<1)&0x7F0000)|(pcaddress&0x7FFF)|0x8000)|0x800000 diff --git a/worlds/sm/variaRandomizer/rom/rom_patches.py b/worlds/sm/variaRandomizer/rom/rom_patches.py index 26e8a84e94..ff5e2aa823 100644 --- a/worlds/sm/variaRandomizer/rom/rom_patches.py +++ b/worlds/sm/variaRandomizer/rom/rom_patches.py @@ -1,4 +1,4 @@ -from worlds.sm.variaRandomizer.logic.smbool import SMBool +from ..logic.smbool import SMBool # layout patches added by randomizers class RomPatches: diff --git a/worlds/sm/variaRandomizer/rom/rompatcher.py b/worlds/sm/variaRandomizer/rom/rompatcher.py index 85cd3bf312..0e052f20c1 100644 --- a/worlds/sm/variaRandomizer/rom/rompatcher.py +++ b/worlds/sm/variaRandomizer/rom/rompatcher.py @@ -1,13 +1,13 @@ import os, random, re -from worlds.sm.variaRandomizer.rando.Items import ItemManager -from worlds.sm.variaRandomizer.rom.ips import IPS_Patch -from worlds.sm.variaRandomizer.utils.doorsmanager import DoorsManager -from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses -from worlds.sm.variaRandomizer.logic.logic import Logic -from worlds.sm.variaRandomizer.rom.rom import RealROM, snes_to_pc, pc_to_snes -from worlds.sm.variaRandomizer.patches.patchaccess import PatchAccess -from worlds.sm.variaRandomizer.utils.parameters import appDir -from worlds.sm.variaRandomizer.utils import log +from ..rando.Items import ItemManager +from ..rom.ips import IPS_Patch +from ..utils.doorsmanager import DoorsManager +from ..graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses +from ..logic.logic import Logic +from ..rom.rom import RealROM, snes_to_pc, pc_to_snes +from ..patches.patchaccess import PatchAccess +from ..utils.parameters import appDir +from ..utils import log def getWord(w): return (w & 0x00FF, (w & 0xFF00) >> 8) diff --git a/worlds/sm/variaRandomizer/utils/doorsmanager.py b/worlds/sm/variaRandomizer/utils/doorsmanager.py index c8feacdbab..0b2a414993 100644 --- a/worlds/sm/variaRandomizer/utils/doorsmanager.py +++ b/worlds/sm/variaRandomizer/utils/doorsmanager.py @@ -1,10 +1,10 @@ import random import copy -from worlds.sm.variaRandomizer.logic.smbool import SMBool -from worlds.sm.variaRandomizer.rom.rom_patches import RomPatches +from ..logic.smbool import SMBool +from ..rom.rom_patches import RomPatches import logging -from worlds.sm.variaRandomizer.utils import log +from ..utils import log LOG = log.get('DoorsManager') colorsList = ['red', 'green', 'yellow', 'wave', 'spazer', 'plasma', 'ice'] diff --git a/worlds/sm/variaRandomizer/utils/parameters.py b/worlds/sm/variaRandomizer/utils/parameters.py index 6bae03b465..2d9313b580 100644 --- a/worlds/sm/variaRandomizer/utils/parameters.py +++ b/worlds/sm/variaRandomizer/utils/parameters.py @@ -1,4 +1,4 @@ -from worlds.sm.variaRandomizer.logic.smbool import SMBool +from ..logic.smbool import SMBool import os import sys from pathlib import Path @@ -61,7 +61,7 @@ def diff4solver(difficulty): return "mania" # allow multiple local repo -appDir = Path(__file__).parents[4] +appDir = str(Path(__file__).parents[4]) def isKnows(knows): return knows[0:len('__')] != '__' and knows[0] == knows[0].upper() diff --git a/worlds/sm/variaRandomizer/utils/utils.py b/worlds/sm/variaRandomizer/utils/utils.py index ba43d710a3..46c14a16f1 100644 --- a/worlds/sm/variaRandomizer/utils/utils.py +++ b/worlds/sm/variaRandomizer/utils/utils.py @@ -1,8 +1,59 @@ +import io import os, json, re, random +import pathlib +import sys +from typing import Any +import zipfile -from worlds.sm.variaRandomizer.utils.parameters import Knows, Settings, Controller, isKnows, isSettings, isButton -from worlds.sm.variaRandomizer.utils.parameters import easy, medium, hard, harder, hardcore, mania, text2diff -from worlds.sm.variaRandomizer.logic.smbool import SMBool +from ..utils.parameters import Knows, Settings, Controller, isKnows, isSettings, isButton +from ..utils.parameters import easy, medium, hard, harder, hardcore, mania, text2diff +from ..logic.smbool import SMBool + + +# support for AP world +isAPWorld = ".apworld" in sys.modules[__name__].__file__ + +def getZipFile(): + filename = sys.modules[__name__].__file__ + apworldExt = ".apworld" + zipPath = pathlib.Path(filename[:filename.index(apworldExt) + len(apworldExt)]) + return (zipfile.ZipFile(zipPath), zipPath.stem) + +def openFile(resource: str, mode: str = "r", encoding: None = None): + if isAPWorld: + (zipFile, stem) = getZipFile() + with zipFile as zf: + zipFilePath = resource[resource.index(stem + "/"):] + if mode == 'rb': + return zf.open(zipFilePath, 'r') + else: + return io.TextIOWrapper(zf.open(zipFilePath, mode), encoding) + else: + return open(resource, mode) + +def listDir(resource: str): + if isAPWorld: + (zipFile, stem) = getZipFile() + with zipFile as zf: + zipFilePath = resource[resource.index(stem + "/"):] + path = zipfile.Path(zf, zipFilePath + "/") + files = [f.at[len(zipFilePath)+1:] for f in path.iterdir()] + return files + else: + return os.listdir(resource) + +def exists(resource: str): + if isAPWorld: + (zipFile, stem) = getZipFile() + with zipFile as zf: + if (stem in resource): + zipFilePath = resource[resource.index(stem + "/"):] + path = zipfile.Path(zf, zipFilePath) + return path.exists() + else: + return False + else: + return os.path.exists(resource) def isStdPreset(preset): return preset in ['newbie', 'casual', 'regular', 'veteran', 'expert', 'master', 'samus', 'solution', 'Season_Races', 'SMRAT2021'] @@ -253,7 +304,7 @@ class PresetLoader(object): class PresetLoaderJson(PresetLoader): # when called from the test suite def __init__(self, jsonFileName): - with open(jsonFileName) as jsonFile: + with openFile(jsonFileName) as jsonFile: self.params = json.load(jsonFile) super(PresetLoaderJson, self).__init__() @@ -264,7 +315,7 @@ class PresetLoaderDict(PresetLoader): super(PresetLoaderDict, self).__init__() def getDefaultMultiValues(): - from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils + from ..graph.graph_utils import GraphUtils defaultMultiValues = { 'startLocation': GraphUtils.getStartAccessPointNames(), 'majorsSplit': ['Full', 'FullWithHUD', 'Major', 'Chozo', 'Scavenger'], diff --git a/worlds/smz3/Client.py b/worlds/smz3/Client.py index c942c66c71..687a43b00f 100644 --- a/worlds/smz3/Client.py +++ b/worlds/smz3/Client.py @@ -86,8 +86,8 @@ class SMZ3SNIClient(SNIClient): recv_index += 1 snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0x680, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) - from worlds.smz3.TotalSMZ3.Location import locations_start_id - from worlds.smz3 import convertLocSMZ3IDToAPID + from .TotalSMZ3.Location import locations_start_id + from . import convertLocSMZ3IDToAPID location_id = locations_start_id + convertLocSMZ3IDToAPID(item_index) ctx.locations_checked.add(location_id) @@ -101,7 +101,7 @@ class SMZ3SNIClient(SNIClient): item_out_ptr = data[2] | (data[3] << 8) - from worlds.smz3.TotalSMZ3.Item import items_start_id + from .TotalSMZ3.Item import items_start_id if item_out_ptr < len(ctx.items_received): item = ctx.items_received[item_out_ptr] item_id = item.item - items_start_id diff --git a/worlds/smz3/TotalSMZ3/Item.py b/worlds/smz3/TotalSMZ3/Item.py index 2aced8bfac..b4fc9d5925 100644 --- a/worlds/smz3/TotalSMZ3/Item.py +++ b/worlds/smz3/TotalSMZ3/Item.py @@ -3,7 +3,7 @@ import re import copy from typing import List -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic +from .Config import Config, SMLogic class ItemType(Enum): Nothing = 0 @@ -787,15 +787,15 @@ class Progression: return self.Flute and self.CanLiftHeavy() def CanAccessMaridiaPortal(self, world): - import worlds.smz3.TotalSMZ3.Region + from .Region import RewardType if (world.Config.SMLogic == SMLogic.Normal): return self.MoonPearl and self.Flippers and \ self.Gravity and self.Morph and \ - (world.CanAcquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) + (world.CanAcquire(self, RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) else: return self.MoonPearl and self.Flippers and \ (self.CanSpringBallJump() or self.HiJump or self.Gravity) and self.Morph and \ - (world.CanAcquire(self, worlds.smz3.TotalSMZ3.Region.RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) + (world.CanAcquire(self, RewardType.Agahnim) or self.Hammer and self.CanLiftLight() or self.CanLiftHeavy()) # Start of AP integration items_start_id = 84000 diff --git a/worlds/smz3/TotalSMZ3/Location.py b/worlds/smz3/TotalSMZ3/Location.py index 377d1a3338..cb18ddc708 100644 --- a/worlds/smz3/TotalSMZ3/Location.py +++ b/worlds/smz3/TotalSMZ3/Location.py @@ -1,8 +1,8 @@ from enum import Enum from typing import List, Callable -from worlds.smz3.TotalSMZ3.Item import Progression -import worlds.smz3.TotalSMZ3.Region as Region -import worlds.smz3.TotalSMZ3.World as World +from .Item import Progression +from . import Region +from . import World class LocationType(Enum): Regular = 0 diff --git a/worlds/smz3/TotalSMZ3/Patch.py b/worlds/smz3/TotalSMZ3/Patch.py index 2b8d278d49..7924386af1 100644 --- a/worlds/smz3/TotalSMZ3/Patch.py +++ b/worlds/smz3/TotalSMZ3/Patch.py @@ -4,30 +4,30 @@ from typing import Any, Callable, List, Sequence import random import typing from BaseClasses import Location -from worlds.smz3.TotalSMZ3.Item import Item, ItemType -from worlds.smz3.TotalSMZ3.Location import LocationType -from worlds.smz3.TotalSMZ3.Region import IReward, RewardType, SMRegion, Z3Region -from worlds.smz3.TotalSMZ3.Regions.Zelda.EasternPalace import EasternPalace -from worlds.smz3.TotalSMZ3.Regions.Zelda.DesertPalace import DesertPalace -from worlds.smz3.TotalSMZ3.Regions.Zelda.TowerOfHera import TowerOfHera -from worlds.smz3.TotalSMZ3.Regions.Zelda.PalaceOfDarkness import PalaceOfDarkness -from worlds.smz3.TotalSMZ3.Regions.Zelda.SwampPalace import SwampPalace -from worlds.smz3.TotalSMZ3.Regions.Zelda.SkullWoods import SkullWoods -from worlds.smz3.TotalSMZ3.Regions.Zelda.ThievesTown import ThievesTown -from worlds.smz3.TotalSMZ3.Regions.Zelda.IcePalace import IcePalace -from worlds.smz3.TotalSMZ3.Regions.Zelda.MiseryMire import MiseryMire -from worlds.smz3.TotalSMZ3.Regions.Zelda.TurtleRock import TurtleRock -from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Brinstar.Kraid import Kraid -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.WreckedShip import WreckedShip -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Maridia.Inner import Inner -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.NorfairLower.East import East -from worlds.smz3.TotalSMZ3.Text.StringTable import StringTable +from .Item import Item, ItemType +from .Location import LocationType +from .Region import IReward, RewardType, SMRegion, Z3Region +from .Regions.Zelda.EasternPalace import EasternPalace +from .Regions.Zelda.DesertPalace import DesertPalace +from .Regions.Zelda.TowerOfHera import TowerOfHera +from .Regions.Zelda.PalaceOfDarkness import PalaceOfDarkness +from .Regions.Zelda.SwampPalace import SwampPalace +from .Regions.Zelda.SkullWoods import SkullWoods +from .Regions.Zelda.ThievesTown import ThievesTown +from .Regions.Zelda.IcePalace import IcePalace +from .Regions.Zelda.MiseryMire import MiseryMire +from .Regions.Zelda.TurtleRock import TurtleRock +from .Regions.Zelda.GanonsTower import GanonsTower +from .Regions.SuperMetroid.Brinstar.Kraid import Kraid +from .Regions.SuperMetroid.WreckedShip import WreckedShip +from .Regions.SuperMetroid.Maridia.Inner import Inner +from .Regions.SuperMetroid.NorfairLower.East import East +from .Text.StringTable import StringTable -from worlds.smz3.TotalSMZ3.World import World -from worlds.smz3.TotalSMZ3.Config import Config, OpenTourian, Goal -from worlds.smz3.TotalSMZ3.Text.Texts import Texts -from worlds.smz3.TotalSMZ3.Text.Dialog import Dialog +from .World import World +from .Config import Config, OpenTourian, Goal +from .Text.Texts import Texts +from .Text.Dialog import Dialog class KeycardPlaque: Level1 = 0xe0 @@ -147,7 +147,7 @@ class Patch: return {patch[0]:patch[1] for patch in self.patches} def WriteMedallions(self): - from worlds.smz3.TotalSMZ3.WorldState import Medallion + from .WorldState import Medallion turtleRock = next(region for region in self.myWorld.Regions if isinstance(region, TurtleRock)) miseryMire = next(region for region in self.myWorld.Regions if isinstance(region, MiseryMire)) diff --git a/worlds/smz3/TotalSMZ3/Region.py b/worlds/smz3/TotalSMZ3/Region.py index 00e209ce45..2274c556a9 100644 --- a/worlds/smz3/TotalSMZ3/Region.py +++ b/worlds/smz3/TotalSMZ3/Region.py @@ -1,7 +1,6 @@ from enum import Enum from typing import Dict, List -from worlds.smz3.TotalSMZ3.Config import * -from worlds.smz3.TotalSMZ3.Item import Item, ItemType +from .Config import * class RewardType(Enum): Null = 0 @@ -28,7 +27,7 @@ class IMedallionAccess: Medallion = None class Region: - import worlds.smz3.TotalSMZ3.Location as Location + from . import Location Name: str Area: str diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Blue.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Blue.py index 0b3d1f8b71..eac3aaf6f9 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Blue.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Blue.py @@ -1,6 +1,6 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType +from ....Region import SMRegion +from ....Config import Config, SMLogic +from ....Location import Location, LocationType class Blue(SMRegion): Name = "Brinstar Blue" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Green.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Green.py index 380431df5a..8aa555ce80 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Green.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Green.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import SMRegion +from ....Config import Config, SMLogic +from ....Location import Location, LocationType +from ....Item import Progression class Green(SMRegion): Name = "Brinstar Green" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py index 2b99081dd3..82ba144b63 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Kraid.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion, IReward, RewardType -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import SMRegion, IReward, RewardType +from ....Config import Config +from ....Location import Location, LocationType +from ....Item import Progression class Kraid(SMRegion, IReward): Name = "Brinstar Kraid" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py index bb1036fb81..4a727ea493 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Pink.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import SMRegion +from ....Config import Config, SMLogic +from ....Location import Location, LocationType +from ....Item import Progression class Pink(SMRegion): Name = "Brinstar Pink" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Red.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Red.py index cca64e4c3b..1a0dd1fb8c 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Red.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Brinstar/Red.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import SMRegion +from ....Config import Config, SMLogic +from ....Location import Location, LocationType +from ....Item import Progression class Red(SMRegion): Name = "Brinstar Red" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/Central.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/Central.py index 14c8b08fdc..d673373b60 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/Central.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/Central.py @@ -1,6 +1,6 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType +from ....Region import SMRegion +from ....Config import Config, SMLogic +from ....Location import Location, LocationType class Central(SMRegion): Name = "Crateria Central" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py index 72d10a4496..44834f2178 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/East.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import SMRegion +from ....Config import Config, SMLogic +from ....Location import Location, LocationType +from ....Item import Progression class East(SMRegion): Name = "Crateria East" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/West.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/West.py index 5554b08871..9a4d6019da 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/West.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Crateria/West.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import SMRegion +from ....Config import Config, SMLogic +from ....Location import Location, LocationType +from ....Item import Progression class West(SMRegion): Name = "Crateria West" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py index 7de0798bae..c129b87685 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Inner.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion, IReward, RewardType -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import SMRegion, IReward, RewardType +from ....Config import Config, SMLogic +from ....Location import Location, LocationType +from ....Item import Progression class Inner(SMRegion, IReward): Name = "Maridia Inner" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Outer.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Outer.py index 64907452a0..0766566a1e 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Outer.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/Maridia/Outer.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import SMRegion +from ....Config import Config, SMLogic +from ....Location import Location, LocationType +from ....Item import Progression class Outer(SMRegion): Name = "Maridia Outer" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py index f1a325a12b..6e070f7d58 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/East.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion, IReward, RewardType -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import SMRegion, IReward, RewardType +from ....Config import Config, SMLogic +from ....Location import Location, LocationType +from ....Item import Progression class East(SMRegion, IReward): Name = "Norfair Lower East" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py index 4e44d28ca5..ab00598880 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairLower/West.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import SMRegion +from ....Config import Config, SMLogic +from ....Location import Location, LocationType +from ....Item import Progression class West(SMRegion): Name = "Norfair Lower West" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py index b38bbe70c6..792bd42f8e 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/Crocomire.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import SMRegion +from ....Config import Config, SMLogic +from ....Location import Location, LocationType +from ....Item import Progression class Crocomire(SMRegion): Name = "Norfair Upper Crocomire" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/East.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/East.py index a2dc2b1be8..1c27e7d3eb 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/East.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/East.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import SMRegion +from ....Config import Config, SMLogic +from ....Location import Location, LocationType +from ....Item import Progression class East(SMRegion): Name = "Norfair Upper East" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/West.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/West.py index 5dcafdf48c..45add5b339 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/West.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/NorfairUpper/West.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import SMRegion +from ....Config import Config, SMLogic +from ....Location import Location, LocationType +from ....Item import Progression class West(SMRegion): Name = "Norfair Upper West" diff --git a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py index e83c6f539c..d4624f53b8 100644 --- a/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py +++ b/worlds/smz3/TotalSMZ3/Regions/SuperMetroid/WreckedShip.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import SMRegion, IReward, RewardType -from worlds.smz3.TotalSMZ3.Config import Config, SMLogic -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ...Region import SMRegion, IReward, RewardType +from ...Config import Config, SMLogic +from ...Location import Location, LocationType +from ...Item import Progression class WreckedShip(SMRegion, IReward): Name = "Wrecked Ship" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/CastleTower.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/CastleTower.py index 8a1eba58a6..bca50c00fa 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/CastleTower.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/CastleTower.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType, IReward -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression, ItemType +from ...Region import Z3Region, RewardType, IReward +from ...Config import Config +from ...Location import Location, LocationType +from ...Item import Progression, ItemType class CastleTower(Z3Region, IReward): Name = "Castle Tower" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/DeathMountain/East.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/DeathMountain/East.py index be9eb1ba84..b860d3c737 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/DeathMountain/East.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/DeathMountain/East.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import Z3Region -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from .....Region import Z3Region +from .....Config import Config +from .....Location import Location, LocationType +from .....Item import Progression class East(Z3Region): Name = "Dark World Death Mountain East" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/DeathMountain/West.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/DeathMountain/West.py index dfe3de55a7..0a0fa5deee 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/DeathMountain/West.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/DeathMountain/West.py @@ -1,6 +1,6 @@ -from worlds.smz3.TotalSMZ3.Region import Z3Region -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType +from .....Region import Z3Region +from .....Config import Config +from .....Location import Location, LocationType class West(Z3Region): Name = "Dark World Death Mountain West" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/Mire.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/Mire.py index d7d7345afe..01f6d1703f 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/Mire.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/Mire.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import Z3Region -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import Z3Region +from ....Config import Config +from ....Location import Location, LocationType +from ....Item import Progression class Mire(Z3Region): Name = "Dark World Mire" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py index 7ca34cb031..2f23a38120 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthEast.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import Z3Region, RewardType +from ....Config import Config +from ....Location import Location, LocationType +from ....Item import Progression class NorthEast(Z3Region): Name = "Dark World North East" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py index 28a318e80d..73bea10e32 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/NorthWest.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import Z3Region, RewardType +from ....Config import Config +from ....Location import Location, LocationType +from ....Item import Progression class NorthWest(Z3Region): Name = "Dark World North West" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py index 14f4515c6d..417c668af7 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DarkWorld/South.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from ....Region import Z3Region, RewardType +from ....Config import Config +from ....Location import Location, LocationType +from ....Item import Progression class South(Z3Region): Name = "Dark World South" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/DesertPalace.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/DesertPalace.py index 117f2166f7..0782d65a8c 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/DesertPalace.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/DesertPalace.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType, IReward -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import ItemType, Progression +from ...Region import Z3Region, RewardType, IReward +from ...Config import Config +from ...Location import Location, LocationType +from ...Item import ItemType, Progression class DesertPalace(Z3Region, IReward): Name = "Desert Palace" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/EasternPalace.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/EasternPalace.py index 77ded23960..74dbdb6419 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/EasternPalace.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/EasternPalace.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType, IReward -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression, ItemType +from ...Region import Z3Region, RewardType, IReward +from ...Config import Config +from ...Location import Location, LocationType +from ...Item import Progression, ItemType class EasternPalace(Z3Region, IReward): Name = "Eastern Palace" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py index c1a6d79f6e..fe81069164 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/GanonsTower.py @@ -1,8 +1,8 @@ from typing import List -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType -from worlds.smz3.TotalSMZ3.Config import Config, GameMode, KeyShuffle -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Item, Progression, ItemType +from ...Region import Z3Region, RewardType +from ...Config import Config, GameMode, KeyShuffle +from ...Location import Location, LocationType +from ...Item import Item, Progression, ItemType class GanonsTower(Z3Region): Name = "Ganon's Tower" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/HyruleCastle.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/HyruleCastle.py index 20a68a3f4d..498c412418 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/HyruleCastle.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/HyruleCastle.py @@ -1,8 +1,8 @@ from typing import List -from worlds.smz3.TotalSMZ3.Region import Z3Region -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import ItemType +from ...Region import Z3Region +from ...Config import Config +from ...Location import Location, LocationType +from ...Item import ItemType class HyruleCastle(Z3Region): Name = "Hyrule Castle" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py index 9b16a08b4d..db29e0dd2e 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/IcePalace.py @@ -1,8 +1,8 @@ from typing import List -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType, IReward -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression, ItemType +from ...Region import Z3Region, RewardType, IReward +from ...Config import Config +from ...Location import Location, LocationType +from ...Item import Progression, ItemType class IcePalace(Z3Region, IReward): Name = "Ice Palace" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/DeathMountain/East.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/DeathMountain/East.py index 10cd333a94..5975b1dbf0 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/DeathMountain/East.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/DeathMountain/East.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import Z3Region -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from .....Region import Z3Region +from .....Config import Config +from .....Location import Location, LocationType +from .....Item import Progression class East(Z3Region): Name = "Light World Death Mountain East" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/DeathMountain/West.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/DeathMountain/West.py index 47f20f5e56..c3fadd5289 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/DeathMountain/West.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/DeathMountain/West.py @@ -1,7 +1,7 @@ -from worlds.smz3.TotalSMZ3.Region import Z3Region -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression +from .....Region import Z3Region +from .....Config import Config +from .....Location import Location, LocationType +from .....Item import Progression class West(Z3Region): Name = "Light World Death Mountain West" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py index c111b07dfd..6991bc9d49 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthEast.py @@ -1,6 +1,6 @@ -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType +from ....Region import Z3Region, RewardType +from ....Config import Config +from ....Location import Location, LocationType class NorthEast(Z3Region): Name = "Light World North East" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py index 46f830dc8b..75ec33f7b1 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/NorthWest.py @@ -1,6 +1,6 @@ -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType +from ....Region import Z3Region, RewardType +from ....Config import Config +from ....Location import Location, LocationType class NorthWest(Z3Region): Name = "Light World North West" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/South.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/South.py index 50f5cee78d..70431b876b 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/South.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/LightWorld/South.py @@ -1,6 +1,6 @@ -from worlds.smz3.TotalSMZ3.Region import Z3Region -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType +from ....Region import Z3Region +from ....Config import Config +from ....Location import Location, LocationType class South(Z3Region): Name = "Light World South" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py index b1746184d3..c008fbb114 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/MiseryMire.py @@ -1,8 +1,8 @@ from typing import List -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType, IReward, IMedallionAccess -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression, ItemType +from ...Region import Z3Region, RewardType, IReward, IMedallionAccess +from ...Config import Config +from ...Location import Location, LocationType +from ...Item import Progression, ItemType class MiseryMire(Z3Region, IReward, IMedallionAccess): Name = "Misery Mire" @@ -35,7 +35,7 @@ class MiseryMire(Z3Region, IReward, IMedallionAccess): # // Need "CanKillManyEnemies" if implementing swordless def CanEnter(self, items: Progression): - from worlds.smz3.TotalSMZ3.WorldState import Medallion + from ...WorldState import Medallion return (items.Bombos if self.Medallion == Medallion.Bombos else ( items.Ether if self.Medallion == Medallion.Ether else items.Quake)) and items.Sword and \ items.MoonPearl and (items.Boots or items.Hookshot) and \ diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/PalaceOfDarkness.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/PalaceOfDarkness.py index 9184fd28fc..31e5c5fdf3 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/PalaceOfDarkness.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/PalaceOfDarkness.py @@ -1,8 +1,8 @@ from typing import List -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType, IReward -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression, ItemType +from ...Region import Z3Region, RewardType, IReward +from ...Config import Config +from ...Location import Location, LocationType +from ...Item import Progression, ItemType class PalaceOfDarkness(Z3Region, IReward): Name = "Palace of Darkness" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/SkullWoods.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/SkullWoods.py index f991fce339..b84c03f55d 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/SkullWoods.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/SkullWoods.py @@ -1,8 +1,8 @@ from typing import List -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType, IReward -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression, ItemType +from ...Region import Z3Region, RewardType, IReward +from ...Config import Config +from ...Location import Location, LocationType +from ...Item import Progression, ItemType class SkullWoods(Z3Region, IReward): Name = "Skull Woods" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py index 27b5a1db43..35e8863339 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/SwampPalace.py @@ -1,8 +1,8 @@ from typing import List -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType, IReward -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression, ItemType +from ...Region import Z3Region, RewardType, IReward +from ...Config import Config +from ...Location import Location, LocationType +from ...Item import Progression, ItemType class SwampPalace(Z3Region, IReward): Name = "Swamp Palace" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/ThievesTown.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/ThievesTown.py index ef68b953c0..003f1f1777 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/ThievesTown.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/ThievesTown.py @@ -1,8 +1,8 @@ from typing import List -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType, IReward -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression, ItemType +from ...Region import Z3Region, RewardType, IReward +from ...Config import Config +from ...Location import Location, LocationType +from ...Item import Progression, ItemType class ThievesTown(Z3Region, IReward): Name = "Thieves' Town" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/TowerOfHera.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/TowerOfHera.py index 45743ac39e..521c6c7a8d 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/TowerOfHera.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/TowerOfHera.py @@ -1,8 +1,8 @@ from typing import List -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType, IReward -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression, ItemType +from ...Region import Z3Region, RewardType, IReward +from ...Config import Config +from ...Location import Location, LocationType +from ...Item import Progression, ItemType class TowerOfHera(Z3Region, IReward): Name = "Tower of Hera" diff --git a/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py b/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py index 45546e9e99..e797bddad2 100644 --- a/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py +++ b/worlds/smz3/TotalSMZ3/Regions/Zelda/TurtleRock.py @@ -1,8 +1,8 @@ from typing import List -from worlds.smz3.TotalSMZ3.Region import Z3Region, RewardType, IReward, IMedallionAccess -from worlds.smz3.TotalSMZ3.Config import Config -from worlds.smz3.TotalSMZ3.Location import Location, LocationType -from worlds.smz3.TotalSMZ3.Item import Progression, ItemType +from ...Region import Z3Region, RewardType, IReward, IMedallionAccess +from ...Config import Config +from ...Location import Location, LocationType +from ...Item import Progression, ItemType class TurtleRock(Z3Region, IReward, IMedallionAccess): Name = "Turtle Rock" @@ -47,7 +47,7 @@ class TurtleRock(Z3Region, IReward, IMedallionAccess): return items.Firerod and items.Icerod def CanEnter(self, items: Progression): - from worlds.smz3.TotalSMZ3.WorldState import Medallion + from ...WorldState import Medallion return (items.Bombos if self.Medallion == Medallion.Bombos else ( items.Ether if self.Medallion == Medallion.Ether else items.Quake)) and items.Sword and \ items.MoonPearl and items.CanLiftHeavy() and items.Hammer and items.Somaria and \ diff --git a/worlds/smz3/TotalSMZ3/Text/Dialog.py b/worlds/smz3/TotalSMZ3/Text/Dialog.py index 92e034af67..553eb016d5 100644 --- a/worlds/smz3/TotalSMZ3/Text/Dialog.py +++ b/worlds/smz3/TotalSMZ3/Text/Dialog.py @@ -158,7 +158,7 @@ class Dialog: value = Dialog.letters.get(c, None) return value if value else [ 0xFF ] - #region letter bytes lookup + #regions letter bytes lookup letters = { ' ' : [ 0x4F ], diff --git a/worlds/smz3/TotalSMZ3/Text/StringTable.py b/worlds/smz3/TotalSMZ3/Text/StringTable.py index 13f3f5edb5..ed5f794f27 100644 --- a/worlds/smz3/TotalSMZ3/Text/StringTable.py +++ b/worlds/smz3/TotalSMZ3/Text/StringTable.py @@ -1,15 +1,15 @@  from typing import Any, List import copy -from worlds.smz3.TotalSMZ3.Text.Dialog import Dialog -from worlds.smz3.TotalSMZ3.Text.Texts import text_folder +from ..Text.Dialog import Dialog +from ..Text.Texts import openFile from Utils import unsafe_parse_yaml class StringTable: @staticmethod def ParseEntries(resource: str): - with open(resource, 'rb') as f: + with openFile(resource, 'rb') as f: yaml = str(f.read(), "utf-8") content = unsafe_parse_yaml(yaml) @@ -23,7 +23,7 @@ class StringTable: else: raise Exception(f"Did not expect an object of type {type(value)}") return result - template = ParseEntries.__func__(text_folder + "/Scripts/StringTable.yaml") + template = ParseEntries.__func__("smz3/TotalSMZ3/Text/Scripts/StringTable.yaml") def __init__(self): self.entries = copy.deepcopy(StringTable.template) diff --git a/worlds/smz3/TotalSMZ3/Text/Texts.py b/worlds/smz3/TotalSMZ3/Text/Texts.py index dfaeee06da..377baf016c 100644 --- a/worlds/smz3/TotalSMZ3/Text/Texts.py +++ b/worlds/smz3/TotalSMZ3/Text/Texts.py @@ -1,30 +1,49 @@ -from typing import Any, List -from worlds.smz3.TotalSMZ3.Region import Region -from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower -from worlds.smz3.TotalSMZ3.Item import Item, ItemType +import io +from pathlib import Path +import sys +from typing import Any, List +import zipfile +from ..Region import Region +from ..Regions.Zelda.GanonsTower import GanonsTower +from ..Item import Item, ItemType from Utils import unsafe_parse_yaml import random import os -text_folder = os.path.dirname(__file__) +text_folder = Path(__file__).parents[3] + +def openFile(resource: str, mode: str = "r", encoding: None = None): + filename = sys.modules[__name__].__file__ + apworldExt = ".apworld" + game = "smz3/" + if apworldExt in filename: + zip_path = Path(filename[:filename.index(apworldExt) + len(apworldExt)]) + with zipfile.ZipFile(zip_path) as zf: + zipFilePath = resource[resource.index(game):] + if mode == 'rb': + return zf.open(zipFilePath, 'r') + else: + return io.TextIOWrapper(zf.open(zipFilePath, 'r'), encoding) + else: + return open(os.path.join(text_folder, resource), mode) class Texts: @staticmethod def ParseYamlScripts(resource: str): - with open(resource, 'rb') as f: + with openFile(resource, 'rb') as f: yaml = str(f.read(), "utf-8") return unsafe_parse_yaml(yaml) @staticmethod def ParseTextScript(resource: str): - with open(resource, 'r', encoding="utf-8-sig") as file: + with openFile(resource, 'r') as file: return [text.rstrip('\n') for text in file.read().replace("\r", "").split("---\n") if text] - scripts: Any = ParseYamlScripts.__func__(text_folder + "/Scripts/General.yaml") - blind: List[str] = ParseTextScript.__func__(text_folder + "/Scripts/Blind.txt") - ganon: List[str] = ParseTextScript.__func__(text_folder + "/Scripts/Ganon.txt") - tavernMan: List[str] = ParseTextScript.__func__(text_folder + "/Scripts/TavernMan.txt") - triforceRoom: List[str] = ParseTextScript.__func__(text_folder + "/Scripts/TriforceRoom.txt") + scripts: Any = ParseYamlScripts.__func__("smz3/TotalSMZ3/Text/Scripts/General.yaml") + blind: List[str] = ParseTextScript.__func__("smz3/TotalSMZ3/Text/Scripts/Blind.txt") + ganon: List[str] = ParseTextScript.__func__("smz3/TotalSMZ3/Text/Scripts/Ganon.txt") + tavernMan: List[str] = ParseTextScript.__func__("smz3/TotalSMZ3/Text/Scripts/TavernMan.txt") + triforceRoom: List[str] = ParseTextScript.__func__("smz3/TotalSMZ3/Text/Scripts/TriforceRoom.txt") @staticmethod def SahasrahlaReveal(dungeon: Region): diff --git a/worlds/smz3/TotalSMZ3/World.py b/worlds/smz3/TotalSMZ3/World.py index 722d5858e6..60249760c9 100644 --- a/worlds/smz3/TotalSMZ3/World.py +++ b/worlds/smz3/TotalSMZ3/World.py @@ -1,51 +1,51 @@ from typing import Dict, List import random -import worlds.smz3.TotalSMZ3.Region as Region -import worlds.smz3.TotalSMZ3.Config as Config -import worlds.smz3.TotalSMZ3.Item as Item -import worlds.smz3.TotalSMZ3.Location as Location +from . import Region +from . import Config +from . import Item +from . import Location -from worlds.smz3.TotalSMZ3.Regions.Zelda.CastleTower import CastleTower -from worlds.smz3.TotalSMZ3.Regions.Zelda.EasternPalace import EasternPalace -from worlds.smz3.TotalSMZ3.Regions.Zelda.DesertPalace import DesertPalace -from worlds.smz3.TotalSMZ3.Regions.Zelda.TowerOfHera import TowerOfHera -from worlds.smz3.TotalSMZ3.Regions.Zelda.PalaceOfDarkness import PalaceOfDarkness -from worlds.smz3.TotalSMZ3.Regions.Zelda.SwampPalace import SwampPalace -from worlds.smz3.TotalSMZ3.Regions.Zelda.SkullWoods import SkullWoods -from worlds.smz3.TotalSMZ3.Regions.Zelda.ThievesTown import ThievesTown -from worlds.smz3.TotalSMZ3.Regions.Zelda.IcePalace import IcePalace -from worlds.smz3.TotalSMZ3.Regions.Zelda.MiseryMire import MiseryMire -from worlds.smz3.TotalSMZ3.Regions.Zelda.TurtleRock import TurtleRock -from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower -from worlds.smz3.TotalSMZ3.Regions.Zelda.LightWorld.DeathMountain.West import West as LightWorldDeathMountainWest -from worlds.smz3.TotalSMZ3.Regions.Zelda.LightWorld.DeathMountain.East import East as LightWorldDeathMountainEast -from worlds.smz3.TotalSMZ3.Regions.Zelda.LightWorld.NorthWest import NorthWest as LightWorldNorthWest -from worlds.smz3.TotalSMZ3.Regions.Zelda.LightWorld.NorthEast import NorthEast as LightWorldNorthEast -from worlds.smz3.TotalSMZ3.Regions.Zelda.LightWorld.South import South as LightWorldSouth -from worlds.smz3.TotalSMZ3.Regions.Zelda.HyruleCastle import HyruleCastle -from worlds.smz3.TotalSMZ3.Regions.Zelda.DarkWorld.DeathMountain.West import West as DarkWorldDeathMountainWest -from worlds.smz3.TotalSMZ3.Regions.Zelda.DarkWorld.DeathMountain.East import East as DarkWorldDeathMountainEast -from worlds.smz3.TotalSMZ3.Regions.Zelda.DarkWorld.NorthWest import NorthWest as DarkWorldNorthWest -from worlds.smz3.TotalSMZ3.Regions.Zelda.DarkWorld.NorthEast import NorthEast as DarkWorldNorthEast -from worlds.smz3.TotalSMZ3.Regions.Zelda.DarkWorld.South import South as DarkWorldSouth -from worlds.smz3.TotalSMZ3.Regions.Zelda.DarkWorld.Mire import Mire as DarkWorldMire -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Crateria.Central import Central -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Crateria.West import West as CrateriaWest -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Crateria.East import East as CrateriaEast -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Brinstar.Blue import Blue -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Brinstar.Green import Green -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Brinstar.Kraid import Kraid -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Brinstar.Pink import Pink -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Brinstar.Red import Red -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Maridia.Outer import Outer -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.Maridia.Inner import Inner -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.NorfairUpper.West import West as NorfairUpperWest -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.NorfairUpper.East import East as NorfairUpperEast -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.NorfairUpper.Crocomire import Crocomire -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.NorfairLower.West import West as NorfairLowerWest -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.NorfairLower.East import East as NorfairLowerEast -from worlds.smz3.TotalSMZ3.Regions.SuperMetroid.WreckedShip import WreckedShip +from .Regions.Zelda.CastleTower import CastleTower +from .Regions.Zelda.EasternPalace import EasternPalace +from .Regions.Zelda.DesertPalace import DesertPalace +from .Regions.Zelda.TowerOfHera import TowerOfHera +from .Regions.Zelda.PalaceOfDarkness import PalaceOfDarkness +from .Regions.Zelda.SwampPalace import SwampPalace +from .Regions.Zelda.SkullWoods import SkullWoods +from .Regions.Zelda.ThievesTown import ThievesTown +from .Regions.Zelda.IcePalace import IcePalace +from .Regions.Zelda.MiseryMire import MiseryMire +from .Regions.Zelda.TurtleRock import TurtleRock +from .Regions.Zelda.GanonsTower import GanonsTower +from .Regions.Zelda.LightWorld.DeathMountain.West import West as LightWorldDeathMountainWest +from .Regions.Zelda.LightWorld.DeathMountain.East import East as LightWorldDeathMountainEast +from .Regions.Zelda.LightWorld.NorthWest import NorthWest as LightWorldNorthWest +from .Regions.Zelda.LightWorld.NorthEast import NorthEast as LightWorldNorthEast +from .Regions.Zelda.LightWorld.South import South as LightWorldSouth +from .Regions.Zelda.HyruleCastle import HyruleCastle +from .Regions.Zelda.DarkWorld.DeathMountain.West import West as DarkWorldDeathMountainWest +from .Regions.Zelda.DarkWorld.DeathMountain.East import East as DarkWorldDeathMountainEast +from .Regions.Zelda.DarkWorld.NorthWest import NorthWest as DarkWorldNorthWest +from .Regions.Zelda.DarkWorld.NorthEast import NorthEast as DarkWorldNorthEast +from .Regions.Zelda.DarkWorld.South import South as DarkWorldSouth +from .Regions.Zelda.DarkWorld.Mire import Mire as DarkWorldMire +from .Regions.SuperMetroid.Crateria.Central import Central +from .Regions.SuperMetroid.Crateria.West import West as CrateriaWest +from .Regions.SuperMetroid.Crateria.East import East as CrateriaEast +from .Regions.SuperMetroid.Brinstar.Blue import Blue +from .Regions.SuperMetroid.Brinstar.Green import Green +from .Regions.SuperMetroid.Brinstar.Kraid import Kraid +from .Regions.SuperMetroid.Brinstar.Pink import Pink +from .Regions.SuperMetroid.Brinstar.Red import Red +from .Regions.SuperMetroid.Maridia.Outer import Outer +from .Regions.SuperMetroid.Maridia.Inner import Inner +from .Regions.SuperMetroid.NorfairUpper.West import West as NorfairUpperWest +from .Regions.SuperMetroid.NorfairUpper.East import East as NorfairUpperEast +from .Regions.SuperMetroid.NorfairUpper.Crocomire import Crocomire +from .Regions.SuperMetroid.NorfairLower.West import West as NorfairLowerWest +from .Regions.SuperMetroid.NorfairLower.East import East as NorfairLowerEast +from .Regions.SuperMetroid.WreckedShip import WreckedShip class World: Locations: List[Location.Location] diff --git a/worlds/smz3/TotalSMZ3/WorldState.py b/worlds/smz3/TotalSMZ3/WorldState.py index c857b539d1..bbffffa107 100644 --- a/worlds/smz3/TotalSMZ3/WorldState.py +++ b/worlds/smz3/TotalSMZ3/WorldState.py @@ -2,9 +2,9 @@ from enum import Enum from typing import List from copy import copy -from worlds.smz3.TotalSMZ3.Patch import DropPrize -from worlds.smz3.TotalSMZ3.Region import RewardType -from worlds.smz3.TotalSMZ3.Config import OpenTower, GanonVulnerable, OpenTourian +from .Patch import DropPrize +from .Region import RewardType +from .Config import OpenTower, GanonVulnerable, OpenTourian class Medallion(Enum): Bombos = 0 diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 5e73f5db0c..969ad9dfcc 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -8,16 +8,17 @@ from typing import Dict, Set, TextIO from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, CollectionState, \ Tutorial from worlds.generic.Rules import set_rule -from worlds.smz3.TotalSMZ3.Item import ItemType -import worlds.smz3.TotalSMZ3.Item as TotalSMZ3Item -from worlds.smz3.TotalSMZ3.World import World as TotalSMZ3World -from worlds.smz3.TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower -from worlds.smz3.TotalSMZ3.Config import Config, GameMode, Goal, KeyShuffle, MorphLocation, SMLogic, SwordLocation, Z3Logic, OpenTower, GanonVulnerable, OpenTourian -from worlds.smz3.TotalSMZ3.Location import LocationType, locations_start_id, Location as TotalSMZ3Location -from worlds.smz3.TotalSMZ3.Patch import Patch as TotalSMZ3Patch, getWord, getWordArray -from worlds.smz3.TotalSMZ3.WorldState import WorldState -from worlds.smz3.TotalSMZ3.Region import IReward, IMedallionAccess -from ..AutoWorld import World, AutoLogicRegister, WebWorld +from .TotalSMZ3.Item import ItemType +from .TotalSMZ3 import Item as TotalSMZ3Item +from .TotalSMZ3.World import World as TotalSMZ3World +from .TotalSMZ3.Regions.Zelda.GanonsTower import GanonsTower +from .TotalSMZ3.Config import Config, GameMode, Goal, KeyShuffle, MorphLocation, SMLogic, SwordLocation, Z3Logic, OpenTower, GanonVulnerable, OpenTourian +from .TotalSMZ3.Location import LocationType, locations_start_id, Location as TotalSMZ3Location +from .TotalSMZ3.Patch import Patch as TotalSMZ3Patch, getWord, getWordArray +from .TotalSMZ3.WorldState import WorldState +from .TotalSMZ3.Region import IReward, IMedallionAccess +from .TotalSMZ3.Text.Texts import openFile +from worlds.AutoWorld import World, AutoLogicRegister, WebWorld from .Client import SMZ3SNIClient from .Rom import get_base_rom_bytes, SMZ3DeltaPatch from .ips import IPS_Patch @@ -272,7 +273,7 @@ class SMZ3World(World): idx = 0 offworldSprites = {} for fileName in itemSprites: - with open(world_folder + "/data/custom_sprite/" + fileName, 'rb') as stream: + with openFile(world_folder + "/data/custom_sprite/" + fileName, 'rb') as stream: buffer = bytearray(stream.read()) offworldSprites[0x04Eff2 + 10*((0x6B + 0x40) + idx)] = bytearray(getWordArray(itemSpritesAddress[idx])) + buffer[0:8] offworldSprites[0x090000 + itemSpritesAddress[idx]] = buffer[8:264] diff --git a/worlds/smz3/ips.py b/worlds/smz3/ips.py index 0da2bd491a..7e6516c974 100644 --- a/worlds/smz3/ips.py +++ b/worlds/smz3/ips.py @@ -1,5 +1,7 @@ import itertools +from .TotalSMZ3.Text.Texts import openFile + def range_union(ranges): ret = [] for rg in sorted([[r.start, r.stop] for r in ranges]): @@ -33,7 +35,7 @@ class IPS_Patch(object): @staticmethod def load(filename): loaded_patch = IPS_Patch() - with open(filename, 'rb') as file: + with openFile(filename, 'rb') as file: header = file.read(5) if header != b'PATCH': raise Exception('Not a valid IPS patch file!') From 6628e8c85dcb148fdcc17330678e6d1574892fc3 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sun, 9 Apr 2023 08:53:14 -0500 Subject: [PATCH 012/489] Docs: Add some more details to running from source doc (#1680) * make build tools step more obviously optional and give better directions * review commit --- docs/running from source.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/running from source.md b/docs/running from source.md index cb1a8fa50b..611ac04e1b 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -32,13 +32,11 @@ Recommended steps * Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads) * **Python 3.11 does not work currently** - * Download and install full Visual Studio from - [Visual Studio Downloads](https://visualstudio.microsoft.com/downloads/) - or an older "Build Tools for Visual Studio" from - [Visual Studio Older Downloads](https://visualstudio.microsoft.com/vs/older-downloads/). - - * Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details - * This step is optional. Pre-compiled modules are pinned on + * **Optional**: Download and install Visual Studio Build Tools from + [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/). + * Refer to [Windows Compilers on the python wiki](https://wiki.python.org/moin/WindowsCompilers) for details. + Generally, selecting the box for "Desktop Development with C++" will provide what you need. + * Build tools are not required if all modules are installed pre-compiled. Pre-compiled modules are pinned on [Discord in #archipelago-dev](https://discord.com/channels/731205301247803413/731214280439103580/905154456377757808) * It is recommended to use [PyCharm IDE](https://www.jetbrains.com/pycharm/) From 8e7bbb4ea8d1339d583cca5612de68e456fff38b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 9 Apr 2023 20:58:24 +0200 Subject: [PATCH 013/489] Factorio: flatten science pack curve (#1660) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- worlds/factorio/Locations.py | 12 ++- worlds/factorio/Mod.py | 2 +- worlds/factorio/Shapes.py | 2 +- worlds/factorio/Technologies.py | 28 ++++--- worlds/factorio/__init__.py | 133 ++++++++++++++++---------------- 5 files changed, 87 insertions(+), 90 deletions(-) diff --git a/worlds/factorio/Locations.py b/worlds/factorio/Locations.py index 1903e589be..f9db5f4a2b 100644 --- a/worlds/factorio/Locations.py +++ b/worlds/factorio/Locations.py @@ -1,32 +1,30 @@ from typing import Dict, List -from .Technologies import factorio_base_id, factorio_id +from .Technologies import factorio_base_id from .Options import MaxSciencePack boundary: int = 0xff total_locations: int = 0xff assert total_locations <= boundary -assert factorio_base_id != factorio_id def make_pools() -> Dict[str, List[str]]: pools: Dict[str, List[str]] = {} for i, pack in enumerate(MaxSciencePack.get_ordered_science_packs(), start=1): - max_needed: int = sum(divmod(boundary, i)) - scale: float = boundary / max_needed + max_needed: int = 0xff prefix: str = f"AP-{i}-" - pools[pack] = [prefix + hex(int(x * scale))[2:].upper().zfill(2) for x in range(1, max_needed + 1)] + pools[pack] = [prefix + hex(x)[2:].upper().zfill(2) for x in range(1, max_needed + 1)] return pools location_pools: Dict[str, List[str]] = make_pools() location_table: Dict[str, int] = {} -end_id: int = factorio_id +end_id: int = factorio_base_id for pool in location_pools.values(): location_table.update({name: ap_id for ap_id, name in enumerate(pool, start=end_id)}) end_id += len(pool) -assert end_id - len(location_table) == factorio_id +assert end_id - len(location_table) == factorio_base_id del pool diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index bce4bb2d16..4f1f3fd9d0 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -96,7 +96,7 @@ def generate_mod(world: "Factorio", output_directory: str): settings_template = template_env.get_template("settings.lua") # get data for templates locations = [(location, location.item) - for location in world.locations] + for location in world.science_locations] mod_name = f"AP-{multiworld.seed_name}-P{player}-{multiworld.get_file_safe_player_name(player)}" random = multiworld.per_slot_randoms[player] diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index 7ec6f07a86..84bcb06cab 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -24,7 +24,7 @@ def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Se player = factorio_world.player prerequisites: Dict["FactorioScienceLocation", Set["FactorioScienceLocation"]] = {} layout = world.tech_tree_layout[player].value - locations: List["FactorioScienceLocation"] = sorted(factorio_world.locations, key=lambda loc: loc.name) + locations: List["FactorioScienceLocation"] = sorted(factorio_world.science_locations, key=lambda loc: loc.name) world.random.shuffle(locations) if layout == TechTreeLayout.option_single: diff --git a/worlds/factorio/Technologies.py b/worlds/factorio/Technologies.py index 1c1939ee24..d68c6f2f77 100644 --- a/worlds/factorio/Technologies.py +++ b/worlds/factorio/Technologies.py @@ -4,6 +4,7 @@ import json import logging import os import string +import pkgutil from collections import Counter from concurrent.futures import ThreadPoolExecutor from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any @@ -11,7 +12,7 @@ from typing import Dict, Set, FrozenSet, Tuple, Union, List, Any import Utils from . import Options -factorio_id = factorio_base_id = 2 ** 17 +factorio_tech_id = factorio_base_id = 2 ** 17 # Factorio technologies are imported from a .json document in /data source_folder = os.path.join(os.path.dirname(__file__), "data") @@ -19,7 +20,6 @@ pool = ThreadPoolExecutor(1) def load_json_data(data_name: str) -> Union[List[str], Dict[str, Any]]: - import pkgutil return json.loads(pkgutil.get_data(__name__, "data/" + data_name + ".json").decode()) @@ -33,7 +33,9 @@ items_future = pool.submit(load_json_data, "items") tech_table: Dict[str, int] = {} technology_table: Dict[str, Technology] = {} -always = lambda state: True + +def always(state): + return True class FactorioElement: @@ -49,7 +51,6 @@ class FactorioElement: class Technology(FactorioElement): # maybe make subclass of Location? has_modifier: bool factorio_id: int - name: str ingredients: Set[str] progressive: Tuple[str] unlocks: Union[Set[str], bool] # bool case is for progressive technologies @@ -192,9 +193,9 @@ recipe_sources: Dict[str, Set[str]] = {} # recipe_name -> technology source # recipes and technologies can share names in Factorio for technology_name, data in sorted(techs_future.result().items()): current_ingredients = set(data["ingredients"]) - technology = Technology(technology_name, current_ingredients, factorio_id, + technology = Technology(technology_name, current_ingredients, factorio_tech_id, has_modifier=data["has_modifier"], unlocks=set(data["unlocks"])) - factorio_id += 1 + factorio_tech_id += 1 tech_table[technology_name] = technology.factorio_id technology_table[technology_name] = technology for recipe_name in technology.unlocks: @@ -391,17 +392,13 @@ progressive_rows["progressive-energy-shield"] = ("energy-shield-equipment", "ene progressive_rows["progressive-wall"] = ("stone-wall", "gate") progressive_rows["progressive-follower"] = ("defender", "distractor", "destroyer") progressive_rows["progressive-inserter"] = ("fast-inserter", "stack-inserter") - -sorted_rows = sorted(progressive_rows) -# to keep ID mappings the same. -# If there's a breaking change at some point, then this should be moved in with the sorted ordering progressive_rows["progressive-turret"] = ("gun-turret", "laser-turret") -sorted_rows.append("progressive-turret") progressive_rows["progressive-flamethrower"] = ("flamethrower",) # leaving out flammables, as they do nothing -sorted_rows.append("progressive-flamethrower") progressive_rows["progressive-personal-roboport-equipment"] = ("personal-roboport-equipment", "personal-roboport-mk2-equipment") -sorted_rows.append("progressive-personal-roboport-equipment") + +sorted_rows = sorted(progressive_rows) + # integrate into source_target_mapping: Dict[str, str] = { "progressive-braking-force": "progressive-train-network", @@ -421,8 +418,8 @@ progressive_technology_table: Dict[str, Technology] = {} for root in sorted_rows: progressive = progressive_rows[root] assert all(tech in tech_table for tech in progressive), "declared a progressive technology without base technology" - factorio_id += 1 - progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_id, + factorio_tech_id += 1 + progressive_technology = Technology(root, technology_table[progressive[0]].ingredients, factorio_tech_id, progressive, has_modifier=any(technology_table[tech].has_modifier for tech in progressive), unlocks=any(technology_table[tech].unlocks for tech in progressive)) @@ -504,3 +501,4 @@ valid_ingredients: Set[str] = stacking_items | fluids # cleanup async helpers pool.shutdown() del pool +del factorio_tech_id diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 567ab0bbda..269ec45566 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -6,6 +6,9 @@ import typing from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification from worlds.AutoWorld import World, WebWorld +from worlds.LauncherComponents import Component, components +from worlds.generic import Rules +from .Locations import location_pools, location_table from .Mod import generate_mod from .Options import factorio_options, MaxSciencePack, Silo, Satellite, TechTreeInformation, Goal, TechCostDistribution from .Shapes import get_shapes @@ -14,8 +17,6 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \ get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \ fluids, stacking_items, valid_ingredients, progressive_rows -from .Locations import location_pools, location_table -from worlds.LauncherComponents import Component, components components.append(Component("Factorio Client", "FactorioClient")) @@ -64,18 +65,19 @@ class Factorio(World): item_name_groups = { "Progressive": set(progressive_tech_table.keys()), } - data_version = 7 - required_client_version = (0, 3, 6) + data_version = 8 + required_client_version = (0, 4, 0) ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() tech_mix: int = 0 skip_silo: bool = False + science_locations: typing.List[FactorioScienceLocation] def __init__(self, world, player: int): super(Factorio, self).__init__(world, player) self.advancement_technologies = set() self.custom_recipes = {} - self.locations = [] + self.science_locations = [] generate_output = generate_mod @@ -115,18 +117,18 @@ class Factorio(World): raise Exception("Too many traps for too few locations. Either decrease the trap count, " f"or increase the location count (higher max science pack). (Player {self.player})") from e - self.locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) - for loc_name in location_names] + self.science_locations = [FactorioScienceLocation(player, loc_name, self.location_name_to_id[loc_name], nauvis) + for loc_name in location_names] distribution: TechCostDistribution = self.multiworld.tech_cost_distribution[self.player] min_cost = self.multiworld.min_tech_cost[self.player] max_cost = self.multiworld.max_tech_cost[self.player] if distribution == distribution.option_even: - rand_values = (random.randint(min_cost, max_cost) for _ in self.locations) + rand_values = (random.randint(min_cost, max_cost) for _ in self.science_locations) else: mode = {distribution.option_low: min_cost, distribution.option_middle: (min_cost+max_cost)//2, distribution.option_high: max_cost}[distribution.value] - rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.locations) + rand_values = (random.triangular(min_cost, max_cost, mode) for _ in self.science_locations) rand_values = sorted(rand_values) if self.multiworld.ramping_tech_costs[self.player]: def sorter(loc: FactorioScienceLocation): @@ -134,10 +136,10 @@ class Factorio(World): else: def sorter(loc: FactorioScienceLocation): return loc.rel_cost - for i, location in enumerate(sorted(self.locations, key=sorter)): + for i, location in enumerate(sorted(self.science_locations, key=sorter)): location.count = rand_values[i] del rand_values - nauvis.locations.extend(self.locations) + nauvis.locations.extend(self.science_locations) location = FactorioLocation(player, "Rocket Launch", None, nauvis) nauvis.locations.append(location) event = FactorioItem("Victory", ItemClassification.progression, None, player) @@ -154,73 +156,25 @@ class Factorio(World): def create_items(self) -> None: player = self.player + self.custom_technologies = self.set_custom_technologies() + self.set_custom_recipes() traps = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery", "Atomic Rocket") for trap_name in traps: self.multiworld.itempool.extend(self.create_item(f"{trap_name} Trap") for _ in range(getattr(self.multiworld, f"{trap_name.lower().replace(' ', '_')}_traps")[player])) - def set_rules(self): - world = self.multiworld - player = self.player - self.custom_technologies = self.set_custom_technologies() - self.set_custom_recipes() - shapes = get_shapes(self) - if world.logic[player] != 'nologic': - from worlds.generic import Rules - for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs(): - location = world.get_location(f"Automate {ingredient}", player) - - if self.multiworld.recipe_ingredients[self.player]: - custom_recipe = self.custom_recipes[ingredient] - - 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]) - else: - location.access_rule = lambda state, ingredient=ingredient: \ - all(state.has(technology.name, player) for technology in required_technologies[ingredient]) - - for location in self.locations: - Rules.set_rule(location, lambda state, ingredients=location.ingredients: - all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients)) - prerequisites = shapes.get(location) - if prerequisites: - Rules.add_rule(location, lambda state, locations= - prerequisites: all(state.can_reach(loc) for loc in locations)) - - silo_recipe = None - if self.multiworld.silo[self.player] == Silo.option_spawn: - silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \ - else next(iter(all_product_sources.get("rocket-silo"))) - part_recipe = self.custom_recipes["rocket-part"] - satellite_recipe = None - if self.multiworld.goal[self.player] == Goal.option_satellite: - satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \ - else next(iter(all_product_sources.get("satellite"))) - victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe) - if self.multiworld.silo[self.player] != Silo.option_spawn: - victory_tech_names.add("rocket-silo") - world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) - for technology in - victory_tech_names) - - world.completion_condition[player] = lambda state: state.has('Victory', player) - - def generate_basic(self): - player = self.player want_progressives = collections.defaultdict(lambda: self.multiworld.progressive[player]. want_progressives(self.multiworld.random)) - cost_sorted_locations = sorted(self.locations, key=lambda location: location.name) + cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name) special_index = {"automation": 0, "logistics": 1, "rocket-silo": -1} loc: FactorioScienceLocation if self.multiworld.tech_tree_information[player] == TechTreeInformation.option_full: # mark all locations as pre-hinted - for loc in self.locations: + for loc in self.science_locations: loc.revealed = True if self.skip_silo: removed = useless_technologies | {"rocket-silo"} @@ -244,13 +198,60 @@ class Factorio(World): loc.place_locked_item(tech_item) loc.revealed = True - map_basic_settings = self.multiworld.world_gen[player].value["basic"] + def set_rules(self): + world = self.multiworld + player = self.player + shapes = get_shapes(self) + + for ingredient in self.multiworld.max_science_pack[self.player].get_allowed_packs(): + location = world.get_location(f"Automate {ingredient}", player) + + if self.multiworld.recipe_ingredients[self.player]: + custom_recipe = self.custom_recipes[ingredient] + + 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]) + else: + location.access_rule = lambda state, ingredient=ingredient: \ + all(state.has(technology.name, player) for technology in required_technologies[ingredient]) + + for location in self.science_locations: + Rules.set_rule(location, lambda state, ingredients=location.ingredients: + all(state.has(f"Automated {ingredient}", player) for ingredient in ingredients)) + prerequisites = shapes.get(location) + if prerequisites: + Rules.add_rule(location, lambda state, locations= + prerequisites: all(state.can_reach(loc) for loc in locations)) + + silo_recipe = None + if self.multiworld.silo[self.player] == Silo.option_spawn: + silo_recipe = self.custom_recipes["rocket-silo"] if "rocket-silo" in self.custom_recipes \ + else next(iter(all_product_sources.get("rocket-silo"))) + part_recipe = self.custom_recipes["rocket-part"] + satellite_recipe = None + if self.multiworld.goal[self.player] == Goal.option_satellite: + satellite_recipe = self.custom_recipes["satellite"] if "satellite" in self.custom_recipes \ + else next(iter(all_product_sources.get("satellite"))) + victory_tech_names = get_rocket_requirements(silo_recipe, part_recipe, satellite_recipe) + if self.multiworld.silo[self.player] != Silo.option_spawn: + victory_tech_names.add("rocket-silo") + world.get_location("Rocket Launch", player).access_rule = lambda state: all(state.has(technology, player) + for technology in + victory_tech_names) + + world.completion_condition[player] = lambda state: state.has('Victory', player) + + def generate_basic(self): + map_basic_settings = self.multiworld.world_gen[self.player].value["basic"] if map_basic_settings.get("seed", None) is None: # allow seed 0 - map_basic_settings["seed"] = self.multiworld.per_slot_randoms[player].randint(0, 2 ** 32 - 1) # 32 bit uint + # 32 bit uint + map_basic_settings["seed"] = self.multiworld.per_slot_randoms[self.player].randint(0, 2 ** 32 - 1) start_location_hints: typing.Set[str] = self.multiworld.start_location_hints[self.player].value - for loc in self.locations: + for loc in self.science_locations: # show start_location_hints ingame if loc.name in start_location_hints: loc.revealed = True From bbef7a4cbcf514df01c77d45dc3c0b6f1b561339 Mon Sep 17 00:00:00 2001 From: axe-y <58866768+axe-y@users.noreply.github.com> Date: Sun, 9 Apr 2023 15:06:59 -0400 Subject: [PATCH 014/489] DLCQuest : implement new game (#1628) adding DLC Quest as a new game --- worlds/dlcquest/Items.py | 133 ++++++++ worlds/dlcquest/Locations.py | 79 +++++ worlds/dlcquest/Options.py | 114 +++++++ worlds/dlcquest/Regions.py | 325 ++++++++++++++++++ worlds/dlcquest/Rules.py | 370 +++++++++++++++++++++ worlds/dlcquest/Script/__init__.py | 0 worlds/dlcquest/Script/export_items.py | 26 ++ worlds/dlcquest/Script/export_locations.py | 20 ++ worlds/dlcquest/__init__.py | 82 +++++ worlds/dlcquest/data/items.csv | 48 +++ worlds/dlcquest/docs/en_DLCQuest.md | 51 +++ worlds/dlcquest/docs/setup_en.md | 57 ++++ 12 files changed, 1305 insertions(+) create mode 100644 worlds/dlcquest/Items.py create mode 100644 worlds/dlcquest/Locations.py create mode 100644 worlds/dlcquest/Options.py create mode 100644 worlds/dlcquest/Regions.py create mode 100644 worlds/dlcquest/Rules.py create mode 100644 worlds/dlcquest/Script/__init__.py create mode 100644 worlds/dlcquest/Script/export_items.py create mode 100644 worlds/dlcquest/Script/export_locations.py create mode 100644 worlds/dlcquest/__init__.py create mode 100644 worlds/dlcquest/data/items.csv create mode 100644 worlds/dlcquest/docs/en_DLCQuest.md create mode 100644 worlds/dlcquest/docs/setup_en.md diff --git a/worlds/dlcquest/Items.py b/worlds/dlcquest/Items.py new file mode 100644 index 0000000000..a740ecf549 --- /dev/null +++ b/worlds/dlcquest/Items.py @@ -0,0 +1,133 @@ +import csv +import enum +import math +from typing import Protocol, Union, Dict, List +from BaseClasses import Item, ItemClassification +from . import Options, data +from dataclasses import dataclass, field +from random import Random + + +class DLCQuestItem(Item): + game: str = "DLCQuest" + + +offset = 120_000 + + +class Group(enum.Enum): + DLC = enum.auto() + DLCQuest = enum.auto() + Freemium = enum.auto() + Item = enum.auto() + Coin = enum.auto() + Trap = enum.auto() + + +@dataclass(frozen=True) +class ItemData: + code_without_offset: offset + name: str + classification: ItemClassification + groups: set[Group] = field(default_factory=frozenset) + + def __post_init__(self): + if not isinstance(self.groups, frozenset): + super().__setattr__("groups", frozenset(self.groups)) + + @property + def code(self): + return offset + self.code_without_offset if self.code_without_offset is not None else None + + def has_any_group(self, *group: Group) -> bool: + groups = set(group) + return bool(groups.intersection(self.groups)) + + +def load_item_csv(): + try: + from importlib.resources import files + except ImportError: + from importlib_resources import files # noqa + + items = [] + with files(data).joinpath("items.csv").open() as file: + item_reader = csv.DictReader(file) + for item in item_reader: + id = int(item["id"]) if item["id"] else None + classification = ItemClassification[item["classification"]] + groups = {Group[group] for group in item["groups"].split(",") if group} + items.append(ItemData(id, item["name"], classification, groups)) + return items + + +all_items: List[ItemData] = load_item_csv() +item_table: Dict[str, ItemData] = {} +items_by_group: Dict[Group, List[ItemData]] = {} + + +def initialize_item_table(): + item_table.update({item.name: item for item in all_items}) + + +def initialize_groups(): + for item in all_items: + for group in item.groups: + item_group = items_by_group.get(group, list()) + item_group.append(item) + items_by_group[group] = item_group + + +initialize_item_table() +initialize_groups() + + +def create_trap_items(world, World_Options: Options.DLCQuestOptions, trap_needed: int, random: Random) -> List[Item]: + traps = [] + for i in range(trap_needed): + trap = random.choice(items_by_group[Group.Trap]) + traps.append(world.create_item(trap)) + + return traps + + +def create_items(world, World_Options: Options.DLCQuestOptions, locations_count: int, random: Random): + created_items = [] + if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[ + Options.Campaign] == Options.Campaign.option_both: + for item in items_by_group[Group.DLCQuest]: + if item.has_any_group(Group.DLC): + created_items.append(world.create_item(item)) + if item.has_any_group(Group.Item) and World_Options[ + Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + created_items.append(world.create_item(item)) + if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: + coin_bundle_needed = math.floor(825 / World_Options[Options.CoinSanityRange]) + for item in items_by_group[Group.DLCQuest]: + if item.has_any_group(Group.Coin): + for i in range(coin_bundle_needed): + created_items.append(world.create_item(item)) + if 825 % World_Options[Options.CoinSanityRange] != 0: + created_items.append(world.create_item(item)) + + if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[ + Options.Campaign] == Options.Campaign.option_both: + for item in items_by_group[Group.Freemium]: + if item.has_any_group(Group.DLC): + created_items.append(world.create_item(item)) + if item.has_any_group(Group.Item) and World_Options[ + Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + created_items.append(world.create_item(item)) + if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: + coin_bundle_needed = math.floor(889 / World_Options[Options.CoinSanityRange]) + for item in items_by_group[Group.Freemium]: + if item.has_any_group(Group.Coin): + for i in range(coin_bundle_needed): + created_items.append(world.create_item(item)) + if 889 % World_Options[Options.CoinSanityRange] != 0: + created_items.append(world.create_item(item)) + + trap_items = create_trap_items(world, World_Options, locations_count - len(created_items), random) + created_items += trap_items + + return created_items diff --git a/worlds/dlcquest/Locations.py b/worlds/dlcquest/Locations.py new file mode 100644 index 0000000000..08d37e7812 --- /dev/null +++ b/worlds/dlcquest/Locations.py @@ -0,0 +1,79 @@ +from BaseClasses import Location, MultiWorld +from . import Options + + +class DLCQuestLocation(Location): + game: str = "DLCQuest" + + +offset = 120_000 + +location_table = { + "Movement Pack": offset + 0, + "Animation Pack": offset + 1, + "Audio Pack": offset + 2, + "Pause Menu Pack": offset + 3, + "Time is Money Pack": offset + 4, + "Double Jump Pack": offset + 5, + "Pet Pack": offset + 6, + "Sexy Outfits Pack": offset + 7, + "Top Hat Pack": offset + 8, + "Map Pack": offset + 9, + "Gun Pack": offset + 10, + "The Zombie Pack": offset + 11, + "Night Map Pack": offset + 12, + "Psychological Warfare Pack": offset + 13, + "Armor for your Horse Pack": offset + 14, + "Finish the Fight Pack": offset + 15, + "Particles Pack": offset + 16, + "Day One Patch Pack": offset + 17, + "Checkpoint Pack": offset + 18, + "Incredibly Important Pack": offset + 19, + "Wall Jump Pack": offset + 20, + "Health Bar Pack": offset + 21, + "Parallax Pack": offset + 22, + "Harmless Plants Pack": offset + 23, + "Death of Comedy Pack": offset + 24, + "Canadian Dialog Pack": offset + 25, + "DLC NPC Pack": offset + 26, + "Cut Content Pack": offset + 27, + "Name Change Pack": offset + 28, + "Season Pass": offset + 29, + "High Definition Next Gen Pack": offset + 30, + "Increased HP Pack": offset + 31, + "Remove Ads Pack": offset + 32, + "Big Sword Pack": offset + 33, + "Really Big Sword Pack": offset + 34, + "Unfathomable Sword Pack": offset + 35, + "Pickaxe": offset + 36, + "Gun": offset + 37, + "Sword": offset + 38, + "Wooden Sword": offset + 39, + "Box of Various Supplies": offset + 40, + "Humble Indie Bindle": offset + 41, + "Double Jump Alcove Sheep": offset + 42, + "Double Jump Floating Sheep": offset + 43, + "Sexy Outfits Sheep": offset + 44, + "Forest High Sheep": offset + 45, + "Forest Low Sheep": offset + 46, + "Between Trees Sheep": offset + 47, + "Hole in the Wall Sheep": offset + 48, + "Shepherd Sheep": offset + 49, + "Top Hat Sheep": offset + 50, + "North West Ceiling Sheep": offset + 51, + "North West Alcove Sheep": offset + 52, + "West Cave Sheep": offset + 53, + "Cutscene Sheep": offset + 54, + "Not Exactly Noble": offset + 55, + "Story is Important": offset + 56, + "Nice Try": offset + 57, + "I Get That Reference!": offset + 58, +} + +for i in range(1, 826): + item_coin = f"DLC Quest: {i} Coin" + location_table[item_coin] = offset + 58 + i + +for i in range(1, 890): + item_coin_freemium = f"Live Freemium or Die: {i} Coin" + location_table[item_coin_freemium] = offset + 825 + 58 + i diff --git a/worlds/dlcquest/Options.py b/worlds/dlcquest/Options.py new file mode 100644 index 0000000000..c89c938b5b --- /dev/null +++ b/worlds/dlcquest/Options.py @@ -0,0 +1,114 @@ +from typing import Union, Dict, runtime_checkable, Protocol +from Options import Option, DeathLink, Choice, Toggle, SpecialRange +from dataclasses import dataclass + + +@runtime_checkable +class DLCQuestOption(Protocol): + internal_name: str + + +@dataclass +class DLCQuestOptions: + options: Dict[str, Union[bool, int]] + + def __getitem__(self, item: Union[str, DLCQuestOption]) -> Union[bool, int]: + if isinstance(item, DLCQuestOption): + item = item.internal_name + + return self.options.get(item, None) + + +class FalseDoubleJump(Choice): + """If you can do a double jump without the pack for it (glitch).""" + internal_name = "double_jump_glitch" + display_name = "Double Jump glitch" + option_none = 0 + option_simple = 1 + option_all = 2 + default = 0 + + +class TimeIsMoney(Choice): + """Is your time worth the money, are you ready to grind your sword by hand?""" + internal_name = "time_is_money" + display_name = "Time Is Money" + option_required = 0 + option_optional = 1 + default = 0 + + +class CoinSanity(Choice): + """This is for the insane it can be 825 check, it is coin sanity""" + internal_name = "coinsanity" + display_name = "CoinSanity" + option_none = 0 + option_coin = 1 + default = 0 + + +class CoinSanityRange(SpecialRange): + """This is the amount of coin in a coin bundle""" + internal_name = "coinbundlequantity" + display_name = "Coin Bundle Quantity" + range_start = 1 + range_end = 100 + default = 20 + + +class EndingChoice(Choice): + """This is for the ending type of the basic game""" + internal_name = "ending_choice" + display_name = "Ending Choice" + option_any = 0 + option_true = 1 + default = 1 + + +class Campaign(Choice): + """Whitch game you wana play to end""" + internal_name = "campaign" + display_name = "Campaign" + option_basic = 0 + option_live_freemium_or_die = 1 + option_both = 2 + default = 0 + + +class ItemShuffle(Choice): + """Should Inventory Items be separate from their DLCs and shuffled in the item pool""" + internal_name = "item_shuffle" + display_name = "Item Shuffle" + option_disabled = 0 + option_shuffled = 1 + default = 0 + + +DLCQuest_options: Dict[str, type(Option)] = { + option.internal_name: option + for option in [ + FalseDoubleJump, + CoinSanity, + CoinSanityRange, + TimeIsMoney, + EndingChoice, + Campaign, + ItemShuffle, + ] +} +default_options = {option.internal_name: option.default for option in DLCQuest_options.values()} +DLCQuest_options["death_link"] = DeathLink + + +def fetch_options(world, player: int) -> DLCQuestOptions: + return DLCQuestOptions({option: get_option_value(world, player, option) for option in DLCQuest_options}) + + +def get_option_value(world, player: int, name: str) -> Union[bool, int]: + assert name in DLCQuest_options, f"{name} is not a valid option for DLC Quest." + + value = getattr(world, name) + + if issubclass(DLCQuest_options[name], Toggle): + return bool(value[player].value) + return value[player].value diff --git a/worlds/dlcquest/Regions.py b/worlds/dlcquest/Regions.py new file mode 100644 index 0000000000..5553cb4842 --- /dev/null +++ b/worlds/dlcquest/Regions.py @@ -0,0 +1,325 @@ +import math +from BaseClasses import MultiWorld, Region, Location, Entrance, ItemClassification +from .Locations import DLCQuestLocation, location_table +from .Rules import create_event +from . import Options + +DLCQuestRegion = ["Movement Pack", "Behind Tree", "Psychological Warfare", "Double Jump Left", + "Double Jump Behind the Tree", "The Forest", "Final Room"] + + +def add_coin_freemium(region: Region, Coin: int, player: int): + number_coin = f"{Coin} coins freemium" + location_coin = f"{region.name} coins freemium" + location = DLCQuestLocation(player, location_coin, None, region) + region.locations.append(location) + location.place_locked_item(create_event(player, number_coin)) + + +def add_coin_dlcquest(region: Region, Coin: int, player: int): + number_coin = f"{Coin} coins" + location_coin = f"{region.name} coins" + location = DLCQuestLocation(player, location_coin, None, region) + region.locations.append(location) + location.place_locked_item(create_event(player, number_coin)) + + +def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQuestOptions): + Regmenu = Region("Menu", player, world) + if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[ + Options.Campaign] == Options.Campaign.option_both: + Regmenu.exits += [Entrance(player, "DLC Quest Basic", Regmenu)] + if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[ + Options.Campaign] == Options.Campaign.option_both: + Regmenu.exits += [Entrance(player, "Live Freemium or Die", Regmenu)] + world.regions.append(Regmenu) + + if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[ + Options.Campaign] == Options.Campaign.option_both: + + Regmoveright = Region("Move Right", player, world, "Start of the basic game") + Locmoveright_name = ["Movement Pack", "Animation Pack", "Audio Pack", "Pause Menu Pack"] + Regmoveright.exits = [Entrance(player, "Moving", Regmoveright)] + Regmoveright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmoveright) for + loc_name in Locmoveright_name] + add_coin_dlcquest(Regmoveright, 4, player) + if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: + coin_bundle_needed = math.floor(825 / World_Options[Options.CoinSanityRange]) + for i in range(coin_bundle_needed): + item_coin = f"DLC Quest: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin" + Regmoveright.locations += [ + DLCQuestLocation(player, item_coin, location_table[item_coin], Regmoveright)] + if 825 % World_Options[Options.CoinSanityRange] != 0: + Regmoveright.locations += [ + DLCQuestLocation(player, "DLC Quest: 825 Coin", location_table["DLC Quest: 825 Coin"], + Regmoveright)] + world.regions.append(Regmoveright) + + Regmovpack = Region("Movement Pack", player, world) + Locmovpack_name = ["Time is Money Pack", "Psychological Warfare Pack", "Armor for your Horse Pack", + "Shepherd Sheep"] + if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + Locmovpack_name += ["Sword"] + Regmovpack.exits = [Entrance(player, "Tree", Regmovpack), Entrance(player, "Cloud", Regmovpack)] + Regmovpack.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regmovpack) for loc_name + in Locmovpack_name] + add_coin_dlcquest(Regmovpack, 46, player) + world.regions.append(Regmovpack) + + Regbtree = Region("Behind Tree", player, world) + Locbtree_name = ["Double Jump Pack", "Map Pack", "Between Trees Sheep", "Hole in the Wall Sheep"] + if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + Locbtree_name += ["Gun"] + Regbtree.exits = [Entrance(player, "Behind Tree Double Jump", Regbtree), + Entrance(player, "Forest Entrance", Regbtree)] + Regbtree.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbtree) for loc_name in + Locbtree_name] + add_coin_dlcquest(Regbtree, 60, player) + world.regions.append(Regbtree) + + Regpsywarfare = Region("Psychological Warfare", player, world) + Locpsywarfare_name = ["West Cave Sheep"] + Regpsywarfare.exits = [Entrance(player, "Cloud Double Jump", Regpsywarfare)] + Regpsywarfare.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regpsywarfare) for + loc_name in Locpsywarfare_name] + add_coin_dlcquest(Regpsywarfare, 100, player) + world.regions.append(Regpsywarfare) + + Regdoubleleft = Region("Double Jump Total Left", player, world) + Locdoubleleft_name = ["Pet Pack", "Top Hat Pack", "North West Alcove Sheep"] + Regdoubleleft.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleft) for + loc_name in + Locdoubleleft_name] + Regdoubleleft.exits = [Entrance(player, "Cave Tree", Regdoubleleft), + Entrance(player, "Cave Roof", Regdoubleleft)] + add_coin_dlcquest(Regdoubleleft, 50, player) + world.regions.append(Regdoubleleft) + + Regdoubleleftcave = Region("Double Jump Total Left Cave", player, world) + Locdoubleleftcave_name = ["Top Hat Sheep"] + Regdoubleleftcave.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleftcave) + for loc_name in Locdoubleleftcave_name] + add_coin_dlcquest(Regdoubleleftcave, 9, player) + world.regions.append(Regdoubleleftcave) + + Regdoubleleftroof = Region("Double Jump Total Left Roof", player, world) + Locdoubleleftroof_name = ["North West Ceiling Sheep"] + Regdoubleleftroof.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubleleftroof) + for loc_name in Locdoubleleftroof_name] + add_coin_dlcquest(Regdoubleleftroof, 10, player) + world.regions.append(Regdoubleleftroof) + + Regdoubletree = Region("Double Jump Behind Tree", player, world) + Locdoubletree_name = ["Sexy Outfits Pack", "Double Jump Alcove Sheep", "Sexy Outfits Sheep"] + Regdoubletree.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regdoubletree) for + loc_name in + Locdoubletree_name] + Regdoubletree.exits = [Entrance(player, "True Double Jump", Regdoubletree)] + add_coin_dlcquest(Regdoubletree, 89, player) + world.regions.append(Regdoubletree) + + Regtruedoublejump = Region("True Double Jump Behind Tree", player, world) + Loctruedoublejump_name = ["Double Jump Floating Sheep", "Cutscene Sheep"] + Regtruedoublejump.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regtruedoublejump) + for loc_name in Loctruedoublejump_name] + add_coin_dlcquest(Regtruedoublejump, 7, player) + world.regions.append(Regtruedoublejump) + + Regforest = Region("The Forest", player, world) + Locforest_name = ["Gun Pack", "Night Map Pack"] + Regforest.exits = [Entrance(player, "Behind Ogre", Regforest), + Entrance(player, "Forest Double Jump", Regforest)] + Regforest.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regforest) for loc_name in + Locforest_name] + add_coin_dlcquest(Regforest, 169, player) + world.regions.append(Regforest) + + Regforestdoublejump = Region("The Forest whit double Jump", player, world) + Locforestdoublejump_name = ["The Zombie Pack", "Forest Low Sheep"] + Regforestdoublejump.exits = [Entrance(player, "Forest True Double Jump", Regforestdoublejump)] + Regforestdoublejump.locations += [ + DLCQuestLocation(player, loc_name, location_table[loc_name], Regforestdoublejump) for loc_name in + Locforestdoublejump_name] + add_coin_dlcquest(Regforestdoublejump, 76, player) + world.regions.append(Regforestdoublejump) + + Regforesttruedoublejump = Region("The Forest whit double Jump Part 2", player, world) + Locforesttruedoublejump_name = ["Forest High Sheep"] + Regforesttruedoublejump.locations += [ + DLCQuestLocation(player, loc_name, location_table[loc_name], Regforesttruedoublejump) + for loc_name in Locforesttruedoublejump_name] + add_coin_dlcquest(Regforesttruedoublejump, 203, player) + world.regions.append(Regforesttruedoublejump) + + Regfinalroom = Region("The Final Boss Room", player, world) + Locfinalroom_name = ["Finish the Fight Pack"] + Regfinalroom.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfinalroom) for + loc_name in + Locfinalroom_name] + world.regions.append(Regfinalroom) + + loc_win = DLCQuestLocation(player, "Winning Basic", None, world.get_region("The Final Boss Room", player)) + world.get_region("The Final Boss Room", player).locations.append(loc_win) + loc_win.place_locked_item(create_event(player, "Victory Basic")) + + world.get_entrance("DLC Quest Basic", player).connect(world.get_region("Move Right", player)) + + world.get_entrance("Moving", player).connect(world.get_region("Movement Pack", player)) + + world.get_entrance("Tree", player).connect(world.get_region("Behind Tree", player)) + + world.get_entrance("Cloud", player).connect(world.get_region("Psychological Warfare", player)) + + world.get_entrance("Cloud Double Jump", player).connect(world.get_region("Double Jump Total Left", player)) + + world.get_entrance("Cave Tree", player).connect(world.get_region("Double Jump Total Left Cave", player)) + + world.get_entrance("Cave Roof", player).connect(world.get_region("Double Jump Total Left Roof", player)) + + world.get_entrance("Forest Entrance", player).connect(world.get_region("The Forest", player)) + + world.get_entrance("Behind Tree Double Jump", player).connect( + world.get_region("Double Jump Behind Tree", player)) + + world.get_entrance("Behind Ogre", player).connect(world.get_region("The Final Boss Room", player)) + + world.get_entrance("Forest Double Jump", player).connect( + world.get_region("The Forest whit double Jump", player)) + + world.get_entrance("Forest True Double Jump", player).connect( + world.get_region("The Forest whit double Jump Part 2", player)) + + world.get_entrance("True Double Jump", player).connect(world.get_region("True Double Jump Behind Tree", player)) + + if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[ + Options.Campaign] == Options.Campaign.option_both: + + Regfreemiumstart = Region("Freemium Start", player, world) + Locfreemiumstart_name = ["Particles Pack", "Day One Patch Pack", "Checkpoint Pack", "Incredibly Important Pack", + "Nice Try", "Story is Important", "I Get That Reference!"] + if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + Locfreemiumstart_name += ["Wooden Sword"] + Regfreemiumstart.exits = [Entrance(player, "Vines", Regfreemiumstart)] + Regfreemiumstart.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfreemiumstart) + for loc_name in + Locfreemiumstart_name] + add_coin_freemium(Regfreemiumstart, 50, player) + if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: + coin_bundle_needed = math.floor(889 / World_Options[Options.CoinSanityRange]) + for i in range(coin_bundle_needed): + item_coin_freemium = f"Live Freemium or Die: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin" + Regfreemiumstart.locations += [ + DLCQuestLocation(player, item_coin_freemium, location_table[item_coin_freemium], + Regfreemiumstart)] + if 889 % World_Options[Options.CoinSanityRange] != 0: + Regfreemiumstart.locations += [ + DLCQuestLocation(player, "Live Freemium or Die: 889 Coin", + location_table["Live Freemium or Die: 889 Coin"], + Regfreemiumstart)] + world.regions.append(Regfreemiumstart) + + Regbehindvine = Region("Behind the Vines", player, world) + Locbehindvine_name = ["Wall Jump Pack", "Health Bar Pack", "Parallax Pack"] + if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + Locbehindvine_name += ["Pickaxe"] + Regbehindvine.exits = [Entrance(player, "Wall Jump Entrance", Regbehindvine)] + Regbehindvine.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regbehindvine) for + loc_name in Locbehindvine_name] + add_coin_freemium(Regbehindvine, 95, player) + world.regions.append(Regbehindvine) + + Regwalljump = Region("Wall Jump", player, world) + Locwalljump_name = ["Harmless Plants Pack", "Death of Comedy Pack", "Canadian Dialog Pack", "DLC NPC Pack"] + Regwalljump.exits = [Entrance(player, "Harmless Plants", Regwalljump), + Entrance(player, "Pickaxe Hard Cave", Regwalljump)] + Regwalljump.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regwalljump) for + loc_name in Locwalljump_name] + add_coin_freemium(Regwalljump, 150, player) + world.regions.append(Regwalljump) + + Regfakeending = Region("Fake Ending", player, world) + Locfakeending_name = ["Cut Content Pack", "Name Change Pack"] + Regfakeending.exits = [Entrance(player, "Name Change Entrance", Regfakeending), + Entrance(player, "Cut Content Entrance", Regfakeending)] + Regfakeending.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfakeending) for + loc_name in Locfakeending_name] + world.regions.append(Regfakeending) + + Reghardcave = Region("Hard Cave", player, world) + add_coin_freemium(Reghardcave, 20, player) + Reghardcave.exits = [Entrance(player, "Hard Cave Wall Jump", Reghardcave)] + world.regions.append(Reghardcave) + + Reghardcavewalljump = Region("Hard Cave Wall Jump", player, world) + Lochardcavewalljump_name = ["Increased HP Pack"] + Reghardcavewalljump.locations += [ + DLCQuestLocation(player, loc_name, location_table[loc_name], Reghardcavewalljump) for + loc_name in Lochardcavewalljump_name] + add_coin_freemium(Reghardcavewalljump, 130, player) + world.regions.append(Reghardcavewalljump) + + Regcutcontent = Region("Cut Content", player, world) + Loccutcontent_name = [] + if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + Loccutcontent_name += ["Humble Indie Bindle"] + Regcutcontent.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regcutcontent) for + loc_name in Loccutcontent_name] + add_coin_freemium(Regcutcontent, 200, player) + world.regions.append(Regcutcontent) + + Regnamechange = Region("Name Change", player, world) + Locnamechange_name = [] + if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + Locnamechange_name += ["Box of Various Supplies"] + Regnamechange.exits = [Entrance(player, "Behind Rocks", Regnamechange)] + Regnamechange.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regnamechange) for + loc_name in Locnamechange_name] + world.regions.append(Regnamechange) + + Regtopright = Region("Top Right", player, world) + Loctopright_name = ["Season Pass", "High Definition Next Gen Pack"] + Regtopright.exits = [Entrance(player, "Blizzard", Regtopright)] + Regtopright.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regtopright) for + loc_name in Loctopright_name] + add_coin_freemium(Regtopright, 90, player) + world.regions.append(Regtopright) + + Regseason = Region("Season", player, world) + Locseason_name = ["Remove Ads Pack", "Not Exactly Noble"] + Regseason.exits = [Entrance(player, "Boss Door", Regseason)] + Regseason.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regseason) for + loc_name in Locseason_name] + add_coin_freemium(Regseason, 154, player) + world.regions.append(Regseason) + + Regfinalboss = Region("Final Boss", player, world) + Locfinalboss_name = ["Big Sword Pack", "Really Big Sword Pack", "Unfathomable Sword Pack"] + Regfinalboss.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regfinalboss) for + loc_name in Locfinalboss_name] + world.regions.append(Regfinalboss) + + loc_wining = DLCQuestLocation(player, "Winning Freemium", None, world.get_region("Final Boss", player)) + world.get_region("Final Boss", player).locations.append(loc_wining) + loc_wining.place_locked_item(create_event(player, "Victory Freemium")) + + world.get_entrance("Live Freemium or Die", player).connect(world.get_region("Freemium Start", player)) + + world.get_entrance("Vines", player).connect(world.get_region("Behind the Vines", player)) + + world.get_entrance("Wall Jump Entrance", player).connect(world.get_region("Wall Jump", player)) + + world.get_entrance("Harmless Plants", player).connect(world.get_region("Fake Ending", player)) + + world.get_entrance("Pickaxe Hard Cave", player).connect(world.get_region("Hard Cave", player)) + + world.get_entrance("Hard Cave Wall Jump", player).connect(world.get_region("Hard Cave Wall Jump", player)) + + world.get_entrance("Name Change Entrance", player).connect(world.get_region("Name Change", player)) + + world.get_entrance("Cut Content Entrance", player).connect(world.get_region("Cut Content", player)) + + world.get_entrance("Behind Rocks", player).connect(world.get_region("Top Right", player)) + + world.get_entrance("Blizzard", player).connect(world.get_region("Season", player)) + + world.get_entrance("Boss Door", player).connect(world.get_region("Final Boss", player)) diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py new file mode 100644 index 0000000000..ac9cd23d53 --- /dev/null +++ b/worlds/dlcquest/Rules.py @@ -0,0 +1,370 @@ +import math +import re +from .Locations import DLCQuestLocation +from ..generic.Rules import add_rule, set_rule +from .Items import DLCQuestItem +from BaseClasses import ItemClassification +from . import Options + + +def create_event(player, event: str): + return DLCQuestItem(event, ItemClassification.progression, None, player) + + +def set_rules(world, player, World_Options: Options.DLCQuestOptions): + def has_enough_coin(player: int, coin: int): + def has_coin(state, player: int, coins: int): + coin_possessed = 0 + for i in [4, 7, 9, 10, 46, 50, 60, 76, 89, 100, 169, 203]: + name_coin = f"{i} coins" + if state.has(name_coin, player): + coin_possessed += i + + return coin_possessed >= coins + + return lambda state: has_coin(state, player, coin) + + def has_enough_coin_freemium(player: int, coin: int): + def has_coin(state, player: int, coins: int): + coin_possessed = 0 + for i in [20, 50, 90, 95, 130, 150, 154, 200]: + name_coin = f"{i} coins freemium" + if state.has(name_coin, player): + coin_possessed += i + + return coin_possessed >= coins + + return lambda state: has_coin(state, player, coin) + + if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[ + Options.Campaign] == Options.Campaign.option_both: + set_rule(world.get_entrance("Moving", player), + lambda state: state.has("Movement Pack", player)) + set_rule(world.get_entrance("Cloud", player), + lambda state: state.has("Psychological Warfare Pack", player)) + set_rule(world.get_entrance("Forest Entrance", player), + lambda state: state.has("Map Pack", player)) + set_rule(world.get_entrance("Forest True Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) + + if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_disabled: + set_rule(world.get_entrance("Behind Ogre", player), + lambda state: state.has("Gun Pack", player)) + + if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required: + set_rule(world.get_entrance("Tree", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_entrance("Cave Tree", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_location("Shepherd Sheep", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_location("North West Ceiling Sheep", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_location("North West Alcove Sheep", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_location("West Cave Sheep", player), + lambda state: state.has("Time is Money Pack", player)) + + if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + set_rule(world.get_entrance("Behind Ogre", player), + lambda state: state.has("Gun", player)) + set_rule(world.get_entrance("Tree", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_entrance("Cave Tree", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_entrance("True Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) + set_rule(world.get_location("Shepherd Sheep", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_location("North West Ceiling Sheep", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_location("North West Alcove Sheep", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_location("West Cave Sheep", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + + if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required: + set_rule(world.get_location("Sword", player), + lambda state: state.has("Time is Money Pack", player)) + + if World_Options[Options.FalseDoubleJump] == Options.FalseDoubleJump.option_none: + set_rule(world.get_entrance("Cloud Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) + set_rule(world.get_entrance("Forest Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) + + if World_Options[Options.FalseDoubleJump] == Options.FalseDoubleJump.option_none or World_Options[ + Options.FalseDoubleJump] == Options.FalseDoubleJump.option_simple: + set_rule(world.get_entrance("Behind Tree Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) + set_rule(world.get_entrance("Cave Roof", player), + lambda state: state.has("Double Jump Pack", player)) + + if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: + number_of_bundle = math.floor(825 / World_Options[Options.CoinSanityRange]) + for i in range(number_of_bundle): + + item_coin = "DLC Quest: number Coin" + item_coin_loc = re.sub("number", str(World_Options[Options.CoinSanityRange] * (i + 1)), item_coin) + set_rule(world.get_location(item_coin_loc, player), + has_enough_coin(player, World_Options[Options.CoinSanityRange] * (i + 1))) + if 825 % World_Options[Options.CoinSanityRange] != 0: + set_rule(world.get_location("DLC Quest: 825 Coin", player), + has_enough_coin(player, 825)) + + set_rule(world.get_location("Movement Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(4 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Animation Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Audio Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Pause Menu Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Time is Money Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(20 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Double Jump Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(100 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Pet Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Sexy Outfits Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Top Hat Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Map Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(140 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Gun Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(75 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("The Zombie Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Night Map Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(75 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Psychological Warfare Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(50 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Armor for your Horse Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(250 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Finish the Fight Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + + if World_Options[Options.CoinSanity] == Options.CoinSanity.option_none: + set_rule(world.get_location("Movement Pack", player), + has_enough_coin(player, 4)) + set_rule(world.get_location("Animation Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Audio Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Pause Menu Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Time is Money Pack", player), + has_enough_coin(player, 20)) + set_rule(world.get_location("Double Jump Pack", player), + has_enough_coin(player, 100)) + set_rule(world.get_location("Pet Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Sexy Outfits Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Top Hat Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Map Pack", player), + has_enough_coin(player, 140)) + set_rule(world.get_location("Gun Pack", player), + has_enough_coin(player, 75)) + set_rule(world.get_location("The Zombie Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Night Map Pack", player), + has_enough_coin(player, 75)) + set_rule(world.get_location("Psychological Warfare Pack", player), + has_enough_coin(player, 50)) + set_rule(world.get_location("Armor for your Horse Pack", player), + has_enough_coin(player, 250)) + set_rule(world.get_location("Finish the Fight Pack", player), + has_enough_coin(player, 5)) + + + if World_Options[Options.EndingChoice] == Options.EndingChoice.option_any: + set_rule(world.get_location("Winning Basic", player), + lambda state: state.has("Finish the Fight Pack", player)) + if World_Options[Options.EndingChoice] == Options.EndingChoice.option_true: + set_rule(world.get_location("Winning Basic", player), + lambda state: state.has("Armor for your Horse Pack", player) and state.has("Finish the Fight Pack", + player)) + + if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[ + Options.Campaign] == Options.Campaign.option_both: + set_rule(world.get_entrance("Wall Jump Entrance", player), + lambda state: state.has("Wall Jump Pack", player)) + set_rule(world.get_entrance("Harmless Plants", player), + lambda state: state.has("Harmless Plants Pack", player)) + set_rule(world.get_entrance("Pickaxe Hard Cave", player), + lambda state: state.has("Pickaxe", player)) + set_rule(world.get_entrance("Name Change Entrance", player), + lambda state: state.has("Name Change Pack", player)) + set_rule(world.get_entrance("Cut Content Entrance", player), + lambda state: state.has("Cut Content Pack", player)) + set_rule(world.get_entrance("Blizzard", player), + lambda state: state.has("Season Pass", player)) + set_rule(world.get_entrance("Boss Door", player), + lambda state: state.has("Big Sword Pack", player) and state.has("Really Big Sword Pack", + player) and state.has( + "Unfathomable Sword Pack", player)) + set_rule(world.get_location("I Get That Reference!", player), + lambda state: state.has("Death of Comedy Pack", player)) + set_rule(world.get_location("Story is Important", player), + lambda state: state.has("DLC NPC Pack", player)) + + if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_disabled: + set_rule(world.get_entrance("Vines", player), + lambda state: state.has("Incredibly Important Pack", player)) + set_rule(world.get_entrance("Behind Rocks", player), + lambda state: state.can_reach("Cut Content", 'region', player)) + + if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: + set_rule(world.get_entrance("Vines", player), + lambda state: state.has("Wooden Sword", player) or state.has("Pickaxe", player)) + set_rule(world.get_entrance("Behind Rocks", player), + lambda state: state.has("Pickaxe", player)) + + set_rule(world.get_location("Wooden Sword", player), + lambda state: state.has("Incredibly Important Pack", player)) + set_rule(world.get_location("Pickaxe", player), + lambda state: state.has("Humble Indie Bindle", player)) + set_rule(world.get_location("Humble Indie Bindle", player), + lambda state: state.has("Box of Various Supplies", player) and state.can_reach("Cut Content", + 'region', player)) + set_rule(world.get_location("Box of Various Supplies", player), + lambda state: state.can_reach("Cut Content", 'region', player)) + + if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: + number_of_bundle = math.floor(889 / World_Options[Options.CoinSanityRange]) + for i in range(number_of_bundle): + + item_coin_freemium = "Live Freemium or Die: number Coin" + item_coin_loc_freemium = re.sub("number", str(World_Options[Options.CoinSanityRange] * (i + 1)), + item_coin_freemium) + set_rule(world.get_location(item_coin_loc_freemium, player), + has_enough_coin_freemium(player, World_Options[Options.CoinSanityRange] * (i + 1))) + if 889 % World_Options[Options.CoinSanityRange] != 0: + set_rule(world.get_location("Live Freemium or Die: 889 Coin", player), + has_enough_coin_freemium(player, 889)) + + set_rule(world.get_entrance("Boss Door", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(889 / World_Options[Options.CoinSanityRange]))) + + set_rule(world.get_location("Particles Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Day One Patch Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Checkpoint Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Incredibly Important Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(15 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Wall Jump Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(35 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Health Bar Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Parallax Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Harmless Plants Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(130 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Death of Comedy Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(15 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Canadian Dialog Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(10 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("DLC NPC Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(15 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Cut Content Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(40 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Name Change Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(150 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Season Pass", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(199 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("High Definition Next Gen Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(20 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Increased HP Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(10 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Remove Ads Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(25 / World_Options[Options.CoinSanityRange]))) + + if World_Options[Options.CoinSanity] == Options.CoinSanity.option_none: + set_rule(world.get_entrance("Boss Door", player), + has_enough_coin_freemium(player, 889)) + + set_rule(world.get_location("Particles Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Day One Patch Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Checkpoint Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Incredibly Important Pack", player), + has_enough_coin_freemium(player, 15)) + set_rule(world.get_location("Wall Jump Pack", player), + has_enough_coin_freemium(player, 35)) + set_rule(world.get_location("Health Bar Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Parallax Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Harmless Plants Pack", player), + has_enough_coin_freemium(player, 130)) + set_rule(world.get_location("Death of Comedy Pack", player), + has_enough_coin_freemium(player, 15)) + set_rule(world.get_location("Canadian Dialog Pack", player), + has_enough_coin_freemium(player, 10)) + set_rule(world.get_location("DLC NPC Pack", player), + has_enough_coin_freemium(player, 15)) + set_rule(world.get_location("Cut Content Pack", player), + has_enough_coin_freemium(player, 40)) + set_rule(world.get_location("Name Change Pack", player), + has_enough_coin_freemium(player, 150)) + set_rule(world.get_location("Season Pass", player), + has_enough_coin_freemium(player, 199)) + set_rule(world.get_location("High Definition Next Gen Pack", player), + has_enough_coin_freemium(player, 20)) + set_rule(world.get_location("Increased HP Pack", player), + has_enough_coin_freemium(player, 10)) + set_rule(world.get_location("Remove Ads Pack", player), + has_enough_coin_freemium(player, 25)) + + + + if World_Options[Options.Campaign] == Options.Campaign.option_basic: + world.completion_condition[player] = lambda state: state.has("Victory Basic", player) + + if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die: + world.completion_condition[player] = lambda state: state.has("Victory Freemium", player) + + if World_Options[Options.Campaign] == Options.Campaign.option_both: + world.completion_condition[player] = lambda state: state.has("Victory Basic", player) and state.has( + "Victory Freemium", player) diff --git a/worlds/dlcquest/Script/__init__.py b/worlds/dlcquest/Script/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/worlds/dlcquest/Script/export_items.py b/worlds/dlcquest/Script/export_items.py new file mode 100644 index 0000000000..d946238d32 --- /dev/null +++ b/worlds/dlcquest/Script/export_items.py @@ -0,0 +1,26 @@ +"""Items export script +This script can be used to export all the AP items into a json file in the output folder. This file is used by the tests +of the mod to ensure it can handle all possible items. + +To run the script, use `python -m worlds.dlcquest.Script.export_items` from the repository root. +""" + +import json +import os.path + +from worlds.dlcquest import item_table + +if not os.path.isdir("output"): + os.mkdir("output") + +if __name__ == "__main__": + with open("output/dlc_quest_item_table.json", "w+") as f: + items = { + item.name: { + "code": item.code, + "classification": item.classification.name + } + for item in item_table.values() + if item.code is not None + } + json.dump({"items": items}, f) diff --git a/worlds/dlcquest/Script/export_locations.py b/worlds/dlcquest/Script/export_locations.py new file mode 100644 index 0000000000..392e05ab3e --- /dev/null +++ b/worlds/dlcquest/Script/export_locations.py @@ -0,0 +1,20 @@ +"""Locations export script +This script can be used to export all the AP locations into a json file in the output folder. This file is used by the +tests of the mod to ensure it can handle all possible locations. + +To run the script, use `python -m worlds.stardew_valley.scripts.export_locations` from the repository root. +""" + +import json +import os + +from worlds.dlcquest import location_table + +if not os.path.isdir("output"): + os.mkdir("output") + +if __name__ == "__main__": + with open("output/dlc_quest_location_table.json", "w+") as f: + locations = location_table + + json.dump({"locations": locations}, f) diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py new file mode 100644 index 0000000000..9afde0ea26 --- /dev/null +++ b/worlds/dlcquest/__init__.py @@ -0,0 +1,82 @@ +from typing import Dict, Any, Iterable, Optional, Union +from BaseClasses import Tutorial +from worlds.AutoWorld import World, WebWorld +from .Items import DLCQuestItem, item_table, ItemData, create_items +from .Locations import location_table, DLCQuestLocation +from .Options import DLCQuest_options, DLCQuestOptions, fetch_options +from .Rules import set_rules +from .Regions import create_regions + +client_version = 0 + + +class DLCqwebworld(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Tutorial", + "A guide to setting up the Archipelago DLCQuest game on your computer.", + "English", + "setup_en.md", + "setup/en", + ["axe_y"] + )] + + +class DLCqworld(World): + """ + DLCQuest is a metroid ish game where everything is an in-game dlc. + """ + game = "DLCQuest" + topology_present = False + web = DLCqwebworld() + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = location_table + + data_version = 0 + + option_definitions = DLCQuest_options + + def generate_early(self): + self.options = fetch_options(self.multiworld, self.player) + + def create_regions(self): + create_regions(self.multiworld, self.player, self.options) + + def set_rules(self): + set_rules(self.multiworld, self.player, self.options) + + def create_event(self, event: str): + return DLCQuestItem(event, True, None, self.player) + + def create_items(self): + locations_count = len([location + for location in self.multiworld.get_locations(self.player) + if not location.event]) + + items_to_exclude = [excluded_items + for excluded_items in self.multiworld.precollected_items[self.player]] + + created_items = create_items(self, self.options, locations_count + len(items_to_exclude), self.multiworld.random) + + self.multiworld.itempool += created_items + + for item in items_to_exclude: + if item in self.multiworld.itempool: + self.multiworld.itempool.remove(item) + + def create_item(self, item: Union[str, ItemData]) -> DLCQuestItem: + if isinstance(item, str): + item = item_table[item] + + return DLCQuestItem(item.name, item.classification, item.code, self.player) + + def fill_slot_data(self): + return { + "death_link": self.multiworld.death_link[self.player].value, + "ending_choice": self.multiworld.ending_choice[self.player].value, + "campaign": self.multiworld.campaign[self.player].value, + "coinsanity": self.multiworld.coinsanity[self.player].value, + "coinbundlerange": self.multiworld.coinbundlequantity[self.player].value, + "item_shuffle": self.multiworld.item_shuffle[self.player].value, + "seed": self.multiworld.per_slot_randoms[self.player].randrange(99999999) + } diff --git a/worlds/dlcquest/data/items.csv b/worlds/dlcquest/data/items.csv new file mode 100644 index 0000000000..cc5ac0bbe4 --- /dev/null +++ b/worlds/dlcquest/data/items.csv @@ -0,0 +1,48 @@ +id,name,classification,groups +0,Movement Pack,progression,"DLC,DLCQuest" +1,Animation Pack,filler,"DLC,DLCQuest" +2,Audio Pack,filler,"DLC,DLCQuest" +3,Pause Menu Pack,useful,"DLC,DLCQuest" +4,Time is Money Pack,progression,"DLC,DLCQuest" +5,Double Jump Pack,progression,"DLC,DLCQuest" +6,Pet Pack,filler,"DLC,DLCQuest" +7,Sexy Outfits Pack,filler,"DLC,DLCQuest" +8,Top Hat Pack,filler,"DLC,DLCQuest" +9,Map Pack,progression,"DLC,DLCQuest" +10,Gun Pack,progression,"DLC,DLCQuest" +11,The Zombie Pack,filler,"DLC,DLCQuest" +12,Night Map Pack,useful,"DLC,DLCQuest" +13,Psychological Warfare Pack,progression,"DLC,DLCQuest" +14,Armor for your Horse Pack,progression,"DLC,DLCQuest" +15,Finish the Fight Pack,progression,"DLC,DLCQuest" +16,Particles Pack,filler,"DLC,Freemium" +17,Day One Patch Pack,useful,"DLC,Freemium" +18,Checkpoint Pack,useful,"DLC,Freemium" +19,Incredibly Important Pack,progression,"DLC,Freemium" +20,Wall Jump Pack,progression,"DLC,Freemium" +21,Health Bar Pack,useful,"DLC,Freemium" +22,Parallax Pack,filler,"DLC,Freemium" +23,Harmless Plants Pack,progression,"DLC,Freemium" +24,Death of Comedy Pack,progression,"DLC,Freemium" +25,Canadian Dialog Pack,filler,"DLC,Freemium" +26,DLC NPC Pack,progression,"DLC,Freemium" +27,Cut Content Pack,progression,"DLC,Freemium" +28,Name Change Pack,progression,"DLC,Freemium" +29,Pickaxe,progression,"Item,Freemium" +30,Season Pass,progression,"DLC,Freemium" +31,High Definition Next Gen Pack,filler,"DLC,Freemium" +32,Increased HP Pack,useful,"DLC,Freemium" +33,Removed Ads Pack,filler,"DLC,Freemium" +34,Big Sword Pack,progression,"DLC,Freemium" +35,Really Big Sword Pack,progression,"DLC,Freemium" +36,Unfathomable Sword Pack,progression,"DLC,Freemium" +37,Gun,progression,"Item,DLCQuest" +38,Sword,progression,"Item,DLCQuest" +39,Wooden Sword,progression,"Item,Freemium" +40,Box of Various Supplies,progression,"Item,Freemium" +41,Humble Indie Bindle,progression,"Item,Freemium" +42,DLC Quest: Coin Bundle,progression,"Coin,DLCQuest" +43,Live Freemium or Die: Coin Bundle,progression,"Coin,Freemium" +44,Zombie Sheep,trap,Trap +45,Temporary Spike,trap,Trap +46,Loading Screen,trap,Trap \ No newline at end of file diff --git a/worlds/dlcquest/docs/en_DLCQuest.md b/worlds/dlcquest/docs/en_DLCQuest.md new file mode 100644 index 0000000000..333b1e8ed9 --- /dev/null +++ b/worlds/dlcquest/docs/en_DLCQuest.md @@ -0,0 +1,51 @@ +# DLC Quest + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +DLCs are obtained as checks for the multiworld. There are also some other optional checks in DLC Quest + + +## What is the goal of DLC Quest? + +DLC Quest has two campaigns, and the player can choose which one they will play for their slot. +They can also choose to do both campaigns. + + +## What are location checks in DLC Quest? + +Location checks in DLC Quest always include: +- DLC Purchases from the shopkeeper +- Awardment-related objectives + - Killing Sheep in DLC Quest + - Specific Awardment objectives in Live Freemium or Die + +There also are a number of location checks that are optional, and individual players choose to include them or not in their shuffling: +- Items that your character can obtain in various ways + - Swords + - Gun + - Box of Various Supplies + - Humble Indie Bindle + - Pickaxe +- Coinsanity: Coins, either individually or as custom-sized bundles + + +## Which items can be in another player's world? + +Every DLC in the game is shuffled in the item pool. The items related to the optional checks described above are also in the pool + +There are also some brand new trap items, used as filler, based on vanilla game annoyances +- Zombie Sheep +- Loading Screens +- Temporary Spikes + +## When the player receives an item, what happens? + +Every time an item is received while online, a notification appears on screen informing the player of it. +Some items also include an animation or cutscene that will play immediately upon receiving it. + +Items that are received while offline will not play any animation or cutscene, and simply be active when logging in. \ No newline at end of file diff --git a/worlds/dlcquest/docs/setup_en.md b/worlds/dlcquest/docs/setup_en.md new file mode 100644 index 0000000000..8111c654df --- /dev/null +++ b/worlds/dlcquest/docs/setup_en.md @@ -0,0 +1,57 @@ +# Stardew Valley Randomizer Setup Guide + +## Required Software + +- DLC Quest on PC (Recommended: [Steam version](https://store.steampowered.com/app/230050/DLC_Quest/)) +- [DLCQuestipelago](https://github.com/agilbert1412/DLCQuestipelago/releases) +- BepinEx (Used as a modloader for DLCQuest. The Mod release above includes BepInEx if you pick the full installer version) + +## Optional Software +- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) + - (Only for the TextClient) + +## Configuring your YAML file + +### What is a YAML file and why do I need one? + +See the guide on setting up a basic YAML at the Archipelago setup +guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en) + +### Where do I get a YAML file? + +You can customize your settings by visiting the [DLC Quest Player Settings Page](../player-settings) + +## Joining a MultiWorld Game + +### Installing the mod + +- Download the [DLCQuestipelago mod release](https://github.com/agilbert1412/DLCQuestipelago/releases). If this is your first time installing the mod, or if you are not comfortable with manually editing files, you should pick the Installer. It will handle most of the work for you + + +- Extract the .zip archive to a location of your choice + + +- Run "DLCQuestipelagoInstaller.exe" +![image](https://i.imgur.com/2sPhMgs.png) +- The installer should describe what it is doing each step of the way, and will ask for your input when necessary. + - It will allow you to choose where to install your modded game, and offer a default location + - It will **try** to find your DLCQuest game on your computer, and should it fail, it will ask you to input the path to it + - It will offer the choice of creating a desktop shortcut for the modded launcher + +### Connect to the MultiServer + +- Locate the file "ArchipelagoConnectionInfo.json", at the root of your modded installation. You can edit this file with any text editor, and you need to enter the server ip address, port and your slotname into the relevant fields. + + +- Run BepInEx.NET.Framework.Launcher.exe. If you opted for a desktop shortcut, you will find it with an icon and a more recognizable name. +- ![image](https://i.imgur.com/ZUiFrhf.png) + + +- Your game should launch alongside a modloader console, which will contain important debugging information if you run into problems. +- The game should automatically connect, and attempt reconnecting if your internet or the server fails, during your playthrough. + +### Interacting with the MultiWorld from in-game + +You cannot send commands to the server or chat with the other players from DLC Quest, as the game lacks a proper way to input text. +You can keep track of the server activity in your BepInEx console, as Archipelago chat messages will be displayed in it. +You will need to use an [Archipelago Text Client](https://github.com/ArchipelagoMW/Archipelago/releases) if you want to send commands. \ No newline at end of file From 11fdb293579c33a76158983b201bd8f8ed1ffe58 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 9 Apr 2023 18:43:39 +0200 Subject: [PATCH 015/489] Doc: apworlds have to be all lower case https://discord.com/channels/731205301247803413/731214280439103580/1094655639600508999 --- docs/apworld specification.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/apworld specification.md b/docs/apworld specification.md index ac89a46e10..ba2f2dbbb8 100644 --- a/docs/apworld specification.md +++ b/docs/apworld specification.md @@ -7,10 +7,12 @@ See [world api.md](world%20api.md) for details. apworld provides a way to package and ship a world that is not part of the main distribution by placing a `*.apworld` file into the worlds folder. +**Warning:** apworlds have to be all lower case, otherwise they raise a bogus Exception when trying to import in frozen python 3.10+! + ## File Format -apworld files are zip archives with the case-sensitive file ending `.apworld`. +apworld files are zip archives, all lower case, with the file ending `.apworld`. The zip has to contain a folder with the same name as the zip, case-sensitive, that contains what would normally be in the world's folder in `worlds/`. I.e. `worlds/ror2.apworld` containing `ror2/__init__.py`. From 0bc5a3bc8dda38fa609d8cbfd7beb4910b8083a1 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 9 Apr 2023 22:53:23 +0200 Subject: [PATCH 016/489] Readme: add missing game DLC Quest (#1682) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 9454d0f168..e5419072a9 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Currently, the following games are supported: * The Legend of Zelda: Link's Awakening DX * Clique * Adventure +* DLC Quest For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled From 6059b5ef66da8de8d3f0c7a329f2af17ac953d63 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sun, 9 Apr 2023 18:35:46 -0400 Subject: [PATCH 017/489] SM: 20221101 update (#1479) This adds support to most of Varia's 20221101 update. Notably, added Options for: - Objectives - Tourian - RelaxedRoundRobinCF As well as previously unsupported Options: - EscapeRando - RemoveEscapeEnemies - HideItems --- worlds/sm/Options.py | 113 +- worlds/sm/__init__.py | 84 +- .../multiworld-basepatch.ips | Bin 18658 -> 18658 bytes .../data/SMBasepatch_prebuilt/multiworld.sym | 306 +- .../sm-basepatch-symbols.json | 102 +- .../SMBasepatch_prebuilt/variapatches.ips | Bin 27590 -> 35247 bytes worlds/sm/variaRandomizer/graph/graph.py | 11 +- .../sm/variaRandomizer/graph/graph_utils.py | 56 +- worlds/sm/variaRandomizer/graph/location.py | 72 +- .../graph/vanilla/graph_access.py | 41 +- .../graph/vanilla/graph_helpers.py | 100 +- .../graph/vanilla/graph_locations.py | 87 +- worlds/sm/variaRandomizer/logic/helpers.py | 75 +- .../sm/variaRandomizer/logic/smboolmanager.py | 104 +- .../patches/common/ips/beam_doors_plms.ips | Bin 3215 -> 3078 bytes .../patches/common/ips/color_blind.ips | Bin 0 -> 3924 bytes .../patches/common/ips/credits_varia.ips | Bin 0 -> 14493 bytes .../patches/common/ips/custom_music.ips | Bin 0 -> 362 bytes .../common/ips/disable_screen_shake.ips | Bin 0 -> 92 bytes .../common/ips/door_indicators_plms.ips | Bin 0 -> 797 bytes .../common/ips/elevators_doors_speed.ips | Bin 203 -> 416 bytes .../patches/common/ips/elevators_speed.ips | Bin 0 -> 91 bytes .../patches/common/ips/endingtotals.ips | Bin 0 -> 293 bytes .../patches/common/ips/fast_doors.ips | Bin 0 -> 333 bytes .../patches/common/ips/fast_doors_old.ips | Bin 0 -> 122 bytes .../patches/common/ips/g4_skip.ips | Bin 62 -> 0 bytes .../patches/common/ips/gameend.ips | Bin 37 -> 62 bytes .../patches/common/ips/hell.ips | Bin 0 -> 34 bytes .../patches/common/ips/intro_text.ips | Bin 0 -> 300 bytes .../patches/common/ips/lava_acid_physics.ips | Bin 0 -> 240 bytes .../patches/common/ips/minimizer_bosses.ips | Bin 130 -> 121 bytes .../patches/common/ips/minimizer_tourian.ips | Bin 387 -> 393 bytes .../patches/common/ips/new_game.ips | Bin 0 -> 332 bytes .../patches/common/ips/noflashing.ips | Bin 0 -> 2157 bytes .../patches/common/ips/objectives.ips | Bin 0 -> 2451 bytes .../patches/common/ips/percent.ips | Bin 0 -> 332 bytes .../patches/common/ips/plm_spawn.ips | Bin 0 -> 125 bytes .../patches/common/ips/rando_escape.ips | Bin 996 -> 1044 bytes .../common/ips/relaxed_round_robin_cf.ips | Bin 0 -> 165 bytes .../common/ips/remove_Infinite_Space_Jump.ips | Bin 0 -> 15 bytes .../ips/remove_elevators_doors_speed.ips | Bin 176 -> 434 bytes .../common/ips/remove_elevators_speed.ips | Bin 0 -> 109 bytes .../patches/common/ips/remove_fast_doors.ips | Bin 0 -> 333 bytes .../patches/common/ips/remove_rando_speed.ips | Bin 0 -> 124 bytes .../common/ips/remove_spinjumprestart.ips | Bin 0 -> 3397 bytes .../patches/common/ips/seed_display.ips | Bin 0 -> 679 bytes .../patches/common/ips/tracking.ips | Bin 0 -> 819 bytes .../patches/common/ips/vanilla_bugfixes.ips | Bin 0 -> 206 bytes .../patches/common/ips/varia_hud.ips | Bin 2242 -> 2617 bytes .../patches/common/ips/widescreen.ips | Bin 0 -> 9903 bytes .../variaRandomizer/patches/common/patches.py | 151 +- .../vanilla/ips/aqueduct_bomb_blocks.ips | Bin 0 -> 3345 bytes .../vanilla/ips/climb_disable_bomb_blocks.ips | Bin 0 -> 1502 bytes .../sm/variaRandomizer/rando/GraphBuilder.py | 283 +- .../variaRandomizer/rando/ItemLocContainer.py | 5 +- worlds/sm/variaRandomizer/rando/Items.py | 92 +- worlds/sm/variaRandomizer/rando/MiniSolver.py | 63 - worlds/sm/variaRandomizer/rando/RandoExec.py | 19 +- .../sm/variaRandomizer/rando/RandoServices.py | 10 +- .../sm/variaRandomizer/rando/RandoSettings.py | 27 +- worlds/sm/variaRandomizer/rando/RandoSetup.py | 93 +- .../sm/variaRandomizer/rando/Restrictions.py | 8 +- worlds/sm/variaRandomizer/rando/palettes.py | 23489 ---------------- worlds/sm/variaRandomizer/randomizer.py | 429 +- worlds/sm/variaRandomizer/rom/addressTypes.py | 72 + worlds/sm/variaRandomizer/rom/addresses.py | 51 + worlds/sm/variaRandomizer/rom/ips.py | 11 +- .../rom/objectivesAddresses.py | 64 + worlds/sm/variaRandomizer/rom/rom.py | 59 +- worlds/sm/variaRandomizer/rom/rom_patches.py | 16 +- worlds/sm/variaRandomizer/rom/rompatcher.py | 580 +- worlds/sm/variaRandomizer/solver.py | 224 - .../standard_presets/Torneio_SGPT3.json | 1 + .../sm/variaRandomizer/utils/doorsmanager.py | 120 +- worlds/sm/variaRandomizer/utils/objectives.py | 804 + worlds/sm/variaRandomizer/utils/parameters.py | 18 +- worlds/sm/variaRandomizer/utils/utils.py | 59 +- worlds/sm/variaRandomizer/utils/version.py | 4 +- 78 files changed, 3214 insertions(+), 24689 deletions(-) create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/color_blind.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/credits_varia.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/custom_music.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/disable_screen_shake.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/door_indicators_plms.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/elevators_speed.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/endingtotals.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/fast_doors.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/fast_doors_old.ips delete mode 100644 worlds/sm/variaRandomizer/patches/common/ips/g4_skip.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/hell.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/intro_text.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/lava_acid_physics.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/new_game.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/noflashing.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/objectives.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/percent.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/plm_spawn.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/relaxed_round_robin_cf.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/remove_Infinite_Space_Jump.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/remove_elevators_speed.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/remove_fast_doors.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/remove_rando_speed.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/remove_spinjumprestart.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/seed_display.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/tracking.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/vanilla_bugfixes.ips create mode 100644 worlds/sm/variaRandomizer/patches/common/ips/widescreen.ips create mode 100644 worlds/sm/variaRandomizer/patches/vanilla/ips/aqueduct_bomb_blocks.ips create mode 100644 worlds/sm/variaRandomizer/patches/vanilla/ips/climb_disable_bomb_blocks.ips delete mode 100644 worlds/sm/variaRandomizer/rando/MiniSolver.py delete mode 100644 worlds/sm/variaRandomizer/rando/palettes.py create mode 100644 worlds/sm/variaRandomizer/rom/addressTypes.py create mode 100644 worlds/sm/variaRandomizer/rom/addresses.py create mode 100644 worlds/sm/variaRandomizer/rom/objectivesAddresses.py delete mode 100644 worlds/sm/variaRandomizer/solver.py create mode 100644 worlds/sm/variaRandomizer/standard_presets/Torneio_SGPT3.json create mode 100644 worlds/sm/variaRandomizer/utils/objectives.py diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index 26b3e246e3..02599841d3 100644 --- a/worlds/sm/Options.py +++ b/worlds/sm/Options.py @@ -1,5 +1,6 @@ import typing from Options import Choice, Range, OptionDict, OptionList, Option, Toggle, DefaultOnToggle +from .variaRandomizer.utils.objectives import _goals class StartItemsRemovesFromPool(Toggle): """Remove items in starting inventory from pool.""" @@ -125,7 +126,7 @@ class AreaRandomization(Choice): display_name = "Area Randomization" option_off = 0 option_light = 1 - option_on = 2 + option_full = 2 default = 0 class AreaLayout(Toggle): @@ -183,9 +184,13 @@ class GravityBehaviour(Choice): option_Progressive = 2 default = 1 -class ElevatorsDoorsSpeed(DefaultOnToggle): - """Accelerate doors and elevators transitions.""" - display_name = "Elevators doors speed" +class ElevatorsSpeed(DefaultOnToggle): + """Accelerate elevators transitions.""" + display_name = "Elevators speed" + +class DoorsSpeed(DefaultOnToggle): + """Accelerate doors transitions.""" + display_name = "Doors speed" class SpinJumpRestart(Toggle): """Allows Samus to start spinning in mid air after jumping or falling.""" @@ -239,6 +244,94 @@ class VariaCustomPreset(OptionList): display_name = "Varia Custom Preset" default = {} + +class EscapeRando(Toggle): + """ + When leaving Tourian, get teleported to the exit of a random Map station (between Brinstar/Maridia/Norfair/Wrecked Ship). + You then have to find your way to the ship in the remaining time. Allotted time depends on area layout, but not on skill settings and is pretty generous. + + During the escape sequence: + - All doors are opened + - Maridia tube is opened + - The Hyper Beam can destroy Bomb , Power Bomb and Super Missile blocks and open blue/green gates from both sides + - All mini bosses are defeated + - All minor enemies are removed to allow you to move faster and remove lag + + During regular game only Crateria Map station door can be opened and activating the station will act as if all map stations were activated at once. + + Animals Challenges: + You can use the extra available time to: + - find the animals that are hidden behind a (now blue) map station door + - go to the vanilla animals door to cycle through the 4 available escapes, and complete as many escapes as you can + + Pick your challenge, or try to do both, but watch your timer! + """ + display_name = "Randomize the escape sequence" + +class RemoveEscapeEnemies(Toggle): + """Remove enemies during escape sequence, disable it to blast through enemies with your Hyper Beam and cause lag.""" + display_name = "Remove enemies during escape" + +class Tourian(Choice): + """ + Choose endgame Tourian behaviour: + Vanilla: regular vanilla Tourian + Fast: speed up Tourian to skip Metroids, Zebetites, and all cutscenes (including Mother Brain 3 fight). Golden Four statues are replaced by an invincible Gadora until all objectives are completed. + Disabled: skip Tourian entirely, ie. escape sequence is triggered as soon as all objectives are completed. + """ + display_name = "Endgame behavior with Tourian" + option_Vanilla = 0 + option_Fast = 1 + option_Disabled = 2 + default = 0 + +class Objective(OptionList): + """ + Choose which objectives are required to sink the Golden Four statue and to open access to Tourian. + You can choose from 0 to 5 objectives. + Note: If you leave the list empty no objective is required to access Tourian, ie. it's open. + Note: See the Tourian parameter to enable fast Tourian or trigger the escape when all objectives are completed. + Note: Current percentage of collected items is displayed in the inventory pause menu. + Note: Collect 100% items is excluded by default when randomizing the objectives list as it requires you to complete all the objectives. + Note: In AP, Items% and areas objectives are counted toward location checks, not items collected or received, except for "collect all upgrades" + + Format as a comma-separated list of objective names: ["kill three G4", "collect 75% items"]. + A full list of supported objectives can be found at: + https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/sm/utils/objectives.py + """ + display_name = "Objectives" + default = ["kill all G4"] + valid_keys = frozenset({name: goal for (name, goal) in _goals.items()}) + #valid_keys_casefold = True + +class HideItems(Toggle): + """ + Hides half of the visible items. + Items always visible: + - Energy Tank, Gauntlet + - Energy Tank, Terminator + - Morphing Ball + - Missile (Crateria moat) + - Missile (green Brinstar below super missile) + - Missile (above Crocomire) + - Power Bomb (lower Norfair above fire flea room) + - Missile (Gravity Suit) + - Missile (green Maridia shinespark) + """ + display_name = "Hide half the items" + +class RelaxedRoundRobinCF(Toggle): + """ + Changes Crystal Flashes behavior and requirements as follows: + + You can perform a Crystal Flash with any amount of ammo, but you need at least one Power Bomb to begin the process. + After consuming 1 ammo, Samus gains 50 energy, and it will try a different ammo type next, + cycling through Missiles, Supers, and Power Bombs as available. The cycling is to keep the consumption even between ammo types. + If one of your ammo types is at 0, it will be skipped. + The Crystal Flash ends when Samus is out of ammo or a total of 30 ammo has been consumed. + """ + display_name = "Relaxed round robin Crystal Flash" + sm_options: typing.Dict[str, type(Option)] = { "start_inventory_removes_from_pool": StartItemsRemovesFromPool, "preset": Preset, @@ -254,7 +347,7 @@ sm_options: typing.Dict[str, type(Option)] = { #"progression_difficulty": "normal", "morph_placement": MorphPlacement, #"suits_restriction": SuitsRestriction, - #"hide_items": "off", + "hide_items": HideItems, "strict_minors": StrictMinors, "missile_qty": MissileQty, "super_qty": SuperQty, @@ -269,8 +362,8 @@ sm_options: typing.Dict[str, type(Option)] = { #"minimizer": "off", #"minimizer_qty": "45", #"minimizer_tourian": "off", - #"escape_rando": "off", - #"remove_escape_enemies": "off", + "escape_rando": EscapeRando, + "remove_escape_enemies": RemoveEscapeEnemies, "fun_combat": FunCombat, "fun_movement": FunMovement, "fun_suits": FunSuits, @@ -279,7 +372,8 @@ sm_options: typing.Dict[str, type(Option)] = { "nerfed_charge": NerfedCharge, "gravity_behaviour": GravityBehaviour, #"item_sounds": "on", - "elevators_doors_speed": ElevatorsDoorsSpeed, + "elevators_speed": ElevatorsSpeed, + "fast_doors": DoorsSpeed, "spin_jump_restart": SpinJumpRestart, "rando_speed": SpeedKeep, "infinite_space_jump": InfiniteSpaceJump, @@ -290,4 +384,7 @@ sm_options: typing.Dict[str, type(Option)] = { "random_music": RandomMusic, "custom_preset": CustomPreset, "varia_custom_preset": VariaCustomPreset, + "tourian": Tourian, + "objective": Objective, + "relaxed_round_robin_cf": RelaxedRoundRobinCF, } diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index e8e4c56a90..24d676034b 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -86,7 +86,7 @@ class SMWorld(World): game: str = "Super Metroid" topology_present = True - data_version = 2 + data_version = 3 option_definitions = sm_options item_name_to_id = {value.Name: items_start_id + value.Id for key, value in ItemManager.Items.items() if value.Id != None} @@ -140,8 +140,8 @@ class SMWorld(World): if (item in itemPool): itemPool.remove(item) - missingPool = 105 - len(itemPool) + 1 - for i in range(1, missingPool): + missingPool = 109 - len(itemPool) + for i in range(missingPool): itemPool.append(ItemManager.Items['Nothing']) # Generate item pool @@ -209,10 +209,11 @@ class SMWorld(World): """ little-endian convert a 16-bit number to an array of numbers <= 255 each """ return [w & 0x00FF, (w & 0xFF00) >> 8] - # used for remote location Credits Spoiler of local items + # used for remote location Credits Spoiler of local items and Objectives' writeItemsMasks class DummyLocation: def __init__(self, name): self.Name = name + self.restricted = False def isBoss(self): return False @@ -337,7 +338,7 @@ class SMWorld(World): idx = 0 vanillaItemTypesCount = 21 for itemLoc in self.multiworld.get_locations(): - if itemLoc.player == self.player and locationsDict[itemLoc.name].Id != None: + if itemLoc.player == self.player and "Boss" not in locationsDict[itemLoc.name].Class: # item to place in this SM world: write full item data to tables if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items: itemId = ItemManager.Items[itemLoc.item.type].Id @@ -522,22 +523,38 @@ class SMWorld(World): # commit all the changes we've made here to the ROM romPatcher.commitIPS() - itemLocs = [ - ItemLocation(ItemManager.Items[itemLoc.item.type - if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else - 'ArchipelagoItem'], - locationsDict[itemLoc.name], True) - for itemLoc in self.multiworld.get_locations() if itemLoc.player == self.player - ] - romPatcher.writeItemsLocs(itemLocs) - itemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.multiworld.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.multiworld.get_locations() if itemLoc.item.player == self.player] progItemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type], locationsDict[itemLoc.name] if itemLoc.name in locationsDict and itemLoc.player == self.player else self.DummyLocation(self.multiworld.get_player_name(itemLoc.player) + " " + itemLoc.name), True) for itemLoc in self.multiworld.get_locations() if itemLoc.item.player == self.player and itemLoc.item.advancement == True] - # progItemLocs = [ItemLocation(ItemManager.Items[itemLoc.item.type if itemLoc.item.type in ItemManager.Items else 'ArchipelagoItem'], locationsDict[itemLoc.name], True) for itemLoc in self.multiworld.get_locations() if itemLoc.player == self.player and itemLoc.item.player == self.player and itemLoc.item.advancement == True] - + + + romPatcher.writeObjectives(itemLocs, romPatcher.settings["tourian"]) + romPatcher.writeItemsLocs(self.itemLocs) + # romPatcher.writeSplitLocs(self.variaRando.args.majorsSplit, itemLocs, progItemLocs) + romPatcher.writeItemsNumber() + if not romPatcher.settings["isPlando"]: + romPatcher.writeSeed(romPatcher.settings["seed"]) # lol if race mode romPatcher.writeSpoiler(itemLocs, progItemLocs) romPatcher.writeRandoSettings(self.variaRando.randoExec.randoSettings, itemLocs) + romPatcher.writeDoorConnections(romPatcher.settings["doors"]) + romPatcher.writeVersion(romPatcher.settings["displayedVersion"]) + if romPatcher.settings["ctrlDict"] is not None: + romPatcher.writeControls(romPatcher.settings["ctrlDict"]) + if romPatcher.settings["moonWalk"] == True: + romPatcher.enableMoonWalk() + + romPatcher.writeMagic() + romPatcher.writeMajorsSplit(romPatcher.settings["majorsSplit"]) + + #if self.settings["isPlando"] and self.race is None: + # doorsPtrs = GraphUtils.getAps2DoorsPtrs() + # self.writePlandoTransitions(self.settings["plando"]["graphTrans"], doorsPtrs, + # self.settings["plando"]["maxTransitions"]) + # self.writePlandoAddresses(self.settings["plando"]["visitedLocations"]) + #if self.settings["isPlando"] and self.settings["plando"]["additionalETanks"] != 0: + # self.writeAdditionalETanks(self.settings["plando"]["additionalETanks"]) + + romPatcher.end() def generate_output(self, output_directory: str): outfilebase = self.multiworld.get_out_file_name_base(self.player) @@ -682,6 +699,41 @@ class SMWorld(World): loc.place_locked_item(item) loc.address = loc.item.code = None + def post_fill(self): + self.itemLocs = [ + ItemLocation(ItemManager.Items[itemLoc.item.type + if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else + 'ArchipelagoItem'], + locationsDict[itemLoc.name], itemLoc.item.player, True) + for itemLoc in self.multiworld.get_locations(self.player) + ] + self.progItemLocs = [ + ItemLocation(ItemManager.Items[itemLoc.item.type + if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else + 'ArchipelagoItem'], + locationsDict[itemLoc.name], itemLoc.item.player, True) + for itemLoc in self.multiworld.get_locations(self.player) if itemLoc.item.advancement + ] + for itemLoc in self.itemLocs: + if itemLoc.Item.Class == "Boss": + itemLoc.Item.Class = "Minor" + for itemLoc in self.progItemLocs: + if itemLoc.Item.Class == "Boss": + itemLoc.Item.Class = "Minor" + + localItemLocs = [il for il in self.itemLocs if il.player == self.player] + localprogItemLocs = [il for il in self.progItemLocs if il.player == self.player] + + escapeTrigger = (localItemLocs, localprogItemLocs, 'Full') if self.variaRando.randoExec.randoSettings.restrictions["EscapeTrigger"] else None + escapeOk = self.variaRando.randoExec.graphBuilder.escapeGraph(self.variaRando.container, self.variaRando.randoExec.areaGraph, self.variaRando.randoExec.randoSettings.maxDiff, escapeTrigger) + assert escapeOk, "Could not find a solution for escape" + + self.variaRando.doors = GraphUtils.getDoorConnections(self.variaRando.randoExec.areaGraph, + self.variaRando.args.area, self.variaRando.args.bosses, + self.variaRando.args.escapeRando) + + self.variaRando.randoExec.postProcessItemLocs(self.itemLocs, self.variaRando.args.hideItems) + @classmethod def stage_post_fill(cls, world): new_state = CollectionState(world) diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips index b44fa72656dea2a6b4c65798678d4ddc635fe094..88a4a308cfbf5e99129afe3229368fe1f0ea41bd 100644 GIT binary patch delta 479 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 H&mR6?0Uv@g delta 1746 zcmZ`)eN0nV6hHU1uV1wIfKod?#^d1_A6o`PGmP1sOkK?Y9nNLbMp$NIXEz|(W*9Mj zrI9Rt;3Ioxl7$W1X||%QW))EvSK~4TGMBpOV2!gV3Kd%DaGTID*m=bz?hpUD=XcKU zp5Hz9-gnRQh}6Xaa$-iAzJ1OPlm%qv2OVJ7fsFkt3U(g-MSqA|Rk4ylHORRq#d1~g zwiHCKPw#`&JCN>z11ITky6Mn?ZhGPreW7ULH0_s7oTcq@Fi@bfM;l|7!ecLj4Pe3z zAl#bgh1>JIXEw9GufN}~_RPgWR4R~6F zqKO4<=h>Y)RCTFN2z{)3gAyeAii8pk{eD0nRrNY>XL?K8~ z4_(2+@7~`>->)Z&#q&vr#4X7UDz^r>-O*O7_q~)Y6zLWFQlG-{@6^X}Of{@b=$*5( zt^ur$>lLdFjU>YxFl4B_yRt45ab$A@J8hesSIMQGHm^`(Ewlo0;3wNN&Zly<_0ZzR3GFM2CRDsrH86UsEcInGnSOlQf#I&#W31J}a@Ed1+J#l-Q2y{R$*f zj_My!?F-a(qzn&2Uo3=fl-S&xONAk1j1L&s0&@$Q9i_q%G>xUg?GjsK>{wn6Ja!WR zQoTOYaT%XG8-7+B(IQ``Eoz_0BJ zsCC!Xx*KZUklHF;rvGUV;FYHL%`sEUfR3h{@-bxvD8^ell$LyEstgLDWuH>gjy2ic zG}vWsalD)<%ni5zeAJW^C)GkGMD+pbXniLJZMspq15GSsAC7U{WRsX(u}=K7;)$a8 z1tuRYc+R@5qiFptyeVp3*56I2C!cOwm zP6S^fcw3ENLIULo<`Hb)hTtlKUATYq4-uz17;&nWCtJdX{5H*Y}n zLJx+T5v)=o_zFS!4XKuCRRdt4KH_A;00{y1T$ueS9J2~?CaeV|kMFj060sg+yc9En zn%~Qv7z`Tg(Br_!w`w^1U@)|oV+Ml~s__BvLLw7hfm1^vA1}FSOn42dm7?4vQWcUK z+E+m#wS-Be-;hFR&5*#iX*zw_ER1zH#R59M107Vtu?}bK1Wi0rV+wdwOt=XQykCYz zd%jcJI(-ZX8sj@k5Rhua-N$(RKEt1b0W@Y3ba3oo&`p67B=#qN+}hf(c*3l~{E+-n`KL1?6fyz=Q`NgLlaQsfFWmtDpr1c8oeCO^ONsiAB4a z;g)u_dC|&YhlgS!6!i0HE?P=*Z;Ufl!wz>|yb`IX_D&-fVSOm#ULvVl#qT#B?(dW` 0})) areas = [startAp.GraphArea] + if startAp.GraphArea in forcedAreas: + forcedAreas.remove(startAp.GraphArea) GraphUtils.log.debug("availAreas: {}".format(availAreas)) + GraphUtils.log.debug("forcedAreas: {}".format(forcedAreas)) GraphUtils.log.debug("areas: {}".format(areas)) inBossCheck = lambda ap: ap.Boss and ap.Name.endswith("In") nLocs = 0 @@ -260,22 +266,27 @@ class GraphUtils: def openTransitions(): nonlocal areas, inBossCheck, usedAPs return GraphUtils.getAPs(lambda ap: ap.GraphArea in areas and not ap.isInternal() and not inBossCheck(ap) and not ap in usedAPs) - while nLocs < locLimit or len(openTransitions()) < trLimit: + while nLocs < locLimit or len(openTransitions()) < trLimit or len(forcedAreas) > 0: GraphUtils.log.debug("openTransitions="+str([ap.Name for ap in openTransitions()])) fromAreas = availAreas - if nLocs >= locLimit: + if len(openTransitions()) <= 1: # dont' get stuck by adding dead ends + GraphUtils.log.debug("avoid being stuck") + fromAreas = [area for area in fromAreas if len(GraphUtils.getAPs(lambda ap: ap.GraphArea == area and not ap.isInternal())) > 1] + elif len(forcedAreas) > 0: # no risk to get stuck, we can pick a forced area if necessary + GraphUtils.log.debug("add forced area") + fromAreas = forcedAreas + elif nLocs >= locLimit: # we just need transitions, avoid adding a huge area GraphUtils.log.debug("not enough open transitions") - # we just need transitions, avoid adding a huge area fromAreas = [] n = trLimit - len(openTransitions()) while len(fromAreas) == 0: fromAreas = [area for area in availAreas if len(GraphUtils.getAPs(lambda ap: not ap.isInternal())) > n] n -= 1 - minLocs = min([getNLocs(lambda loc: loc.GraphArea == area) for area in fromAreas]) + minLocs = min([getNLocs(lambda loc: loc.GraphArea == area) for area in fromAreas]) fromAreas = [area for area in fromAreas if getNLocs(lambda loc: loc.GraphArea == area) == minLocs] - elif len(openTransitions()) <= 1: # dont' get stuck by adding dead ends - fromAreas = [area for area in fromAreas if len(GraphUtils.getAPs(lambda ap: ap.GraphArea == area and not ap.isInternal())) > 1] nextArea = random.choice(fromAreas) + if nextArea in forcedAreas: + forcedAreas.remove(nextArea) GraphUtils.log.debug("nextArea="+str(nextArea)) apCheck = lambda ap: not ap.isInternal() and not inBossCheck(ap) and ap not in usedAPs possibleSources = GraphUtils.getAPs(lambda ap: ap.GraphArea in areas and apCheck(ap)) @@ -399,18 +410,37 @@ class GraphUtils: def escapeAnimalsTransitions(graph, possibleTargets, firstEscape): n = len(possibleTargets) assert (n < 4 and firstEscape is not None) or (n <= 4 and firstEscape is None), "Invalid possibleTargets list: " + str(possibleTargets) - # first get our list of 4 entries for escape patch + GraphUtils.log.debug("escapeAnimalsTransitions. possibleTargets="+str(possibleTargets)+", firstEscape="+str(firstEscape)) if n >= 1: - # get actual animals: pick one of the remaining targets - animalsAccess = possibleTargets.pop() - graph.EscapeAttributes['Animals'] = animalsAccess - # we now have at most 3 targets left, fill up to fill cycling 4 targets for animals suprise - possibleTargets.append('Climb Bottom Left') + # complete possibleTargets. we need at least 2: one to + # hide the animals in, and one to connect the vanilla + # animals door to + if not any(t[1].Name == 'Climb Bottom Left' for t in graph.InterAreaTransitions): + # add standard Climb if not already in graph: it can be in Crateria-less minimizer + Disabled Tourian case + possibleTargets.append('Climb Bottom Left') + # make the escape possibilities loop by adding back the first escape if firstEscape is not None: possibleTargets.append(firstEscape) poss = possibleTargets[:] while len(possibleTargets) < 4: possibleTargets.append(poss.pop(random.randint(0, len(poss)-1))) + n = len(possibleTargets) + # check if we can both hide the animals and connect the vanilla animals door to a cycling escape + if n >= 2: + # get actual animals: pick the first of the remaining targets (will contain a map room AP) + animalsAccess = possibleTargets.pop(0) + graph.EscapeAttributes['Animals'] = animalsAccess + # poss will contain the remaining map room AP(s) + optional AP(s) added above, to + # get the cycling 4 escapes from vanilla animals room + poss = possibleTargets[:] + GraphUtils.log.debug("escapeAnimalsTransitions. poss="+str(poss)) + while len(possibleTargets) < 4: + if len(poss) > 1: + possibleTargets.append(poss.pop(random.randint(0, len(poss)-1))) + else: + # no more possible variety, spam the last possible escape + possibleTargets.append(poss[0]) + else: # failsafe: if not enough targets left, abort and do vanilla animals animalsAccess = 'Flyway Right' diff --git a/worlds/sm/variaRandomizer/graph/location.py b/worlds/sm/variaRandomizer/graph/location.py index fb20585332..5831a4cc4c 100644 --- a/worlds/sm/variaRandomizer/graph/location.py +++ b/worlds/sm/variaRandomizer/graph/location.py @@ -64,6 +64,8 @@ class Location: smbm.removeItem(self.itemName) self.difficulty = self.difficulty & postAvailable + if self.locDifficulty is not None: + self.locDifficulty = self.locDifficulty & postAvailable def evalComeBack(self, smbm, areaGraph, ap): if self.difficulty.bool == True: @@ -102,7 +104,7 @@ class Location: def define_location( Area, GraphArea, SolveArea, Name, Class, CanHidden, Address, Id, - Visibility, Room, VanillaItemType=None, AccessFrom=None, Available=None, PostAvailable=None, HUD=None): + Visibility, Room, VanillaItemType=None, BossItemType=None, AccessFrom=None, Available=None, PostAvailable=None, HUD=None): name = Name.replace(' ', '').replace(',', '') + 'Location' subclass = type(name, (Location,), { 'Area': Area, @@ -116,6 +118,7 @@ def define_location( 'Visibility': Visibility, 'Room': Room, 'VanillaItemType': VanillaItemType, + 'BossItemType': BossItemType, 'HUD': HUD, 'AccessFrom': AccessFrom, 'Available': Available, @@ -322,6 +325,7 @@ define_location( Id=None, Visibility="Hidden", Room='Kraid Room', + BossItemType="Kraid" ), "Varia Suit": define_location( @@ -445,12 +449,15 @@ define_location( GraphArea="LowerNorfair", SolveArea="Ridley Boss", Name="Ridley", - Class=["Boss"], + Class=["Boss", "Scavenger"], CanHidden=False, Address=0xB055B056, - Id=None, + Id=0xaa, Visibility="Hidden", Room="Ridley's Room", + VanillaItemType="Ridley", + BossItemType="Ridley", + HUD=16 ), "Energy Tank, Ridley": define_location( @@ -531,6 +538,7 @@ define_location( Id=None, Visibility="Hidden", Room="Phantoon's Room", + BossItemType="Phantoon" ), "Right Super, Wrecked Ship": define_location( @@ -641,6 +649,7 @@ define_location( Id=None, Visibility="Hidden", Room="Draygon's Room", + BossItemType="Draygon" ), "Space Jump": define_location( @@ -669,6 +678,63 @@ define_location( Visibility="Hidden", CanHidden=False, Room='Mother Brain Room', + BossItemType="MotherBrain" +), + "Spore Spawn": +define_location( + Area="Brinstar", + GraphArea="GreenPinkBrinstar", + SolveArea="Pink Brinstar", + Name="Spore Spawn", + Class=["Boss"], + CanHidden=False, + Address=0xB055B055, + Id=None, + Visibility="Hidden", + Room='Spore Spawn Room', + BossItemType="SporeSpawn" +), + "Botwoon": +define_location( + Area="Maridia", + GraphArea="EastMaridia", + SolveArea="Maridia Pink Top", + Name="Botwoon", + Class=["Boss"], + CanHidden=False, + Address=0xB055B055, + Id=None, + Visibility="Hidden", + Room="Botwoon's Room", + BossItemType="Botwoon" +), + "Crocomire": +define_location( + Area="Norfair", + GraphArea="Crocomire", + SolveArea="Crocomire", + Name="Crocomire", + Class=["Boss"], + CanHidden=False, + Address=0xB055B055, + Id=None, + Visibility="Hidden", + Room="Crocomire's Room", + BossItemType="Crocomire" +), + "Golden Torizo": +define_location( + Area="LowerNorfair", + GraphArea="LowerNorfair", + SolveArea="Lower Norfair Screw Attack", + Name="Golden Torizo", + Class=["Boss"], + CanHidden=False, + Address=0xB055B055, + Id=None, + Visibility="Hidden", + Room="Golden Torizo's Room", + BossItemType="GoldenTorizo" ), ###### MINORS "Power Bomb (Crateria surface)": diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py index 501dab8f26..f49fc6fc26 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py @@ -42,9 +42,8 @@ accessPoints = [ }, traverse = Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.AreaRandoMoreBlueDoors), sm.traverse('GreenPiratesShaftBottomRight'))), roomInfo = {'RoomPtr':0x99bd, "area": 0x0, 'songs':[0x99ce]}, - # the doorAsmPtr 7FE00 is set by the g4_skip.ips patch, we have to call it exitInfo = {'DoorPtr':0x8c52, 'direction': 0x4, "cap": (0x1, 0x6), "bitFlag": 0x0, - "screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xfe00}, + "screen": (0x0, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000}, entryInfo = {'SamusX':0xcc, 'SamusY':0x688, 'song': 0x9}, dotOrientation = 'e'), AccessPoint('Moat Right', 'Crateria', { @@ -173,7 +172,9 @@ accessPoints = [ sm.canPassSpongeBath()), sm.wand(sm.wnot(Bosses.bossDead(sm, 'Phantoon')), RomPatches.has(sm.player, RomPatches.SpongeBathBlueDoor)))), - 'PhantoonRoomOut': Cache.ldeco(lambda sm: sm.wand(sm.traverse('WreckedShipMainShaftBottom'), sm.canPassBombPassages())) + 'PhantoonRoomOut': Cache.ldeco(lambda sm: sm.wand(sm.traverse('WreckedShipMainShaftBottom'), sm.canPassBombPassages())), + 'Bowling': Cache.ldeco(lambda sm: sm.wand(sm.canMorphJump(), + sm.canPassBowling())) }, internal=True, start={'spawn':0x0300, 'doors':[0x83,0x8b], 'patches':[RomPatches.SpongeBathBlueDoor, RomPatches.WsEtankBlueDoor], @@ -183,6 +184,9 @@ accessPoints = [ 'Wrecked Ship Main': lambda sm: SMBool(True), 'Crab Maze Left': Cache.ldeco(lambda sm: sm.canPassForgottenHighway(True)) }, internal=True), + AccessPoint('Bowling', 'WreckedShip', { + 'West Ocean Left': lambda sm: SMBool(True) + }, internal=True), AccessPoint('Crab Maze Left', 'WreckedShip', { 'Wrecked Ship Back': Cache.ldeco(lambda sm: sm.canPassForgottenHighway(False)) }, traverse=Cache.ldeco(lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.AreaRandoBlueDoors), @@ -245,6 +249,8 @@ accessPoints = [ sm.canUsePowerBombs())) }, internal=True), AccessPoint('LN Above GT', 'LowerNorfair', { + 'LN Entrance': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']), + sm.canPassBombPassages())), 'Screw Attack Bottom': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']), sm.enoughStuffGT())) }, internal=True), @@ -423,11 +429,9 @@ accessPoints = [ entryInfo = {'SamusX':0x134, 'SamusY':0x288, 'song': 0x15}, dotOrientation = 'se'), AccessPoint('Crocomire Speedway Bottom', 'Norfair', { - 'Business Center': Cache.ldeco(lambda sm: sm.wor(sm.wand(sm.canPassFrogSpeedwayRightToLeft(), - sm.canHellRun(**Settings.hellRunsTable['Ice']['Croc -> Norfair Entrance'])), - sm.wand(sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Croc -> Norfair Entrance']), - sm.canGrappleEscape(), - sm.haveItem('Super')))), + 'Grapple Escape': lambda sm: sm.canGrappleEscape(), + 'Business Center': Cache.ldeco(lambda sm: sm.wand(sm.canPassFrogSpeedwayRightToLeft(), + sm.canHellRun(**Settings.hellRunsTable['Ice']['Croc -> Norfair Entrance']))), 'Bubble Mountain Bottom': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['Ice']['Croc -> Bubble Mountain'])), 'Kronic Boost Room Bottom Left': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Kronic Boost Room <-> Croc']), sm.haveItem('Morph'))) @@ -437,6 +441,10 @@ accessPoints = [ "screen": (0x3, 0x0), "distanceToSpawn": 0x8000, "doorAsmPtr": 0x0000}, entryInfo = {'SamusX':0xc57, 'SamusY':0x2b8}, dotOrientation = 'se'), + AccessPoint('Grapple Escape', 'Norfair', { + 'Business Center': lambda sm: sm.haveItem('Super'), + 'Crocomire Speedway Bottom': lambda sm: sm.canHellRunBackFromGrappleEscape() + }, internal=True), AccessPoint('Bubble Mountain', 'Norfair', { 'Business Center': lambda sm: sm.canExitCathedral(Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Norfair Entrance']), 'Bubble Mountain Top': lambda sm: sm.canClimbBubbleMountain(), @@ -494,8 +502,7 @@ accessPoints = [ dotOrientation = 'se'), ### West Maridia AccessPoint('Main Street Bottom', 'WestMaridia', { - 'Red Fish Room Left': Cache.ldeco(lambda sm: sm.wand(sm.canGoUpMtEverest(), - sm.haveItem('Morph'))), + 'Red Fish Room Bottom': lambda sm: sm.canGoUpMtEverest(), 'Crab Hole Bottom Left': Cache.ldeco(lambda sm: sm.wand(sm.haveItem('Morph'), sm.canTraverseCrabTunnelLeftToRight())), # this transition leads to EastMaridia directly @@ -532,12 +539,17 @@ accessPoints = [ entryInfo = {'SamusX':0x28, 'SamusY':0x188}, dotOrientation = 'se'), AccessPoint('Red Fish Room Left', 'WestMaridia', { - 'Main Street Bottom': Cache.ldeco(lambda sm: sm.haveItem('Morph')) # just go down + 'Red Fish Room Bottom': Cache.ldeco(lambda sm: sm.haveItem('Morph')) # just go down }, roomInfo = {'RoomPtr':0xd104, "area": 0x4}, exitInfo = {'DoorPtr':0xa480, 'direction': 0x5, "cap": (0x2e, 0x36), "bitFlag": 0x40, "screen": (0x2, 0x3), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xe367}, entryInfo = {'SamusX':0x34, 'SamusY':0x88}, dotOrientation = 'w'), + AccessPoint('Red Fish Room Bottom', 'WestMaridia', { + 'Main Street Bottom': lambda sm: SMBool(True), # just go down + 'Red Fish Room Left': Cache.ldeco(lambda sm: sm.wand(sm.haveItem('Morph'), + sm.canJumpUnderwater())) + }, internal=True), AccessPoint('Crab Shaft Left', 'WestMaridia', { 'Main Street Bottom': lambda sm: SMBool(True), # fall down 'Beach': lambda sm: sm.canDoOuterMaridia(), @@ -586,7 +598,9 @@ accessPoints = [ dotOrientation = 'ne'), ### East Maridia AccessPoint('Aqueduct Top Left', 'EastMaridia', { - 'Aqueduct Bottom': lambda sm: sm.canUsePowerBombs() + 'Aqueduct Bottom': lambda sm: sm.wor(sm.wand(RomPatches.has(sm.player, RomPatches.AqueductBombBlocks), + sm.canDestroyBombWallsUnderwater()), + sm.canUsePowerBombs()) }, roomInfo = {'RoomPtr':0xd5a7, "area": 0x4}, exitInfo = {'DoorPtr':0xa708, 'direction': 0x5, "cap": (0x1e, 0x36), "bitFlag": 0x0, "screen": (0x1, 0x3), "distanceToSpawn": 0x8000, "doorAsmPtr": 0xe398}, @@ -596,7 +610,8 @@ accessPoints = [ 'Aqueduct Top Left': Cache.ldeco(lambda sm: sm.wand(sm.canDestroyBombWallsUnderwater(), # top left bomb blocks sm.canJumpUnderwater())), 'Post Botwoon': Cache.ldeco(lambda sm: sm.wand(sm.canJumpUnderwater(), - sm.canDefeatBotwoon())), # includes botwoon hallway conditions + sm.canPassBotwoonHallway(), + sm.haveItem('Botwoon'))), 'Left Sandpit': lambda sm: sm.canAccessSandPits(), 'Right Sandpit': lambda sm: sm.canAccessSandPits(), 'Aqueduct': Cache.ldeco(lambda sm: sm.wand(sm.wor(sm.haveItem('SpeedBooster'), diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py index de18ab86d6..3d4d9b4f2d 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_helpers.py @@ -241,6 +241,21 @@ class HelpersGraph(Helpers): sm = self.smbm return sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Bubble -> Speed Booster w/Speed' if sm.haveItem('SpeedBooster') else 'Bubble -> Speed Booster']) + # with door color rando, there can be situations where you have to come back from the missile + # loc without being able to open the speed booster door + @Cache.decorator + def canHellRunBackFromSpeedBoosterMissile(self): + sm = self.smbm + # require more health to count 1st hell run + way back is slower + hellrun = 'MainUpperNorfair' + tbl = Settings.hellRunsTable[hellrun]['Bubble -> Speed Booster'] + mult = tbl['mult'] + minE = tbl['minE'] + mult *= 0.66 if sm.haveItem('SpeedBooster') else 0.33 # speed booster usable for 1st hell run + return sm.wor(RomPatches.has(sm.player, RomPatches.SpeedAreaBlueDoors), + sm.traverse('SpeedBoosterHallRight'), + sm.canHellRun(hellrun, mult, minE)) + @Cache.decorator def canAccessDoubleChamberItems(self): sm = self.smbm @@ -253,6 +268,19 @@ class HelpersGraph(Helpers): sm.knowsDoubleChamberWallJump()), sm.canHellRun(hellRun['hellRun'], hellRun['mult']*0.8, hellRun['minE']))) + @Cache.decorator + def canExitWaveBeam(self): + sm = self.smbm + return sm.wor(sm.haveItem('Morph'), # exit through lower passage under the spikes + sm.wand(sm.wor(sm.haveItem('SpaceJump'), # exit through blue gate + sm.haveItem('Grapple')), + sm.wor(sm.haveItem('Wave'), + sm.wand(sm.heatProof(), # hell run + green gate glitch is too much + sm.canBlueGateGlitch(), + # if missiles were required to open the door, require two packs as no farming around + sm.wor(sm.wnot(SMBool('Missile' in sm.traverse('DoubleChamberRight').items)), + sm.itemCountOk("Missile", 2)))))) + def canExitCathedral(self, hellRun): # from top: can use bomb/powerbomb jumps # from bottom: can do a shinespark or use space jump @@ -272,16 +300,40 @@ class HelpersGraph(Helpers): @Cache.decorator def canGrappleEscape(self): sm = self.smbm - return sm.wor(sm.wor(sm.haveItem('SpaceJump'), - sm.wand(sm.canInfiniteBombJump(), # IBJ from lava...either have grav or freeze the enemy there if hellrunning (otherwise single DBJ at the end) - sm.wor(sm.heatProof(), - sm.haveItem('Gravity'), - sm.haveItem('Ice')))), - sm.haveItem('Grapple'), - sm.wand(sm.haveItem('SpeedBooster'), - sm.wor(sm.haveItem('HiJump'), # jump from the blocks below - sm.knowsShortCharge())), # spark from across the grapple blocks - sm.wand(sm.haveItem('HiJump'), sm.canSpringBallJump())) # jump from the blocks below + access = sm.wor(sm.wor(sm.haveItem('SpaceJump'), + sm.wand(sm.canInfiniteBombJump(), # IBJ from lava...either have grav or freeze the enemy there if hellrunning (otherwise single DBJ at the end) + sm.wor(sm.heatProof(), + sm.haveItem('Gravity'), + sm.haveItem('Ice')))), + sm.haveItem('Grapple'), + sm.wand(sm.haveItem('SpeedBooster'), + sm.wor(sm.haveItem('HiJump'), # jump from the blocks below + sm.knowsShortCharge())), # spark from across the grapple blocks + sm.wand(sm.haveItem('HiJump'), sm.canSpringBallJump())) # jump from the blocks below + hellrun = 'MainUpperNorfair' + tbl = Settings.hellRunsTable[hellrun]['Croc -> Norfair Entrance'] + mult = tbl['mult'] + minE = tbl['minE'] + if 'InfiniteBombJump' in access.knows or 'ShortCharge' in access.knows: + mult *= 0.7 + elif 'SpaceJump' in access.items: + mult *= 1.5 + elif 'Grapple' in access.items: + mult *= 1.25 + return sm.wand(access, + sm.canHellRun(hellrun, mult, minE)) + + @Cache.decorator + def canHellRunBackFromGrappleEscape(self): + sm = self.smbm + # require more health to count 1st hell run from croc speedway bottom to here+hellrun back (which is faster) + hellrun = 'MainUpperNorfair' + tbl = Settings.hellRunsTable[hellrun]['Croc -> Norfair Entrance'] + mult = tbl['mult'] + minE = tbl['minE'] + mult *= 0.6 + return sm.canHellRun(hellrun, mult, minE) + @Cache.decorator def canPassFrogSpeedwayRightToLeft(self): @@ -733,7 +785,9 @@ class HelpersGraph(Helpers): @Cache.decorator def canExitPreciousRoomRandomized(self): sm = self.smbm - suitlessRoomExit = sm.canSpringBallJump() + suitlessRoomExit = sm.wand(sm.wnot(sm.haveItem('Gravity')), + sm.canJumpUnderwater(), + sm.canSpringBallJump()) if suitlessRoomExit.bool == False: if self.getDraygonConnection() == 'KraidRoomIn': suitlessRoomExit = sm.canShortCharge() # charge spark in kraid's room @@ -764,3 +818,27 @@ class HelpersGraph(Helpers): return sm.wand(sm.traverse('MainStreetBottomRight'), sm.wor(sm.haveItem('Super'), RomPatches.has(sm.player, RomPatches.AreaRandoGatesOther))) + + @Cache.decorator + def canAccessShaktoolFromPantsRoom(self): + sm = self.smbm + return sm.wor(sm.wand(sm.haveItem('Ice'), # puyo clip + sm.wor(sm.wand(sm.haveItem('Gravity'), + sm.knowsPuyoClip()), + sm.wand(sm.haveItem('Gravity'), + sm.haveItem('XRayScope'), + sm.knowsPuyoClipXRay()), + sm.knowsSuitlessPuyoClip())), + sm.wand(sm.haveItem('Grapple'), # go through grapple block + sm.wor(sm.wand(sm.haveItem('Gravity'), + sm.wor(sm.wor(sm.wand(sm.haveItem('HiJump'), sm.knowsAccessSpringBallWithHiJump()), + sm.haveItem('SpaceJump')), + sm.knowsAccessSpringBallWithGravJump(), + sm.wand(sm.haveItem('Bomb'), + sm.wor(sm.knowsAccessSpringBallWithBombJumps(), + sm.wand(sm.haveItem('SpringBall'), + sm.knowsAccessSpringBallWithSpringBallBombJumps()))), + sm.wand(sm.haveItem('SpringBall'), sm.knowsAccessSpringBallWithSpringBallJump()))), + sm.wand(sm.haveItem('SpaceJump'), sm.knowsAccessSpringBallWithFlatley()))), + sm.wand(sm.haveItem('XRayScope'), sm.knowsAccessSpringBallWithXRayClimb()), # XRay climb + sm.canCrystalFlashClip()) diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py index 5bf3c0e6c3..d924811a99 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py @@ -157,7 +157,7 @@ locationsDict["Energy Tank, Crocomire"].AccessFrom = { 'Crocomire Room Top': lambda sm: SMBool(True) } locationsDict["Energy Tank, Crocomire"].Available = ( - lambda sm: sm.wand(sm.enoughStuffCroc(), + lambda sm: sm.wand(sm.haveItem('Crocomire'), sm.wor(sm.haveItem('Grapple'), sm.haveItem('SpaceJump'), sm.energyReserveCountOk(3/sm.getDmgReduction()[0]))) @@ -176,7 +176,7 @@ locationsDict["Grapple Beam"].AccessFrom = { 'Crocomire Room Top': lambda sm: SMBool(True) } locationsDict["Grapple Beam"].Available = ( - lambda sm: sm.wand(sm.enoughStuffCroc(), + lambda sm: sm.wand(sm.haveItem('Crocomire'), sm.wor(sm.wand(sm.haveItem('Morph'), sm.canFly()), sm.wand(sm.haveItem('SpeedBooster'), @@ -220,11 +220,7 @@ locationsDict["Wave Beam"].Available = ( lambda sm: sm.traverse('DoubleChamberRight') ) locationsDict["Wave Beam"].PostAvailable = ( - lambda sm: sm.wor(sm.haveItem('Morph'), # exit through lower passage under the spikes - sm.wand(sm.wor(sm.haveItem('SpaceJump'), # exit through blue gate - sm.haveItem('Grapple')), - sm.wor(sm.wand(sm.canBlueGateGlitch(), sm.heatProof()), # hell run + green gate glitch is too much - sm.haveItem('Wave')))) + lambda sm: sm.canExitWaveBeam() ) locationsDict["Ridley"].AccessFrom = { 'RidleyRoomIn': lambda sm: SMBool(True) @@ -233,7 +229,7 @@ locationsDict["Ridley"].Available = ( lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']), sm.enoughStuffsRidley()) ) locationsDict["Energy Tank, Ridley"].AccessFrom = { - 'RidleyRoomIn': lambda sm: sm.haveItem('Ridley') + 'RidleyRoomIn': lambda sm: sm.wand(sm.haveItem('Ridley'), sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])) } locationsDict["Energy Tank, Ridley"].Available = ( lambda sm: sm.haveItem('Morph') @@ -354,27 +350,8 @@ locationsDict["Spring Ball"].AccessFrom = { 'Oasis Bottom': lambda sm: sm.canTraverseSandPits() } locationsDict["Spring Ball"].Available = ( - lambda sm: sm.wand(sm.canUsePowerBombs(), # in Shaktool room to let Shaktool access the sand blocks - sm.wor(sm.wand(sm.haveItem('Ice'), # puyo clip - sm.wor(sm.wand(sm.haveItem('Gravity'), - sm.knowsPuyoClip()), - sm.wand(sm.haveItem('Gravity'), - sm.haveItem('XRayScope'), - sm.knowsPuyoClipXRay()), - sm.knowsSuitlessPuyoClip())), - sm.wand(sm.haveItem('Grapple'), # go through grapple block - sm.wor(sm.wand(sm.haveItem('Gravity'), - sm.wor(sm.wor(sm.wand(sm.haveItem('HiJump'), sm.knowsAccessSpringBallWithHiJump()), - sm.haveItem('SpaceJump')), - sm.knowsAccessSpringBallWithGravJump(), - sm.wand(sm.haveItem('Bomb'), - sm.wor(sm.knowsAccessSpringBallWithBombJumps(), - sm.wand(sm.haveItem('SpringBall'), - sm.knowsAccessSpringBallWithSpringBallBombJumps()))), - sm.wand(sm.haveItem('SpringBall'), sm.knowsAccessSpringBallWithSpringBallJump()))), - sm.wand(sm.haveItem('SpaceJump'), sm.knowsAccessSpringBallWithFlatley()))), - sm.wand(sm.haveItem('XRayScope'), sm.knowsAccessSpringBallWithXRayClimb()), # XRay climb - sm.canCrystalFlashClip()), + lambda sm: sm.wand(sm.canAccessShaktoolFromPantsRoom(), + sm.canUsePowerBombs(), # in Shaktool room to let Shaktool access the sand blocks sm.wor(sm.haveItem('Gravity'), sm.canUseSpringBall())) # acess the item in spring ball room ) locationsDict["Spring Ball"].PostAvailable = ( @@ -406,10 +383,37 @@ locationsDict["Space Jump"].PostAvailable = ( lambda sm: Bosses.bossDead(sm, 'Draygon') ) locationsDict["Mother Brain"].AccessFrom = { - 'Golden Four': lambda sm: Bosses.allBossesDead(sm) + 'Golden Four': lambda sm: sm.canPassG4() } locationsDict["Mother Brain"].Available = ( - lambda sm: sm.enoughStuffTourian() + lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.NoTourian), + sm.enoughStuffTourian()) +) +locationsDict["Spore Spawn"].AccessFrom = { + 'Big Pink': lambda sm: SMBool(True) +} +locationsDict["Spore Spawn"].Available = ( + lambda sm: sm.wand(sm.traverse('BigPinkTopRight'), + sm.enoughStuffSporeSpawn()) +) +locationsDict["Botwoon"].AccessFrom = { + 'Aqueduct Bottom': lambda sm: sm.canJumpUnderwater() +} +locationsDict["Botwoon"].Available = ( + # includes botwoon hallway conditions + lambda sm: sm.canDefeatBotwoon() +) +locationsDict["Crocomire"].AccessFrom = { + 'Crocomire Room Top': lambda sm: SMBool(True) +} +locationsDict["Crocomire"].Available = ( + lambda sm: sm.enoughStuffCroc() +) +locationsDict["Golden Torizo"].AccessFrom = { + 'Screw Attack Bottom': lambda sm: SMBool(True) +} +locationsDict["Golden Torizo"].Available = ( + lambda sm: sm.enoughStuffGT() ) locationsDict["Power Bomb (Crateria surface)"].AccessFrom = { 'Landing Site': lambda sm: SMBool(True) @@ -506,10 +510,10 @@ locationsDict["Super Missile (pink Brinstar)"].AccessFrom = { } locationsDict["Super Missile (pink Brinstar)"].Available = ( lambda sm: sm.wor(sm.wand(sm.traverse('BigPinkTopRight'), - sm.enoughStuffSporeSpawn()), + sm.haveItem('SporeSpawn')), # back way into spore spawn - sm.wand(sm.canOpenGreenDoors(), - sm.canPassBombPassages())) + sm.wand(sm.canOpenGreenDoors(), + sm.canPassBombPassages())) ) locationsDict["Super Missile (pink Brinstar)"].PostAvailable = ( lambda sm: sm.wand(sm.canOpenGreenDoors(), @@ -665,10 +669,10 @@ locationsDict["Missile (below Ice Beam)"].Available = ( lambda sm: SMBool(True) ) locationsDict["Missile (above Crocomire)"].AccessFrom = { - 'Crocomire Speedway Bottom': lambda sm: sm.canHellRun(**Settings.hellRunsTable['MainUpperNorfair']['Croc -> Grapple Escape Missiles']) + 'Grapple Escape': lambda sm: SMBool(True) } locationsDict["Missile (above Crocomire)"].Available = ( - lambda sm: sm.canGrappleEscape() + lambda sm: SMBool(True) ) locationsDict["Missile (Hi-Jump Boots)"].AccessFrom = { 'Business Center': lambda sm: sm.wor(RomPatches.has(sm.player, RomPatches.HiJumpAreaBlueDoor), sm.traverse('BusinessCenterBottomLeft')) @@ -691,7 +695,7 @@ locationsDict["Power Bomb (Crocomire)"].AccessFrom = { } locationsDict["Power Bomb (Crocomire)"].Available = ( lambda sm: sm.wand(sm.traverse('PostCrocomireUpperLeft'), - sm.enoughStuffCroc(), + sm.haveItem('Crocomire'), sm.wor(sm.wor(sm.canFly(), sm.haveItem('Grapple'), sm.wand(sm.haveItem('SpeedBooster'), @@ -706,13 +710,13 @@ locationsDict["Missile (below Crocomire)"].AccessFrom = { 'Crocomire Room Top': lambda sm: SMBool(True) } locationsDict["Missile (below Crocomire)"].Available = ( - lambda sm: sm.wand(sm.traverse('PostCrocomireShaftRight'), sm.enoughStuffCroc(), sm.haveItem('Morph')) + lambda sm: sm.wand(sm.traverse('PostCrocomireShaftRight'), sm.haveItem('Crocomire'), sm.haveItem('Morph')) ) locationsDict["Missile (Grapple Beam)"].AccessFrom = { 'Crocomire Room Top': lambda sm: SMBool(True) } locationsDict["Missile (Grapple Beam)"].Available = ( - lambda sm: sm.wand(sm.enoughStuffCroc(), + lambda sm: sm.wand(sm.haveItem('Crocomire'), sm.wor(sm.wor(sm.wand(sm.haveItem('Morph'), # from below sm.canFly()), sm.wand(sm.haveItem('SpeedBooster'), @@ -754,6 +758,9 @@ locationsDict["Missile (Speed Booster)"].AccessFrom = { locationsDict["Missile (Speed Booster)"].Available = ( lambda sm: sm.canHellRunToSpeedBooster() ) +locationsDict["Missile (Speed Booster)"].PostAvailable = ( + lambda sm: sm.canHellRunBackFromSpeedBoosterMissile() +) locationsDict["Missile (Wave Beam)"].AccessFrom = { 'Bubble Mountain Top': lambda sm: sm.canAccessDoubleChamberItems() } @@ -773,7 +780,7 @@ locationsDict["Super Missile (Gold Torizo)"].AccessFrom = { 'Screw Attack Bottom': lambda sm: SMBool(True) } locationsDict["Super Missile (Gold Torizo)"].Available = ( - lambda sm: SMBool(True) + lambda sm: sm.canDestroyBombWalls() ) locationsDict["Super Missile (Gold Torizo)"].PostAvailable = ( lambda sm: sm.enoughStuffGT() diff --git a/worlds/sm/variaRandomizer/logic/helpers.py b/worlds/sm/variaRandomizer/logic/helpers.py index 717561e7cd..ee1738163e 100644 --- a/worlds/sm/variaRandomizer/logic/helpers.py +++ b/worlds/sm/variaRandomizer/logic/helpers.py @@ -281,14 +281,23 @@ class Helpers(object): def canMorphJump(self): # small hop in morph ball form sm = self.smbm - return sm.wor(sm.canPassBombPassages(), sm.haveItem('SpringBall')) + return sm.wor(sm.canPassBombPassages(), sm.canUseSpringBall()) def canCrystalFlash(self, n=1): sm = self.smbm - return sm.wand(sm.canUsePowerBombs(), - sm.itemCountOk('Missile', 2*n), - sm.itemCountOk('Super', 2*n), - sm.itemCountOk('PowerBomb', 2*n+1)) + if not RomPatches.has(sm.player, RomPatches.RoundRobinCF).bool: + ret = sm.wand(sm.canUsePowerBombs(), + sm.itemCountOk('Missile', 2*n), + sm.itemCountOk('Super', 2*n), + sm.itemCountOk('PowerBomb', 2*n+1)) + else: + # simplified view of actual patch behavior: only count full refills to stick with base CF logic + nAmmo = 5 * (sm.itemCount('Missile') + sm.itemCount('Super') + sm.itemCount('PowerBomb')) + ret = sm.wand(sm.canUsePowerBombs(), + SMBool(nAmmo >= 30*n)) + if ret: + ret._items.append("{}-CrystalFlash".format(n)) + return ret @Cache.decorator def canCrystalFlashClip(self): @@ -363,7 +372,7 @@ class Helpers(object): # - estimation of the fight duration in seconds (well not really, it # is if you fire and land shots perfectly and constantly), giving info # to compute boss fight difficulty - def canInflictEnoughDamages(self, bossEnergy, doubleSuper=False, charge=True, power=False, givesDrops=True, ignoreMissiles=False, ignoreSupers=False): + def canInflictEnoughDamages(self, bossEnergy, doubleSuper=False, charge=True, power=False, givesDrops=True, ignoreMissiles=False, ignoreSupers=False, missilesOffset=0, supersOffset=0, powerBombsOffset=0): # TODO: handle special beam attacks ? (http://deanyd.net/sm/index.php?title=Charge_Beam_Combos) sm = self.smbm items = [] @@ -379,14 +388,14 @@ class Helpers(object): chargeDamage *= 3.0 # missile 100 damages, super missile 300 damages, PBs 200 dmg, 5 in each extension - missilesAmount = sm.itemCount('Missile') * 5 + missilesAmount = max(0, (sm.itemCount('Missile') - missilesOffset)) * 5 if ignoreMissiles == True: missilesDamage = 0 else: missilesDamage = missilesAmount * 100 if missilesAmount > 0: items.append('Missile') - supersAmount = sm.itemCount('Super') * 5 + supersAmount = max(0, (sm.itemCount('Super') - supersOffset)) * 5 if ignoreSupers == True: oneSuper = 0 else: @@ -399,7 +408,7 @@ class Helpers(object): powerDamage = 0 powerAmount = 0 if power == True and sm.haveItem('PowerBomb') == True: - powerAmount = sm.itemCount('PowerBomb') * 5 + powerAmount = max(0, (sm.itemCount('PowerBomb') - powerBombsOffset)) * 5 powerDamage = powerAmount * 200 items.append('PowerBomb') @@ -607,7 +616,9 @@ class Helpers(object): # some ammo to destroy the turrets during the fight if not sm.haveMissileOrSuper(): return smboolFalse - (ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(6000) + (ammoMargin, secs, ammoItems) = self.canInflictEnoughDamages(6000, + # underestimate missiles/supers in case a CF exit is needed + missilesOffset=2, supersOffset=2) # print('DRAY', ammoMargin, secs) if ammoMargin > 0: (diff, defenseItems) = self.computeBossDifficulty(ammoMargin, secs, @@ -749,16 +760,20 @@ class Helpers(object): class Pickup: def __init__(self, itemsPickup): self.itemsPickup = itemsPickup + self.endGameLocations = ["Mother Brain", "Gunship"] + # don't count end game location in the mix as it's now in the available locations def enoughMinors(self, smbm, minorLocations): - if self.itemsPickup == 'all': - return len(minorLocations) == 0 + if self.itemsPickup in ('all', 'all_strict'): + return (len(minorLocations) == 0 + or (len(minorLocations) == 1 and minorLocations[0].Name in self.endGameLocations)) else: return True def enoughMajors(self, smbm, majorLocations): - if self.itemsPickup == 'all': - return len(majorLocations) == 0 + if self.itemsPickup in ('all', 'all_strict'): + return (len(majorLocations) == 0 + or (len(majorLocations) == 1 and majorLocations[0].Name in self.endGameLocations)) else: return True @@ -811,9 +826,24 @@ class Bosses: 'WreckedShip Top': 'Phantoon' } + accessPoints = { + "Kraid": "KraidRoomIn", + "Phantoon": "PhantoonRoomIn", + "Draygon": "Draygon Room Bottom", + "Ridley": "RidleyRoomIn", + "SporeSpawn": "Big Pink", + "Crocomire": "Crocomire Room Top", + "Botwoon": "Aqueduct Bottom", + "GoldenTorizo": "Screw Attack Bottom" + } + @staticmethod def Golden4(): return ['Draygon', 'Kraid', 'Phantoon', 'Ridley'] + + @staticmethod + def miniBosses(): + return ['SporeSpawn', 'Crocomire', 'Botwoon', 'GoldenTorizo'] @staticmethod def bossDead(sm, boss): @@ -831,6 +861,23 @@ class Bosses: Bosses.bossDead(smbm, 'Phantoon'), Bosses.bossDead(smbm, 'Draygon'), Bosses.bossDead(smbm, 'Ridley')) + + @staticmethod + def allMiniBossesDead(smbm): + return smbm.wand(Bosses.bossDead(smbm, 'SporeSpawn'), + Bosses.bossDead(smbm, 'Botwoon'), + Bosses.bossDead(smbm, 'Crocomire'), + Bosses.bossDead(smbm, 'GoldenTorizo')) + + @staticmethod + def xBossesDead(smbm, target): + count = sum([1 for boss in Bosses.Golden4() if Bosses.bossDead(smbm, boss)]) + return SMBool(count >= target) + + @staticmethod + def xMiniBossesDead(smbm, target): + count = sum([1 for miniboss in Bosses.miniBosses() if Bosses.bossDead(smbm, miniboss)]) + return SMBool(count >= target) def diffValue2txt(diff): last = 0 diff --git a/worlds/sm/variaRandomizer/logic/smboolmanager.py b/worlds/sm/variaRandomizer/logic/smboolmanager.py index d4de6dae93..bb31584714 100644 --- a/worlds/sm/variaRandomizer/logic/smboolmanager.py +++ b/worlds/sm/variaRandomizer/logic/smboolmanager.py @@ -5,14 +5,16 @@ from ..logic.smbool import SMBool, smboolFalse from ..logic.helpers import Bosses from ..logic.logic import Logic from ..utils.doorsmanager import DoorsManager +from ..utils.objectives import Objectives from ..utils.parameters import Knows, isKnows import logging import sys class SMBoolManager(object): - items = ['ETank', 'Missile', 'Super', 'PowerBomb', 'Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Reserve', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack', 'Nothing', 'NoEnergy', 'MotherBrain', 'Hyper'] + Bosses.Golden4() + items = ['ETank', 'Missile', 'Super', 'PowerBomb', 'Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Reserve', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack', 'Nothing', 'NoEnergy', 'MotherBrain', 'Hyper', 'Gunship'] + Bosses.Golden4() + Bosses.miniBosses() countItems = ['Missile', 'Super', 'PowerBomb', 'ETank', 'Reserve'] + percentItems = ['Bomb', 'Charge', 'Ice', 'HiJump', 'SpeedBooster', 'Wave', 'Spazer', 'SpringBall', 'Varia', 'Plasma', 'Grapple', 'Morph', 'Gravity', 'XRayScope', 'SpaceJump', 'ScrewAttack'] def __init__(self, player=0, maxDiff=sys.maxsize, onlyBossLeft = False, lastAP = 'Landing Site'): self._items = { } self._counts = { } @@ -24,12 +26,13 @@ class SMBoolManager(object): self.lastAP = lastAP # cache related - self.cacheKey = 0 - self.computeItemsPositions() + #self.cacheKey = 0 + #self.computeItemsPositions() Cache.reset() Logic.factory('vanilla') self.helpers = Logic.HelpersGraph(self) self.doorsManager = DoorsManager() + self.objectives = Objectives.objDict[player] self.createFacadeFunctions() self.createKnowsFunctions(player) self.resetItems() @@ -97,8 +100,8 @@ class SMBoolManager(object): self._items = { item : smboolFalse for item in self.items } self._counts = { item : 0 for item in self.countItems } - self.cacheKey = 0 - Cache.update(self.cacheKey) + #self.cacheKey = 0 + #Cache.update(self.cacheKey) def addItem(self, item): # a new item is available @@ -106,11 +109,11 @@ class SMBoolManager(object): if self.isCountItem(item): count = self._counts[item] + 1 self._counts[item] = count - self.computeNewCacheKey(item, count) - else: - self.computeNewCacheKey(item, 1) + #self.computeNewCacheKey(item, count) + #else: + #self.computeNewCacheKey(item, 1) - Cache.update(self.cacheKey) + #Cache.update(self.cacheKey) def addItems(self, items): if len(items) == 0: @@ -120,11 +123,11 @@ class SMBoolManager(object): if self.isCountItem(item): count = self._counts[item] + 1 self._counts[item] = count - self.computeNewCacheKey(item, count) - else: - self.computeNewCacheKey(item, 1) + #self.computeNewCacheKey(item, count) + #else: + #self.computeNewCacheKey(item, 1) - Cache.update(self.cacheKey) + #Cache.update(self.cacheKey) def removeItem(self, item): # randomizer removed an item (or the item was added to test a post available) @@ -133,12 +136,12 @@ class SMBoolManager(object): self._counts[item] = count if count == 0: self._items[item] = smboolFalse - self.computeNewCacheKey(item, count) + #self.computeNewCacheKey(item, count) else: self._items[item] = smboolFalse - self.computeNewCacheKey(item, 0) + #self.computeNewCacheKey(item, 0) - Cache.update(self.cacheKey) + #Cache.update(self.cacheKey) def createFacadeFunctions(self): for fun in dir(self.helpers): @@ -147,22 +150,51 @@ class SMBoolManager(object): def traverse(self, doorName): return self.doorsManager.traverse(self, doorName) + + def canPassG4(self): + return self.objectives.canClearGoals(self, 'Golden Four') + + def hasItemsPercent(self, percent, totalItemsCount=None): + if totalItemsCount is None: + totalItemsCount = self.objectives.getTotalItemsCount() + currentItemsCount = self.getCollectedItemsCount() + return SMBool(100*(currentItemsCount/totalItemsCount) >= percent) + + def getCollectedItemsCount(self): + return (len([item for item in self._items if self.haveItem(item) and item in self.percentItems]) + + sum([self.itemCount(item) for item in self._items if self.isCountItem(item)])) def createKnowsFunctions(self, player): # for each knows we have a function knowsKnows (ex: knowsAlcatrazEscape()) which # take no parameter for knows in Knows.__dict__: if isKnows(knows): - if player in Knows.knowsDict and knows in Knows.knowsDict[player].__dict__: - setattr(self, 'knows'+knows, lambda knows=knows: SMBool(Knows.knowsDict[player].__dict__[knows].bool, - Knows.knowsDict[player].__dict__[knows].difficulty, - knows=[knows])) - else: - # if knows not in preset, use default values - setattr(self, 'knows'+knows, lambda knows=knows: SMBool(Knows.__dict__[knows].bool, - Knows.__dict__[knows].difficulty, - knows=[knows])) + self._createKnowsFunction(knows, player) + def _setKnowsFunction(self, knows, k): + setattr(self, 'knows'+knows, lambda: SMBool(k.bool, k.difficulty, + knows=[knows])) + + def _createKnowsFunction(self, knows, player): + if player in Knows.knowsDict and knows in Knows.knowsDict[player].__dict__: + self._setKnowsFunction(knows, Knows.knowsDict[player].__dict__[knows]) + else: + self._setKnowsFunction(knows, Knows.__dict__[knows]) + + def changeKnows(self, knows, newVal): + if isKnows(knows): + self._setKnowsFunction(knows, newVal) + #Cache.reset() + else: + raise ValueError("Invalid knows "+str(knows)) + + def restoreKnows(self, knows): + if isKnows(knows): + self._createKnowsFunction(knows) + #Cache.reset() + else: + raise ValueError("Invalid knows "+str(knows)) + def isCountItem(self, item): return item in self.countItems @@ -174,6 +206,12 @@ class SMBoolManager(object): def haveItem(self, item): #return self.state.has(item, self.player) return self._items[item] + + def haveItems(self, items): + for item in items: + if not self.haveItem(item): + return smboolFalse + return SMBool(True) wand = staticmethod(SMBool.wand) wandmax = staticmethod(SMBool.wandmax) @@ -218,11 +256,11 @@ class SMBoolManagerPlando(SMBoolManager): if isCount: count = self._counts[item] + 1 self._counts[item] = count - self.computeNewCacheKey(item, count) - else: - self.computeNewCacheKey(item, 1) + #self.computeNewCacheKey(item, count) + #else: + #self.computeNewCacheKey(item, 1) - Cache.update(self.cacheKey) + #Cache.update(self.cacheKey) def removeItem(self, item): # randomizer removed an item (or the item was added to test a post available) @@ -231,14 +269,14 @@ class SMBoolManagerPlando(SMBoolManager): self._counts[item] = count if count == 0: self._items[item] = smboolFalse - self.computeNewCacheKey(item, count) + #self.computeNewCacheKey(item, count) else: dup = 'dup_'+item if self._items.get(dup, None) is None: self._items[item] = smboolFalse - self.computeNewCacheKey(item, 0) + #self.computeNewCacheKey(item, 0) else: del self._items[dup] - self.computeNewCacheKey(item, 1) + #self.computeNewCacheKey(item, 1) - Cache.update(self.cacheKey) + #Cache.update(self.cacheKey) diff --git a/worlds/sm/variaRandomizer/patches/common/ips/beam_doors_plms.ips b/worlds/sm/variaRandomizer/patches/common/ips/beam_doors_plms.ips index 34f7bda6acb00f4de6a479033a69eb5e54700968..d3beab48d5cca6a7d08b226511ba8dd5b2b140c5 100644 GIT binary patch delta 1403 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 literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/credits_varia.ips b/worlds/sm/variaRandomizer/patches/common/ips/credits_varia.ips new file mode 100644 index 0000000000000000000000000000000000000000..9c35c18d1cf83fc7595cdf0d12f21169adcc8dfe GIT binary patch literal 14493 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 S`9cx(pixbJ)ki*l$NvKjL8~|b literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/custom_music.ips b/worlds/sm/variaRandomizer/patches/common/ips/custom_music.ips new file mode 100644 index 0000000000000000000000000000000000000000..5711721de80e4c0ad31f699453fc876ffb83844c GIT binary patch literal 362 zcmWG=3~}~gV2EX4`v0GG))WS2g#|xYXXgOv2|&6YNYD7edV39!-UXyL1L;FQSbt1l zU|XO2qMwt2^~ZDuwuJS0FZwlEf6QaxXGmDdz@T)nzhQkoM0y@z(<b&8plnN5Y=hSh^r zkyY4XI|K8EKPyE#B7yE&@JFOi97wPDBeL-ykZ$-RvZ)D3cl;4KlM19K0BMlFX8aNP z(*SgTA;=XXf7(H=C<3}dib)^t-u)oT{;`6}bR*V7 OCm7hkp^@$C?*;(xgM=9X literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/disable_screen_shake.ips b/worlds/sm/variaRandomizer/patches/common/ips/disable_screen_shake.ips new file mode 100644 index 0000000000000000000000000000000000000000..b4d4559d2e2958e7ac62f94e3387dbbbe25bea1b GIT binary patch literal 92 zcmWG=3~}~gG0tLOYT#lqZUs^76X!56Ca_Q33+5bD2XRiSfk|U9=>{gFLFCgKFu4S% TR6}G_CrF3b7j6*c>hA^s`WY6( literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/door_indicators_plms.ips b/worlds/sm/variaRandomizer/patches/common/ips/door_indicators_plms.ips new file mode 100644 index 0000000000000000000000000000000000000000..cd940c84bf42ba83740c421fa5b6f87c82f00b66 GIT binary patch literal 797 zcma*l&nrYx7{>9pLycKcH%&@OPFYN{oSiU!B^e7L|G*^KjC7loG;wn&n+atjX~w-t z-7J*SG&R#*Ox8DB8-=pNIcMX^oj>5+JgZNqZ*OO5VmUh}iqz2~$gT*AyuRZuK`l>E z%`3P;L%N5J`a$J>KnW^=TK+#x_<{`RE9hgJeu1Vu>CA(kfaYSF10wD5G^4tu0>bD(E+_)d3yUtkwt6 z1yDw7zA7CXlyTQH&?`{JT|>}kUh5C&FR!&)kNV`CE@A8X4*RKJuqo4t*Cx#&dvM*QNv27 literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/elevators_doors_speed.ips b/worlds/sm/variaRandomizer/patches/common/ips/elevators_doors_speed.ips index 3e08065b02b2759a4e8f77bd00b775a077c08b41..cc95739b0f4d59aaf5ac43a39d31b292d75cd122 100644 GIT binary patch delta 285 zcmX@jxPX~8z%j(xV`6FWL{>ExR|ZDeiJCI?j2sO5-xwH~84R?5B8-tq42*1yae)ks z{ETs-47^!c8(3GKU}#`o*}%xfn5@dc^@?K=3xkFd!|#QR$(CR)1B`o=fhET9_d2>cV`oF~Y6NFn=>!peUFzm*uu8|qnsY$gVYT0zda zf}GiFC$e`kF?`VPWMudty7mv3L?%N+?;oy}tPHuoxO#taH8=qUEnb2J*Z$&q$;to{ z)P*Sj0aAXV_XpQX7KYq!T)p488id#W04WCvuKmXKl7#^%2xMpq{GP?Y$`Et&g=h4a_SW7`YgeKQS;ePh<^dcJ+4y0L7#g8UO$Q diff --git a/worlds/sm/variaRandomizer/patches/common/ips/elevators_speed.ips b/worlds/sm/variaRandomizer/patches/common/ips/elevators_speed.ips new file mode 100644 index 0000000000000000000000000000000000000000..6cbff74aeb61e258bc20e68263a56ef64fb493cf GIT binary patch literal 91 zcmWG=3~}}loZ861qNMP7vEbBJ1_lPvSAtVFFfcL;PQ3&s-+;)^3JltFX0os3U^qL2 r^8(u(M|K~F&nqPu&N{Mx5M1lXF4gPEzPFBFB9ob6ZXJKNtG^on>7N_r literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/endingtotals.ips b/worlds/sm/variaRandomizer/patches/common/ips/endingtotals.ips new file mode 100644 index 0000000000000000000000000000000000000000..56f20c21bcc4ce913ae8030ea0a5714431760825 GIT binary patch literal 293 zcmWG=3~}~g;#k1IZ1AInsltFkz+-*hjXJ4*Ad#We*VvHF`b?c6T1o#|x5Dvf2^`(t zs}C96ih87wA_P*`%jC2&rI*>sBjfe!LkerfoO)TD9w`8Y*kD2;AR(ZH2T+&`Q@B-( z^;su_7*xNAQ>zeAZL3JeBL$$*tj`uPFfy|~Tf)GNWC8254GdZ6mb6M#{aUS&4RYVD zMGOopqb&L#7}u@Lvgls|q^d0XpBUFI75c!c!1BC->BWnsB0$DcF(3hCO8_aL=7cCN dt`$2SpUWcf>7~Ss31g_Od$y+nD@lxaPHwA@1E`-!tR60uG6t34ZWf)+FF-TYI04O5;|z1H)`R9xz)L< zryz2AglI9Cvg#GMH)XztPR6{9@x3B$hcX5k3pI$i^>k diff --git a/worlds/sm/variaRandomizer/patches/common/ips/gameend.ips b/worlds/sm/variaRandomizer/patches/common/ips/gameend.ips index 25c2bbac2b5a392a09d6941db9e25c5fce269c2e..8e8079c7cbf65e70993ecc3b7a64a1994ff4bc66 100644 GIT binary patch literal 62 zcmV-E0KxxIK~zIX0gKE40wDhf{~!P=uOQfdi2y+GAgLw*kC+L5A^_~6t+)k`0Q`Tg Ux&@B{{C}wd03yHF-(W>gM%hgm%K!iX literal 37 tcmWG=3~}~g>^{T5r0}2pzXF5cdW9QxoeT~i*jK7C^v~d|OK|mf0|4zN47~sV diff --git a/worlds/sm/variaRandomizer/patches/common/ips/hell.ips b/worlds/sm/variaRandomizer/patches/common/ips/hell.ips new file mode 100644 index 0000000000000000000000000000000000000000..10560b5826d6506fdd175e4aeadbf6b01bbc6660 GIT binary patch literal 34 qcmWG=3~}~ge=(VXSwY|n`xgNQ-lzqM-<4h->8)C~n?1qR-wgoJnhgX1 literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/intro_text.ips b/worlds/sm/variaRandomizer/patches/common/ips/intro_text.ips new file mode 100644 index 0000000000000000000000000000000000000000..80671d51bb45d5798df9db6a63671ca7cf57553f GIT binary patch literal 300 zcmXBOKTASU9LDkIoa^uE$x`x9rUoLu2j}8axT(+i2_n*br(Qzc$K}I!{I)Kxy@rft(1W;)zTkvmDC1}7`-ud%Py-2iP<;vdNRFR= z-S_kPW4q$ZqK>TH7`rugXY7x$E3qHq%d!iNPg&lRO|s zBXum&p)u-uQ@rkr#%Y2YdO(x(kRH(#P19qVp;?-vd3r(%^pqB9iI(XZMWGYD{(BR= Jy<9f0{s4QaNu>Y) literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/lava_acid_physics.ips b/worlds/sm/variaRandomizer/patches/common/ips/lava_acid_physics.ips new file mode 100644 index 0000000000000000000000000000000000000000..e49df396447b2e02caa12b2981d9c125d84e65f3 GIT binary patch literal 240 zcmWG=3~~10U`S_R@@e8=$YWPXoNh6#0>^!=>|y-$y6x)2|`P`LTKq%5L(6xN*6LP>o#a}sJH^POL5%X2c%3n ezD)yCIvn4=gQ;ItK+1>XwB diff --git a/worlds/sm/variaRandomizer/patches/common/ips/minimizer_tourian.ips b/worlds/sm/variaRandomizer/patches/common/ips/minimizer_tourian.ips index bd40ab1199e632a7ea6c004f2c754dcd71f41591..502fdd222688acda1b8938df3704cf7e955de624 100644 GIT binary patch delta 274 zcmZo>?qp^Sa13$wn8+tnAH~2roBiaGyG&dO3@io`4faeC?F@{O?90R$m||A3-)&%E zD*MiUw~c`*4@k{|@D@OLTOhn$5Z-GBCb3EE-wha?S28gu{c3Dj$qb=bAQUT@QegO= zu#$^G$+)Rug2=6v6E{n7l`yc>C#*MP|6V!qx`B!NACaAW3{2+_ik!dAz}O)2hk-$M z&IO)1*LddK;F)uaXU-j-Irn(xJm6W$(9r9}rSwkZkKAN6Ms+JE29f%eof{M8cyTFY z{88FhA^s(Pn6* delta 273 zcmeBVZf33za13$wV5q;tz~Z-lV*ukM2?j<7##M1(GL3;L;5*~0bqq`f-x8aigx2f$@XpdL1Bxk>P{FdaWCEnv9Gm85u83oGZ)K$-q*d zu-=UQd*8$(2J%LKM0WBqFr7ara{e|0V}r;a1_q9m3=O?rTuSdm{%}s#U{p8LWss_0 z*|{-cju+RQb6j&SaLq~LR@!xM;z|aFelw9j9t?u@YmadD9^zbkg|qh>=gO-a6I}h> E0O8tW3;+NC diff --git a/worlds/sm/variaRandomizer/patches/common/ips/new_game.ips b/worlds/sm/variaRandomizer/patches/common/ips/new_game.ips new file mode 100644 index 0000000000000000000000000000000000000000..e2864ea6f3bc003c0e09e77515497ce75e069079 GIT binary patch literal 332 zcmXAiKS;w+7{sqhA0d?J6QnvQ2>~fxv?w@z=T-fJd2P}h4mB! zn@C>=Ncn)ia%JD9Y=JU>0CX9YQA`9?+3KhWC+m@%{iRGrT6vR|cUk$6i#laqxD=xv5_Au7ynWm=k>@ zD`CIyqcuiX^Ba}0t?_J)b2%r*MLH@WgRT~oMxHC={S#Nr8|Glp*6Qi}>}dh+v+TBI MUH@^8xA%7b03_Cf&;S4c literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/noflashing.ips b/worlds/sm/variaRandomizer/patches/common/ips/noflashing.ips new file mode 100644 index 0000000000000000000000000000000000000000..716bbd3774f42dcacb8a91ff821086c5fa890b7c GIT binary patch literal 2157 zcmZ|PT}TvB6bJBgXJ^mO;!du*rI3-9QYQ96WQCE4X)b+OSVT~m_#l1ICJ`b&RG3tR z^blepAt5DVQ4!H&MT9Lu5f%|*5g`^9A`$M)I@O(XclIta`?8+DOsoGav zr|Ml$z!)eBcs!u0;PnC`_Ujhd}XqRsAj~s;cVuLE~%qO2$`4HHQ3Jot_7Sr1i=$!J4#j+`wPG4kL2A6bp>(ilv{YgExhg)-gOI~xrOgt!k-?u zFxM?Cbqgz9!dNf(T0=X7b=oqF4W!if!Am*DMu4?qEDCHV#$JQUrHZjp&=Sk|;7jUY zDs?cMGB7-;gG{@tk!5!^auQvQ70#x{dU+J19s=dHr^=#v+caJCYiJw>rSUi#&7e6s z8m%I0n{T^B-VaK7EgA#BqG$|)DjMArlaEpxJr~)?{6-%{K9*{Kh-`YkwJ10F11Aq? zW{E794q`604`MD&9KZklLt8WD&s7?vtBe6`|P%EQDq|umxy#fC}Lw z`DmU8WlIFje#r)j4G|l5*h9gdNcNQ2b7C(X78UHRWFLurA~xo*uYyfVHcf1X*l&j! zg8fUfxX)&Bzs=$q2^QzCj6Xs=SF#e3tdYq^TDX}ORyYf*1lulIJ+ZyS8XR^|utv#R zh#e(%++qA`*~e&;>@2bK#5x^zS+J{;^%A>5tj}S$1bYW~Z^LAMa{%LGvYe3Rbh4cD z$WoUje=Ug)bA__pAj?WwhGp3x%Lw4Bl^2Sd@^P+RmYuTfmgOy34$JbfET779RF>ni z?0dBGdbdKt__D+5>tS~{>1v@$`d(a>5*BsWQx5Lrs3P>>ZwNdJN zBsoO#A>~oZUyFx&Ll5R(2>sqDaK mP(oUE&mmw3vU`U4XU-%3A1TNgKL<*wKlJu5C;&C}wf_M^%w638 literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/objectives.ips b/worlds/sm/variaRandomizer/patches/common/ips/objectives.ips new file mode 100644 index 0000000000000000000000000000000000000000..466d6aff27b58e60c5aae79ccc43d48623adf9c7 GIT binary patch literal 2451 zcmaJ?eQZ-z6hHTM^le?)-spy#RrYuccc&urlM>^gTQHzA4m5tnn5U9J0z*Yj{LW)z z$j0j0Au3WC8*FkNwa5~b=$yqw*8L&%AJm!nMM4^b`mzExeQojF_g;x&;(f{a-FwdY z{qF6#=e@T1?{8U2N*OqMGlQg}6oj=o;<+9KZx#a=UOt@$tsUFSEL3NG$7?AR%baNA45j0_g1#S1iyVG64ds zMecKeUdC7Trtdyc^E0=(0tn#hW;^Frn@0*f0KJsW;+bJgbF&hYPrfzoEGMxGSo1ayfXuu^IKv_si^w4-(M zn7r&`oGFe=Zf-6@{i~F>aF*G z8lfXSITVpU%UZvF0!!IIUJXOd=@O{CRM@k26swBSqmVJ9g&Low>e`Rij^VAyvxeo5 zhk5@zDXRSDl(Iyycf!lWh&vXzPGs0@3&em$xs;*YW9ybDDhry->^m$6wxi0A_5a%wz|q_E zf6$rl$%pdqwN!(3qAlXwh3$!yUkCspw;0HR85H6ZLe zC$-gsFoLiciVmGSa!z^>-EAr9(T72J4q+V#Par%6MVnIVQ_|B>C~~GDEuGKf7DUQu z@Wj`~DdR`ujKR+x`D9i@EyQc_Nb*2N8Bg|Yn6{5_cxAXn#8+fO%M9vug$6q8$!eKk znYqN{@v63n$>doVBu>exOr%BBrKprS4cDI^?AAaRSq&)rgIY$#Hgp$Mg4LSNw?Tzy z{`_ErWJurx-6=n6;HK&>cpG-Z9(WI?hst1YjdO2V@k4DfbpD}{Or?ORc*5fv=wsDQk zD#{|uokdn<7FkziP1Y7GiWkOZNs{ZLMPamDIjQm(l5ve}v`9zG`AKyezyCiI$XFAh zFmafZ#|j~wSRtJ7P;}FGuf=P?!kcWo+s3ts=H+HjK@sRzN2Tl!AmMG}*ST`~!%xKt z39x|KvjjEyC)-i2@!#KJ|K3g;gBC7Ml}nKWB`2laV8R_aSaNa>P8>dsRI9AZPTgkX z>o#t;vD?NT8+X{)Yh%F1H!S2esbS6LA}FztH4*W@j9;+Ic3O@S)^AP@FOBRG}2A+Yvt=00tSU7P-B;$%}&Y;8-fMAOt6BF?0Z(5oa&+UUnic1^P<|s?h zx|IcJoysD#V5%HSxW{|T6C!6Ibwfp}4VLgo(KqPCqM@@DG-U5bjYVj=#(varp@oRa z#hP-iXT=}j7&itzleHH9u*<}%bxv@|HR-As0P2)me)<5}i_wC$Gu{Me^MWAayRUjH z2#|~33I|*b7jdN1x;}%0&jBAXU9QibnLXq60L#g#mqZx=cVjX$aANx$6NIUl==N!Bo;IG1#i_! zsJ-~Pp9?j`3LEdl(b;+8L?P!-Jlw{e8+X3a={r%#@ovU1CRqdCXV<`0bu2vldr|+^R2*Hvyag6_1BZB3&a!SL6Hoso z%etjP3SSuOA2P5k6#+4x0U2T-#%l()7cZ7dfS8{ccz-2GWU_F~KVez-;zfcqa{~io zxioVV1D6s51Qsz!Gxstu0?7#sy#LGpD?wq=e`)4<42(c>0Rv-|H1i?`E@y^f1}7k_ z0xDh$*0KSprRu-)|Kk5nKv)G-ybrA900X1CH1i<_t_+6v3=0_)8T5c!&VsdE0BTYH zpYi|w|Aqe*|LXy@+yiOJY+(>_v~;p`wv^$K<&ooYu5<$O94n#bc*()As$s`3 MrNuA$vt9k&08jcft^fc4 literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/rando_escape.ips b/worlds/sm/variaRandomizer/patches/common/ips/rando_escape.ips index 74dac2e6792191792c7d058bc6fd306ec6edda40..bf5efac0be87e2afe526437d8d55eadb932925e2 100644 GIT binary patch delta 252 zcmaFDK7~Uhz%j(xgF#M(fw}qPLdJLt29}tw6L|`PXZ&E|`OLuVv*rhrYZwEwLK~1S zh0uO4fOP*4CVv|!4Hd6QU=Ul$$Dm}~)G$GEuLMsg2g3)!_5Z#u)MTnCXHa68ctV+R z_Qp4cjAHvfES$jI?Y&xI&cxM92R<#_E5VSUv~RL8(+ze;27wRz7(6EPGTZ3`m2EIu zZ+E{*Q{W>zg8;+H7={aMN*_KiY!KSZ!T=PMVBlcrJ^(F!Tdu`WSc_thiR~U-e<3QqR1GY*&9b0JDx*&Hw-a delta 204 zcmbQj@r0cxz%j(xgE8KMfhFeQM3DlYsXv%_J~J@;to*^`8pgn^&tW z$=?P_L&Yl+7|d4kF(?@~HB1oOE5Xyr!SI1^{hzN3Ctg=(oU!q@A*0x;4+|%7cYCi^ zm@{#;(t%G4_ewA%D6N|8#B_tnlVP$VvzBHxR p4MKZa7&HYwvM_Kk^qywl%L?M~PVQsYW42^4o4lXdjM>%S4FJbuNPPeR diff --git a/worlds/sm/variaRandomizer/patches/common/ips/relaxed_round_robin_cf.ips b/worlds/sm/variaRandomizer/patches/common/ips/relaxed_round_robin_cf.ips new file mode 100644 index 0000000000000000000000000000000000000000..0ce5b4b451f85c3cd603e3cc2011e740401b1a99 GIT binary patch literal 165 zcmWG=3~~102))L@$iNYL4@^D-kzqazjB*@d=NTB^aD-iCkUh!BU?8#f7$=Yx&{}&6 zOe?KD2d1Ss!rn8OM_=!a;942K?4F;K&3=CXctM-D8H+XXI80ShO1|^~U6VJWjO>p&h F0|3TOHFf|1 literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/remove_Infinite_Space_Jump.ips b/worlds/sm/variaRandomizer/patches/common/ips/remove_Infinite_Space_Jump.ips new file mode 100644 index 0000000000000000000000000000000000000000..ab00c37eb716a1dae0c81918bf7e92c9892bf71c GIT binary patch literal 15 WcmWG=3~~10P?^lY^nu^i-wgmCqy!59 literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/remove_elevators_doors_speed.ips b/worlds/sm/variaRandomizer/patches/common/ips/remove_elevators_doors_speed.ips index 1c60d26786edbaa2f26b5f01388394a59d486a56..2906ec2c466c8d44fb414c641211b9e65bdbb036 100644 GIT binary patch literal 434 zcmWG=3~}}loZ861vXX(JS8!@8gXm25y$$>lnG6kc8~FFu@nW`6*a-xwHK84R?5;*60=42(RCae)ks0*rB?47{C83>R2eo?vKTUfICNn5@dc zWx#q)l(RwnohT<`vL%=+$l1XBP7ugF%D}Sr8<(cQZw3ZMg8mTrt;A4HP!(+y2>hPK LzzR0n)!z*OC1(5a delta 108 zcmdnQyn&H5z%j(xV`7$Ca0&w>3xj?hn5<=Bv}DkK24;U?U}s_ImE?TD%3z=c6laV~ zVqoN9j0lnG6kc8~FFu@nVXAj=@R* literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/remove_rando_speed.ips b/worlds/sm/variaRandomizer/patches/common/ips/remove_rando_speed.ips new file mode 100644 index 0000000000000000000000000000000000000000..506bb5c890b964f275463bad5f83622ccd2c49d3 GIT binary patch literal 124 zcmWG=3~}~gsK3L&qQHLt2S-@~gWZ3M0LRNd29{n8E(wm8PZ&g&F@Tr}E0;s)2MmuG PmThA^s$&^tn literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/remove_spinjumprestart.ips b/worlds/sm/variaRandomizer/patches/common/ips/remove_spinjumprestart.ips new file mode 100644 index 0000000000000000000000000000000000000000..6197f8f14460ff6a22b600b2b91644f556d7706b GIT binary patch literal 3397 zcmc&$OKenC82-+2rrsH=UcpC&25w>`;&W&O8Wb|hBPdlygNQCnH`Iz16|tIF^n%ZY zi!QprjTuTi(_7}TacOXT#2CZMxFiiRE?jh@){5FVe*b^xHn+pH(v9Am^WAg)*Zux; z&prRiJ+N>4Zf9@{*4CH4oR5FFIz5%2&L24yZx>HJI66|9s>m~HwaG1KW=984e{v>U zS~GTd?CjVzKYpJp zT`0-8r*Nns&vV5K#hK#06GtaX6CaMA9hLXZo=%U+^T&y4m5E&1+(aUX#~Kb!2KnIY z;I?XSRsHA_@eoeZB(AjfU|k?B8|(}EgQ4J=@yhsguwqhdBxd*I&}3&hQ{GWNp#0|J@faxk3*ISQq%9WBtwisQo{{DLN_0n( z=4ZVQz^o&{-imtN2b?G1!^M+C4^I(+JwPIZpNTGhC3^Uc2yCRT*S=27*+F84F^IT= z=$dPZp1F>Ql=P95e%3xLeYJ$6mPk_I;YyL4RD;ESWpO}RJgqExl*KQ~Vn$iaDvM2Y z>oudUgd7Y3ZX)`dRak^ux>@wL}{Q9vqR|G+Bfh`W)WOwrD=) zDr$u{ua;Sbkv3@#t7x<+C)=(u(t<6S$=CAjJyiS74T6cAh{Je}n8))XF^>^zSV_zh z)x%^&5i=pYW`5HXJdk=VzHHC#`GT4vc-zm&xFlM;8>=sS$`p>nAF^4UOnLwQ4a zyUN>B-ca5?9%1---f3a!w+YK(tBkGmp>n9)UuJI5S%A)htFz$gEI?<$$D<5CpMNMb zdnpr5IQuyn&9kl7vmCFa4~)*a=9ks{a++UnV;wwDLu= z$~UWgbIP~3@zrD{h1o2~LoLBbycqTJ`K{J?vZ`9~Q?2b>2ER^?Cu^=1Kh>Jm%iwo> zNq(xeCziqQ#FG3}YmPIXYzSNR-`y%6W{sd-`>&d9>a(qdPiswBrr~7pJHM-oKZqXw zA_DjEjlg!@?KW%NJ&c>dyF}L*qNnKMW$EJ;A}A5|8C=wOe`>rrjdwq_zz&Vqt?_#3 zkimQ6ZTTwhQHnSs5*#Ih68Qs=dO=3X7X{Pr3$`g*MduEYB<>=(5K9kYnZ>h2nDt?d z^kEsby(F0WKrpFj6rDRolDM1TLM%OqWfuKJ*xOso{RY)^H8imqwq2qz8>o?sQS45Y zq|`%d9%W{ij-s7cEPYBi%Zaecxmx8st#XYDw&ac<`g2z4FZIdO`s8YTYSbRq7+#C* z^Xli*pvLdx;tQgOuQ-lZy5>mCg?<}p3w%a&@j21MmmG%`I;_^s8OR*?Hm7spbH9`P z!%O6S4f#QNI?>98+u#3r> zIdMNeNVoXDB{hWHA#zX~X4t1zQc1#HUv_%25;?*=6ToI2X{C-;s3_vw5{p*dVx{|b ze1D4~zC$aY2J)ZAPCxXMGn1#Q$<@`=;tUj?BBGNJHQiirG$+0C%K@~&sK-nC^{4j}}H32&BQh!Y6pS*4)_ z(IldwjwnD$lad-CK^5^2Kok*oTo@Pbm?_TOx!*Z=qzPqgyP|%GUdmmGG>1O-^ug~Z z2M}pLvgd>S$Ye4<_kdp;*MlM*LJpbJ{i8ou>o+^mar5%>@5Mu${5udLb-M87PQKQ` z_lR`53*YSIzjyGxBHix7{}5+)@7cZM2X4mq{`b|gxbOae$L)+q_=2xoZQ(OM;uD{0 zc!m)?$0%N43@QPK8_=Dr{U&sBV%L<}(J+gcZhETz~%q|5xv zL{wFlrCX|e$?9r9uCkUq-Kxv7Wa4;UA#eJYu9z6IU9Dr*ymXA6iup+KII=bK45*T4 zY!veYBc87l9?C0BDlNVuqo<~>YLiMdoD__m92qE+>!>J+xmDDq7GNd5Z~CH9hk(+< Ew?!wiivR!s literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/tracking.ips b/worlds/sm/variaRandomizer/patches/common/ips/tracking.ips new file mode 100644 index 0000000000000000000000000000000000000000..d0cc935508791ea837df405192d62824699b4a6b GIT binary patch literal 819 zcmX|;PiPZC6vp3XlP0@a2daLZp~H{x;SSQ=atU^Xlj*Dievc7kXreoIPE^u*$=)$hGxh) z07Ei{6AxCL*Ag^3V~IBuS5=Akee?fv8u46cWf)()qW8?a_Ob)=XlTG5{WyP!zaw@D Was0VyJZ)P4Z@R2NfMvQbocjmrenP1L literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/vanilla_bugfixes.ips b/worlds/sm/variaRandomizer/patches/common/ips/vanilla_bugfixes.ips new file mode 100644 index 0000000000000000000000000000000000000000..c0ea25d7a8cab65b90ca44d5be89e45930b0137b GIT binary patch literal 206 zcmWG=3~}~gG+X^gUAPFpPO4f_L|F_%-{Zj zB?HLG@L^(I!oawdiA|b;>0%ob+eHTcwOd#{=4@eASk;@+yM;A@iCvt5fq~@}6E80V zqdb%86$WOXo>r!WsbCEm=NK4SnDUr_B(pgS1M>&o-5lFAfwptJVrO7h@cP5?@ehRl yqXB literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/ips/varia_hud.ips b/worlds/sm/variaRandomizer/patches/common/ips/varia_hud.ips index 5002a67cf32456932eba5d5cb78b4288d9dbeabb..d0a0ac8e653afd137db9feb9482ce5e3bf7ae155 100644 GIT binary patch delta 1788 zcmX|B4Qvx-7=C)~y0#8_>wtBmxN~E=B?~l}C?WimMa%>Y>k(os)zlD@%@I%&W1M7n zn@ij<&?XH!o>X`@yQ#1#ZvHZMP2%t?Vn!vmJPs{t{Z@m zcF4aD|7H?inUMp-580o&?gu9|!v7{*s1`QOY_FObc5$R5g2a6=V|qRO{V0)g4Pl~= zz$rQCk=z{@G0H$lcnXzLsvJ3|;%o{po9bqnR11Qx`wGjME^Q{nu|a#viJUFPBe#b5 zgb)7flmO-X6UuU&yfY&(-!RE$=f~0Cq3JfiXa>?765!&4cSTXcYQ>^{e2=5cd zfXb=ZI#%jBrk34}5Dyu_k5DP(%kn--&1QP)L#O_wU}p)Of=N{~pI<1=2$MX*G$HY8 zHX#qSLY#nw3D7G~un4ID za*I;dr}x%(&88$e_@7Mk1f;bXqv08IBmuF8#6h_5I5C*}n~gn4dmjc>k5otUNK7HA~gvguUSzlrQ-3NUEwvS z9$jqR6cJu{33KcrkylRc39mV zeR?%rg;1h}*GA!zyzT*na@&mCu`KZ%uU#PAbzvedvh!)`q%x*p9$HNvSM_^>*RIi> zC6SyiP}&{sN0dp51GUY)^0^9zoMCqwuieE4&|DAXWBMT9Qsm-9JB)CzjeS$nScCM- zOu?Mn43x+s1wLmBpRX$h%s)lrlbcUFaP_LUSqE%BK=uzN_YyXFMzRX{d@DH0GQZ7m z+Cljr+R9*d(Ydt;-Ui=SV-;II NB;s2&wq?ij{{pE=qG$jB delta 1436 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 literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/common/patches.py b/worlds/sm/variaRandomizer/patches/common/patches.py index c61d53f40c..64ee83335c 100644 --- a/worlds/sm/variaRandomizer/patches/common/patches.py +++ b/worlds/sm/variaRandomizer/patches/common/patches.py @@ -51,8 +51,29 @@ patches = { "Enable_Backup_Saves": { 0xef20: [0x1] }, - 'Escape_Scavenger' : { - 0x10F5FC: [0x1] + 'Escape_Trigger' : { + 0x10F5FE: [0x1] + }, + 'Escape_Trigger_Disable' : { + 0x10F5FE: [0x0] + }, + # actually a bitmask: + # high bit is for sfx play on obj completion, low bit for trigger escape + # only in crateria (standard in rando, default in the patch) for nothing objectives. + # we want to play sfx on objective completion only with non-standard objectives + 'Objectives_sfx' : { + 0x10F5FF: [0x81] + }, + # see above, used in plandos so trigger escape whatever the start loc is + # with nothing objective. With this, we'll play sfx even in plandos + # with standard objectives, but it'll prevent to handle these patches + # as anything else that just bytes. + 'Escape_Trigger_Nothing_Objective_Anywhere' : { + 0x10F5FF: [0x80] + }, + # for development/quickmet: disable clear save files on 1st boot + "Disable_Clear_Save_Boot": { + 0x7E39: [0x4c, 0x7c, 0xfe] }, # vanilla data to restore setup asm for plandos "Escape_Animals_Disable": { @@ -96,6 +117,9 @@ patches = { "SpriteSomething_Disable_Spin_Attack": { 0xD93FE: [0x0, 0x0] }, + "Ship_Takeoff_Disable_Hide_Samus": { + 0x112B13: [0x6B] + }, # custom load points for non standard start APs "Save_G4": { # load point entry @@ -314,6 +338,32 @@ patches = { 0x78A34: [0x48, 0xc8, 0x01, 0x16, 0x47, 0x8c], 0x109F37: [0x0] }, + # only set blinking in "zebes asleep" room state to avoid having + # the door blink when not needed + # (only needed for escape peek in Crateria-less minimizer with disabled Tourian) + 'Blinking[Climb Bottom Left]': { + 0x782FE: [0x48, 0xc8, 0x01, 0x86, 0x12, 0x8c], + 0x108683: [0x0] + }, + # Climb always in "zebes asleep" state, except during escape + # (for escape peek in Crateria-less minimizer with disabled Tourian) + 'Climb_Asleep': { + # replace "zebes awake" event ID with an unused event + 0x796CC: [0x7F], + # put "Statues Hall" tension music + 0x796D6: [0x04] + }, + # Indicator PLM IDs set to ffff because they're set dynamically + 'Indicator[KihunterBottom]': { + 0x78256: [0xff, 0xff, 0x06, 0x02, 0x0e, 0x00] + }, + 'Indicator[GreenHillZoneTopRight]': { + 0x78746: [0xff, 0xff, 0x01, 0x26, 0x30, 0x00] + }, + # cancels the gamestate change by new_game.asm + "Restore_Intro": { + 0x16EDA: [0x1E] + } } additional_PLMs = { @@ -550,4 +600,101 @@ additional_PLMs = { [0x48, 0xc8, 0x01, 0x16, 0x63, 0x8c] ] }, + # Indicator PLM IDs set to ffff because they're set dynamically + 'Indicator[LandingSiteRight]': { + 'room': 0x948c, + 'plm_bytes_list': [ + [0xff, 0xff, 0x01, 0x06, 0x00, 0x00] + ] + }, + 'Indicator[KihunterRight]': { + 'room': 0x95ff, + 'plm_bytes_list': [ + [0xff, 0xff, 0x01, 0x06, 0x0d, 0x00] + ] + }, + 'Indicator[NoobBridgeRight]': { + 'room': 0xa253, + 'plm_bytes_list': [ + [0xff, 0xff, 0x01, 0x46, 0x33, 0x00] + ] + }, + 'Indicator[MainShaftBottomRight]': { + 'room': 0x9cb3, + 'plm_bytes_list': [ + [0xff, 0xff, 0x01, 0x06, 0x22, 0x00] + ] + }, + 'Indicator[BigPinkBottomRight]': { + 'room': 0x9e52, + 'plm_bytes_list': [ + [0xff, 0xff, 0x01, 0x06, 0x29, 0x00] + ] + }, + 'Indicator[RedTowerElevatorLeft]': { + 'room': 0xa2f7, + 'plm_bytes_list': [ + [0xff, 0xff, 0x2e, 0x06, 0x3c, 0x00] + ] + }, + 'Indicator[WestOceanRight]': { + 'room': 0xca08, + 'plm_bytes_list': [ + [0xff, 0xff, 0x01, 0x06, 0x0c, 0x00] + ] + }, + 'Indicator[LeCoudeBottom]': { + 'room': 0x94cc, + 'plm_bytes_list': [ + [0xff, 0xff, 0x06, 0x02, 0x0f, 0x00] + ] + }, + 'Indicator[WreckedShipMainShaftBottom]': { + 'room': 0xcc6f, + 'plm_bytes_list': [ + [0xff, 0xff, 0x26, 0x02, 0x84, 0x00] + ] + }, + 'Indicator[CathedralEntranceRight]': { + 'room': 0xa788, + 'plm_bytes_list': [ + [0xff, 0xff, 0x01, 0x06, 0x4a, 0x00] + ] + }, + 'Indicator[CathedralRight]': { + 'room': 0xafa3, + 'plm_bytes_list': [ + [0xff, 0xff, 0x01, 0x06, 0x49, 0x00] + ] + }, + 'Indicator[RedKihunterShaftBottom]': { + 'room': 0xb5d5, + 'plm_bytes_list': [ + [0xff, 0xff, 0x56, 0x02, 0x5e, 0x00] + ] + }, + 'Indicator[WastelandLeft]': { + 'room': 0xb62b, + 'plm_bytes_list': [ + [0xff, 0xff, 0x2e, 0x06, 0x5f, 0x00] + ] + }, + 'Indicator[MainStreetBottomRight]': { + 'room': 0xd08a, + 'plm_bytes_list': [ + [0xff, 0xff, 0x01, 0x06, 0x8d, 0x00] + ] + }, + 'Indicator[CrabShaftRight]': { + 'room': 0xd5a7, + 'plm_bytes_list': [ + [0xff, 0xff, 0x01, 0x16, 0x8f, 0x00] + ] + }, + 'Indicator[ColosseumBottomRight]': { + 'room': 0xd78f, + 'plm_bytes_list': [ + [0xff, 0xff, 0x01, 0x06, 0x9a, 0x00] + ] + } } diff --git a/worlds/sm/variaRandomizer/patches/vanilla/ips/aqueduct_bomb_blocks.ips b/worlds/sm/variaRandomizer/patches/vanilla/ips/aqueduct_bomb_blocks.ips new file mode 100644 index 0000000000000000000000000000000000000000..0ca9a658dfa349f87eb8f64c57abcc4bc6a1d3a6 GIT binary patch literal 3345 zcmW+&dr(x_oj&)R+qWMy4I(0I&@_lFgO4?-afU|^T!U7wQ5XJySN56H1FxGgwTn zDarJ5PZwT2oVPb`_v+nIH#tu(4Sr#L;aTup4Mn^W_tn7f<(uGDc^EiiJ?e2{cb;{_ ztAVcuhSUeDeUMi##LtpP;(I`S5lDi-e*xv0GwCs34IRU0-N*1%{Q44Lu8RFx@^Ny7 z*p&ijIH{~r?C=jB?T&rhSvTRGfAlD0_jwF~SPj5$V#i6NPz;1T20R&XlXGvJbt}9% zNny@#eNcvPEP?bHn1C}}7cQfzM%}IiMBEHF6)Plp3S_!4wgV>WR_o^!0dO^w@BNECH<`6|m>_8r-A;?Z3J)pd}{S$zhf@%Y< zM`FSNW@9{ZN_7U zxP-pU_%banQSZu~Yb`WuJO)UVnTD!c*lwXX-Ao@OoREu{K2GhX3nZ630#D?xL9+QP zph|u(6WM8`N@!qqM1saIQCtdHOeZz7Si{EMf+?=yr8Euf-#9kr|6ge$H1W3#v$;5svP!)mirrj^>9Mw2q6G#vuCkPViwR2hny zJOHx^D$5ifC}6}UBc4IRD-7O~CML{*N{-~nm*hQ6g~!?3Jk=fkZ=0z}jAawp-}!lZ*Lya-Lh_%2fi(uIpvs2Nv4&%qKV^+gB_j{b`dTSWZ6QO7 zx%mL{rXkuxvejHQ`@U?nN_T@m>a1#IjKb3kzlihYL;nL=Q2AB z&9o8F9wA;Z3zlUm4<%EqV|TjZqh;R8n&DTTd2P@8%kVj?P|6C2S#h6@oDdhlG!M3T zJ?X&%mUt4^0j;VqW0uGJETBsMmH$l($r4_q(Q-&t-QMN4B`|>ckb?S=Y#cz6Y%!`R z>R#~lHlOfXpcaW~Z{4$MYad!@`1Xo&@*`!<2id zbY9uyZOsv~5qRdq7VX_RE0$TjD;>S?B%o+Z6P`18Z@Tcv1YR4sfV)>xV+ax?Q4s>+ zF%WGwB8i0{{687Abg9pIGc9%#qK)Eos&BaAYo0&FF5tt6G&`qYpaujE1CjQo7anog zAe_x)WRBK;XZ;GySWa&0@23NykkvzBkqP!IWe6Lmc9HVgeL$&h*?m7WQycf=!=A#?sE717>|cK!oooSQgA#E%s;Q9V^4@C)69_IV6dx z5OokA%o)Fk8*|2o;gC_enb(?Wgac@3f`=*^IP?DU*+oV|%Fm0Kj12v) zcb@+Gdhy(K0JhHk5I~*<+TPEL&=Lz4Q{%cg0Wt@X@BZYpDo( zog1_twZD}5g*M8Pb}e4`^wo1k>)mSsTE4t?7yZgvxqSIu``kZ(^-K|v-|v670IY{| z`CxwQPaymdexUc5z=tl_06x&KnI3O!fZ)*MO4y+PgdhDsePdMrS3Z7EG?fLTW$N?z z8nswI#;cE%g^;OC`8s}KJgWbkkAEYAvRk<$PbO2Vt@|qCcSe$}TABV!ey}9~9m=y{ zfn&5p$9(8;Mf;J8DGNO`_|3RH{eHp5R4m-4bTh&MbosUdZ#V-qdX;*ob{!{+$rO_Auy_Mm^ z6?CAjpR3aHk8IUH;KN$k$TRJw?^f&o#`QaYN{Hs+R6Mf}BY-;Kq;)?4|L z(vG&9{}dQ-2&S?&-<$e>^7l<`iK-3??;~E%&(U7`qKZz!jc|irY9i${JXa5IGo#Fp tsoZRWxy9UcF$vWKZpvBlOy@0T+C&qcue!wmP?YfifMs<{+x~I;Uja@RrF;MY literal 0 HcmV?d00001 diff --git a/worlds/sm/variaRandomizer/patches/vanilla/ips/climb_disable_bomb_blocks.ips b/worlds/sm/variaRandomizer/patches/vanilla/ips/climb_disable_bomb_blocks.ips new file mode 100644 index 0000000000000000000000000000000000000000..6c7c78eb4447a65224c62bee4639cd096741ca33 GIT binary patch literal 1502 zcmX9;Yiv|S6h1R|_x5&Kpe_ap!ABoh-7QcOwMDdDcA*PMf+@xaYMnHuYNJLmqEsPz zXYVd;6Wc;7)D{{WLX?OK_yY~%c<Wf%l%c}gqW{RNWqq|OHGADB;a~|LM z&Y5{|)kER6#i=9iRMH1i=8bq#zqDGPOsnR6h*=9S5IY2!L>+{~-VoNpTFNKF1g4T~ zRz|;hc+nG!9=-WdMC~)c%`v!QvhWps#@A$UMU=5Mc|bdqNFE%I#cbmGMgL9z^&1r~ zCS3rRZ=-oy0*^E(v<9s8@M}^(t=i{AF>#nAt-Jqa4q@_P7%AvV#e#pcoUsc<699AU}coWFXCkK`QCBc#t;37nnj6S7zX>Qu=h8?1g zD3khjg>H3swW+khB@IWJu@>=2jrt?u5ms=D(H0zEpow+Nd6;k9q74ipz9yu$u1AKZ z*>m{19qx{pDwXgpTAH=jA#s@CYra2X&tUNYi(~vP19Uk^^A77d=$vwOGjzSS(d)f-cDz@){1Qqh`S?}Rtb9UcBkJ!jM ze6z%dq-S^*k2Dtjp7(pD%CI)ox42-PT*ova_T@XupA!d4aZbPD!NTd#<7@bySi|Am1 zbrQAAdq9q3%RFnuCido`Wa5moMNVl6p$XixQ?;1$P?K@TDuEKbWJMvr1m>)RZL>m-d9;)bQD~z9W zI|N0`ANuNCGj^}7rAN~qYjEEI*9GP}PIu9cdfEwH<^nh# zU8c|jfu0XMytHAq672RSx;CJcC%WIASQytd<9Gote-A69yJAdEbeDG)FU`qgf5lIt zn(&gR6ddgLiZZhMkc@Crp%csDlY06o9Etw3T)qRN4B~PLi2n_|wsAS|Xary?q2ib6 zC9@u0iM|a+&=0T35V9VU_P}Ifv%SG?uo~?bttQ~SB!lGJIM%*%sB*+3zk~24-B#td zl#s2qn|W=h(*DR%oo0v05i$Bf>cg!y{tGPoDk$N6ZaTXgkUO6(2E@#AK>AzD00mKY z2|!*ft%RIdN?`hIC+Nwq9WDDQ04|dTq3jm`Z*|sI@kgGnmd?EAM0Hn9l?Vs2)xb}j zsFs0vXReAjPFEou%-$|7)O&%6O@ZtR_&pz3Ddj#X_fH)OtmIn`-vhy2<15-~t7J^b z_l~dV?z= n: + n -= len([ap for ap in aps if ap.Boss]) + escAreas = {ap.GraphArea for ap in aps if not ap.Boss} + objForced = forcedAreas.intersection(escAreas) + escAreasList = sorted(list(escAreas)) + while len(objForced) < n and len(escAreasList) > 0: + objForced.add(escAreasList.pop(random.randint(0, len(escAreasList)-1))) + forcedAreas = forcedAreas.union(objForced) + transitions = GraphUtils.createMinimizerTransitions(self.graphSettings.startAP, self.minimizerN, sorted(list(forcedAreas))) else: if not self.bossRando: transitions += vanillaBossesTransitions @@ -31,26 +60,44 @@ class GraphBuilder(object): transitions += vanillaTransitions else: transitions += GraphUtils.createAreaTransitions(self.graphSettings.lightAreaRando) - return AccessGraph(Logic.accessPoints, transitions, self.graphSettings.dotFile) + ret = AccessGraph(Logic.accessPoints, transitions, self.graphSettings.dotFile) + Objectives.objDict[self.graphSettings.player].setGraph(ret, maxDiff) + return ret + + def addForeignItems(self, container, itemLocs): + itemPoolCounts = {} + for item in container.itemPool: + if item.Code is not None: + itemPoolCounts[item.Type] = itemPoolCounts.get(item.Type, 0) + 1 + itemLocsCounts = {} + for il in itemLocs: + if il.Item.Code is not None and il.player == container.sm.player: + itemLocsCounts[il.Item.Type] = itemLocsCounts.get(il.Item.Type, 0) + 1 + + for item, count in itemPoolCounts.items(): + for n in range(max(0, count - itemLocsCounts.get(item, 0))): + container.sm.addItem(item) # fills in escape transitions if escape rando is enabled - # scavEscape = None or (itemLocs, scavItemLocs) couple from filler - def escapeGraph(self, container, graph, maxDiff, scavEscape): + # escapeTrigger = None or (itemLocs, progItemlocs) couple from filler + def escapeGraph(self, container, graph, maxDiff, escapeTrigger): if not self.escapeRando: return True emptyContainer = copy.copy(container) emptyContainer.resetCollected(reassignItemLocs=True) dst = None - if scavEscape is None: + if escapeTrigger is None: possibleTargets, dst, path = self.getPossibleEscapeTargets(emptyContainer, graph, maxDiff) # update graph with escape transition graph.addTransition(escapeSource, dst) + paths = [path] else: - possibleTargets, path = self.getScavengerEscape(emptyContainer, graph, maxDiff, scavEscape) - if path is None: + self.addForeignItems(emptyContainer, escapeTrigger[0]) + possibleTargets, paths = self.escapeTrigger(emptyContainer, graph, maxDiff, escapeTrigger) + if paths is None: return False # get timer value - self.escapeTimer(graph, path, self.areaRando or scavEscape is not None) + self.escapeTimer(graph, paths, self.areaRando or escapeTrigger is not None) self.log.debug("escapeGraph: ({}, {}) timer: {}".format(escapeSource, dst, graph.EscapeAttributes['Timer'])) # animals GraphUtils.escapeAnimalsTransitions(graph, possibleTargets, dst) @@ -68,10 +115,10 @@ class GraphBuilder(object): def getPossibleEscapeTargets(self, emptyContainer, graph, maxDiff): sm = emptyContainer.sm - # setup smbm with item pool - # Ice not usable because of hyper beam - # remove energy to avoid hell runs - # (will add bosses as well) + # setup smbm with item pool: + # - Ice not usable because of hyper beam + # - remove energy to avoid hell runs + # - (will add bosses as well) sm.addItems([item.Type for item in emptyContainer.itemPool if item.Type != 'Ice' and item.Category != 'Energy']) sm.addItem('Hyper') possibleTargets = self._getTargets(sm, graph, maxDiff) @@ -80,55 +127,167 @@ class GraphBuilder(object): path = graph.accessPath(sm, dst, 'Landing Site', maxDiff) return (possibleTargets, dst, path) - def getScavengerEscape(self, emptyContainer, graph, maxDiff, scavEscape): - sm = emptyContainer.sm - itemLocs, lastScavItemLoc = scavEscape[0], scavEscape[1][-1] - # collect all item/locations up until last scav - for il in itemLocs: - emptyContainer.collect(il) - if il == lastScavItemLoc: - break + def escapeTrigger(self, emptyContainer, graph, maxDiff, escapeTrigger): + container = emptyContainer + sm = container.sm + allItemLocs,progItemLocs,split = escapeTrigger[0],escapeTrigger[1],escapeTrigger[2] + # check if crateria is connected, if not replace Tourian + # connection with Climb and add special escape patch to Climb + if not any(il.Location.GraphArea == "Crateria" for il in allItemLocs): + escapeAttr = graph.EscapeAttributes + if "patches" not in escapeAttr: + escapeAttr['patches'] = [] + escapeAttr['patches'] += ['climb_disable_bomb_blocks.ips', "Climb_Asleep"] + src, _ = next(t for t in graph.InterAreaTransitions if t[1].Name == "Golden Four") + graph.removeTransitions("Golden Four") + graph.addTransition(src.Name, "Climb Bottom Left") + # disconnect the other side of G4 + graph.addTransition("Golden Four", "Golden Four") + # remove vanilla escape transition + graph.addTransition('Tourian Escape Room 4 Top Right', 'Tourian Escape Room 4 Top Right') + # filter garbage itemLocs + ilCheck = lambda il: not il.Location.isBoss() and not il.Location.restricted and il.Item.Category != "Nothing" + # update item% objectives + accessibleItems = [il.Item for il in allItemLocs if ilCheck(il)] + majorUpgrades = [item.Type for item in accessibleItems if item.BeamBits != 0 or item.ItemBits != 0] + sm.objectives.setItemPercentFuncs(len(accessibleItems), majorUpgrades) + if split == "Scavenger": + # update escape access for scav with last scav loc + lastScavItemLoc = progItemLocs[-1] + sm.objectives.updateScavengerEscapeAccess(lastScavItemLoc.Location.accessPoint) + sm.objectives.setScavengerHuntFunc(lambda sm, ap: sm.haveItem(lastScavItemLoc.Item.Type)) + else: + # update "collect all items in areas" funcs + availLocsByArea=defaultdict(list) + for itemLoc in allItemLocs: + if ilCheck(itemLoc) and (split.startswith("Full") or itemLoc.Location.isClass(split)): + availLocsByArea[itemLoc.Location.GraphArea].append(itemLoc.Location.Name) + self.log.debug("escapeTrigger. availLocsByArea="+str(availLocsByArea)) + sm.objectives.setAreaFuncs({area:lambda sm,ap:SMBool(len(container.getLocs(lambda loc: loc.Name in availLocsByArea[area]))==0) for area in availLocsByArea}) + self.log.debug("escapeTrigger. collect locs until G4 access") + # collect all item/locations up until we can pass G4 (the escape triggers) + itemLocs = allItemLocs[:] + 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": + continue + self.log.debug("collecting " + getItemLocStr(il)) + container.collect(il) + ap = il.Location.accessPoint + # final update of item% obj + collectedLocsAccessPoints = {il.Location.accessPoint for il in container.itemLocations} + sm.objectives.updateItemPercentEscapeAccess(list(collectedLocsAccessPoints)) possibleTargets = self._getTargets(sm, graph, maxDiff) - path = graph.accessPath(sm, lastScavItemLoc.Location.accessPoint, 'Landing Site', maxDiff) - return (possibleTargets, path) + # try to escape from all the possible objectives APs + possiblePaths = [] + for goal in Objectives.objDict[self.graphSettings.player].activeGoals: + n, possibleAccessPoints = goal.escapeAccessPoints + count = 0 + for ap in possibleAccessPoints: + self.log.debug("escapeTrigger. testing AP " + ap) + path = graph.accessPath(sm, ap, 'Landing Site', maxDiff) + if path is not None: + self.log.debug("escapeTrigger. add path from "+ap) + possiblePaths.append(path) + count += 1 + if count < n: + # there is a goal we cannot escape from + self.log.debug("escapeTrigger. goal %s: found %d/%d possible escapes, abort" % (goal.name, count, n)) + return (None, None) + # try and get a path from all possible areas + self.log.debug("escapeTrigger. completing paths") + allAreas = {il.Location.GraphArea for il in allItemLocs if not il.Location.restricted and not il.Location.GraphArea in ["Tourian", "Ceres"]} + def getStartArea(path): + return path[0].GraphArea + def apCheck(ap): + nonlocal graph, possiblePaths + apObj = graph.accessPoints[ap] + return apObj.GraphArea not in [getStartArea(path) for path in possiblePaths] + escapeAPs = [ap for ap in collectedLocsAccessPoints if apCheck(ap)] + for ap in escapeAPs: + path = graph.accessPath(sm, ap, 'Landing Site', maxDiff) + if path is not None: + self.log.debug("escapeTrigger. add path from "+ap) + possiblePaths.append(path) + def areaPathCheck(): + nonlocal allAreas, possiblePaths + startAreas = {getStartArea(path) for path in possiblePaths} + return len(allAreas - startAreas) == 0 + while not areaPathCheck() and len(itemLocs) > 0: + il = itemLocs.pop(0) + if il.Location.restricted or il.Item.Type == "ArchipelagoItem": + continue + self.log.debug("collecting " + getItemLocStr(il)) + container.collect(il) + ap = il.Location.accessPoint + if apCheck(ap): + path = graph.accessPath(sm, ap, 'Landing Site', maxDiff) + if path is not None: + self.log.debug("escapeTrigger. add path from "+ap) + possiblePaths.append(path) + + return (possibleTargets, possiblePaths) + + def _computeTimer(self, graph, path): + traversedAreas = list(set([ap.GraphArea for ap in path])) + self.log.debug("escapeTimer path: " + str([ap.Name for ap in path])) + self.log.debug("escapeTimer traversedAreas: " + str(traversedAreas)) + # rough estimates of navigation within areas to reach "borders" + # (can obviously be completely off wrt to actual path, but on the generous side) + traversals = { + 'Crateria':90, + 'GreenPinkBrinstar':90, + 'WreckedShip':120, + 'LowerNorfair':135, + 'WestMaridia':75, + 'EastMaridia':100, + 'RedBrinstar':75, + 'Norfair': 120, + 'Kraid': 40, + 'Crocomire': 40, + # can't be on the path + 'Tourian': 0, + } + t = 90 if self.areaRando else 0 + for area in traversedAreas: + t += traversals[area] + t = max(t, 180) + return t + # path: as returned by AccessGraph.accessPath - def escapeTimer(self, graph, path, compute): - if compute == True: - if path[0].Name == 'Climb Bottom Left': - graph.EscapeAttributes['Timer'] = None - return - traversedAreas = list(set([ap.GraphArea for ap in path])) - self.log.debug("escapeTimer path: " + str([ap.Name for ap in path])) - self.log.debug("escapeTimer traversedAreas: " + str(traversedAreas)) - # rough estimates of navigation within areas to reach "borders" - # (can obviously be completely off wrt to actual path, but on the generous side) - traversals = { - 'Crateria':90, - 'GreenPinkBrinstar':90, - 'WreckedShip':120, - 'LowerNorfair':135, - 'WestMaridia':75, - 'EastMaridia':100, - 'RedBrinstar':75, - 'Norfair': 120, - 'Kraid': 40, - 'Crocomire': 40, - # can't be on the path - 'Tourian': 0, - } - t = 90 if self.areaRando else 0 - for area in traversedAreas: - t += traversals[area] - t = max(t, 180) + def escapeTimer(self, graph, paths, compute): + if len(paths) == 1: + path = paths.pop() + if compute == True: + if path[0].Name == 'Climb Bottom Left': + graph.EscapeAttributes['Timer'] = None + return + t = self._computeTimer(graph, path) + else: + escapeTargetsTimer = { + 'Climb Bottom Left': None, # vanilla + 'Green Brinstar Main Shaft Top Left': 210, # brinstar + 'Basement Left': 210, # wrecked ship + 'Business Center Mid Left': 270, # norfair + 'Crab Hole Bottom Right': 270 # maridia + } + t = escapeTargetsTimer[path[0].Name] + self.log.debug("escapeTimer. t="+str(t)) + graph.EscapeAttributes['Timer'] = t else: - escapeTargetsTimer = { - 'Climb Bottom Left': None, # vanilla - 'Green Brinstar Main Shaft Top Left': 210, # brinstar - 'Basement Left': 210, # wrecked ship - 'Business Center Mid Left': 270, # norfair - 'Crab Hole Bottom Right': 270 # maridia - } - t = escapeTargetsTimer[path[0].Name] - self.log.debug("escapeTimer. t="+str(t)) - graph.EscapeAttributes['Timer'] = t + assert compute + graph.EscapeAttributes['Timer'] = 0 + timerValues = {} + graph.EscapeAttributes['TimerTable'] = timerValues + for path in paths: + area = path[0].GraphArea + prev = timerValues.get(area, 0) + t = max(prev, self._computeTimer(graph, path)) + timerValues[area] = t + self.log.debug("escapeTimer. area=%s, t=%d" % (area, t)) + for area in graphAreas[1:-1]: # no Ceres or Tourian + if area not in timerValues: + # area not in graph most probably, still write a 10 minute "ultra failsafe" value + timerValues[area] = 600 diff --git a/worlds/sm/variaRandomizer/rando/ItemLocContainer.py b/worlds/sm/variaRandomizer/rando/ItemLocContainer.py index 524e31907c..cdde5b4236 100644 --- a/worlds/sm/variaRandomizer/rando/ItemLocContainer.py +++ b/worlds/sm/variaRandomizer/rando/ItemLocContainer.py @@ -6,12 +6,13 @@ from ..logic.smboolmanager import SMBoolManager from collections import Counter class ItemLocation(object): - __slots__ = ( 'Item', 'Location', 'Accessible' ) + __slots__ = ( 'Item', 'Location', 'Accessible', 'player' ) - def __init__(self, Item=None, Location=None, accessible=True): + def __init__(self, Item=None, Location=None, player=0, accessible=True): self.Item = Item self.Location = Location self.Accessible = accessible + self.player = player def json(self): return {'Item': self.Item.json(), 'Location': self.Location.json()} diff --git a/worlds/sm/variaRandomizer/rando/Items.py b/worlds/sm/variaRandomizer/rando/Items.py index 6bfa8530ac..ec58b4782a 100644 --- a/worlds/sm/variaRandomizer/rando/Items.py +++ b/worlds/sm/variaRandomizer/rando/Items.py @@ -40,7 +40,7 @@ class ItemManager: 'ETank': Item( Category='Energy', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Energy Tank", Type='ETank', Id=0 @@ -48,7 +48,7 @@ class ItemManager: 'Missile': Item( Category='Ammo', Class='Minor', - Code=0xf870, + Code=0xfc20, Name="Missile", Type='Missile', Id=1 @@ -56,7 +56,7 @@ class ItemManager: 'Super': Item( Category='Ammo', Class='Minor', - Code=0xf870, + Code=0xfc20, Name="Super Missile", Type='Super', Id=2 @@ -64,7 +64,7 @@ class ItemManager: 'PowerBomb': Item( Category='Ammo', Class='Minor', - Code=0xf870, + Code=0xfc20, Name="Power Bomb", Type='PowerBomb', Id=3 @@ -72,7 +72,7 @@ class ItemManager: 'Bomb': Item( Category='Progression', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Bomb", Type='Bomb', ItemBits=0x1000, @@ -81,7 +81,7 @@ class ItemManager: 'Charge': Item( Category='Beam', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Charge Beam", Type='Charge', BeamBits=0x1000, @@ -90,7 +90,7 @@ class ItemManager: 'Ice': Item( Category='Progression', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Ice Beam", Type='Ice', BeamBits=0x2, @@ -99,7 +99,7 @@ class ItemManager: 'HiJump': Item( Category='Progression', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Hi-Jump Boots", Type='HiJump', ItemBits=0x100, @@ -108,7 +108,7 @@ class ItemManager: 'SpeedBooster': Item( Category='Progression', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Speed Booster", Type='SpeedBooster', ItemBits=0x2000, @@ -117,7 +117,7 @@ class ItemManager: 'Wave': Item( Category='Beam', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Wave Beam", Type='Wave', BeamBits=0x1, @@ -126,7 +126,7 @@ class ItemManager: 'Spazer': Item( Category='Beam', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Spazer", Type='Spazer', BeamBits=0x4, @@ -135,7 +135,7 @@ class ItemManager: 'SpringBall': Item( Category='Misc', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Spring Ball", Type='SpringBall', ItemBits=0x2, @@ -144,7 +144,7 @@ class ItemManager: 'Varia': Item( Category='Progression', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Varia Suit", Type='Varia', ItemBits=0x1, @@ -153,7 +153,7 @@ class ItemManager: 'Plasma': Item( Category='Beam', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Plasma Beam", Type='Plasma', BeamBits=0x8, @@ -162,7 +162,7 @@ class ItemManager: 'Grapple': Item( Category='Progression', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Grappling Beam", Type='Grapple', ItemBits=0x4000, @@ -171,7 +171,7 @@ class ItemManager: 'Morph': Item( Category='Progression', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Morph Ball", Type='Morph', ItemBits=0x4, @@ -180,7 +180,7 @@ class ItemManager: 'Reserve': Item( Category='Energy', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Reserve Tank", Type='Reserve', Id=20 @@ -188,7 +188,7 @@ class ItemManager: 'Gravity': Item( Category='Progression', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Gravity Suit", Type='Gravity', ItemBits=0x20, @@ -197,7 +197,7 @@ class ItemManager: 'XRayScope': Item( Category='Misc', Class='Major', - Code=0xf870, + Code=0xfc20, Name="X-Ray Scope", Type='XRayScope', ItemBits=0x8000, @@ -206,7 +206,7 @@ class ItemManager: 'SpaceJump': Item( Category='Progression', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Space Jump", Type='SpaceJump', ItemBits=0x200, @@ -215,7 +215,7 @@ class ItemManager: 'ScrewAttack': Item( Category='Misc', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Screw Attack", Type='ScrewAttack', ItemBits= 0x8, @@ -247,7 +247,7 @@ class ItemManager: Category='Boss', Class='Boss', Name="Phantoon", - Type='Phantoon' + Type='Phantoon', ), 'Draygon': Item( Category='Boss', @@ -267,6 +267,30 @@ class ItemManager: Name="Mother Brain", Type='MotherBrain', ), + 'SporeSpawn': Item( + Category='MiniBoss', + Class='Boss', + Name="Spore Spawn", + Type='SporeSpawn', + ), + 'Crocomire': Item( + Category='MiniBoss', + Class='Boss', + Name="Crocomire", + Type='Crocomire', + ), + 'Botwoon': Item( + Category='MiniBoss', + Class='Boss', + Name="Botwoon", + Type='Botwoon', + ), + 'GoldenTorizo': Item( + Category='MiniBoss', + Class='Boss', + Name="Golden Torizo", + Type='GoldenTorizo', + ), # used only during escape path check 'Hyper': Item( Category='Beam', @@ -278,7 +302,7 @@ class ItemManager: 'ArchipelagoItem': Item( Category='ArchipelagoItem', Class='Major', - Code=0xf870, + Code=0xfc20, Name="Generic", Type='ArchipelagoItem', Id=21 @@ -311,11 +335,12 @@ class ItemManager: itemCode = item.Code + modifier return itemCode - def __init__(self, majorsSplit, qty, sm, nLocs, maxDiff): + def __init__(self, majorsSplit, qty, sm, nLocs, bossesItems, maxDiff): self.qty = qty self.sm = sm self.majorsSplit = majorsSplit self.nLocs = nLocs + self.bossesItems = bossesItems self.maxDiff = maxDiff self.majorClass = 'Chozo' if majorsSplit == 'Chozo' else 'Major' self.itemPool = [] @@ -324,7 +349,7 @@ class ItemManager: self.itemPool = [] if addBosses == True: # for the bosses - for boss in ['Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain']: + for boss in self.bossesItems: self.addMinor(boss) def getItemPool(self): @@ -372,13 +397,17 @@ class ItemManager: return len([item for item in self.itemPool if item.Type == itemName]) >= count class ItemPoolGenerator(object): + # 100 item locs, 5 bosses, 4 mini bosses + maxLocs = 109 + nbBosses = 9 + @staticmethod def factory(majorsSplit, itemManager, qty, sm, exclude, nLocs, maxDiff): if majorsSplit == 'Chozo': return ItemPoolGeneratorChozo(itemManager, qty, sm, maxDiff) elif majorsSplit == 'Plando': return ItemPoolGeneratorPlando(itemManager, qty, sm, exclude, nLocs, maxDiff) - elif nLocs == 105: + elif nLocs == ItemPoolGenerator.maxLocs: if majorsSplit == "Scavenger": return ItemPoolGeneratorScavenger(itemManager, qty, sm, maxDiff) else: @@ -390,7 +419,7 @@ class ItemPoolGenerator(object): self.itemManager = itemManager self.qty = qty self.sm = sm - self.maxItems = 105 # 100 item locs and 5 bosses + self.maxItems = ItemPoolGenerator.maxLocs self.maxEnergy = 18 # 14E, 4R self.maxDiff = maxDiff self.log = log.get('ItemPool') @@ -405,7 +434,7 @@ class ItemPoolGenerator(object): pool = self.itemManager.getItemPool() energy = [item for item in pool if item.Category == 'Energy'] if len(energy) == 0: - self.maxMinors = 0.66*(self.maxItems - 5) # 5 for bosses + self.maxMinors = 0.66*(self.maxItems - ItemPoolGenerator.nbBosses) else: # if energy has been placed, we can be as accurate as possible self.maxMinors = self.maxItems - len(pool) + self.nbMinorsAlready @@ -675,7 +704,8 @@ class ItemPoolGeneratorMinimizer(ItemPoolGeneratorMajors): else: self.maxEnergy = 8 + int(float(nLocs - 55)/50.0 * 8) self.log.debug("maxEnergy: "+str(self.maxEnergy)) - maxItems = self.maxItems - 10 # remove bosses and minimal minore + # remove bosses and minimal minors + maxItems = self.maxItems - (self.nbMinorsAlready + len(self.itemManager.bossesItems)) self.maxEnergy = int(max(self.maxEnergy, maxItems - nMajors - self.minorLocations)) if self.maxEnergy > 18: self.maxEnergy = 18 @@ -707,7 +737,7 @@ class ItemPoolGeneratorPlando(ItemPoolGenerator): if item == 'total': continue itemClass = 'Major' - if item in ['Missile', 'Super', 'PowerBomb', 'Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain']: + if item in ['Missile', 'Super', 'PowerBomb', 'Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain', 'SporeSpawn', 'Crocomire', 'Botwoon', 'GoldenTorizo']: itemClass = 'Minor' for i in range(count): self.itemManager.addItem(item, itemClass) @@ -716,7 +746,7 @@ class ItemPoolGeneratorPlando(ItemPoolGenerator): self.log.debug("Plando: remain start: {}".format(remain)) if remain > 0: # add missing bosses - for boss in ['Kraid', 'Phantoon', 'Draygon', 'Ridley', 'MotherBrain']: + for boss in self.itemManager.bossesItems: if self.exclude['alreadyPlacedItems'][boss] == 0: self.itemManager.addItem(boss, 'Minor') self.exclude['alreadyPlacedItems'][boss] = 1 diff --git a/worlds/sm/variaRandomizer/rando/MiniSolver.py b/worlds/sm/variaRandomizer/rando/MiniSolver.py deleted file mode 100644 index 818632a075..0000000000 --- a/worlds/sm/variaRandomizer/rando/MiniSolver.py +++ /dev/null @@ -1,63 +0,0 @@ - -import utils.log, random - -from logic.smboolmanager import SMBoolManager -from utils.parameters import infinity - -class MiniSolver(object): - def __init__(self, startAP, areaGraph, restrictions): - self.startAP = startAP - self.areaGraph = areaGraph - self.restrictions = restrictions - self.settings = restrictions.settings - self.smbm = SMBoolManager() - self.log = utils.log.get('MiniSolver') - - # if True, does not mean it is actually beatable, unless you're sure of it from another source of information - # if False, it is certain it is not beatable - def isBeatable(self, itemLocations, maxDiff=None): - if maxDiff is None: - maxDiff = self.settings.maxDiff - minDiff = self.settings.minDiff - locations = [] - for il in itemLocations: - loc = il.Location - if loc.restricted: - continue - loc.itemName = il.Item.Type - loc.difficulty = None - locations.append(loc) - self.smbm.resetItems() - ap = self.startAP - onlyBossesLeft = -1 - hasOneLocAboveMinDiff = False - while True: - if not locations: - return hasOneLocAboveMinDiff - # only two loops to collect all remaining locations in only bosses left mode - if onlyBossesLeft >= 0: - onlyBossesLeft += 1 - if onlyBossesLeft > 2: - return False - self.areaGraph.getAvailableLocations(locations, self.smbm, maxDiff, ap) - post = [loc for loc in locations if loc.PostAvailable and loc.difficulty.bool == True] - for loc in post: - self.smbm.addItem(loc.itemName) - postAvailable = loc.PostAvailable(self.smbm) - self.smbm.removeItem(loc.itemName) - loc.difficulty = self.smbm.wand(loc.difficulty, postAvailable) - toCollect = [loc for loc in locations if loc.difficulty.bool == True and loc.difficulty.difficulty <= maxDiff] - if not toCollect: - # mini onlyBossesLeft - if maxDiff < infinity: - maxDiff = infinity - onlyBossesLeft = 0 - continue - return False - if not hasOneLocAboveMinDiff: - hasOneLocAboveMinDiff = any(loc.difficulty.difficulty >= minDiff for loc in locations) - self.smbm.addItems([loc.itemName for loc in toCollect]) - for loc in toCollect: - locations.remove(loc) - # if len(locations) > 0: - # ap = random.choice([loc.accessPoint for loc in locations]) diff --git a/worlds/sm/variaRandomizer/rando/RandoExec.py b/worlds/sm/variaRandomizer/rando/RandoExec.py index 3505b180b2..8dbb5ae49a 100644 --- a/worlds/sm/variaRandomizer/rando/RandoExec.py +++ b/worlds/sm/variaRandomizer/rando/RandoExec.py @@ -31,7 +31,7 @@ class RandoExec(object): vcr = VCR(self.seedName, 'rando') if self.vcr == True else None self.errorMsg = "" split = self.randoSettings.restrictions['MajorMinor'] - graphBuilder = GraphBuilder(self.graphSettings) + self.graphBuilder = GraphBuilder(self.graphSettings) container = None i = 0 attempts = 500 if self.graphSettings.areaRando or self.graphSettings.doorsColorsRando or split == 'Scavenger' else 1 @@ -44,23 +44,28 @@ class RandoExec(object): self.restrictions = Restrictions(self.randoSettings) if self.graphSettings.doorsColorsRando == True: DoorsManager.randomize(self.graphSettings.allowGreyDoors, self.player) - self.areaGraph = graphBuilder.createGraph() + self.areaGraph = self.graphBuilder.createGraph(self.randoSettings.maxDiff) services = RandoServices(self.areaGraph, self.restrictions) setup = RandoSetup(self.graphSettings, Logic.locations, services, self.player) self.setup = setup container = setup.createItemLocContainer(endDate, vcr) if container is None: sys.stdout.write('*') - sys.stdout.flush() i += 1 else: - self.errorMsg += '\n'.join(setup.errorMsgs) + self.errorMsg += '; '.join(setup.errorMsgs) now = time.process_time() if container is None: if self.graphSettings.areaRando: - self.errorMsg += "Could not find an area layout with these settings" - else: - self.errorMsg += "Unable to process settings" + self.errorMsg += "Could not find an area layout with these settings; " + if self.graphSettings.doorsColorsRando: + self.errorMsg += "Could not find a door color combination with these settings; " + if split == "Scavenger": + self.errorMsg += "Scavenger seed generation timed out; " + if setup.errorMsgs: + self.errorMsg += '; '.join(setup.errorMsgs) + if self.errorMsg == "": + self.errorMsg += "Unable to process settings; " self.areaGraph.printGraph() return container diff --git a/worlds/sm/variaRandomizer/rando/RandoServices.py b/worlds/sm/variaRandomizer/rando/RandoServices.py index 60f9df74e9..a3ad1f39b5 100644 --- a/worlds/sm/variaRandomizer/rando/RandoServices.py +++ b/worlds/sm/variaRandomizer/rando/RandoServices.py @@ -26,6 +26,13 @@ class RandoServices(object): self.cache = cache self.log = log.get('RandoServices') + @staticmethod + def printProgress(s): + sys.stdout.write(s) + # avoid flushing I/O on pythonanywhere, as they are very slow + if os.getenv("PYTHONANYWHERE_DOMAIN") is None: + sys.stdout.flush() + # collect an item/loc with logic in a container from a given AP # return new AP def collect(self, ap, container, itemLoc, pickup=True): @@ -36,8 +43,7 @@ class RandoServices(object): self.currentLocations(ap, container) container.collect(itemLoc, pickup=pickup) self.log.debug("COLLECT "+itemLoc.Item.Type+" at "+itemLoc.Location.Name) - sys.stdout.write('.') - sys.stdout.flush() + RandoServices.printProgress('.') return itemLoc.Location.accessPoint if pickup == True else ap # gives all the possible theoretical locations for a given item diff --git a/worlds/sm/variaRandomizer/rando/RandoSettings.py b/worlds/sm/variaRandomizer/rando/RandoSettings.py index 6895b59716..418688f1e5 100644 --- a/worlds/sm/variaRandomizer/rando/RandoSettings.py +++ b/worlds/sm/variaRandomizer/rando/RandoSettings.py @@ -32,11 +32,11 @@ class RandoSettings(object): def isPlandoRando(self): return self.PlandoOptions is not None - def getItemManager(self, smbm, nLocs): + def getItemManager(self, smbm, nLocs, bossesItems): if not self.isPlandoRando(): - return ItemManager(self.restrictions['MajorMinor'], self.qty, smbm, nLocs, self.maxDiff) + return ItemManager(self.restrictions['MajorMinor'], self.qty, smbm, nLocs, bossesItems, self.maxDiff) else: - return ItemManager('Plando', self.qty, smbm, nLocs, self.maxDiff) + return ItemManager('Plando', self.qty, smbm, nLocs, bossesItems, self.maxDiff) def getExcludeItems(self, locations): if not self.isPlandoRando(): @@ -67,7 +67,11 @@ class RandoSettings(object): # Holds settings and utiliy functions related to graph layout class GraphSettings(object): - def __init__(self, startAP, areaRando, lightAreaRando, bossRando, escapeRando, minimizerN, dotFile, doorsColorsRando, allowGreyDoors, plandoRandoTransitions): + def __init__(self, player, startAP, areaRando, lightAreaRando, + bossRando, escapeRando, minimizerN, dotFile, + doorsColorsRando, allowGreyDoors, tourian, + plandoRandoTransitions): + self.player = player self.startAP = startAP self.areaRando = areaRando self.lightAreaRando = lightAreaRando @@ -77,6 +81,7 @@ class GraphSettings(object): self.dotFile = dotFile self.doorsColorsRando = doorsColorsRando self.allowGreyDoors = allowGreyDoors + self.tourian = tourian self.plandoRandoTransitions = plandoRandoTransitions def isMinimizer(self): @@ -122,10 +127,16 @@ class ProgSpeedParameters(object): elif progSpeed == 'fastest': return 0.33 return 0 + + # chozo/slowest can make seed generation fail often, not much + # of a gameplay difference between slow/slowest in Chozo anyway, + # so we merge slow and slowest for some params + def isSlow(self, progSpeed): + return progSpeed == "slow" or (progSpeed == "slowest" and self.restrictions.split == "Chozo") def getItemLimit(self, progSpeed): itemLimit = self.nLocs - if progSpeed == 'slow': + if self.isSlow(progSpeed): itemLimit = int(self.nLocs*0.209) # 21 for 105 elif progSpeed == 'medium': itemLimit = int(self.nLocs*0.095) # 9 for 105 @@ -143,7 +154,7 @@ class ProgSpeedParameters(object): def getLocLimit(self, progSpeed): locLimit = -1 - if progSpeed == 'slow': + if self.isSlow(progSpeed): locLimit = 1 elif progSpeed == 'medium': locLimit = 2 @@ -158,12 +169,12 @@ class ProgSpeedParameters(object): if self.restrictions.isLateDoors(): progTypes += ['Wave','Spazer','Plasma'] progTypes.append('Charge') - if progSpeed == 'slowest': + if progSpeed == 'slowest' and self.restrictions.split != "Chozo": return progTypes else: progTypes.remove('HiJump') progTypes.remove('Charge') - if progSpeed == 'slow': + if self.isSlow(progSpeed): return progTypes else: progTypes.remove('Bomb') diff --git a/worlds/sm/variaRandomizer/rando/RandoSetup.py b/worlds/sm/variaRandomizer/rando/RandoSetup.py index 637a5fed39..943e3fe5f3 100644 --- a/worlds/sm/variaRandomizer/rando/RandoSetup.py +++ b/worlds/sm/variaRandomizer/rando/RandoSetup.py @@ -9,7 +9,9 @@ from ..graph.graph_utils import getAccessPoint, GraphUtils from ..rando.Filler import FrontFiller from ..rando.ItemLocContainer import ItemLocContainer, getLocListStr, ItemLocation, getItemListStr from ..rando.Restrictions import Restrictions +from ..utils.objectives import Objectives from ..utils.parameters import infinity +from ..rom.rom_patches import RomPatches # checks init conditions for the randomizer: processes super fun settings, graph, start location, special restrictions # the entry point is createItemLocContainer @@ -27,7 +29,9 @@ class RandoSetup(object): self.allLocations = locations self.locations = self.areaGraph.getAccessibleLocations(locations, self.startAP) # print("nLocs Setup: "+str(len(self.locations))) - self.itemManager = self.settings.getItemManager(self.sm, len(self.locations)) + # in minimizer we can have some missing boss locs + bossesItems = [loc.BossItemType for loc in self.locations if loc.isBoss()] + self.itemManager = self.settings.getItemManager(self.sm, len(self.locations), bossesItems) self.forbiddenItems = [] self.restrictedLocs = [] self.lastRestricted = [] @@ -67,7 +71,12 @@ class RandoSetup(object): for loc in self.restrictedLocs: self.log.debug("createItemLocContainer: loc is restricted: {}".format(loc.Name)) loc.restricted = True + + # checkDoorBeams calls checkPool, so save error messages + errorMsgsBck = self.errorMsgs[:] self.checkDoorBeams() + self.errorMsgs = errorMsgsBck + self.container = ItemLocContainer(self.sm, self.getItemPool(), self.locations) if self.restrictions.isLateMorph(): self.restrictions.lateMorphInit(self.startAP, self.container, self.services) @@ -122,7 +131,9 @@ class RandoSetup(object): self.log.debug("fillRestrictedLocations. locs="+getLocListStr(locs)) for loc in locs: itemLocation = ItemLocation(None, loc) - if self.container.hasItemInPool(getPred('Nothing', loc)): + if loc.BossItemType is not None: + itemLocation.Item = self.container.getNextItemInPoolMatching(getPred(loc.BossItemType, loc)) + elif self.container.hasItemInPool(getPred('Nothing', loc)): itemLocation.Item = self.container.getNextItemInPoolMatching(getPred('Nothing', loc)) elif self.container.hasItemInPool(getPred('NoEnergy', loc)): itemLocation.Item = self.container.getNextItemInPoolMatching(getPred('NoEnergy', loc)) @@ -168,10 +179,13 @@ class RandoSetup(object): self.log.debug("checkDoorBeams. mandatoryBeams="+str(self.restrictions.mandatoryBeams)) def checkPool(self, forbidden=None): + self.errorMsgs = [] self.log.debug("checkPool. forbidden=" + str(forbidden) + ", self.forbiddenItems=" + str(self.forbiddenItems)) if not self.graphSettings.isMinimizer() and not self.settings.isPlandoRando() and len(self.allLocations) > len(self.locations): # invalid graph with looped areas - self.log.debug("checkPool: not all areas are connected, but minimizer param is off / not a plando rando") + msg = "not all areas are connected, but minimizer param is off / not a plando rando" + self.log.debug("checkPool: {}".format(msg)) + self.errorMsgs.append(msg) return False ret = True if forbidden is not None: @@ -185,7 +199,9 @@ class RandoSetup(object): container = ItemLocContainer(self.sm, pool, self.locations) except AssertionError as e: # invalid graph altogether - self.log.debug("checkPool: AssertionError when creating ItemLocContainer: {}".format(e)) + msg = "AssertionError when creating ItemLocContainer: {}".format(e) + self.log.debug("checkPool: {}".format(msg)) + self.errorMsgs.append(msg) return False # restrict item pool in chozo: game should be finishable with chozo items only contPool = [] @@ -210,25 +226,55 @@ class RandoSetup(object): self.lastRestricted = [loc for loc in self.locations if loc not in totalAvailLocs] self.log.debug("restricted=" + str([loc.Name for loc in self.lastRestricted])) - # check if all inter-area APs can reach each other - interAPs = [ap for ap in self.areaGraph.getAccessibleAccessPoints(self.startAP) if not ap.isInternal() and not ap.isLoop()] - for startAp in interAPs: - availAccessPoints = self.areaGraph.getAvailableAccessPoints(startAp, self.sm, self.settings.maxDiff) - for ap in interAPs: - if not ap in availAccessPoints: - self.log.debug("checkPool: ap {} non accessible from {}".format(ap.Name, startAp.Name)) + # check if objectives are compatible with accessible APs + startAP = self.areaGraph.accessPoints[self.startAP] + availAPs = [ap.Name for ap in self.areaGraph.getAvailableAccessPoints(startAP, self.sm, self.settings.maxDiff)] + self.log.debug("availAPs="+str(availAPs)) + for goal in Objectives.objDict[self.graphSettings.player].activeGoals: + n, aps = goal.escapeAccessPoints + if len(aps) == 0: + continue + escAPs = [ap for ap in aps if ap in availAPs] + self.log.debug("escAPs="+str(escAPs)) + if len(escAPs) < n: + msg = "goal '{}' impossible to complete due to area layout".format(goal.name) + self.log.debug("checkPool. {}".format(msg)) + self.errorMsgs.append(msg) + ret = False + continue + for ap in escAPs: + if not self.areaGraph.canAccess(self.sm, ap, "Golden Four", self.settings.maxDiff): + msg = "goal '{}' impossible to complete due to area layout".format(goal.name) + self.log.debug("checkPool. {}".format(msg)) + self.errorMsgs.append(msg) ret = False - if not ret: - self.log.debug("checkPool. inter-area APs check failed") + break + # check if all inter-area APs can reach each other + if ret: + interAPs = [ap for ap in self.areaGraph.getAccessibleAccessPoints(self.startAP) if not ap.isInternal() and not ap.isLoop()] + for startAp in interAPs: + availAccessPoints = self.areaGraph.getAvailableAccessPoints(startAp, self.sm, self.settings.maxDiff) + for ap in interAPs: + if not ap in availAccessPoints: + self.log.debug("checkPool: ap {} non accessible from {}".format(ap.Name, startAp.Name)) + ret = False + if not ret: + msg = "inter-area APs check failed" + self.log.debug("checkPool. {}".format(msg)) + self.errorMsgs.append(msg) # cleanup self.sm.resetItems() self.restoreBossChecks() # check if we can reach/beat all bosses if ret: + # always add G4 to mandatory bosses, even if not required by objectives + mandatoryBosses = set(Objectives.objDict[self.sm.player].getMandatoryBosses() + Bosses.Golden4()) + for loc in self.lastRestricted: if loc.Name in self.bossesLocs: ret = False - self.log.debug("unavail Boss: " + loc.Name) + msg = "unavail Boss: {}".format(loc.Name) + self.log.debug("checkPool. {}".format(msg)) if ret: # revive bosses self.sm.addItems([item.Type for item in contPool if item.Category != 'Boss']) @@ -238,17 +284,24 @@ class RandoSetup(object): and self.areaGraph.canAccess(self.sm, self.startAP, 'DraygonRoomIn', maxDiff) if ret: # see if we can beat bosses with this equipment (infinity as max diff for a "onlyBossesLeft" type check - beatableBosses = sorted([loc.Name for loc in self.services.currentLocations(self.startAP, container, diff=infinity) if loc.isBoss()]) + beatableBosses = sorted([loc.BossItemType for loc in self.services.currentLocations(self.startAP, container, diff=infinity) if loc.isBoss()]) self.log.debug("checkPool. beatableBosses="+str(beatableBosses)) - ret = beatableBosses == Bosses.Golden4() + self.log.debug("checkPool. mandatoryBosses: {}".format(mandatoryBosses)) + ret = mandatoryBosses.issubset(set(beatableBosses)) and Objectives.objDict[self.sm.player].checkLimitObjectives(beatableBosses) if ret: # check that we can then kill mother brain - self.sm.addItems(Bosses.Golden4()) + self.sm.addItems(Bosses.Golden4() + Bosses.miniBosses()) beatableMotherBrain = [loc.Name for loc in self.services.currentLocations(self.startAP, container, diff=infinity) if loc.Name == 'Mother Brain'] ret = len(beatableMotherBrain) > 0 self.log.debug("checkPool. beatable Mother Brain={}".format(ret)) + else: + msg = "can't kill all mandatory bosses/minibosses: {}".format(', '.join(list(mandatoryBosses - set(beatableBosses)))) + self.log.debug("checkPool. {}".format(msg)) + self.errorMsgs.append(msg) else: - self.log.debug('checkPool. locked by Phantoon or Draygon') + msg = "locked by Phantoon or Draygon" + self.log.debug('checkPool. {}'.format(msg)) + self.errorMsgs.append(msg) self.log.debug('checkPool. boss access sanity check: '+str(ret)) if self.restrictions.isChozo() or self.restrictions.isScavenger(): @@ -319,7 +372,6 @@ class RandoSetup(object): else: forb = [] self.forbiddenItems += forb - self.checkPool() self.addRestricted() return len(forb) @@ -344,6 +396,9 @@ class RandoSetup(object): def getForbiddenMovement(self): self.log.debug("getForbiddenMovement BEGIN. forbidden="+str(self.forbiddenItems)) removableMovement = [mvt for mvt in self.movementItems if self.checkPool([mvt])] + if 'Bomb' in removableMovement and not RomPatches.has(self.sm.player, RomPatches.BombTorizoWake) and Objectives.objDict[self.sm.player].isGoalActive("activate chozo robots"): + # in this objective, without VARIA tweaks, BT has to wake so give bombs + removableMovement.remove('Bomb') self.log.debug("getForbiddenMovement removable="+str(removableMovement)) if len(removableMovement) > 0: # remove at least the most important diff --git a/worlds/sm/variaRandomizer/rando/Restrictions.py b/worlds/sm/variaRandomizer/rando/Restrictions.py index 6880123197..fabdfea455 100644 --- a/worlds/sm/variaRandomizer/rando/Restrictions.py +++ b/worlds/sm/variaRandomizer/rando/Restrictions.py @@ -14,11 +14,9 @@ class Restrictions(object): self.suitsRestrictions = settings.restrictions['Suits'] self.scavLocs = None self.scavIsVanilla = False - self.scavEscape = False self.restrictionDictChecker = None if self.split == 'Scavenger': self.scavIsVanilla = settings.restrictions['ScavengerParams']['vanillaItems'] - self.scavEscape = settings.restrictions['ScavengerParams']['escape'] # checker function chain used by canPlaceAtLocation self.checkers = self.getCheckers() self.static = {} @@ -84,7 +82,7 @@ class Restrictions(object): self.checkers.append(self.restrictionDictChecker) def isLocMajor(self, loc): - return not loc.isBoss() and (self.split == "Full" or loc.isClass(self.split)) + return (not loc.isBoss() and self.split == "Full") or loc.isClass(self.split) def isLocMinor(self, loc): return not loc.isBoss() and (self.split == "Full" or not loc.isClass(self.split)) @@ -93,7 +91,7 @@ class Restrictions(object): if self.split == "Full": return True elif self.split == 'Scavenger': - return not self.isItemMinor(item) + return not self.isItemMinor(item) or item.Type == "Ridley" else: return item.Class == self.split @@ -135,7 +133,7 @@ class Restrictions(object): def getCheckers(self): checkers = [] self.log.debug("add bosses restriction") - checkers.append(lambda item, loc, cont: (item.Category != 'Boss' and not loc.isBoss()) or (item.Category == 'Boss' and item.Name == loc.Name)) + checkers.append(lambda item, loc, cont: (item.Category not in ['Boss', 'MiniBoss'] and not loc.isBoss()) or (item.Category in ['Boss', 'MiniBoss'] and item.Type == loc.BossItemType)) if self.split != 'Full': if self.split != 'Scavenger': self.log.debug("add majorsSplit restriction") diff --git a/worlds/sm/variaRandomizer/rando/palettes.py b/worlds/sm/variaRandomizer/rando/palettes.py deleted file mode 100644 index 970ddd10c9..0000000000 --- a/worlds/sm/variaRandomizer/rando/palettes.py +++ /dev/null @@ -1,23489 +0,0 @@ -palettes = { -131122: 0, -131123: 16, -131124: 186, -131125: 86, -131126: 178, -131127: 65, -131128: 71, -131129: 20, -131130: 3, -131131: 4, -131132: 21, -131133: 78, -131134: 112, -131135: 53, -131136: 203, -131137: 36, -131138: 104, -131139: 24, -131140: 127, -131141: 111, -131142: 248, -131143: 81, -131144: 14, -131145: 65, -131146: 31, -131147: 3, -131148: 218, -131149: 1, -131150: 245, -131151: 0, -131152: 99, -131153: 12, -131154: 0, -131155: 16, -131156: 219, -131157: 86, -131158: 211, -131159: 57, -131160: 71, -131161: 16, -131162: 3, -131163: 4, -131164: 54, -131165: 70, -131166: 145, -131167: 45, -131168: 236, -131169: 32, -131170: 137, -131171: 20, -131172: 155, -131173: 111, -131174: 21, -131175: 82, -131176: 44, -131177: 61, -131178: 59, -131179: 19, -131180: 246, -131181: 13, -131182: 242, -131183: 12, -131184: 99, -131185: 12, -131186: 0, -131187: 16, -131188: 251, -131189: 82, -131190: 244, -131191: 49, -131192: 103, -131193: 16, -131194: 2, -131195: 4, -131196: 118, -131197: 62, -131198: 178, -131199: 37, -131200: 45, -131201: 25, -131202: 169, -131203: 16, -131204: 150, -131205: 111, -131206: 81, -131207: 82, -131208: 106, -131209: 57, -131210: 86, -131211: 39, -131212: 19, -131213: 26, -131214: 15, -131215: 25, -131216: 99, -131217: 8, -131218: 0, -131219: 16, -131220: 28, -131221: 83, -131222: 21, -131223: 42, -131224: 103, -131225: 12, -131226: 2, -131227: 4, -131228: 151, -131229: 54, -131230: 211, -131231: 29, -131232: 78, -131233: 21, -131234: 202, -131235: 12, -131236: 178, -131237: 111, -131238: 110, -131239: 82, -131240: 136, -131241: 53, -131242: 114, -131243: 55, -131244: 47, -131245: 38, -131246: 12, -131247: 37, -131248: 99, -131249: 8, -131250: 0, -131251: 16, -131252: 92, -131253: 79, -131254: 86, -131255: 30, -131256: 136, -131257: 8, -131258: 1, -131259: 0, -131260: 184, -131261: 46, -131262: 19, -131263: 26, -131264: 111, -131265: 17, -131266: 235, -131267: 12, -131268: 173, -131269: 115, -131270: 170, -131271: 78, -131272: 166, -131273: 53, -131274: 141, -131275: 75, -131276: 75, -131277: 54, -131278: 9, -131279: 53, -131280: 67, -131281: 4, -131282: 0, -131283: 16, -131284: 125, -131285: 79, -131286: 119, -131287: 22, -131288: 136, -131289: 4, -131290: 1, -131291: 0, -131292: 217, -131293: 38, -131294: 52, -131295: 18, -131296: 144, -131297: 13, -131298: 12, -131299: 9, -131300: 201, -131301: 115, -131302: 199, -131303: 78, -131304: 196, -131305: 49, -131306: 169, -131307: 91, -131308: 103, -131309: 66, -131310: 6, -131311: 65, -131312: 67, -131313: 4, -131314: 0, -131315: 16, -131316: 157, -131317: 75, -131318: 152, -131319: 14, -131320: 168, -131321: 4, -131322: 0, -131323: 0, -131324: 25, -131325: 31, -131326: 85, -131327: 10, -131328: 209, -131329: 5, -131330: 44, -131331: 5, -131332: 196, -131333: 115, -131334: 3, -131335: 79, -131336: 2, -131337: 46, -131338: 196, -131339: 111, -131340: 132, -131341: 78, -131342: 35, -131343: 77, -131344: 67, -131345: 0, -131346: 0, -131347: 0, -131348: 190, -131349: 75, -131350: 185, -131351: 6, -131352: 168, -131353: 0, -131354: 0, -131355: 0, -131356: 58, -131357: 23, -131358: 118, -131359: 2, -131360: 242, -131361: 1, -131362: 77, -131363: 1, -131364: 224, -131365: 115, -131366: 32, -131367: 79, -131368: 32, -131369: 42, -131370: 224, -131371: 127, -131372: 160, -131373: 90, -131374: 32, -131375: 89, -131376: 67, -131377: 0, -131378: 0, -131379: 16, -131380: 21, -131381: 66, -131382: 13, -131383: 45, -131384: 2, -131385: 0, -131386: 0, -131387: 0, -131388: 112, -131389: 57, -131390: 203, -131391: 32, -131392: 38, -131393: 12, -131394: 3, -131395: 4, -131396: 58, -131397: 70, -131398: 179, -131399: 40, -131400: 9, -131401: 24, -131402: 127, -131403: 111, -131404: 253, -131405: 81, -131406: 19, -131407: 65, -131408: 99, -131409: 12, -131410: 0, -131411: 16, -131412: 54, -131413: 66, -131414: 46, -131415: 37, -131416: 2, -131417: 0, -131418: 0, -131419: 0, -131420: 145, -131421: 49, -131422: 236, -131423: 28, -131424: 71, -131425: 12, -131426: 36, -131427: 4, -131428: 86, -131429: 70, -131430: 208, -131431: 40, -131432: 40, -131433: 20, -131434: 123, -131435: 111, -131436: 249, -131437: 81, -131438: 240, -131439: 64, -131440: 99, -131441: 12, -131442: 0, -131443: 16, -131444: 86, -131445: 62, -131446: 79, -131447: 33, -131448: 2, -131449: 0, -131450: 0, -131451: 0, -131452: 209, -131453: 41, -131454: 13, -131455: 25, -131456: 136, -131457: 8, -131458: 36, -131459: 4, -131460: 147, -131461: 70, -131462: 14, -131463: 41, -131464: 70, -131465: 16, -131466: 118, -131467: 111, -131468: 245, -131469: 77, -131470: 238, -131471: 64, -131472: 66, -131473: 8, -131474: 0, -131475: 16, -131476: 119, -131477: 62, -131478: 112, -131479: 25, -131480: 2, -131481: 0, -131482: 0, -131483: 0, -131484: 242, -131485: 33, -131486: 46, -131487: 21, -131488: 169, -131489: 8, -131490: 69, -131491: 4, -131492: 175, -131493: 70, -131494: 43, -131495: 41, -131496: 101, -131497: 12, -131498: 114, -131499: 111, -131500: 241, -131501: 77, -131502: 203, -131503: 64, -131504: 66, -131505: 8, -131506: 0, -131507: 16, -131508: 183, -131509: 58, -131510: 177, -131511: 21, -131512: 3, -131513: 0, -131514: 0, -131515: 0, -131516: 19, -131517: 26, -131518: 110, -131519: 13, -131520: 202, -131521: 4, -131522: 102, -131523: 0, -131524: 203, -131525: 74, -131526: 104, -131527: 37, -131528: 132, -131529: 12, -131530: 77, -131531: 107, -131532: 12, -131533: 74, -131534: 200, -131535: 68, -131536: 33, -131537: 4, -131538: 0, -131539: 16, -131540: 216, -131541: 58, -131542: 210, -131543: 13, -131544: 3, -131545: 0, -131546: 0, -131547: 0, -131548: 52, -131549: 18, -131550: 143, -131551: 9, -131552: 235, -131553: 4, -131554: 135, -131555: 0, -131556: 231, -131557: 74, -131558: 133, -131559: 37, -131560: 163, -131561: 8, -131562: 73, -131563: 107, -131564: 8, -131565: 74, -131566: 165, -131567: 68, -131568: 33, -131569: 4, -131570: 0, -131571: 16, -131572: 248, -131573: 54, -131574: 243, -131575: 9, -131576: 3, -131577: 0, -131578: 0, -131579: 0, -131580: 116, -131581: 10, -131582: 176, -131583: 5, -131584: 44, -131585: 1, -131586: 135, -131587: 0, -131588: 36, -131589: 75, -131590: 195, -131591: 37, -131592: 193, -131593: 4, -131594: 68, -131595: 107, -131596: 4, -131597: 70, -131598: 163, -131599: 68, -131600: 0, -131601: 0, -131602: 0, -131603: 16, -131604: 25, -131605: 55, -131606: 20, -131607: 2, -131608: 3, -131609: 0, -131610: 0, -131611: 0, -131612: 149, -131613: 2, -131614: 209, -131615: 1, -131616: 77, -131617: 1, -131618: 168, -131619: 0, -131620: 64, -131621: 75, -131622: 224, -131623: 37, -131624: 224, -131625: 0, -131626: 64, -131627: 107, -131628: 0, -131629: 70, -131630: 128, -131631: 68, -131632: 0, -131633: 0, -1312391: 0, -1312392: 56, -1312393: 255, -1312394: 87, -1312395: 247, -1312396: 66, -1312397: 41, -1312398: 9, -1312399: 165, -1312400: 0, -1312401: 90, -1312402: 79, -1312403: 181, -1312404: 54, -1312405: 16, -1312406: 38, -1312407: 140, -1312408: 21, -1312409: 255, -1312410: 3, -1312411: 185, -1312412: 2, -1312413: 112, -1312414: 1, -1312415: 105, -1312416: 0, -1312417: 177, -1312418: 11, -1312419: 169, -1312420: 30, -1312421: 69, -1312422: 1, -890482: 251, -1314700: 0, -1314701: 56, -1314702: 178, -1314703: 114, -1314704: 199, -1314705: 113, -1314706: 97, -1314707: 36, -1314708: 64, -1314709: 24, -1314710: 142, -1314711: 122, -1314712: 11, -1314713: 102, -1314714: 3, -1314715: 77, -1314716: 0, -1314717: 73, -1314718: 224, -1314719: 127, -1314720: 128, -1314721: 126, -1314722: 224, -1314723: 68, -1314724: 32, -1314725: 44, -1314726: 0, -1314727: 0, -1314728: 0, -1314729: 0, -1314730: 0, -1314731: 0, -1315705: 0, -1315706: 56, -1315707: 156, -1315708: 75, -1315709: 148, -1315710: 54, -1315711: 41, -1315712: 9, -1315713: 66, -1315714: 0, -1315715: 24, -1315716: 59, -1315717: 82, -1315718: 42, -1315719: 173, -1315720: 25, -1315721: 107, -1315722: 17, -1315723: 90, -1315724: 127, -1315725: 192, -1315726: 126, -1315727: 224, -1315728: 109, -1315729: 224, -1315730: 84, -1315731: 29, -1315732: 0, -1315733: 20, -1315734: 0, -1315735: 10, -1315736: 0, -1316253: 0, -1316254: 56, -1316255: 156, -1316256: 75, -1316257: 148, -1316258: 54, -1316259: 41, -1316260: 9, -1316261: 66, -1316262: 0, -1316263: 24, -1316264: 59, -1316265: 82, -1316266: 42, -1316267: 173, -1316268: 25, -1316269: 107, -1316270: 17, -1316271: 90, -1316272: 127, -1316273: 192, -1316274: 126, -1316275: 224, -1316276: 109, -1316277: 224, -1316278: 84, -1316279: 29, -1316280: 0, -1316281: 20, -1316282: 0, -1316283: 10, -1316284: 0, -454107: 96, -1317292: 0, -1317293: 56, -1317294: 255, -1317295: 87, -1317296: 247, -1317297: 66, -1317298: 41, -1317299: 9, -1317300: 165, -1317301: 0, -1317302: 90, -1317303: 79, -1317304: 181, -1317305: 54, -1317306: 16, -1317307: 38, -1317308: 206, -1317309: 29, -1317310: 223, -1317311: 1, -1317312: 31, -1317313: 0, -1317314: 24, -1317315: 0, -1317316: 10, -1317317: 0, -1317318: 185, -1317319: 6, -1317320: 234, -1317321: 0, -1317322: 69, -1317323: 0, -454147: 160, -454148: 76, -454149: 0, -1500565: 12, -1318735: 0, -1318736: 56, -1318737: 255, -1318738: 87, -1318739: 247, -1318740: 66, -1318741: 41, -1318742: 9, -1318743: 165, -1318744: 0, -1318745: 90, -1318746: 79, -1318747: 181, -1318748: 54, -1318749: 16, -1318750: 38, -1318751: 206, -1318752: 29, -1318753: 0, -1318754: 0, -1318755: 0, -1318756: 0, -1318757: 0, -1318758: 0, -1318759: 0, -1318760: 0, -1318761: 0, -1318762: 0, -1318763: 0, -1318764: 0, -1318765: 0, -1318766: 0, -1500568: 101, -1640071: 0, -1640072: 56, -1115785: 190, -1115786: 75, -1115787: 185, -1115788: 6, -1115789: 234, -1115790: 0, -1115791: 101, -1115792: 0, -1115793: 58, -1115794: 23, -1115795: 118, -1115796: 2, -1115797: 242, -1115798: 1, -1115799: 77, -1115800: 1, -1115801: 0, -1115802: 0, -1115803: 0, -1115804: 0, -1115805: 0, -1115806: 0, -1640095: 134, -1115808: 0, -1115809: 0, -1115810: 0, -1640099: 22, -1115812: 0, -1640101: 19, -1640102: 1, -1377991: 0, -1377992: 56, -1377993: 159, -1377994: 103, -1377995: 153, -1377996: 82, -1377997: 46, -1377998: 37, -1377999: 170, -1640144: 12, -1378001: 252, -1378002: 94, -1640147: 93, -1378004: 70, -1378005: 178, -1378006: 53, -1378007: 112, -1378008: 45, -1640153: 187, -1378010: 91, -1378011: 248, -1378012: 61, -1378013: 14, -1378014: 45, -1640159: 134, -1640160: 20, -1640161: 59, -1378018: 94, -1640163: 22, -1640164: 2, -1640165: 19, -1640166: 1, -1640167: 0, -1640168: 56, -1640169: 253, -1640170: 2, -1640171: 62, -1640172: 1, -1640173: 108, -1640174: 0, -1640175: 102, -1640176: 0, -1640177: 30, -1640178: 2, -1640179: 95, -1640180: 0, -1640181: 89, -1640182: 0, -1640183: 115, -1640184: 0, -1640185: 187, -1640186: 94, -1378043: 179, -1640188: 61, -1640189: 46, -1640190: 41, -1640191: 134, -1640192: 20, -1640193: 59, -1640194: 3, -1640195: 22, -1640196: 2, -1640197: 19, -1640198: 1, -1640199: 0, -1640200: 56, -1640201: 255, -1640202: 107, -1640203: 214, -1640204: 78, -1640205: 164, -1640206: 20, -1640207: 32, -1640208: 4, -1640209: 123, -1640210: 91, -1640211: 82, -1640212: 62, -1640213: 205, -1640214: 49, -1640215: 73, -1640216: 33, -1640217: 187, -1640218: 94, -1640219: 179, -1640220: 61, -1640221: 46, -1640222: 41, -1640223: 134, -1640224: 20, -1640225: 59, -1640226: 3, -1640227: 22, -1640228: 2, -1640229: 19, -1640230: 1, -1640231: 0, -1640232: 56, -1640233: 190, -1640234: 75, -1321726: 0, -1321727: 56, -1321728: 255, -1321729: 87, -1321730: 247, -1640235: 185, -1321732: 41, -1321733: 9, -1321734: 165, -1321735: 0, -1321736: 90, -1640236: 6, -1321738: 181, -1321739: 54, -1321740: 16, -1321741: 38, -1321742: 206, -1640237: 234, -1321744: 223, -1321745: 1, -1321746: 31, -1321747: 0, -1321748: 24, -1321749: 0, -1321750: 10, -1321751: 0, -1321752: 185, -1321753: 6, -1321754: 234, -1640239: 101, -1321756: 69, -1321757: 0, -1640240: 0, -1640241: 58, -1640242: 23, -1640243: 118, -1640244: 2, -1640245: 242, -1640246: 1, -1640247: 77, -1640248: 1, -1640249: 187, -1640250: 94, -1640251: 179, -1640252: 61, -1640253: 46, -1640254: 41, -1640255: 134, -1640256: 20, -1640257: 59, -1640258: 3, -1640259: 22, -1640260: 2, -1640261: 19, -1640262: 1, -1152620: 0, -1152621: 224, -1152622: 127, -1152623: 160, -1152624: 125, -1152625: 224, -1152627: 160, -1152628: 48, -1152629: 224, -1152630: 59, -1152631: 128, -1152632: 38, -1152633: 128, -1152634: 21, -454365: 23, -1324638: 0, -1324639: 56, -1324640: 255, -1324641: 87, -1324642: 247, -1324643: 66, -1324644: 140, -1324645: 21, -1324646: 165, -1324647: 0, -1324648: 90, -1324649: 79, -1324650: 181, -1324651: 54, -1324652: 16, -1324653: 38, -1324654: 206, -1324655: 29, -1324656: 223, -1324657: 28, -1324658: 224, -1324659: 79, -1324660: 32, -1324661: 59, -1324662: 32, -1324663: 42, -1324664: 151, -1324665: 16, -1324666: 223, -1324667: 107, -1324668: 46, -1324669: 4, -890832: 121, -890833: 103, -890835: 46, -890836: 123, -1194109: 0, -1194110: 56, -1194111: 255, -1194112: 127, -1194113: 255, -1194114: 13, -1194115: 191, -1194116: 8, -1194117: 149, -1194118: 8, -1194119: 108, -1194120: 8, -1194121: 71, -1194122: 4, -1194123: 126, -1194124: 107, -1194125: 30, -1194126: 87, -1194127: 88, -1194128: 58, -1194129: 113, -1194130: 33, -1194131: 203, -1194132: 12, -1194133: 159, -1194134: 3, -1194135: 58, -1194136: 2, -1194137: 118, -1194138: 1, -1194139: 0, -1194140: 0, -1194141: 0, -1194142: 0, -1194143: 255, -1194144: 127, -1194145: 255, -1194146: 13, -1194147: 191, -1194148: 8, -1194149: 149, -1194150: 8, -1194151: 108, -1194152: 8, -1194153: 71, -1194154: 4, -1194155: 126, -1194156: 107, -1194157: 30, -1194158: 87, -1194159: 88, -1194160: 58, -1194161: 113, -1194162: 33, -1194163: 203, -1194164: 12, -1194165: 159, -1194166: 3, -1194167: 58, -1194168: 2, -1194169: 118, -1194170: 1, -1194171: 0, -1194172: 0, -1194187: 123, -1194188: 74, -1194189: 144, -1194190: 28, -1194191: 105, -1194192: 20, -1194193: 36, -1194194: 20, -1194195: 8, -1194196: 0, -1194197: 191, -1194198: 36, -1194199: 149, -1194200: 36, -1194201: 108, -1194202: 28, -1194203: 69, -1194204: 16, -1194237: 0, -1194238: 56, -1194239: 255, -1194240: 127, -1194241: 126, -1194242: 107, -1194243: 30, -1194244: 87, -1194245: 88, -1194246: 58, -1194247: 113, -1194248: 33, -1194249: 203, -1194250: 12, -1194251: 126, -1194252: 107, -1194253: 30, -1194254: 87, -1194255: 88, -1194256: 58, -1194257: 113, -1194258: 33, -1194259: 203, -1194260: 12, -1194261: 159, -1194262: 3, -1194263: 58, -1194264: 2, -1194265: 118, -1194266: 1, -1194267: 0, -1194268: 0, -890884: 31, -890886: 5, -890891: 33, -890892: 159, -890893: 87, -890894: 210, -890895: 74, -890897: 58, -890899: 0, -890900: 20, -890902: 170, -890903: 48, -890906: 4, -1327427: 0, -1327428: 56, -1327429: 255, -1327430: 87, -1327431: 247, -1327432: 66, -1327433: 41, -1327434: 9, -1327435: 165, -1327436: 0, -1327437: 90, -1327438: 79, -1327439: 181, -1327440: 54, -1327441: 16, -1327442: 38, -1327443: 206, -1327444: 29, -1327445: 255, -1327446: 2, -1327447: 191, -1327448: 1, -1327449: 15, -1327450: 0, -1327451: 8, -1327452: 0, -1327453: 255, -1327454: 3, -1327455: 55, -1327456: 2, -1327457: 209, -1327458: 0, -1291699: 0, -1291700: 56, -1291701: 157, -1291702: 85, -1291703: 22, -1291704: 24, -1291705: 13, -1291706: 16, -1291707: 29, -1291708: 51, -1291709: 183, -1291710: 42, -1291711: 145, -1291712: 42, -1291713: 12, -1291714: 30, -1291715: 170, -1291716: 25, -1291717: 72, -1160646: 36, -1291719: 229, -1291720: 12, -1291721: 132, -1160650: 20, -1160651: 71, -1291724: 0, -1160653: 224, -1160654: 3, -1160655: 160, -1160656: 2, -1291729: 0, -1160658: 1, -541665: 0, -541666: 56, -541667: 255, -541668: 127, -541669: 255, -541670: 25, -541671: 85, -541672: 29, -541673: 173, -541674: 16, -541675: 255, -541676: 83, -541677: 158, -541678: 3, -541679: 95, -541680: 41, -541681: 223, -541682: 24, -541683: 0, -541684: 0, -541685: 0, -541686: 0, -541687: 0, -541688: 0, -541689: 0, -541690: 0, -541691: 0, -541692: 0, -541693: 0, -541694: 0, -541695: 74, -541696: 16, -541697: 0, -541698: 56, -541699: 255, -541700: 127, -541701: 192, -541702: 126, -541703: 224, -541704: 109, -541705: 224, -541706: 84, -541707: 110, -541708: 127, -541709: 10, -541710: 115, -541711: 101, -541712: 102, -541713: 34, -541714: 94, -541715: 160, -541716: 127, -541717: 96, -541718: 123, -541719: 32, -541720: 119, -541721: 160, -541722: 114, -541723: 96, -541724: 110, -541725: 145, -541726: 127, -541727: 192, -541728: 32, -541729: 0, -541730: 56, -541731: 255, -541732: 127, -541733: 31, -541734: 124, -541735: 22, -541736: 88, -541737: 12, -541738: 48, -541739: 223, -541740: 126, -541741: 223, -541742: 125, -541743: 223, -541744: 124, -541745: 26, -541746: 104, -541747: 0, -541748: 0, -541749: 0, -541750: 0, -541751: 0, -541752: 0, -541753: 0, -541754: 0, -541755: 0, -541756: 0, -541757: 0, -541758: 0, -541759: 9, -541760: 36, -541761: 0, -541762: 56, -541763: 255, -541764: 127, -541765: 224, -541766: 59, -541767: 128, -541768: 38, -541769: 128, -541770: 21, -541771: 250, -541772: 107, -541773: 242, -541774: 75, -541775: 234, -541776: 43, -541777: 224, -541778: 3, -541779: 0, -541780: 0, -541781: 0, -541782: 0, -541783: 0, -541784: 0, -541785: 0, -541786: 0, -541787: 0, -541788: 0, -541789: 0, -541790: 0, -541791: 32, -541792: 9, -541793: 0, -541794: 56, -541795: 255, -541796: 127, -541797: 255, -541798: 3, -541799: 22, -541800: 2, -541801: 238, -541802: 0, -541803: 255, -541804: 107, -541805: 255, -541806: 75, -541807: 255, -541808: 43, -541809: 123, -541810: 3, -541811: 0, -541812: 0, -541813: 0, -541814: 0, -541815: 0, -541816: 0, -541817: 0, -541818: 0, -541819: 0, -541820: 0, -541821: 0, -541822: 0, -541823: 173, -541824: 0, -1328819: 0, -1328820: 56, -1328821: 255, -1328822: 87, -1328823: 247, -1328824: 66, -1328825: 41, -1328826: 9, -1328827: 165, -1328828: 0, -1328829: 90, -1328830: 79, -1328831: 181, -1328832: 54, -1328833: 16, -1328834: 38, -1328835: 206, -1328836: 29, -1328837: 31, -1328838: 0, -1328839: 24, -1328840: 0, -1328841: 15, -1328842: 0, -1328843: 8, -1328844: 0, -1328845: 10, -1328846: 0, -1328847: 255, -1328848: 3, -1328849: 181, -1328850: 2, -1500638: 66, -454551: 2, -454552: 8, -454555: 0, -454556: 0, -454557: 0, -454558: 0, -454559: 0, -454560: 0, -454565: 19, -454566: 60, -454567: 11, -454568: 24, -454570: 20, -454571: 3, -454572: 12, -454573: 1, -454574: 4, -454577: 0, -454579: 0, -454580: 0, -454591: 4, -454592: 16, -454593: 2, -454594: 8, -454595: 0, -454596: 0, -1270464: 176, -1270465: 0, -1270466: 54, -1270467: 2, -1270468: 83, -454597: 0, -1270469: 1, -1270470: 175, -1270471: 0, -1270472: 20, -1270473: 2, -454598: 0, -1270474: 49, -1270475: 1, -1270476: 142, -1270477: 0, -1270478: 210, -454599: 0, -1270479: 1, -1270480: 15, -1270481: 1, -1270482: 140, -1270483: 0, -454600: 0, -1270484: 144, -1270485: 1, -1270486: 237, -1270487: 0, -1270488: 107, -1270489: 0, -1270490: 110, -1270491: 1, -1270492: 204, -1270493: 0, -1270494: 105, -1270495: 0, -454605: 21, -454606: 68, -454607: 13, -454608: 32, -454609: 7, -454610: 28, -454611: 5, -454612: 20, -454613: 3, -454614: 12, -454615: 1, -454616: 4, -454617: 0, -1270569: 4, -454618: 0, -1270749: 20, -1333281: 0, -1333282: 56, -1333283: 87, -1333284: 63, -1333285: 77, -1333286: 46, -1333287: 226, -1333288: 0, -1333289: 96, -1333290: 0, -1333291: 176, -1333292: 58, -1333293: 11, -1333294: 34, -1333295: 102, -1333296: 17, -1333297: 36, -1333298: 9, -1333299: 90, -1333300: 67, -1333301: 148, -1333302: 54, -1333303: 173, -1333304: 21, -1333305: 8, -1333306: 5, -1333307: 255, -1333308: 3, -1333309: 55, -1333310: 2, -1333311: 209, -1333312: 0, -454670: 0, -454671: 17, -454672: 0, -1270871: 65, -1270872: 71, -1270873: 20, -1270874: 3, -1270875: 4, -1270876: 21, -1270877: 78, -1270878: 112, -1270879: 53, -1270880: 203, -1270881: 36, -1270882: 104, -1270883: 24, -1270884: 95, -1270885: 94, -1270886: 63, -1270887: 24, -1270888: 20, -1270889: 16, -1270890: 122, -1270891: 78, -1270892: 115, -1270893: 61, -1270894: 73, -1270895: 20, -1270896: 5, -1270897: 4, -1270898: 214, -1270899: 69, -1270900: 81, -1270901: 49, -1270902: 173, -1334215: 0, -1334216: 56, -1334217: 255, -1334218: 2, -1334219: 191, -1334220: 1, -1334221: 15, -1334222: 0, -1334223: 8, -1334224: 0, -1334225: 191, -1334226: 1, -1334227: 27, -1334228: 1, -1334229: 186, -1334230: 0, -1334231: 17, -1334232: 0, -1334233: 92, -1334234: 90, -1334235: 180, -1334236: 65, -1334237: 13, -1334238: 41, -1334239: 101, -1334240: 16, -1334241: 255, -1334242: 3, -1334243: 55, -1334244: 2, -1334245: 209, -1334246: 0, -1270971: 5, -1153049: 8, -1140109: 92, -1140115: 101, -1335856: 0, -1335857: 56, -1335858: 255, -1335859: 127, -1335860: 224, -1335861: 86, -1335862: 128, -1335863: 49, -1335864: 192, -1335865: 24, -1335866: 192, -1335867: 107, -1335868: 192, -1335869: 94, -1335870: 32, -1335871: 74, -1335872: 160, -1335873: 53, -1335874: 255, -1335875: 127, -1335876: 156, -1335877: 3, -1335878: 55, -1335879: 2, -1335880: 209, -1335881: 0, -1335882: 255, -1335883: 3, -1335884: 55, -1335885: 2, -1335886: 209, -1335887: 0, -1336711: 0, -1336712: 56, -1336713: 255, -1336714: 127, -1336715: 224, -1336716: 59, -1336717: 128, -1336718: 38, -1336719: 128, -1336720: 21, -1336721: 250, -1336722: 107, -1336723: 242, -1336724: 75, -1336725: 234, -1336726: 43, -1336727: 224, -1336728: 3, -1336729: 0, -1336730: 0, -1336731: 0, -1336732: 0, -1336733: 0, -1336734: 0, -1336735: 0, -1336736: 0, -1336737: 0, -1336738: 0, -1336739: 0, -1336740: 0, -1336741: 0, -1336742: 0, -1337260: 0, -1337261: 56, -1337262: 178, -1337263: 114, -1337264: 199, -1337265: 113, -1337266: 97, -1337267: 36, -1337268: 64, -1337269: 24, -1337270: 142, -1337271: 122, -1337272: 11, -1337273: 102, -1337274: 3, -1337275: 77, -1337276: 164, -1337277: 48, -1337278: 164, -1337279: 48, -1337280: 97, -1337281: 36, -1337282: 64, -1337283: 24, -1337284: 0, -1337285: 8, -1337286: 0, -1337287: 0, -1337288: 0, -1337289: 0, -1337290: 0, -1337291: 0, -1337754: 0, -1337755: 56, -1337756: 87, -1337757: 75, -1337758: 77, -1337759: 58, -1337760: 226, -1337761: 12, -1337762: 64, -1337763: 0, -1337764: 176, -1337765: 70, -1337766: 11, -1337767: 46, -1337768: 102, -1337769: 29, -1337770: 36, -1337771: 21, -1337772: 255, -1337773: 72, -1337774: 182, -1337775: 56, -1337776: 110, -1337777: 36, -1337778: 72, -1337779: 20, -1337780: 255, -1337781: 3, -1337782: 55, -1337783: 2, -1337784: 209, -1337785: 0, -1337786: 0, -1337787: 56, -1337788: 255, -1337789: 87, -1337790: 247, -1337791: 66, -1337792: 140, -1337793: 21, -1337794: 165, -1337795: 0, -1337796: 90, -1337797: 79, -1337798: 181, -1337799: 54, -1337800: 16, -1337801: 38, -1337802: 206, -1337803: 29, -1337804: 224, -1337805: 3, -1337806: 1, -1337807: 3, -1337808: 161, -1337809: 1, -1337810: 193, -1337811: 0, -1337812: 250, -1337813: 107, -1337814: 176, -1337815: 66, -1337816: 69, -1337817: 21, -1337818: 0, -1337819: 56, -1337820: 223, -1337821: 90, -1337822: 117, -1337823: 57, -1337824: 10, -1337825: 12, -1337826: 6, -1337827: 0, -1337828: 26, -1337829: 78, -1337830: 51, -1337831: 45, -1337832: 142, -1337833: 28, -1337834: 43, -1337835: 16, -1337836: 17, -1337837: 111, -1337838: 42, -1337839: 82, -1337840: 36, -1337841: 49, -1337842: 36, -1337843: 24, -1337844: 255, -1337845: 2, -1337846: 191, -1337847: 1, -1337848: 15, -1337849: 0, -1127019: 40, -1127021: 24, -1127022: 16, -1127023: 98, -1127024: 107, -1127025: 73, -1127026: 198, -1127027: 56, -1127028: 99, -1127029: 44, -1127030: 90, -1127031: 47, -1127032: 148, -1127033: 34, -1127034: 173, -1127035: 1, -1127036: 8, -1127037: 1, -1127038: 255, -1127039: 127, -1127040: 181, -1127041: 86, -1127042: 74, -1127043: 41, -1141135: 65, -1127100: 0, -1127101: 56, -1127102: 255, -1127103: 2, -1127104: 191, -1127105: 1, -1127106: 15, -1127107: 0, -1127108: 8, -1127109: 0, -1127110: 191, -1127111: 1, -1127112: 27, -1127113: 1, -1127114: 21, -1127115: 0, -1127117: 0, -1127118: 31, -1127119: 36, -1127120: 23, -1127121: 28, -1127122: 47, -1127123: 20, -1127124: 71, -1127125: 12, -1127126: 224, -1127127: 3, -1127128: 160, -1127129: 2, -1127130: 64, -1127131: 1, -1142227: 181, -1348722: 0, -1348723: 56, -1348724: 159, -1348725: 38, -1348726: 89, -1348727: 1, -1348728: 76, -1348729: 0, -1348730: 4, -1348731: 0, -1348732: 57, -1348733: 87, -1348734: 115, -1348735: 66, -1348736: 173, -1348737: 45, -1348738: 198, -1348739: 20, -1348740: 127, -1348741: 54, -1348742: 249, -1348743: 41, -1348744: 115, -1348745: 33, -1348746: 12, -1348747: 21, -1348748: 134, -1348749: 12, -1348750: 255, -1348751: 127, -1348752: 0, -1348753: 0, -1348754: 0, -1348755: 56, -1348756: 0, -1348757: 0, -1348758: 0, -1348759: 0, -1348760: 0, -1348761: 0, -1348762: 36, -1348763: 0, -1348764: 173, -1348765: 41, -1348766: 74, -1348767: 33, -1348768: 231, -1348769: 20, -1348770: 99, -1348771: 12, -1348772: 0, -1348773: 0, -1348774: 0, -1348775: 0, -1348776: 0, -1348777: 0, -1348778: 0, -1348779: 0, -1348780: 0, -1348781: 0, -1348782: 173, -1348783: 41, -1348784: 0, -1348785: 0, -1348786: 0, -1348787: 56, -1348788: 224, -1348789: 127, -1348790: 32, -1348791: 107, -1348792: 64, -1348793: 86, -1348794: 255, -1348795: 3, -1348796: 191, -1348797: 2, -1348798: 95, -1348799: 1, -1348800: 31, -1348801: 0, -1348802: 249, -1348803: 41, -1348804: 115, -1348805: 33, -1348806: 12, -1348807: 21, -1348808: 24, -1348809: 99, -1348810: 49, -1348811: 70, -1348812: 74, -1348813: 41, -1348814: 255, -1348815: 127, -1348816: 99, -1348817: 12, -1348818: 0, -1348819: 56, -1348820: 184, -1348821: 87, -1348822: 17, -1348823: 11, -1348824: 70, -1348825: 22, -1348826: 227, -1348827: 0, -1348828: 255, -1348829: 114, -1348830: 223, -1348831: 44, -1348832: 185, -1348833: 36, -1348834: 175, -1348835: 28, -1348836: 169, -1348837: 24, -1348838: 159, -1348839: 79, -1348840: 216, -1348841: 62, -1348842: 18, -1348843: 46, -1348844: 205, -1348845: 8, -1348846: 255, -1348847: 127, -1348848: 0, -1348849: 0, -1348850: 0, -1348851: 56, -1348852: 24, -1348853: 99, -1348854: 24, -1348855: 99, -1348856: 2, -1348857: 8, -1348858: 148, -1348859: 82, -1348860: 206, -1348861: 57, -1348862: 8, -1348863: 33, -1348864: 132, -1348865: 16, -1348866: 25, -1348867: 0, -1348868: 18, -1348869: 0, -1348870: 0, -1348871: 92, -1348872: 0, -1348873: 64, -1348874: 132, -1348875: 16, -1348876: 127, -1348877: 25, -1348878: 255, -1348879: 127, -1348880: 0, -1348881: 0, -1348882: 0, -1348883: 56, -1348884: 148, -1348885: 82, -1348886: 206, -1348887: 57, -1348888: 8, -1348889: 33, -1348890: 139, -1348891: 127, -1348892: 10, -1348893: 111, -1348894: 136, -1348895: 94, -1348896: 7, -1348897: 78, -1348898: 134, -1348899: 57, -1348900: 5, -1348901: 41, -1348902: 131, -1348903: 24, -1348904: 2, -1348905: 8, -1348906: 132, -1348907: 16, -1348908: 24, -1348909: 99, -1348910: 255, -1348911: 127, -1348912: 0, -1348913: 0, -1218223: 25, -1218224: 3, -1218225: 84, -1218226: 2, -1218227: 143, -1218228: 1, -1218229: 202, -1218230: 0, -1218231: 186, -1218232: 2, -1218233: 244, -1218234: 1, -1218235: 79, -1218236: 1, -1218237: 170, -1218238: 0, -1218239: 59, -1218240: 2, -1218241: 181, -1218242: 1, -1218243: 47, -1218244: 1, -1218245: 137, -1218246: 0, -1218247: 220, -1218248: 1, -1218249: 85, -1218250: 1, -1218251: 239, -1218252: 0, -1218253: 105, -1218254: 0, -1218255: 92, -1218256: 1, -1218257: 22, -1218258: 1, -1218259: 176, -1218260: 0, -1218261: 105, -1218262: 0, -1218263: 253, -1218264: 0, -1218265: 182, -1218266: 0, -1218267: 112, -1218268: 0, -1218269: 73, -1218270: 0, -1218271: 126, -1218272: 0, -1218273: 119, -1218274: 0, -1218275: 80, -1218276: 0, -1218277: 40, -1218278: 0, -1218279: 31, -1218280: 0, -1218281: 23, -1218282: 0, -1218283: 16, -1218284: 0, -1218285: 8, -1218286: 0, -1502684: 57, -1502685: 87, -1677266: 20, -1502686: 115, -1677315: 107, -1677317: 158, -1677328: 24, -1677330: 24, -1677333: 47, -1677336: 71, -1221111: 0, -1221112: 56, -1221113: 87, -1221114: 63, -1221115: 77, -1221116: 46, -1221117: 226, -1221118: 0, -1221119: 96, -1221120: 0, -1221121: 176, -1221122: 58, -1221123: 11, -1221124: 34, -1221125: 102, -1221126: 17, -1221127: 36, -1221128: 9, -1221129: 25, -1221130: 3, -1221131: 84, -1221132: 2, -1221133: 143, -1221134: 1, -1221135: 202, -1221136: 0, -1221137: 27, -1221138: 88, -1221139: 146, -1221140: 24, -1221141: 69, -1221142: 1, -1221143: 0, -1221144: 56, -1221145: 87, -1221146: 63, -1221147: 77, -1221148: 46, -1221149: 226, -1221150: 0, -1221151: 96, -1221152: 0, -1221153: 176, -1221154: 58, -1221155: 11, -1221156: 34, -1221157: 102, -1221158: 17, -1221159: 36, -1221160: 9, -1221161: 25, -1221162: 3, -1221163: 84, -1221164: 2, -1221165: 143, -1221166: 1, -1221167: 202, -1221168: 0, -1221169: 27, -1221170: 88, -1221171: 146, -1221172: 24, -1221173: 69, -1221174: 1, -1221175: 0, -1221176: 56, -1221177: 90, -1221178: 107, -1221179: 82, -1221180: 86, -1221181: 231, -1221182: 40, -1221183: 99, -1221184: 24, -1221185: 181, -1221186: 98, -1221187: 16, -1221188: 74, -1221189: 107, -1221190: 57, -1221191: 41, -1221192: 49, -1221193: 255, -1221194: 67, -1221195: 19, -1221196: 1, -1221197: 15, -1221198: 0, -1221199: 92, -1221200: 23, -1221201: 153, -1221202: 2, -1221203: 214, -1221204: 1, -1221205: 224, -1221206: 3, -1221207: 0, -1221208: 56, -1221209: 156, -1221210: 75, -1221211: 148, -1221212: 54, -1221213: 41, -1221214: 9, -1221215: 66, -1221216: 0, -1221217: 247, -1221218: 66, -1221219: 82, -1221220: 42, -1221221: 173, -1221222: 25, -1221223: 107, -1221224: 17, -1221225: 32, -1221226: 20, -1221227: 32, -1221228: 20, -1221229: 32, -1221230: 20, -1221231: 32, -1221232: 20, -1221233: 32, -1221234: 20, -1221235: 32, -1221236: 20, -1221237: 32, -1221238: 20, -1221239: 0, -1221240: 56, -1221241: 87, -1221242: 63, -1221243: 77, -1221244: 46, -1221245: 226, -1221246: 0, -1221247: 96, -1221248: 0, -1221249: 176, -1221250: 58, -1221251: 11, -1221252: 34, -1221253: 102, -1221254: 17, -1221255: 36, -1221256: 9, -1221257: 25, -1221258: 3, -1221259: 84, -1221260: 2, -1221261: 143, -1221262: 1, -1221263: 202, -1221264: 0, -1221265: 27, -1221266: 88, -1221267: 146, -1221268: 24, -1221269: 69, -1221270: 1, -1364098: 22, -1364099: 74, -1364100: 145, -1364101: 57, -1364102: 44, -1364103: 45, -1364104: 167, -1364105: 28, -1364106: 229, -1364107: 32, -1364108: 164, -1364109: 24, -1364110: 131, -1364111: 16, -1364112: 65, -1364113: 8, -1364114: 0, -1364115: 0, -1364116: 0, -1364117: 0, -1364118: 223, -1364119: 2, -1364120: 0, -1364121: 0, -1364122: 2, -1364123: 8, -1364124: 148, -1364125: 82, -1364126: 206, -1364127: 57, -1364128: 8, -1364129: 33, -1364130: 132, -1364131: 16, -1364132: 25, -1364133: 0, -1364134: 18, -1364135: 0, -1364136: 0, -1364137: 92, -1364138: 0, -1364139: 64, -1364140: 132, -1364141: 16, -1364142: 127, -1364143: 25, -1364144: 255, -1364145: 127, -1364146: 185, -1364147: 78, -1364148: 86, -1364149: 70, -1364150: 18, -1364151: 62, -1364152: 207, -1364153: 49, -1364154: 238, -1364155: 53, -1364156: 205, -1364157: 45, -1364158: 172, -1364159: 41, -1364160: 139, -1364161: 37, -1364162: 74, -1364163: 29, -1364164: 74, -1364165: 29, -1364166: 63, -1364167: 31, -1364168: 74, -1364169: 29, -1364170: 76, -1364171: 37, -1364172: 239, -1364173: 61, -1364174: 107, -1364175: 45, -1364176: 198, -1364177: 24, -1364178: 99, -1364179: 12, -1364180: 19, -1364181: 0, -1364182: 14, -1364183: 0, -1364184: 0, -1364185: 68, -1364186: 0, -1364187: 48, -1364188: 99, -1364189: 12, -1364190: 23, -1364191: 21, -1364192: 247, -1364193: 94, -1364194: 92, -1364195: 87, -1364196: 58, -1364197: 79, -1364198: 25, -1364199: 75, -1364200: 215, -1364201: 70, -1364202: 246, -1364203: 70, -1364204: 214, -1364205: 70, -1364206: 214, -1364207: 66, -1364208: 181, -1364209: 62, -1364210: 181, -1364211: 62, -1364212: 181, -1364213: 62, -1364214: 159, -1364215: 63, -1364216: 181, -1364217: 62, -1364218: 181, -1364219: 62, -1364220: 74, -1364221: 41, -1364222: 231, -1364223: 28, -1364224: 132, -1364225: 16, -1364226: 66, -1364227: 8, -1364228: 13, -1364229: 0, -1364230: 9, -1364231: 0, -1364232: 0, -1364233: 48, -1364234: 0, -1364235: 32, -1364236: 66, -1364237: 8, -1364238: 208, -1364239: 12, -1364240: 16, -1364241: 66, -1364242: 255, -1364243: 91, -1364244: 255, -1364245: 91, -1364246: 255, -1364247: 91, -1364248: 255, -1364249: 91, -1364250: 255, -1364251: 91, -1364252: 255, -1364253: 91, -1364254: 255, -1364255: 91, -1364256: 255, -1364257: 91, -1364258: 255, -1364259: 91, -1364260: 255, -1364261: 91, -1364262: 255, -1364263: 91, -1364264: 255, -1364265: 91, -1364266: 255, -1364267: 91, -1364268: 165, -1364269: 20, -1364270: 132, -1364271: 16, -1364272: 66, -1364273: 8, -1364274: 33, -1364275: 4, -1364276: 6, -1364277: 0, -1364278: 5, -1364279: 0, -1364280: 0, -1364281: 24, -1364282: 0, -1364283: 16, -1364284: 33, -1364285: 4, -1364286: 104, -1364287: 8, -1364288: 8, -1364289: 33, -1364580: 223, -1364581: 46, -1364582: 155, -1364583: 1, -1364584: 142, -1364585: 0, -1364586: 6, -1364587: 0, -1364588: 159, -1364589: 38, -1364590: 89, -1364591: 1, -1364592: 76, -1364593: 0, -1364594: 4, -1364595: 0, -1364596: 93, -1364597: 30, -1364598: 23, -1364599: 1, -1364600: 10, -1364601: 0, -1364602: 2, -1364603: 0, -1364604: 27, -1364605: 22, -1364606: 213, -1364607: 0, -1364608: 8, -1364609: 0, -1364610: 0, -1364611: 0, -1364612: 27, -1364613: 22, -1364614: 213, -1364615: 0, -1364616: 8, -1364617: 0, -1364618: 0, -1364619: 0, -1364620: 93, -1364621: 30, -1364622: 23, -1364623: 1, -1364624: 10, -1364625: 0, -1364626: 2, -1364627: 0, -1364628: 159, -1364629: 38, -1364630: 89, -1364631: 1, -1364632: 76, -1364633: 0, -1364634: 4, -1364635: 0, -1364636: 223, -1364637: 46, -1364638: 155, -1364639: 1, -1364640: 142, -1364641: 0, -1364642: 6, -1364643: 0, -1364644: 159, -1364645: 38, -1364646: 89, -1364647: 1, -1364648: 76, -1364649: 0, -1364650: 4, -1364651: 0, -1364652: 159, -1364653: 34, -1364654: 121, -1364655: 1, -1364656: 142, -1364657: 0, -1364658: 103, -1364659: 0, -1364660: 159, -1364661: 26, -1364662: 186, -1364663: 1, -1364664: 241, -1364665: 0, -1364666: 203, -1364667: 0, -1364668: 191, -1364669: 22, -1364670: 251, -1364671: 1, -1364672: 83, -1364673: 1, -1364674: 46, -1364675: 1, -1364676: 191, -1364677: 22, -1364678: 251, -1364679: 1, -1364680: 83, -1364681: 1, -1364682: 46, -1364683: 1, -1364684: 159, -1364685: 26, -1364686: 186, -1364687: 1, -1364688: 241, -1364689: 0, -1364690: 203, -1364691: 0, -1364692: 159, -1364693: 34, -1364694: 121, -1364695: 1, -1364696: 142, -1364697: 0, -1364698: 103, -1364699: 0, -1364700: 159, -1364701: 38, -1364702: 89, -1364703: 1, -1364704: 76, -1364705: 0, -1364706: 4, -1364707: 0, -449387: 0, -449388: 56, -449389: 8, -449390: 1, -449391: 189, -449392: 3, -449393: 5, -449394: 20, -449395: 224, -449396: 59, -449397: 168, -449398: 33, -449399: 159, -449400: 87, -449401: 210, -449402: 74, -449403: 78, -449404: 58, -449405: 187, -449406: 0, -449407: 181, -449408: 2, -449409: 107, -449410: 1, -449411: 82, -449412: 2, -449413: 4, -449414: 17, -449415: 116, -449416: 0, -449417: 13, -449418: 0, -449423: 0, -449424: 0, -449425: 174, -449426: 82, -449427: 189, -449428: 95, -449429: 229, -449430: 101, -449431: 224, -449432: 127, -449433: 136, -449434: 115, -449435: 255, -449436: 127, -449437: 242, -449438: 127, -449439: 238, -449440: 127, -449441: 155, -449442: 86, -449443: 85, -449444: 95, -449445: 11, -449446: 94, -449447: 246, -449448: 83, -449449: 228, -449450: 98, -449451: 84, -449452: 82, -449453: 237, -449454: 81, -449466: 0, -449467: 56, -449468: 8, -449469: 1, -449470: 189, -449471: 3, -449472: 5, -449473: 20, -449474: 224, -449475: 59, -449476: 168, -449477: 33, -449478: 159, -449479: 87, -449480: 210, -449481: 74, -449482: 78, -449483: 58, -449484: 187, -449485: 0, -449486: 181, -449487: 2, -449488: 107, -449489: 1, -449490: 82, -449491: 2, -449492: 4, -449493: 17, -449494: 116, -449495: 0, -449496: 13, -449497: 0, -449502: 0, -449503: 0, -449504: 174, -449505: 82, -449506: 189, -449507: 95, -449508: 229, -449509: 101, -449510: 224, -449511: 127, -449512: 136, -449513: 115, -449514: 255, -449515: 127, -449516: 242, -449517: 127, -449518: 238, -449519: 127, -449520: 155, -449521: 86, -449522: 85, -449523: 95, -449524: 11, -449525: 94, -449526: 246, -449527: 83, -449528: 228, -449529: 98, -449530: 84, -449531: 82, -449532: 237, -449533: 81, -449545: 0, -449546: 56, -449547: 8, -449548: 1, -449549: 189, -449550: 3, -449551: 5, -449552: 20, -449553: 224, -449554: 59, -449555: 168, -449556: 33, -449557: 159, -449558: 87, -449559: 210, -449560: 74, -449561: 78, -449562: 58, -449563: 187, -449564: 0, -449565: 181, -449566: 2, -449567: 107, -449568: 1, -449569: 82, -449570: 2, -449571: 4, -449572: 17, -449573: 116, -449574: 0, -449575: 13, -449576: 0, -449581: 3, -449582: 32, -449583: 110, -449584: 81, -449585: 189, -449586: 75, -449587: 165, -449588: 100, -449589: 224, -449590: 127, -449591: 72, -449592: 114, -449593: 255, -449594: 127, -449595: 114, -449596: 127, -449597: 238, -449598: 126, -449599: 91, -449600: 85, -449601: 181, -449602: 74, -449603: 107, -449604: 73, -449605: 182, -449606: 82, -449607: 164, -449608: 97, -449609: 20, -449610: 81, -449611: 173, -449612: 80, -449624: 0, -449625: 56, -449626: 8, -449627: 1, -449628: 189, -449629: 3, -449630: 5, -449631: 20, -449632: 224, -449633: 59, -449634: 168, -449635: 33, -449636: 159, -449637: 87, -449638: 210, -449639: 74, -449640: 78, -449641: 58, -449642: 187, -449643: 0, -449644: 181, -449645: 2, -449646: 107, -449647: 1, -449648: 82, -449649: 2, -449650: 4, -449651: 17, -449652: 116, -449653: 0, -449654: 13, -449655: 0, -449660: 3, -449661: 32, -449662: 206, -449663: 40, -449664: 189, -449665: 55, -449666: 5, -449667: 60, -449668: 224, -449669: 99, -449670: 168, -449671: 73, -449672: 159, -449673: 127, -449674: 210, -449675: 114, -449676: 78, -449677: 98, -449678: 187, -449679: 40, -449680: 181, -449681: 54, -449682: 107, -449683: 53, -449684: 22, -449685: 42, -449686: 4, -449687: 57, -449688: 116, -449689: 40, -449690: 13, -449691: 40, -449700: 0, -449701: 56, -449702: 8, -449703: 1, -449704: 189, -449705: 3, -449706: 5, -449707: 20, -449708: 224, -449709: 59, -449710: 168, -449711: 33, -449712: 159, -449713: 87, -449714: 210, -449715: 74, -449716: 78, -449717: 58, -449718: 187, -449719: 0, -449720: 181, -449721: 2, -449722: 107, -449723: 1, -449724: 82, -449725: 2, -449726: 4, -449727: 17, -449728: 116, -449729: 0, -449730: 13, -449731: 0, -449745: 0, -449746: 0, -449747: 8, -449748: 1, -449749: 255, -449750: 2, -449751: 5, -449752: 20, -449753: 224, -449754: 59, -449755: 168, -449756: 33, -449757: 159, -449758: 87, -449759: 210, -449760: 74, -449761: 78, -449762: 58, -449763: 187, -449764: 0, -449765: 190, -449766: 1, -449767: 142, -449768: 0, -449769: 82, -449770: 2, -449771: 4, -449772: 17, -449773: 116, -449774: 0, -449775: 13, -449776: 0, -449781: 0, -449782: 0, -449783: 168, -449784: 121, -449785: 159, -449786: 123, -449787: 229, -449788: 101, -449789: 224, -449790: 127, -449791: 136, -449792: 115, -449793: 255, -449794: 127, -449795: 242, -449796: 127, -449797: 238, -449798: 127, -449799: 155, -449800: 86, -449801: 94, -449802: 118, -449803: 46, -449804: 117, -449805: 77, -449806: 82, -449807: 228, -449808: 98, -449809: 84, -449810: 82, -449811: 237, -449812: 81, -449824: 0, -449825: 0, -449826: 8, -449827: 1, -449828: 255, -449829: 2, -449830: 5, -449831: 20, -449832: 224, -449833: 59, -449834: 168, -449835: 33, -449836: 159, -449837: 87, -449838: 210, -449839: 74, -449840: 78, -449841: 58, -449842: 187, -449843: 0, -449844: 190, -449845: 1, -449846: 142, -449847: 0, -449848: 82, -449849: 2, -449850: 4, -449851: 17, -449852: 116, -449853: 0, -449854: 13, -449855: 0, -449860: 0, -449861: 0, -449862: 168, -449863: 121, -449864: 159, -449865: 123, -449866: 229, -449867: 101, -449868: 224, -449869: 127, -449870: 136, -449871: 115, -449872: 255, -449873: 127, -449874: 242, -449875: 127, -449876: 238, -449877: 127, -449878: 155, -449879: 86, -449880: 94, -449881: 118, -449882: 46, -449883: 117, -449884: 77, -449885: 82, -449886: 228, -449887: 98, -449888: 84, -449889: 82, -449890: 237, -449891: 81, -449903: 0, -449904: 0, -449905: 8, -449906: 1, -449907: 255, -449908: 2, -449909: 5, -449910: 20, -449911: 224, -449912: 59, -449913: 168, -449914: 33, -449915: 159, -449916: 87, -449917: 210, -449918: 74, -449919: 78, -449920: 58, -449921: 187, -449922: 0, -449923: 190, -449924: 1, -449925: 142, -449926: 0, -449927: 82, -449928: 2, -449929: 4, -449930: 17, -449931: 116, -449932: 0, -449933: 13, -449934: 0, -449939: 3, -449940: 32, -449941: 8, -449942: 81, -449943: 255, -449944: 82, -449945: 165, -449946: 100, -449947: 224, -449948: 127, -449949: 72, -449950: 114, -449951: 255, -449952: 127, -449953: 114, -449954: 127, -449955: 238, -449956: 126, -449957: 91, -449958: 85, -449959: 190, -449960: 81, -449961: 142, -449962: 80, -449963: 77, -449964: 62, -449965: 164, -449966: 97, -449967: 20, -449968: 81, -449969: 173, -449970: 80, -449982: 0, -449983: 0, -449984: 8, -449985: 1, -449986: 255, -449987: 2, -449988: 5, -449989: 20, -449990: 224, -449991: 59, -449992: 168, -449993: 33, -449994: 159, -449995: 87, -449996: 210, -449997: 74, -449998: 78, -449999: 58, -450000: 187, -450001: 0, -450002: 190, -450003: 1, -450004: 142, -450005: 0, -450006: 82, -450007: 2, -450008: 4, -450009: 17, -450010: 116, -450011: 0, -450012: 13, -450013: 0, -450018: 3, -450019: 32, -450020: 8, -450021: 41, -450022: 255, -450023: 42, -450024: 5, -450025: 60, -450026: 224, -450027: 99, -450028: 168, -450029: 73, -450030: 159, -450031: 127, -450032: 210, -450033: 114, -450034: 78, -450035: 98, -450036: 187, -450037: 40, -450038: 190, -450039: 41, -450040: 142, -450041: 40, -450042: 77, -450043: 42, -450044: 4, -450045: 57, -450046: 116, -450047: 40, -450048: 13, -450049: 40, -450058: 0, -450059: 0, -450060: 8, -450061: 1, -450062: 255, -450063: 2, -450064: 5, -450065: 20, -450066: 224, -450067: 59, -450068: 168, -450069: 33, -450070: 159, -450071: 87, -450072: 210, -450073: 74, -450074: 78, -450075: 58, -450076: 187, -450077: 0, -450078: 190, -450079: 1, -450080: 142, -450081: 0, -450082: 82, -450083: 2, -450084: 4, -450085: 17, -450086: 116, -450087: 0, -450088: 13, -450089: 0, -450103: 0, -450104: 56, -450105: 8, -450106: 1, -450107: 31, -450108: 66, -450109: 5, -450110: 20, -450111: 224, -450112: 59, -450113: 168, -450114: 33, -450115: 159, -450116: 87, -450117: 210, -450118: 74, -450119: 78, -450120: 58, -450121: 187, -450122: 0, -450123: 20, -450124: 89, -450125: 170, -450126: 48, -450127: 116, -450128: 2, -450129: 4, -450130: 17, -450131: 116, -450132: 0, -450133: 13, -450134: 0, -450139: 0, -450140: 0, -450141: 174, -450142: 82, -450143: 255, -450144: 127, -450145: 229, -450146: 101, -450147: 224, -450148: 127, -450149: 136, -450150: 115, -450151: 255, -450152: 127, -450153: 242, -450154: 127, -450155: 238, -450156: 127, -450157: 155, -450158: 86, -450159: 244, -450160: 126, -450161: 138, -450162: 126, -450163: 246, -450164: 83, -450165: 228, -450166: 98, -450167: 84, -450168: 82, -450169: 237, -450170: 81, -450182: 0, -450183: 56, -450184: 8, -450185: 1, -450186: 31, -450187: 66, -450188: 5, -450189: 20, -450190: 224, -450191: 59, -450192: 168, -450193: 33, -450194: 159, -450195: 87, -450196: 210, -450197: 74, -450198: 78, -450199: 58, -450200: 187, -450201: 0, -450202: 20, -450203: 89, -450204: 170, -450205: 48, -450206: 116, -450207: 2, -450208: 4, -450209: 17, -450210: 116, -450211: 0, -450212: 13, -450213: 0, -450218: 0, -450219: 0, -450220: 174, -450221: 82, -450222: 255, -450223: 127, -450224: 229, -450225: 101, -450226: 224, -450227: 127, -450228: 136, -450229: 115, -450230: 255, -450231: 127, -450232: 242, -450233: 127, -450234: 238, -450235: 127, -450236: 155, -450237: 86, -450238: 244, -450239: 126, -450240: 138, -450241: 126, -450242: 246, -450243: 83, -450244: 228, -450245: 98, -450246: 84, -450247: 82, -450248: 237, -450249: 81, -450261: 0, -450262: 56, -450263: 8, -450264: 1, -450265: 31, -450266: 66, -450267: 5, -450268: 20, -450269: 224, -450270: 59, -450271: 168, -450272: 33, -450273: 159, -450274: 87, -450275: 210, -450276: 74, -450277: 78, -450278: 58, -450279: 187, -450280: 0, -450281: 20, -450282: 89, -450283: 170, -450284: 48, -450285: 116, -450286: 2, -450287: 4, -450288: 17, -450289: 116, -450290: 0, -450291: 13, -450292: 0, -450297: 3, -450298: 32, -450299: 110, -450300: 81, -450301: 191, -450302: 126, -450303: 165, -450304: 100, -450305: 224, -450306: 127, -450307: 72, -450308: 114, -450309: 255, -450310: 127, -450311: 114, -450312: 127, -450313: 238, -450314: 126, -450315: 91, -450316: 85, -450317: 180, -450318: 125, -450319: 74, -450320: 125, -450321: 182, -450322: 82, -450323: 164, -450324: 97, -450325: 20, -450326: 81, -450327: 173, -450328: 80, -450340: 0, -450341: 56, -450342: 8, -450343: 1, -450344: 31, -450345: 66, -450346: 5, -450347: 20, -450348: 224, -450349: 59, -450350: 168, -450351: 33, -450352: 159, -450353: 87, -450354: 210, -450355: 74, -450356: 78, -450357: 58, -450358: 187, -450359: 0, -450360: 20, -450361: 89, -450362: 170, -450363: 48, -450364: 116, -450365: 2, -450366: 4, -450367: 17, -450368: 116, -450369: 0, -450370: 13, -450371: 0, -450376: 3, -450377: 32, -450378: 206, -450379: 40, -450380: 31, -450381: 110, -450382: 5, -450383: 60, -450384: 224, -450385: 99, -450386: 168, -450387: 73, -450388: 159, -450389: 127, -450390: 210, -450391: 114, -450392: 78, -450393: 98, -450394: 187, -450395: 40, -450396: 20, -450397: 125, -450398: 170, -450399: 88, -450400: 22, -450401: 42, -450402: 4, -450403: 57, -450404: 116, -450405: 40, -450406: 13, -450407: 40, -450416: 0, -450417: 56, -450418: 8, -450419: 1, -450420: 31, -450421: 66, -450422: 5, -450423: 20, -450424: 224, -450425: 59, -450426: 168, -450427: 33, -450428: 159, -450429: 87, -450430: 210, -450431: 74, -450432: 78, -450433: 58, -450434: 187, -450435: 0, -450436: 20, -450437: 89, -450438: 170, -450439: 48, -450440: 116, -450441: 2, -450442: 4, -450443: 17, -450444: 116, -450445: 0, -450446: 13, -450447: 0, -1246881: 65, -1246882: 8, -1246884: 110, -451138: 255, -451139: 87, -451140: 255, -451141: 43, -451142: 60, -451143: 31, -451144: 120, -451145: 2, -451146: 176, -451147: 1, -451148: 11, -451149: 1, -451150: 135, -451151: 0, -451158: 222, -451159: 91, -451160: 222, -451161: 51, -451162: 26, -451163: 39, -451164: 87, -451165: 14, -451166: 175, -451167: 9, -451168: 10, -451169: 5, -451170: 134, -451171: 4, -451178: 221, -451179: 99, -451180: 156, -451181: 63, -451182: 249, -451183: 46, -451184: 85, -451185: 22, -451186: 142, -451187: 17, -451188: 10, -451189: 13, -451190: 134, -451191: 4, -451198: 188, -451199: 103, -451200: 123, -451201: 71, -451202: 215, -451203: 54, -451204: 52, -451205: 34, -451206: 141, -451207: 25, -451208: 9, -451209: 17, -451210: 133, -451211: 8, -451218: 155, -451219: 111, -451220: 89, -451221: 83, -451222: 214, -451223: 66, -451224: 50, -451225: 42, -451226: 141, -451227: 29, -451228: 233, -451229: 20, -451230: 133, -451231: 8, -451238: 122, -451239: 115, -451240: 56, -451241: 91, -451242: 180, -451243: 74, -451244: 17, -451245: 54, -451246: 140, -451247: 37, -451248: 232, -451249: 24, -451250: 132, -451251: 12, -451258: 121, -451259: 123, -451260: 246, -451261: 102, -451262: 147, -451263: 82, -451264: 15, -451265: 62, -451266: 107, -451267: 45, -451268: 232, -451269: 32, -451270: 132, -451271: 12, -451278: 88, -451279: 127, -451280: 213, -451281: 110, -451282: 113, -451283: 90, -451284: 238, -451285: 73, -451286: 106, -451287: 53, -451288: 231, -451289: 36, -451290: 131, -451291: 16, -1237849: 0, -1237850: 0, -1237851: 87, -1237852: 63, -1237853: 77, -1237854: 46, -1237855: 226, -1237856: 0, -1237857: 96, -1237858: 0, -1237859: 176, -1237860: 58, -1237861: 11, -1237862: 34, -1237863: 102, -1237864: 17, -1237865: 36, -1237866: 9, -1237867: 255, -1237868: 87, -1237869: 181, -1237870: 58, -1237871: 206, -1237872: 29, -1237873: 231, -1237874: 0, -1237875: 255, -1237876: 3, -1237877: 22, -1237878: 2, -1237879: 176, -1237880: 0, -1237881: 0, -1237882: 0, -1237883: 87, -1237884: 63, -1237885: 77, -1237886: 46, -1237887: 226, -1237888: 0, -1237889: 96, -1237890: 0, -1237891: 176, -1237892: 58, -1237893: 11, -1237894: 34, -1237895: 102, -1237896: 17, -1237897: 36, -1237898: 9, -1237899: 255, -1237900: 87, -1237901: 181, -1237902: 58, -1237903: 206, -1237904: 29, -1237905: 231, -1237906: 0, -1237907: 255, -1237908: 3, -1237909: 22, -1237910: 2, -1237911: 176, -1237912: 0, -1237913: 0, -1237914: 56, -1237915: 146, -1237916: 42, -1237917: 204, -1237918: 33, -1237919: 196, -1237920: 0, -1237921: 98, -1237922: 0, -1237923: 14, -1237924: 38, -1237925: 170, -1237926: 21, -1237927: 39, -1237928: 13, -1237929: 229, -1237930: 4, -1237931: 90, -1237932: 71, -1237933: 82, -1237934: 46, -1237935: 140, -1237936: 25, -1237937: 198, -1237938: 0, -1237939: 63, -1237940: 3, -1237941: 182, -1237942: 1, -1237943: 143, -1237944: 0, -1237945: 0, -1237946: 56, -1237947: 239, -1237948: 21, -1237949: 107, -1237950: 21, -1237951: 165, -1237952: 0, -1237953: 99, -1237954: 0, -1237955: 172, -1237956: 21, -1237957: 73, -1237958: 13, -1237959: 7, -1237960: 9, -1237961: 198, -1237962: 4, -1237963: 214, -1237964: 54, -1237965: 208, -1237966: 33, -1237967: 75, -1237968: 17, -1237969: 166, -1237970: 0, -1237971: 95, -1237972: 2, -1237973: 55, -1237974: 1, -1237975: 141, -1237976: 0, -1500232: 0, -1500233: 0, -1500234: 127, -1500235: 4, -1500236: 121, -1500237: 4, -1500238: 51, -1500239: 0, -1500240: 45, -1500241: 0, -1500242: 59, -1500243: 58, -1500244: 183, -1500245: 45, -1500246: 51, -1500247: 29, -1500248: 142, -1500249: 12, -1500250: 191, -1500251: 37, -1500252: 91, -1500253: 29, -1500254: 247, -1500255: 20, -1500256: 178, -1500257: 12, -1500258: 110, -1500259: 8, -1500260: 191, -1500261: 86, -451686: 16, -1500263: 0, -1500264: 80, -1500265: 4, -1500266: 77, -1500267: 4, -451692: 5, -1500269: 0, -1500270: 39, -1500271: 0, -1500272: 46, -1500273: 29, -1500274: 236, -1500275: 24, -451700: 210, -1500277: 16, -1500278: 71, -451703: 58, -451704: 187, -1500281: 20, -1500282: 174, -1500283: 16, -1500284: 140, -451709: 1, -451710: 82, -1500287: 8, -451712: 4, -1500289: 4, -1500290: 112, -1500291: 45, -1500292: 5, -1500293: 0, -1500294: 31, -1500295: 5, -451720: 4, -1500297: 4, -1500298: 211, -1500299: 0, -1500300: 173, -451725: 3, -1500302: 187, -1500303: 58, -451728: 225, -451729: 59, -451730: 202, -451731: 37, -451732: 159, -451733: 87, -451734: 211, -451735: 74, -451736: 79, -451737: 58, -451738: 188, -451739: 0, -451740: 182, -451741: 2, -451742: 108, -451743: 1, -451744: 83, -451745: 2, -451746: 5, -451747: 17, -451748: 117, -451749: 0, -451750: 14, -451751: 0, -1500328: 106, -1500329: 0, -451754: 4, -451755: 0, -451756: 9, -451757: 1, -451758: 189, -451759: 3, -1500336: 234, -451761: 20, -451762: 225, -451763: 59, -451764: 202, -451765: 37, -451766: 159, -451767: 87, -451768: 211, -451769: 74, -451770: 79, -451771: 58, -451772: 188, -451773: 0, -451774: 182, -451775: 2, -451776: 108, -451777: 1, -451778: 83, -451779: 2, -451780: 5, -451781: 17, -451782: 117, -451783: 0, -451784: 14, -451785: 0, -1500362: 123, -1500363: 59, -451788: 5, -451789: 0, -451790: 10, -451791: 1, -451792: 190, -451793: 3, -451794: 7, -451795: 20, -451796: 226, -451797: 59, -451798: 204, -451799: 37, -451800: 159, -451801: 87, -451802: 212, -451803: 74, -451804: 80, -451805: 58, -451806: 188, -451807: 0, -451808: 183, -451809: 2, -451810: 109, -451811: 1, -451812: 84, -451813: 2, -451814: 6, -451815: 17, -451816: 118, -451817: 0, -451818: 15, -451819: 0, -1500396: 74, -1500397: 17, -451822: 6, -451823: 0, -451824: 10, -451825: 1, -451826: 190, -451827: 3, -451828: 7, -451829: 20, -451830: 226, -451831: 59, -451832: 204, -451833: 37, -451834: 159, -451835: 87, -451836: 212, -451837: 74, -451838: 80, -451839: 58, -451840: 188, -451841: 0, -451842: 183, -451843: 2, -451844: 109, -451845: 1, -451846: 84, -451847: 2, -451848: 6, -451849: 17, -451850: 118, -451851: 0, -451852: 15, -451853: 0, -1500430: 245, -1500431: 38, -451856: 7, -451857: 0, -451858: 11, -451859: 1, -451860: 190, -451861: 3, -451862: 8, -451863: 20, -451864: 227, -451865: 59, -451866: 238, -451867: 41, -451868: 159, -451869: 87, -451870: 213, -451871: 74, -451872: 81, -451873: 58, -451874: 189, -451875: 0, -451876: 184, -451877: 2, -451878: 110, -451879: 1, -1500456: 69, -451881: 2, -451882: 7, -451883: 17, -451884: 119, -451885: 0, -451886: 16, -451887: 0, -1500464: 39, -1500465: 13, -451890: 8, -451891: 0, -451892: 11, -1500469: 4, -451894: 190, -451895: 3, -451896: 8, -1500473: 0, -451898: 227, -1500475: 13, -451900: 238, -451901: 41, -451902: 159, -1500479: 9, -451904: 213, -451905: 74, -451906: 81, -451907: 58, -1500484: 173, -451909: 0, -1500486: 41, -451911: 2, -451912: 110, -451913: 1, -451914: 85, -451915: 2, -451916: 7, -451917: 17, -451918: 119, -451919: 0, -451920: 16, -451921: 0, -1500498: 100, -1500499: 17, -451924: 8, -1500501: 95, -451926: 13, -451927: 1, -451928: 190, -451929: 3, -451930: 10, -451931: 20, -451932: 229, -451933: 59, -451934: 15, -1500511: 4, -451936: 159, -451937: 87, -451938: 215, -451939: 74, -1500516: 37, -451941: 58, -451942: 189, -451943: 0, -451944: 186, -451945: 2, -451946: 112, -451947: 1, -451948: 87, -451949: 2, -451950: 9, -451951: 17, -451952: 121, -451953: 0, -451954: 18, -451955: 0, -1500532: 128, -1500533: 4, -451958: 8, -451959: 0, -451960: 13, -451961: 1, -451962: 190, -451963: 3, -451964: 10, -451965: 20, -451966: 229, -451967: 59, -451968: 15, -451969: 46, -451970: 159, -451971: 87, -451972: 215, -451973: 74, -451974: 83, -451975: 58, -451976: 189, -451977: 0, -451978: 186, -451979: 2, -451980: 112, -451981: 1, -451982: 87, -451983: 2, -451984: 9, -1500561: 107, -451986: 121, -451987: 0, -451988: 18, -451989: 0, -1500566: 136, -1500567: 12, -451992: 8, -451993: 0, -451994: 11, -1500571: 12, -451996: 190, -451997: 3, -451998: 8, -451999: 20, -1500576: 229, -452001: 59, -452002: 238, -452003: 41, -1500580: 43, -452005: 87, -452006: 213, -452007: 74, -452008: 81, -452009: 58, -452010: 189, -452011: 0, -452012: 184, -452013: 2, -452014: 110, -452015: 1, -452016: 85, -452017: 2, -452018: 7, -452019: 17, -452020: 119, -452021: 0, -452022: 16, -452023: 0, -1500600: 35, -1500601: 40, -452026: 7, -452027: 0, -452028: 11, -452029: 1, -1500606: 41, -452031: 3, -452032: 8, -452033: 20, -452034: 227, -452035: 59, -452036: 238, -452037: 41, -452038: 159, -452039: 87, -452040: 213, -452041: 74, -452042: 81, -452043: 58, -452044: 189, -452045: 0, -452046: 184, -452047: 2, -452048: 110, -452049: 1, -452050: 85, -452051: 2, -452052: 7, -452053: 17, -452054: 119, -452055: 0, -452056: 16, -452057: 0, -1500634: 231, -1500635: 44, -1500636: 165, -452061: 0, -452062: 10, -452063: 1, -452064: 190, -452065: 3, -452066: 7, -452067: 20, -452068: 226, -452069: 59, -452070: 204, -452071: 37, -452072: 159, -452073: 87, -452074: 212, -452075: 74, -452076: 80, -452077: 58, -452078: 188, -452079: 0, -452080: 183, -452081: 2, -452082: 109, -452083: 1, -452084: 84, -452085: 2, -452086: 6, -452087: 17, -452088: 118, -452089: 0, -452090: 15, -452091: 0, -1500668: 137, -1500669: 56, -452094: 5, -452095: 0, -452096: 10, -452097: 1, -452098: 190, -452099: 3, -452100: 7, -452101: 20, -452102: 226, -452103: 59, -452104: 204, -452105: 37, -452106: 159, -452107: 87, -452108: 212, -452109: 74, -452110: 80, -452111: 58, -452112: 188, -452113: 0, -452114: 183, -452115: 2, -1500692: 43, -1500693: 49, -452118: 84, -1500695: 44, -452120: 6, -452121: 17, -452122: 118, -452123: 0, -452124: 15, -452125: 0, -1500702: 171, -1500703: 36, -452128: 4, -452129: 0, -452130: 9, -452131: 1, -452132: 189, -1500709: 24, -452134: 6, -452135: 20, -452136: 225, -452137: 59, -1500714: 127, -452139: 37, -452140: 159, -1500717: 44, -452142: 211, -452143: 74, -452144: 79, -452145: 58, -452146: 188, -452147: 0, -452148: 182, -452149: 2, -1500726: 51, -1500727: 73, -1500728: 142, -1500729: 56, -1500730: 191, -1500731: 77, -1500732: 91, -452157: 0, -1500734: 247, -1500735: 64, -1500736: 178, -1500737: 56, -452162: 4, -452163: 0, -452164: 9, -452165: 1, -452166: 189, -452167: 3, -452168: 6, -452169: 20, -452170: 225, -452171: 59, -452172: 202, -452173: 37, -452174: 159, -452175: 87, -452176: 211, -452177: 74, -452178: 79, -452179: 58, -452180: 188, -452181: 0, -452182: 182, -452183: 2, -452184: 108, -452185: 1, -452186: 83, -452187: 2, -452188: 5, -452189: 17, -452190: 117, -452191: 0, -452192: 14, -452193: 0, -1500770: 112, -1500771: 65, -1500772: 5, -452197: 0, -452198: 9, -452199: 1, -452200: 189, -452201: 3, -1500778: 51, -1500779: 20, -452204: 225, -452205: 59, -452206: 202, -452207: 37, -452208: 159, -1500785: 65, -452210: 211, -452211: 74, -452212: 79, -452213: 58, -452214: 188, -452215: 0, -452216: 182, -452217: 2, -452218: 108, -452219: 1, -452220: 83, -452221: 2, -452222: 5, -452223: 17, -452224: 117, -452225: 0, -452226: 14, -452227: 0, -1500804: 80, -1500805: 12, -1500806: 77, -1500807: 12, -1500808: 42, -1500809: 12, -1500810: 39, -1500811: 12, -1500812: 46, -1500813: 41, -1500814: 236, -1500815: 32, -1500816: 170, -1500817: 24, -452242: 16, -1500819: 16, -452244: 8, -452245: 1, -452246: 255, -1500823: 24, -1500824: 140, -452249: 20, -452250: 224, -452251: 59, -452252: 168, -452253: 33, -452254: 159, -452255: 87, -452256: 210, -452257: 74, -452258: 78, -452259: 58, -452260: 187, -452261: 0, -452262: 190, -452263: 1, -452264: 142, -452265: 0, -452266: 82, -452267: 2, -452268: 4, -452269: 17, -452270: 116, -1500847: 1, -452272: 13, -452273: 0, -1500850: 4, -1500851: 0, -452276: 4, -452277: 0, -452278: 9, -452279: 1, -452280: 255, -452281: 2, -452282: 6, -452283: 20, -452284: 225, -452285: 59, -452286: 202, -1500863: 41, -1500864: 115, -1500865: 33, -1500866: 12, -1500867: 21, -452292: 79, -452293: 58, -452294: 188, -452295: 0, -452296: 190, -452297: 1, -452298: 143, -452299: 0, -452300: 83, -452301: 2, -452302: 5, -452303: 17, -452304: 117, -1500881: 0, -1500882: 249, -452307: 0, -1500884: 51, -1500885: 62, -452310: 4, -452311: 0, -452312: 9, -452313: 1, -452314: 255, -452315: 2, -452316: 6, -452317: 20, -1500894: 83, -452319: 59, -452320: 202, -452321: 37, -452322: 159, -452323: 87, -452324: 211, -452325: 74, -452326: 79, -452327: 58, -452328: 188, -452329: 0, -452330: 190, -1500907: 1, -452332: 143, -1500909: 0, -452334: 83, -452335: 2, -452336: 5, -452337: 17, -452338: 117, -452339: 0, -452340: 14, -452341: 0, -1500918: 139, -1500919: 16, -452344: 5, -452345: 0, -452346: 10, -452347: 1, -452348: 255, -452349: 2, -1500926: 207, -1500927: 16, -1500928: 107, -1500929: 8, -452354: 204, -452355: 37, -452356: 159, -452357: 87, -452358: 212, -452359: 74, -1500936: 219, -452361: 58, -452362: 188, -452363: 0, -452364: 190, -1500941: 0, -452366: 144, -452367: 0, -452368: 84, -452369: 2, -1500946: 244, -452371: 17, -1500948: 112, -1500949: 12, -1500950: 127, -452375: 0, -1500952: 59, -1500953: 25, -1500954: 215, -1500955: 16, -452380: 10, -452381: 1, -452382: 255, -452383: 2, -452384: 7, -452385: 20, -452386: 226, -452387: 59, -452388: 204, -452389: 37, -452390: 159, -452391: 87, -452392: 212, -452393: 74, -452394: 80, -452395: 58, -452396: 188, -452397: 0, -452398: 190, -452399: 1, -452400: 144, -452401: 0, -452402: 84, -452403: 2, -452404: 6, -452405: 17, -452406: 118, -452407: 0, -452408: 15, -452409: 0, -1500986: 147, -1500987: 12, -452412: 7, -452413: 0, -452414: 11, -1500991: 74, -452416: 255, -452417: 2, -452418: 8, -452419: 20, -452420: 227, -452421: 59, -452422: 238, -452423: 41, -452424: 159, -452425: 87, -452426: 213, -452427: 74, -452428: 81, -452429: 58, -1501006: 0, -452431: 0, -452432: 190, -452433: 1, -452434: 145, -452435: 0, -452436: 85, -452437: 2, -452438: 7, -452439: 17, -452440: 119, -452441: 0, -452442: 16, -452443: 0, -1501020: 0, -1501021: 0, -452446: 8, -452447: 0, -452448: 11, -452449: 1, -452450: 255, -452451: 2, -452452: 8, -452453: 20, -452454: 227, -452455: 59, -452456: 238, -452457: 41, -452458: 159, -1501035: 0, -452460: 213, -452461: 74, -452462: 81, -1501039: 0, -452464: 189, -452465: 0, -1501042: 111, -452467: 1, -452468: 145, -452469: 0, -452470: 85, -452471: 2, -452472: 7, -452473: 17, -452474: 119, -452475: 0, -452476: 16, -452477: 0, -1501054: 4, -1501055: 0, -452480: 8, -452481: 0, -452482: 13, -452483: 1, -452484: 255, -452485: 2, -452486: 10, -452487: 20, -452488: 229, -452489: 59, -452490: 15, -452491: 46, -452492: 159, -452493: 87, -452494: 215, -452495: 74, -452496: 83, -452497: 58, -452498: 189, -452499: 0, -452500: 191, -452501: 1, -452502: 147, -1501079: 8, -1501080: 8, -452505: 2, -1501082: 8, -1501083: 0, -452508: 121, -1501085: 0, -452510: 18, -452511: 0, -1501088: 8, -1501089: 0, -452514: 8, -452515: 0, -452516: 13, -452517: 1, -452518: 255, -452519: 2, -452520: 10, -452521: 20, -452522: 229, -452523: 59, -452524: 15, -452525: 46, -1501102: 245, -1501103: 20, -452528: 215, -452529: 74, -452530: 83, -452531: 58, -452532: 189, -1501109: 4, -452534: 191, -452535: 1, -452536: 147, -452537: 0, -452538: 87, -452539: 2, -452540: 9, -452541: 17, -452542: 121, -452543: 0, -452544: 18, -452545: 0, -1501122: 14, -1501123: 0, -452548: 8, -452549: 0, -452550: 11, -452551: 1, -452552: 255, -452553: 2, -452554: 8, -452555: 20, -452556: 227, -452557: 59, -452558: 238, -452559: 41, -452560: 159, -452561: 87, -452562: 213, -452563: 74, -452564: 81, -452565: 58, -452566: 189, -452567: 0, -452568: 190, -452569: 1, -452570: 145, -452571: 0, -1501148: 14, -452573: 2, -1501150: 245, -452575: 17, -452576: 119, -452577: 0, -452578: 16, -452579: 0, -452582: 7, -452583: 0, -452584: 11, -452585: 1, -452586: 255, -452587: 2, -452588: 8, -452589: 20, -452590: 227, -452591: 59, -452592: 238, -452593: 41, -452594: 159, -1501171: 87, -1501172: 17, -1501173: 11, -452598: 81, -452599: 58, -452600: 189, -1501177: 0, -452602: 190, -452603: 1, -452604: 145, -452605: 0, -452606: 85, -452607: 2, -452608: 7, -452609: 17, -452610: 119, -1501187: 127, -452612: 16, -452613: 0, -1501190: 18, -1501191: 11, -452616: 6, -452617: 0, -452618: 10, -1501195: 0, -452620: 255, -452621: 2, -1501198: 183, -1501199: 58, -452624: 226, -452625: 59, -452626: 204, -452627: 37, -452628: 159, -452629: 87, -452630: 212, -452631: 74, -452632: 80, -452633: 58, -452634: 188, -452635: 0, -452636: 190, -452637: 1, -452638: 144, -1501215: 71, -1501216: 150, -1501217: 58, -452642: 6, -452643: 17, -1501220: 172, -452645: 0, -452646: 15, -1501223: 115, -1501224: 185, -1501225: 63, -1501226: 243, -1501227: 10, -1501228: 9, -1501229: 14, -1501230: 229, -452655: 2, -452656: 7, -452657: 20, -452658: 226, -452659: 59, -452660: 204, -452661: 37, -1501238: 172, -452663: 87, -452664: 212, -452665: 74, -452666: 80, -452667: 58, -452668: 188, -452669: 0, -452670: 190, -452671: 1, -452672: 144, -452673: 0, -452674: 84, -452675: 2, -1501252: 84, -1501253: 50, -1501254: 175, -452679: 0, -452680: 15, -1501257: 4, -1501258: 222, -1501259: 103, -452684: 4, -452685: 0, -452686: 9, -452687: 1, -452688: 255, -452689: 2, -452690: 6, -452691: 20, -452692: 225, -452693: 59, -452694: 202, -1501271: 46, -452696: 159, -1501273: 33, -452698: 211, -452699: 74, -452700: 79, -452701: 58, -1501278: 154, -1501279: 35, -452704: 190, -1501281: 6, -452706: 143, -1501283: 9, -452708: 83, -452709: 2, -1501286: 151, -1501287: 58, -1501288: 18, -1501289: 46, -452714: 14, -452715: 0, -1501292: 138, -1501293: 4, -452718: 4, -1501295: 91, -452720: 9, -452721: 1, -1501298: 213, -452723: 2, -1501300: 204, -1501301: 5, -452726: 225, -1501303: 0, -452728: 202, -1501305: 54, -452730: 159, -452731: 87, -1501308: 109, -452733: 74, -452734: 79, -452735: 58, -452736: 188, -452737: 0, -452738: 190, -452739: 1, -452740: 143, -452741: 0, -452742: 83, -452743: 2, -452744: 5, -452745: 17, -452746: 117, -452747: 0, -452748: 14, -452749: 0, -452752: 16, -452753: 0, -1501330: 255, -452755: 1, -452756: 255, -452757: 2, -452758: 5, -452759: 20, -1501336: 175, -1501337: 28, -452762: 168, -1501339: 24, -452764: 159, -452765: 87, -452766: 210, -1501343: 44, -452768: 78, -1501345: 40, -452770: 187, -1501347: 32, -1501348: 168, -1501349: 24, -1501350: 155, -1501351: 106, -452776: 82, -1501353: 48, -1501354: 150, -1501355: 40, -1501356: 141, -452781: 0, -1501358: 136, -1501359: 20, -1501360: 89, -1501361: 102, -1501362: 185, -1501363: 48, -1501364: 148, -1501365: 44, -1501366: 140, -1501367: 36, -1501368: 135, -1501369: 20, -1501370: 54, -1501371: 102, -1501372: 150, -1501373: 52, -1501374: 146, -1501375: 44, -1501376: 139, -1501377: 40, -1501378: 102, -1501379: 16, -1501380: 244, -1501381: 97, -1501382: 148, -1501383: 52, -1501384: 144, -1501385: 48, -1501386: 138, -1501387: 44, -1501388: 101, -1501389: 16, -1501390: 210, -1501391: 93, -1501392: 114, -1501393: 56, -1501394: 111, -1501395: 48, -1501396: 105, -1501397: 44, -1501398: 69, -1501399: 12, -1501400: 144, -1501401: 89, -1501402: 112, -1501403: 56, -1501404: 109, -1501405: 52, -1501406: 104, -1501407: 48, -1501408: 68, -1501409: 12, -452834: 9, -452835: 1, -452836: 31, -452837: 66, -452838: 6, -452839: 20, -452840: 225, -452841: 59, -452842: 202, -452843: 37, -452844: 159, -452845: 87, -452846: 211, -452847: 74, -1501424: 22, -1501425: 23, -1501426: 82, -1501427: 2, -1501428: 138, -1501429: 1, -1501430: 198, -1501431: 0, -1501432: 77, -1501433: 73, -1501434: 77, -1501435: 48, -1501436: 75, -1501437: 44, -1501438: 70, -1501439: 40, -1501440: 35, -1501441: 8, -1501442: 18, -1501443: 46, -1501444: 142, -1501445: 33, -1501446: 43, -1501447: 25, -1501448: 104, -1501449: 0, -1501450: 57, -1501451: 75, -1501452: 146, -1501453: 18, -1501454: 239, -1501455: 1, -1501456: 72, -1501457: 1, -1501458: 165, -1501459: 0, -1501460: 11, -1501461: 61, -1501462: 75, -1501463: 40, -1501464: 73, -1501465: 36, -1501466: 69, -1501467: 32, -1501468: 34, -1501469: 8, -1501470: 175, -1501471: 37, -1501472: 76, -1501473: 29, -1501474: 233, -1501475: 20, -1501476: 71, -1501477: 0, -1501478: 181, -1501479: 62, -1501480: 14, -1501481: 14, -1501482: 140, -1501483: 1, -1501484: 6, -1501485: 1, -1501486: 132, -1501487: 0, -1501488: 201, -1501489: 48, -1501490: 41, -1501491: 32, -1501492: 39, -1501493: 28, -1501494: 36, -1501495: 24, -1501496: 34, -1501497: 4, -1501498: 76, -1501499: 29, -1501500: 9, -1501501: 21, -1501502: 199, -1501503: 16, -1501504: 69, -1501505: 0, -1501506: 49, -1501507: 50, -1501508: 139, -1501509: 9, -1501510: 41, -1501511: 1, -1501512: 197, -1501513: 0, -1501514: 99, -1501515: 0, -1501516: 166, -1501517: 36, -1501518: 38, -1501519: 24, -1501520: 37, -1501521: 20, -1501522: 35, -1501523: 20, -1501524: 1, -1501525: 4, -1501526: 9, -1501527: 21, -1501528: 199, -1501529: 16, -1501530: 133, -1501531: 12, -1501532: 36, -1501533: 0, -1501534: 140, -1501535: 37, -1501536: 7, -1501537: 5, -1501538: 198, -1501539: 0, -1501540: 131, -1501541: 0, -1501542: 66, -1501543: 0, -1501544: 100, -1501545: 24, -1501546: 4, -1501547: 16, -1501548: 3, -1501549: 12, -1501550: 2, -1501551: 12, -1501552: 1, -1501553: 0, -1501554: 166, -1501555: 12, -1501556: 132, -1501557: 8, -1501558: 99, -1501559: 8, -1501560: 34, -1501561: 0, -1501562: 8, -1501563: 25, -1501564: 131, -1501565: 0, -1501566: 99, -1501567: 0, -1501568: 65, -1501569: 0, -1501570: 33, -1501571: 0, -1501572: 34, -1501573: 12, -1501574: 2, -1501575: 8, -1501576: 1, -1501577: 4, -1501578: 1, -1501579: 4, -1501580: 0, -1501581: 0, -1501582: 67, -1501583: 4, -1501584: 66, -1501585: 4, -1501586: 33, -1501587: 4, -1501588: 1, -1501589: 0, -1501590: 132, -1501591: 12, -1501592: 0, -1501593: 0, -1501594: 0, -1501595: 0, -1501596: 0, -1501597: 0, -1501598: 0, -1501599: 0, -1501600: 0, -1501601: 0, -1501602: 0, -1501603: 0, -1501604: 0, -1501605: 0, -1501606: 0, -1501607: 0, -1501608: 0, -1501609: 0, -1501610: 0, -1501611: 0, -1501612: 0, -1501613: 0, -1501614: 0, -1501615: 0, -1501616: 0, -1501617: 0, -1501618: 0, -1501619: 0, -453044: 229, -453045: 59, -453046: 15, -453047: 46, -453048: 159, -453049: 87, -453050: 215, -453051: 74, -453052: 83, -453053: 58, -453054: 189, -453055: 0, -453056: 25, -453057: 89, -453058: 175, -453059: 48, -453060: 87, -453061: 2, -453062: 9, -453063: 17, -453064: 121, -453065: 0, -453066: 18, -453067: 0, -453070: 8, -453071: 0, -453072: 13, -453073: 1, -453074: 31, -453075: 66, -453076: 10, -453077: 20, -453078: 229, -453079: 59, -453080: 15, -453081: 46, -453082: 159, -453083: 87, -453084: 215, -453085: 74, -453086: 83, -453087: 58, -453088: 189, -453089: 0, -453090: 25, -453091: 89, -453092: 175, -453093: 48, -453094: 87, -453095: 2, -453096: 9, -453097: 17, -453098: 121, -453099: 0, -453100: 18, -453101: 0, -453104: 8, -453105: 0, -453106: 11, -453107: 1, -453108: 31, -453109: 66, -453110: 8, -453111: 20, -453112: 227, -453113: 59, -453114: 238, -453115: 41, -453116: 159, -453117: 87, -453118: 213, -453119: 74, -453120: 81, -453121: 58, -453122: 189, -453123: 0, -453124: 23, -453125: 89, -453126: 173, -453127: 48, -1501704: 0, -1501705: 0, -1501706: 159, -1501707: 21, -1501708: 219, -1501709: 0, -1501710: 51, -1501711: 0, -1501712: 14, -1501713: 0, -1501714: 251, -1501715: 49, -1501716: 119, -1501717: 37, -1501718: 244, -1501719: 24, -1501720: 112, -1501721: 12, -1501722: 127, -1501723: 29, -1501724: 59, -1501725: 25, -1501726: 215, -1501727: 16, -1501728: 147, -1501729: 12, -1501730: 80, -1501731: 4, -1501732: 95, -1501733: 74, -1501734: 14, -1501735: 0, -1501736: 14, -1501737: 0, -1501738: 14, -1501739: 0, -1501740: 16, -1501741: 0, -1501742: 245, -1501743: 20, -1501744: 179, -1501745: 16, -1501746: 114, -1501747: 8, -1501748: 48, -1501749: 4, -1501750: 14, -1501751: 0, -1501752: 14, -1501753: 0, -1501754: 14, -1501755: 0, -1501756: 14, -1501757: 0, -1501758: 14, -1501759: 0, -1501760: 245, -1501761: 20, -1501762: 125, -1501763: 17, -1501764: 185, -1501765: 0, -1501766: 49, -1501767: 0, -1501768: 13, -1501769: 0, -1501770: 217, -1501771: 45, -1501772: 85, -1501773: 33, -1501774: 210, -1501775: 20, -1501776: 79, -1501777: 8, -1501778: 93, -1501779: 25, -1501780: 25, -1501781: 21, -1501782: 181, -1501783: 12, -1501784: 113, -1501785: 8, -1501786: 47, -1501787: 4, -1501788: 29, -1501789: 66, -1501790: 13, -1501791: 0, -1501792: 13, -1501793: 0, -1501794: 13, -1501795: 0, -1501796: 15, -1501797: 0, -1501798: 211, -1501799: 16, -1501800: 145, -1501801: 12, -1501802: 80, -1501803: 4, -1501804: 47, -1501805: 4, -1501806: 13, -1501807: 0, -1501808: 13, -1501809: 0, -1501810: 13, -1501811: 0, -1501812: 13, -1501813: 0, -1501814: 13, -1501815: 0, -1501816: 211, -1501817: 16, -1501818: 90, -1501819: 17, -1501820: 183, -1501821: 0, -1501822: 16, -1501823: 0, -1501824: 12, -1501825: 0, -1501826: 183, -1501827: 41, -1501828: 52, -1501829: 29, -1501830: 209, -1501831: 20, -1501832: 77, -1501833: 8, -1501834: 58, -1501835: 25, -1501836: 247, -1501837: 20, -1501838: 180, -1501839: 12, -1501840: 112, -1501841: 8, -1501842: 45, -1501843: 0, -1501844: 250, -1501845: 61, -1501846: 12, -1501847: 0, -1501848: 12, -1501849: 0, -1501850: 12, -1501851: 0, -1501852: 13, -1501853: 0, -1501854: 210, -1501855: 16, -1501856: 144, -1501857: 12, -1501858: 79, -1501859: 4, -1501860: 13, -1501861: 0, -1501862: 12, -1501863: 0, -1501864: 12, -1501865: 0, -1501866: 12, -1501867: 0, -1501868: 12, -1501869: 0, -1501870: 12, -1501871: 0, -1501872: 210, -1501873: 16, -1501874: 56, -1501875: 17, -1501876: 149, -1501877: 0, -1501878: 15, -1501879: 0, -1501880: 11, -1501881: 0, -1501882: 149, -1501883: 37, -1501884: 18, -1501885: 29, -1501886: 176, -1501887: 16, -1501888: 76, -1501889: 8, -1501890: 24, -1501891: 21, -1501892: 245, -1501893: 16, -1501894: 146, -1501895: 12, -1501896: 111, -1501897: 8, -1501898: 44, -1501899: 0, -1501900: 216, -1501901: 57, -1501902: 11, -1501903: 0, -1501904: 11, -1501905: 0, -1501906: 11, -1501907: 0, -1501908: 12, -1501909: 0, -1501910: 176, -1501911: 16, -1501912: 143, -1501913: 12, -1501914: 78, -1501915: 4, -1501916: 12, -1501917: 0, -1501918: 11, -1501919: 0, -1501920: 11, -1501921: 0, -1501922: 11, -1501923: 0, -1501924: 11, -1501925: 0, -1501926: 11, -1501927: 0, -1501928: 176, -1501929: 16, -1501930: 22, -1501931: 13, -1501932: 147, -1501933: 0, -1501934: 14, -1501935: 0, -1501936: 10, -1501937: 0, -1501938: 115, -1501939: 33, -1501940: 16, -1501941: 25, -1501942: 174, -1501943: 16, -1501944: 75, -1501945: 8, -1501946: 22, -1501947: 21, -1501948: 211, -1501949: 16, -1501950: 144, -1501951: 12, -1501952: 110, -1501953: 8, -1501954: 43, -1501955: 0, -1501956: 182, -1501957: 53, -1501958: 10, -1501959: 0, -1501960: 10, -1501961: 0, -1501962: 10, -1501963: 0, -1501964: 11, -1501965: 0, -1501966: 175, -1501967: 12, -1501968: 110, -1501969: 12, -1501970: 77, -1501971: 4, -1501972: 11, -1501973: 0, -1501974: 10, -1501975: 0, -1501976: 10, -1501977: 0, -1501978: 10, -1501979: 0, -1501980: 10, -1501981: 0, -1501982: 10, -1501983: 0, -1501984: 175, -1501985: 12, -1501986: 20, -1501987: 13, -1501988: 146, -1501989: 0, -1501990: 12, -1501991: 0, -1501992: 9, -1501993: 0, -1501994: 82, -1501995: 33, -1501996: 239, -1501997: 24, -1501998: 141, -1501999: 16, -1502000: 74, -1502001: 8, -1502002: 244, -1502003: 16, -1502004: 210, -1502005: 16, -1502006: 143, -1502007: 8, -1502008: 76, -1502009: 8, -1502010: 42, -1502011: 0, -1502012: 148, -1502013: 49, -1502014: 9, -1502015: 0, -1502016: 9, -1502017: 0, -1502018: 9, -1502019: 0, -1502020: 10, -1502021: 0, -1502022: 142, -1502023: 12, -1502024: 108, -1502025: 8, -1502026: 76, -1502027: 4, -1502028: 10, -1502029: 0, -1502030: 9, -1502031: 0, -1502032: 9, -1502033: 0, -1502034: 9, -1502035: 0, -1502036: 9, -1502037: 0, -1502038: 9, -1502039: 0, -1502040: 142, -1502041: 12, -1502042: 242, -1502043: 12, -1502044: 112, -1502045: 0, -1502046: 11, -1502047: 0, -1502048: 8, -1502049: 0, -1502050: 48, -1502051: 29, -1502052: 205, -1502053: 20, -1502054: 140, -1502055: 12, -1502056: 41, -1502057: 4, -1502058: 210, -1502059: 16, -1502060: 176, -1502061: 12, -1502062: 109, -1502063: 8, -1502064: 75, -1502065: 4, -1502066: 41, -1502067: 0, -1502068: 82, -1502069: 41, -1502070: 8, -1502071: 0, -1502072: 8, -1502073: 0, -1502074: 8, -1502075: 0, -1502076: 9, -1502077: 0, -1502078: 140, -1502079: 12, -1502080: 107, -1502081: 8, -1502082: 42, -1502083: 4, -1502084: 9, -1502085: 0, -1502086: 8, -1502087: 0, -1502088: 8, -1502089: 0, -1502090: 8, -1502091: 0, -1502092: 8, -1502093: 0, -1502094: 8, -1502095: 0, -1502096: 140, -1502097: 12, -1502098: 208, -1502099: 8, -1502100: 110, -1502101: 0, -1502102: 10, -1502103: 0, -1502104: 7, -1502105: 0, -1502106: 14, -1502107: 25, -1502108: 172, -1502109: 16, -1502110: 106, -1502111: 12, -1502112: 40, -1502113: 4, -1502114: 176, -1502115: 12, -1502116: 142, -1502117: 12, -1502118: 108, -1502119: 8, -1502120: 74, -1502121: 4, -1502122: 40, -1502123: 0, -1502124: 48, -1502125: 37, -1502126: 7, -1502127: 0, -1502128: 7, -1502129: 0, -1502130: 7, -1502131: 0, -1502132: 8, -1502133: 0, -1502134: 107, -1502135: 8, -1502136: 74, -1502137: 8, -1502138: 41, -1502139: 4, -1502140: 8, -1502141: 0, -1502142: 7, -1502143: 0, -1502144: 7, -1502145: 0, -1502146: 7, -1502147: 0, -1502148: 7, -1502149: 0, -1502150: 7, -1502151: 0, -1502152: 107, -1502153: 8, -1502154: 174, -1502155: 8, -1502156: 76, -1502157: 0, -1502158: 8, -1502159: 0, -1502160: 6, -1502161: 0, -1502162: 236, -1502163: 20, -1502164: 170, -1502165: 16, -1502166: 105, -1502167: 8, -1502168: 39, -1502169: 4, -1502170: 174, -1502171: 12, -1502172: 140, -1502173: 8, -1502174: 74, -1502175: 4, -1502176: 40, -1502177: 4, -1502178: 39, -1502179: 0, -1502180: 14, -1502181: 33, -1502182: 6, -1502183: 0, -1502184: 6, -1502185: 0, -1502186: 6, -1502187: 0, -1502188: 7, -1502189: 0, -1502190: 105, -1502191: 8, -1502192: 72, -1502193: 4, -1502194: 40, -1502195: 4, -1502196: 7, -1502197: 0, -1502198: 6, -1502199: 0, -1502200: 6, -1502201: 0, -1502202: 6, -1502203: 0, -1502204: 6, -1502205: 0, -1502206: 6, -1502207: 0, -1502208: 105, -1502209: 8, -1502210: 140, -1502211: 8, -1502212: 74, -1502213: 0, -1502214: 7, -1502215: 0, -1502216: 5, -1502217: 0, -1502218: 202, -1502219: 16, -1502220: 137, -1502221: 12, -1502222: 72, -1502223: 8, -1502224: 38, -1502225: 4, -1502226: 140, -1502227: 8, -1502228: 106, -1502229: 8, -1502230: 73, -1502231: 4, -1502232: 39, -1502233: 4, -1502234: 6, -1502235: 0, -1502236: 236, -1502237: 28, -1502238: 5, -1502239: 0, -1502240: 5, -1502241: 0, -1502242: 5, -1502243: 0, -1502244: 6, -1502245: 0, -1502246: 72, -1502247: 8, -1502248: 71, -1502249: 4, -1502250: 39, -1502251: 0, -1502252: 6, -1502253: 0, -1502254: 5, -1502255: 0, -1502256: 5, -1502257: 0, -1502258: 5, -1502259: 0, -1502260: 5, -1502261: 0, -1502262: 5, -1502263: 0, -1502264: 72, -1502265: 8, -1502266: 138, -1502267: 4, -1502268: 73, -1502269: 0, -1502270: 6, -1502271: 0, -1502272: 4, -1502273: 0, -1502274: 169, -1502275: 16, -1502276: 103, -1502277: 12, -1502278: 70, -1502279: 8, -1502280: 37, -1502281: 4, -1502282: 106, -1502283: 8, -1502284: 105, -1502285: 8, -1502286: 71, -1502287: 4, -1502288: 38, -1502289: 4, -1502290: 5, -1502291: 0, -1502292: 202, -1502293: 24, -1502294: 4, -1502295: 0, -1502296: 4, -1502297: 0, -1502298: 4, -1502299: 0, -1502300: 5, -1502301: 0, -1502302: 71, -1502303: 4, -1502304: 38, -1502305: 4, -1502306: 38, -1502307: 0, -1502308: 5, -1502309: 0, -1502310: 4, -1502311: 0, -1502312: 4, -1502313: 0, -1502314: 4, -1502315: 0, -1502316: 4, -1502317: 0, -1502318: 4, -1502319: 0, -1502320: 71, -1502321: 4, -1502322: 104, -1502323: 4, -1502324: 39, -1502325: 0, -1502326: 5, -1502327: 0, -1502328: 3, -1502329: 0, -1502330: 135, -1502331: 12, -1502332: 102, -1502333: 8, -1502334: 37, -1502335: 4, -1502336: 4, -1502337: 0, -1502338: 104, -1502339: 4, -1502340: 71, -1502341: 4, -1502342: 38, -1502343: 4, -1502344: 37, -1502345: 0, -1502346: 4, -1502347: 0, -1502348: 136, -1502349: 16, -1502350: 3, -1502351: 0, -1502352: 3, -1502353: 0, -1502354: 3, -1502355: 0, -1502356: 4, -1502357: 0, -1502358: 37, -1502359: 4, -1502360: 37, -1502361: 4, -1502362: 4, -1502363: 0, -1502364: 4, -1502365: 0, -1502366: 3, -1502367: 0, -1502368: 3, -1502369: 0, -1502370: 3, -1502371: 0, -1502372: 3, -1502373: 0, -1502374: 3, -1502375: 0, -1502376: 37, -1502377: 4, -1502378: 70, -1502379: 4, -1502380: 37, -1502381: 0, -1502382: 3, -1502383: 0, -1502384: 2, -1502385: 0, -1502386: 101, -1502387: 8, -1502388: 68, -1502389: 4, -1502390: 36, -1502391: 4, -1502392: 3, -1502393: 0, -1502394: 70, -1502395: 4, -1502396: 37, -1502397: 4, -1502398: 36, -1502399: 0, -1502400: 3, -1502401: 0, -1502402: 3, -1502403: 0, -1502404: 102, -1502405: 12, -1502406: 2, -1502407: 0, -1502408: 2, -1502409: 0, -1502410: 2, -1502411: 0, -1502412: 3, -1502413: 0, -1502414: 36, -1502415: 4, -1502416: 35, -1502417: 0, -1502418: 3, -1502419: 0, -1502420: 3, -1502421: 0, -1502422: 2, -1502423: 0, -1502424: 2, -1502425: 0, -1502426: 2, -1502427: 0, -1502428: 2, -1502429: 0, -1502430: 2, -1502431: 0, -1502432: 36, -1502433: 4, -1502434: 36, -1502435: 0, -1502436: 3, -1502437: 0, -1502438: 2, -1502439: 0, -1502440: 1, -1502441: 0, -1502442: 67, -1502443: 4, -1502444: 35, -1502445: 4, -1502446: 34, -1502447: 0, -1502448: 2, -1502449: 0, -1502450: 36, -1502451: 4, -1502452: 35, -1502453: 0, -1502454: 3, -1502455: 0, -1502456: 2, -1502457: 0, -1502458: 2, -1502459: 0, -1502460: 68, -1502461: 8, -1502462: 1, -1502463: 0, -1502464: 1, -1502465: 0, -1502466: 1, -1502467: 0, -1502468: 2, -1502469: 0, -1502470: 34, -1502471: 0, -1502472: 2, -1502473: 0, -1502474: 2, -1502475: 0, -1502476: 2, -1502477: 0, -1502478: 1, -1502479: 0, -1502480: 1, -1502481: 0, -1502482: 1, -1502483: 0, -1502484: 1, -1502485: 0, -1502486: 1, -1502487: 0, -1502488: 34, -1502489: 0, -1502490: 2, -1502491: 0, -1502492: 1, -1502493: 0, -1502494: 1, -1502495: 0, -1502496: 1, -1502497: 0, -1502498: 33, -1502499: 0, -1502500: 1, -1502501: 0, -1502502: 1, -1502503: 0, -1502504: 1, -1502505: 0, -1502506: 2, -1502507: 0, -1502508: 1, -1502509: 0, -1502510: 1, -1502511: 0, -1502512: 1, -1502513: 0, -1502514: 1, -1502515: 0, -1502516: 34, -1502517: 4, -1502518: 1, -1502519: 0, -1502520: 1, -1502521: 0, -1502522: 1, -1502523: 0, -1502524: 1, -1502525: 0, -1502526: 1, -1502527: 0, -1502528: 1, -1502529: 0, -1502530: 1, -1502531: 0, -1502532: 1, -1502533: 0, -1502534: 1, -1502535: 0, -1502536: 1, -1502537: 0, -1502538: 1, -1502539: 0, -1502540: 1, -1502541: 0, -1502542: 1, -1502543: 0, -1502544: 1, -1502545: 0, -1502546: 0, -1502547: 0, -1502548: 0, -1502549: 0, -1502550: 0, -1502551: 0, -1502552: 0, -1502553: 0, -1502554: 0, -1502555: 0, -1502556: 0, -1502557: 0, -1502558: 0, -1502559: 0, -1502560: 0, -1502561: 0, -1502562: 0, -1502563: 0, -1502564: 0, -1502565: 0, -1502566: 0, -1502567: 0, -1502568: 0, -1502569: 0, -1502570: 0, -1502571: 0, -1502572: 0, -1502573: 0, -1502574: 0, -1502575: 0, -1502576: 0, -1502577: 0, -1502578: 0, -1502579: 0, -1502580: 0, -1502581: 0, -1502582: 0, -1502583: 0, -1502584: 0, -1502585: 0, -1502586: 0, -1502587: 0, -1502588: 0, -1502589: 0, -1502590: 0, -1502591: 0, -1502592: 0, -1502593: 0, -1502594: 0, -1502595: 0, -1502596: 0, -1502597: 0, -1502598: 0, -1502599: 0, -1502600: 0, -1502601: 0, -454047: 34, -454048: 93, -454049: 99, -454050: 68, -454051: 64, -454052: 24, -454057: 1, -454058: 89, -454059: 66, -1502636: 0, -1502637: 0, -1502638: 159, -1502639: 38, -1502640: 89, -1502641: 1, -1502642: 76, -1502643: 0, -1502644: 4, -1502645: 0, -1502646: 57, -1502647: 87, -1502648: 115, -1502649: 66, -1502650: 173, -1502651: 45, -1502652: 198, -1502653: 20, -1502654: 127, -1502655: 54, -1502656: 249, -1502657: 41, -1502658: 115, -1502659: 33, -1502660: 12, -1502661: 21, -1502662: 134, -1502663: 12, -1502664: 36, -1502665: 0, -1502666: 173, -1502667: 41, -1502668: 74, -1502669: 33, -1502670: 231, -1502671: 20, -1502672: 99, -1502673: 12, -1502674: 173, -1502675: 41, -1502676: 190, -1502677: 42, -1502678: 120, -1502679: 9, -1502680: 108, -1502681: 4, -1502682: 36, -1502683: 0, -454108: 68, -454109: 0, -454110: 52, -1502687: 66, -1502688: 173, -1502689: 45, -1502690: 198, -1502691: 20, -1502692: 158, -1502693: 58, -1502694: 24, -1502695: 46, -1502696: 147, -1502697: 37, -1502698: 44, -1502699: 25, -1502700: 166, -1502701: 12, -1502702: 36, -1502703: 0, -1502704: 140, -1502705: 37, -1502706: 41, -1502707: 29, -1502708: 231, -1502709: 20, -1502710: 99, -1502711: 12, -1502712: 140, -1502713: 37, -1502714: 189, -1502715: 50, -1502716: 151, -1502717: 17, -1502718: 172, -1502719: 8, -1502720: 36, -1502721: 4, -1502722: 57, -1502723: 87, -1502724: 115, -1502725: 66, -1502726: 173, -1502727: 45, -1502728: 198, -1502729: 20, -1502730: 189, -1502731: 62, -1502732: 56, -1502733: 50, -1502734: 178, -1502735: 37, -1502736: 76, -1502737: 25, -1502738: 199, -1502739: 16, -1502740: 35, -1502741: 0, -1502742: 107, -1502743: 37, -1502744: 41, -1502745: 29, -1502746: 198, -1502747: 16, -1502748: 99, -1502749: 12, -1502750: 107, -1502751: 37, -1502752: 220, -1502753: 54, -1502754: 182, -1502755: 25, -1502756: 204, -1502757: 12, -1502758: 68, -1502759: 4, -1502760: 57, -1502761: 87, -1502762: 115, -1502763: 66, -1502764: 173, -1502765: 45, -1502766: 198, -1502767: 20, -1502768: 220, -1502769: 66, -1502770: 87, -1502771: 54, -1502772: 210, -1502773: 41, -1502774: 108, -1502775: 29, -1502776: 231, -1502777: 16, -1502778: 35, -1502779: 0, -1502780: 74, -1502781: 33, -1502782: 8, -1502783: 25, -1502784: 198, -1502785: 16, -1502786: 99, -1502787: 12, -1502788: 74, -1502789: 33, -1502790: 251, -1502791: 62, -1502792: 245, -1502793: 33, -1502794: 11, -1502795: 21, -1502796: 101, -1502797: 8, -1502798: 57, -1502799: 87, -1502800: 115, -1502801: 66, -1502802: 173, -1502803: 45, -1502804: 198, -1502805: 20, -1502806: 219, -1502807: 66, -1502808: 86, -1502809: 54, -1502810: 210, -1502811: 45, -1502812: 109, -1502813: 33, -1502814: 232, -1502815: 20, -1502816: 35, -1502817: 0, -1502818: 74, -1502819: 29, -1502820: 231, -1502821: 24, -1502822: 165, -1502823: 16, -1502824: 66, -1502825: 8, -1502826: 74, -1502827: 29, -1502828: 26, -1502829: 67, -1502830: 20, -1502831: 42, -1502832: 43, -1502833: 25, -1502834: 133, -1502835: 8, -1502836: 57, -1502837: 87, -1502838: 115, -1502839: 66, -1502840: 173, -1502841: 45, -1502842: 198, -1502843: 20, -1502844: 250, -1502845: 70, -1502846: 117, -1502847: 58, -1502848: 17, -1502849: 50, -1502850: 141, -1502851: 37, -1502852: 8, -1502853: 21, -1502854: 35, -1502855: 0, -1502856: 41, -1502857: 25, -1502858: 198, -1502859: 20, -1502860: 165, -1502861: 16, -1502862: 66, -1502863: 8, -1502864: 41, -1502865: 25, -1502866: 25, -1502867: 75, -1502868: 51, -1502869: 50, -1502870: 107, -1502871: 29, -1502872: 133, -1502873: 12, -1502874: 57, -1502875: 87, -1502876: 115, -1502877: 66, -1502878: 173, -1502879: 45, -1502880: 198, -1502881: 20, -1502882: 25, -1502883: 75, -1502884: 149, -1502885: 62, -1502886: 17, -1502887: 50, -1502888: 173, -1502889: 37, -1502890: 41, -1502891: 25, -1502892: 34, -1502893: 0, -1502894: 8, -1502895: 25, -1502896: 198, -1502897: 20, -1502898: 132, -1502899: 12, -1502900: 66, -1502901: 8, -1502902: 8, -1502903: 25, -1502904: 56, -1502905: 79, -1502906: 82, -1502907: 58, -1502908: 139, -1502909: 33, -1502910: 165, -1502911: 12, -1502912: 57, -1502913: 87, -1502914: 115, -1502915: 66, -1502916: 173, -1502917: 45, -1502918: 198, -1502919: 20, -1502920: 56, -1502921: 79, -1502922: 180, -1502923: 66, -1502924: 49, -1502925: 54, -1502926: 205, -1502927: 41, -1502928: 73, -1502929: 25, -1502930: 34, -1502931: 0, -1502932: 231, -1502933: 20, -1502934: 165, -1502935: 16, -1502936: 132, -1502937: 12, -1502938: 66, -1502939: 8, -1502940: 231, -1502941: 20, -454366: 76, -454367: 15, -454368: 40, -454369: 9, -454370: 36, -454371: 7, -454372: 28, -454373: 5, -454374: 20, -454375: 3, -454376: 12, -454377: 2, -454378: 8, -454379: 1, -454380: 4, -454385: 22, -454386: 72, -454387: 14, -454388: 36, -454389: 8, -454390: 32, -454391: 6, -454392: 24, -454393: 4, -454394: 16, -454395: 2, -454396: 8, -454397: 1, -454398: 4, -454399: 0, -454400: 0, -454405: 21, -454406: 68, -454407: 13, -454408: 32, -454409: 7, -454410: 28, -454411: 5, -454412: 20, -454413: 3, -454414: 12, -454415: 1, -454416: 4, -454417: 0, -454418: 0, -454419: 0, -454420: 0, -454425: 20, -454426: 64, -454427: 12, -454428: 28, -454429: 6, -454430: 24, -454431: 4, -454432: 16, -454433: 2, -454434: 8, -454435: 0, -454436: 0, -454437: 0, -454438: 0, -454439: 0, -454440: 0, -454445: 19, -454446: 60, -454447: 11, -454448: 24, -454449: 5, -454450: 20, -454451: 3, -454452: 12, -454453: 1, -454454: 4, -454455: 0, -454456: 0, -454457: 0, -454458: 0, -454459: 0, -454460: 0, -454465: 18, -454466: 56, -454467: 10, -454468: 20, -454469: 4, -454470: 16, -454471: 2, -454472: 8, -454473: 0, -454474: 0, -454475: 0, -454476: 0, -454477: 0, -454478: 0, -454479: 0, -454480: 0, -454485: 17, -454486: 52, -454487: 9, -454488: 16, -454489: 3, -454490: 12, -454491: 1, -454492: 4, -454493: 0, -454494: 0, -454495: 0, -454496: 0, -454497: 0, -454498: 0, -454499: 0, -454500: 0, -454505: 16, -454506: 48, -454507: 8, -454508: 12, -454509: 2, -454510: 8, -454511: 0, -454512: 0, -454513: 0, -454514: 0, -454515: 0, -454516: 0, -454517: 0, -454518: 0, -454519: 0, -454520: 0, -454525: 17, -454526: 52, -454527: 9, -454528: 16, -454529: 3, -454530: 12, -454531: 1, -454532: 4, -454533: 0, -454534: 0, -454535: 0, -454536: 0, -454537: 0, -454538: 0, -454539: 0, -454540: 0, -454545: 18, -454546: 56, -454547: 10, -454548: 20, -454549: 4, -454550: 16, -1503127: 0, -1503128: 0, -1503129: 117, -1503130: 44, -1503131: 111, -1503132: 44, -1503133: 41, -1503134: 40, -1503135: 35, -1503136: 40, -1503137: 49, -1503138: 98, -1503139: 173, -1503140: 85, -1503141: 41, -1503142: 73, -1503143: 132, -1503144: 56, -1503145: 181, -1503146: 77, -1503147: 81, -1503148: 69, -1503149: 237, -1503150: 64, -1503151: 168, -1503152: 56, -1503153: 100, -1503154: 48, -1503155: 181, -1503156: 126, -1503157: 0, -1503158: 40, -1503159: 34, -1503160: 20, -1503161: 41, -1503162: 49, -1503163: 231, -1503164: 44, -1503165: 165, -1503166: 36, -1503167: 66, -1503168: 28, -1503169: 107, -1503170: 65, -1503171: 213, -1503172: 48, -1503173: 175, -1503174: 44, -1503175: 105, -1503176: 40, -1503177: 67, -1503178: 36, -1503179: 82, -1503180: 98, -1503181: 206, -1503182: 81, -1503183: 74, -1503184: 69, -1503185: 132, -1503186: 52, -1503187: 245, -1503188: 77, -1503189: 145, -1503190: 69, -1503191: 14, -1503192: 65, -1503193: 201, -1503194: 52, -1503195: 133, -1503196: 44, -1503197: 214, -1503198: 126, -1503199: 0, -1503200: 36, -1503201: 34, -1503202: 16, -1503203: 41, -1503204: 45, -1503205: 231, -1503206: 40, -1503207: 165, -1503208: 32, -1503209: 66, -1503210: 24, -1503211: 74, -1503212: 57, -1503213: 54, -1503214: 53, -1503215: 240, -1503216: 48, -1503217: 138, -1503218: 36, -1503219: 68, -1503220: 32, -1503221: 115, -1503222: 94, -1503223: 239, -1503224: 81, -1503225: 74, -1503226: 65, -1503227: 165, -1503228: 44, -1503229: 22, -1503230: 78, -1503231: 178, -1503232: 69, -1503233: 78, -1503234: 61, -1503235: 9, -1503236: 53, -1503237: 165, -1503238: 40, -1503239: 24, -1503240: 127, -1503241: 0, -1503242: 28, -1503243: 34, -1503244: 16, -1503245: 8, -1503246: 41, -1503247: 198, -1503248: 36, -1503249: 165, -1503250: 28, -1503251: 66, -1503252: 24, -1503253: 74, -1503254: 53, -1503255: 150, -1503256: 57, -1503257: 48, -1503258: 49, -1503259: 202, -1503260: 36, -1503261: 100, -1503262: 28, -1503263: 148, -1503264: 94, -1503265: 16, -1503266: 78, -1503267: 107, -1503268: 61, -1503269: 165, -1503270: 40, -1503271: 86, -1503272: 78, -1503273: 242, -1503274: 69, -1503275: 111, -1503276: 61, -1503277: 42, -1503278: 49, -1503279: 198, -1503280: 36, -1503281: 57, -1503282: 127, -1503283: 0, -1503284: 24, -1503285: 34, -1503286: 12, -1503287: 8, -1503288: 37, -1503289: 198, -1503290: 32, -1503291: 165, -1503292: 24, -1503293: 66, -1503294: 20, -1503295: 41, -1503296: 45, -1503297: 23, -1503298: 66, -1503299: 145, -1503300: 53, -1503301: 234, -1503302: 36, -1503303: 100, -1503304: 24, -1503305: 214, -1503306: 90, -1503307: 16, -1503308: 74, -1503309: 107, -1503310: 57, -1503311: 165, -1503312: 36, -1503313: 151, -1503314: 78, -1503315: 19, -1503316: 66, -1503317: 175, -1503318: 57, -1503319: 75, -1503320: 49, -1503321: 231, -1503322: 36, -1503323: 123, -1503324: 127, -1503325: 0, -1503326: 16, -1503327: 34, -1503328: 8, -1503329: 8, -454754: 84, -1503331: 198, -1503332: 28, -1503333: 132, -454758: 11, -1503335: 66, -1503336: 16, -1503337: 41, -1503338: 41, -1503339: 119, -1503340: 70, -1503341: 209, -1503342: 53, -1503343: 42, -1503344: 37, -1503345: 132, -1503346: 20, -1503347: 247, -1503348: 90, -1503349: 49, -1503350: 70, -1503351: 140, -1503352: 53, -1503353: 165, -1503354: 32, -1503355: 215, -1503356: 78, -1503357: 83, -1503358: 66, -1503359: 208, -1503360: 57, -1503361: 108, -1503362: 45, -1503363: 8, -1503364: 33, -1503365: 156, -1503366: 127, -1503367: 0, -1503368: 12, -1503369: 34, -1503370: 4, -1503371: 8, -1503372: 29, -1503373: 198, -1503374: 24, -1503375: 132, -1503376: 20, -1503377: 66, -1503378: 12, -1503379: 8, -1503380: 33, -1503381: 216, -1503382: 74, -1503383: 18, -1503384: 58, -1503385: 75, -1503386: 33, -1503387: 133, -1503388: 16, -1503389: 24, -1503390: 87, -1503391: 82, -1503392: 70, -1503393: 140, -1503394: 49, -1503395: 198, -1503396: 24, -1503397: 248, -1503398: 78, -1503399: 116, -1503400: 66, -1503401: 16, -1503402: 54, -1503403: 172, -1503404: 45, -1503405: 40, -1503406: 29, -1503407: 222, -1503408: 127, -1503409: 0, -1503410: 4, -1503411: 34, -1503412: 4, -1503413: 231, -1503414: 24, -1503415: 165, -1503416: 20, -1503417: 132, -1503418: 16, -1503419: 66, -1503420: 12, -1503421: 8, -1503422: 29, -1503423: 56, -1503424: 79, -1503425: 82, -1503426: 58, -1503427: 139, -1503428: 33, -1503429: 165, -1503430: 12, -1503431: 57, -1503432: 87, -1503433: 115, -1503434: 66, -1503435: 173, -1503436: 45, -1503437: 198, -1503438: 20, -1503439: 56, -1503440: 79, -1503441: 180, -1503442: 66, -1503443: 49, -1503444: 54, -1503445: 205, -1503446: 41, -1503447: 73, -1503448: 25, -1503449: 255, -1503450: 127, -1503451: 0, -1503452: 0, -1503453: 34, -1503454: 0, -1503455: 231, -1503456: 20, -1503457: 165, -1503458: 16, -1503459: 132, -1503460: 12, -1503461: 66, -1503462: 8, -1503463: 231, -1503464: 20, -1503511: 0, -1503512: 0, -1503513: 159, -1503514: 21, -1503515: 219, -1503516: 0, -1503517: 51, -1503518: 0, -1503519: 14, -1503520: 0, -1503521: 251, -1503522: 49, -1503523: 119, -1503524: 37, -1503525: 244, -1503526: 24, -1503527: 112, -1503528: 12, -1503529: 127, -1503530: 29, -1503531: 59, -1503532: 25, -1503533: 215, -1503534: 16, -1503535: 147, -1503536: 12, -1503537: 80, -1503538: 4, -1503539: 95, -1503540: 74, -1503541: 12, -1503542: 0, -1503543: 222, -1503544: 29, -1503545: 26, -1503546: 9, -1503547: 114, -1503548: 4, -1503549: 45, -1503550: 0, -1503551: 27, -1503552: 54, -1503553: 150, -1503554: 41, -1503555: 19, -1503556: 29, -1503557: 111, -1503558: 12, -1503559: 190, -1503560: 37, -1503561: 122, -1503562: 29, -1503563: 22, -1503564: 21, -1503565: 178, -1503566: 16, -1503567: 111, -1503568: 8, -2175588: 27, -1503570: 82, -1503571: 10, -1503572: 0, -1503573: 29, -1503574: 38, -1503575: 56, -1503576: 17, -1503577: 145, -1503578: 8, -1503579: 43, -1503580: 4, -1503581: 90, -1503582: 62, -1503583: 182, -1503584: 45, -1503585: 50, -1503586: 29, -1503587: 141, -1503588: 16, -1503589: 253, -1503590: 41, -1503591: 153, -1503592: 37, -1503593: 53, -1503594: 29, -1503595: 241, -1503596: 20, -1503597: 142, -1503598: 8, -2175589: 0, -1503600: 90, -1503601: 9, -1503602: 0, -1503603: 92, -1503604: 46, -1503605: 119, -1503606: 25, -1503607: 208, -1503608: 12, -1503609: 74, -1503610: 4, -1503611: 122, -1503612: 66, -1503613: 213, -1503614: 49, -1503615: 81, -1503616: 33, -1503617: 140, -1503618: 16, -1503619: 60, -1503620: 50, -1503621: 216, -1503622: 41, -1503623: 116, -1503624: 33, -1503625: 16, -1503626: 25, -1503627: 173, -1503628: 12, -2175590: 0, -1503630: 99, -1503631: 7, -1503632: 0, -1503633: 123, -1503634: 54, -1503635: 182, -1503636: 33, -1503637: 238, -1503638: 20, -1503639: 105, -1503640: 8, -1503641: 186, -1503642: 70, -1503643: 21, -1503644: 54, -1503645: 80, -1503646: 37, -1503647: 170, -1503648: 16, -1503649: 123, -1503650: 58, -1503651: 23, -1503652: 50, -1503653: 148, -1503654: 37, -1503655: 80, -1503656: 29, -1503657: 236, -1503658: 16, -2175591: 223, -1503660: 103, -1503661: 5, -1503662: 0, -1503663: 186, -1503664: 62, -1503665: 245, -1503666: 41, -1503667: 45, -1503668: 25, -1503669: 136, -1503670: 8, -1503671: 218, -1503672: 74, -1503673: 52, -1503674: 58, -1503675: 111, -1503676: 41, -1503677: 169, -1503678: 16, -1503679: 186, -1503680: 66, -1503681: 86, -1503682: 54, -1503683: 211, -1503684: 41, -1503685: 111, -1503686: 33, -1503687: 11, -1503688: 21, -2175592: 2, -1503690: 111, -1503691: 3, -1503692: 0, -1503693: 249, -1503694: 70, -1503695: 19, -1503696: 50, -1503697: 76, -1503698: 29, -1503699: 134, -1503700: 12, -1503701: 25, -1503702: 83, -1503703: 84, -1503704: 62, -1503705: 142, -1503706: 41, -1503707: 199, -1503708: 20, -1503709: 249, -1503710: 70, -1503711: 117, -1503712: 62, -1503713: 242, -1503714: 49, -1503715: 174, -1503716: 37, -1503717: 42, -1503718: 21, -2175593: 215, -1503720: 119, -1503721: 2, -1503722: 0, -1503723: 56, -1503724: 79, -1503725: 82, -1503726: 58, -1503727: 139, -1503728: 33, -1503729: 165, -1503730: 12, -1503731: 57, -1503732: 87, -1503733: 115, -1503734: 66, -1503735: 173, -1503736: 45, -1503737: 198, -1503738: 20, -1503739: 56, -1503740: 79, -1503741: 180, -1503742: 66, -1503743: 49, -1503744: 54, -1503745: 205, -1503746: 41, -1503747: 73, -1503748: 25, -2175594: 1, -1503750: 127, -1503751: 0, -1503752: 0, -2175596: 0, -1503873: 0, -1503874: 0, -1503875: 178, -1503876: 114, -1503877: 199, -1503878: 113, -1503879: 99, -1503880: 68, -1503881: 22, -1503882: 74, -1503883: 145, -1503884: 57, -1503885: 44, -1503886: 45, -1503887: 167, -1503888: 28, -1503889: 229, -1503890: 32, -1503891: 164, -1503892: 24, -1503893: 131, -1503894: 16, -1503895: 65, -1503896: 8, -1503897: 0, -1503898: 0, -1503899: 0, -1503900: 0, -1503901: 223, -1503902: 2, -1503903: 24, -1503904: 99, -1503905: 24, -1503906: 99, -1503907: 2, -1503908: 8, -1503909: 148, -1503910: 82, -1503911: 206, -1503912: 57, -1503913: 8, -1503914: 33, -1503915: 132, -1503916: 16, -1503917: 25, -1503918: 0, -1503919: 18, -1503920: 0, -1503921: 0, -1503922: 92, -1503923: 0, -1503924: 64, -1503925: 132, -1503926: 16, -1503927: 127, -1503928: 25, -1503929: 255, -1503930: 127, -1503931: 79, -1503932: 98, -1503933: 134, -1503934: 97, -1503935: 66, -1503936: 56, -1503937: 178, -1503938: 61, -1503939: 78, -1503940: 49, -1503941: 234, -1503942: 36, -1503943: 134, -1503944: 24, -1503945: 196, -1503946: 24, -1503947: 131, -1503948: 20, -1503949: 98, -1503950: 12, -1503951: 32, -1503952: 4, -1503953: 0, -1503954: 0, -1503955: 0, -1503956: 0, -1503957: 90, -1503958: 2, -1503959: 148, -1503960: 82, -1503961: 148, -1503962: 82, -1503963: 1, -1503964: 4, -1503965: 49, -1503966: 70, -1503967: 140, -1503968: 49, -1503969: 198, -1503970: 24, -1503971: 99, -1503972: 12, -1503973: 21, -1503974: 0, -1503975: 15, -1503976: 0, -1503977: 0, -1503978: 76, -1503979: 0, -1503980: 52, -1503981: 99, -1503982: 12, -1503983: 58, -1503984: 21, -1503985: 90, -1503986: 107, -1503987: 236, -1503988: 81, -1503989: 69, -1503990: 81, -1503991: 66, -1503992: 48, -1503993: 111, -1503994: 49, -1503995: 12, -1503996: 41, -1503997: 200, -1503998: 28, -1503999: 101, -1504000: 20, -1504001: 163, -1504002: 20, -1504003: 98, -1504004: 16, -1504005: 66, -1504006: 8, -1504007: 32, -1504008: 4, -1504009: 0, -1504010: 0, -1504011: 0, -1504012: 0, -1504013: 246, -1504014: 1, -1504015: 49, -1504016: 70, -1504017: 49, -1504018: 70, -1504019: 1, -1504020: 4, -1504021: 206, -1504022: 57, -1504023: 74, -1504024: 41, -1504025: 165, -1504026: 20, -1504027: 66, -1504028: 8, -1504029: 17, -1504030: 0, -1504031: 12, -1504032: 0, -1504033: 0, -1504034: 64, -1504035: 0, -1504036: 44, -1504037: 66, -1504038: 8, -1504039: 246, -1504040: 16, -1504041: 214, -1504042: 90, -1504043: 138, -1504044: 65, -1504045: 4, -1504046: 65, -1504047: 33, -1504048: 36, -1504049: 44, -1504050: 41, -1504051: 201, -1504052: 32, -1504053: 166, -1504054: 24, -1504055: 68, -1504056: 16, -1504057: 130, -1504058: 16, -1504059: 66, -1504060: 12, -1504061: 65, -1504062: 8, -1504063: 32, -1504064: 4, -1504065: 0, -1504066: 0, -1504067: 0, -1504068: 0, -1504069: 145, -1504070: 1, -1504071: 173, -1504072: 53, -1504073: 173, -1504074: 53, -1504075: 1, -1504076: 4, -1504077: 107, -1504078: 45, -1504079: 8, -1504080: 33, -1504081: 132, -1504082: 16, -1504083: 66, -1504084: 8, -1504085: 14, -1504086: 0, -1504087: 10, -1504088: 0, -1504089: 0, -1504090: 52, -1504091: 0, -1504092: 36, -1504093: 66, -1504094: 8, -1504095: 209, -1504096: 12, -1504097: 49, -1504098: 70, -1504099: 39, -1504100: 49, -1504101: 195, -1504102: 48, -1504103: 33, -1504104: 28, -1504105: 201, -1504106: 28, -1504107: 167, -1504108: 24, -1504109: 101, -1504110: 16, -1504111: 67, -1504112: 12, -1504113: 98, -1504114: 12, -1504115: 65, -1504116: 8, -1504117: 33, -1504118: 4, -1504119: 0, -1504120: 0, -1504121: 0, -1504122: 0, -1504123: 0, -1504124: 0, -1504125: 45, -1504126: 1, -1504127: 74, -1504128: 41, -1504129: 74, -1504130: 41, -1504131: 0, -1504132: 0, -1504133: 8, -1504134: 33, -1504135: 198, -1504136: 24, -1504137: 99, -1504138: 12, -1504139: 33, -1504140: 4, -1504141: 10, -1504142: 0, -1504143: 7, -1504144: 0, -1504145: 0, -1504146: 36, -1504147: 0, -1504148: 24, -1504149: 33, -1504150: 4, -1504151: 141, -1504152: 8, -1504153: 173, -1504154: 53, -1504155: 197, -1504156: 32, -1504157: 130, -1504158: 32, -1504159: 0, -1504160: 16, -1504161: 134, -1504162: 20, -1504163: 100, -1504164: 16, -1504165: 67, -1504166: 12, -1504167: 34, -1504168: 8, -1504169: 65, -1504170: 8, -1504171: 33, -1504172: 4, -1504173: 32, -1504174: 4, -1504175: 0, -1504176: 0, -1504177: 0, -1504178: 0, -1504179: 0, -1504180: 0, -1504181: 200, -1504182: 0, -1504183: 198, -1504184: 24, -1504185: 198, -1504186: 24, -1504187: 0, -1504188: 0, -1504189: 165, -1504190: 20, -1504191: 132, -1504192: 16, -1504193: 66, -1504194: 8, -1504195: 33, -1504196: 4, -1504197: 7, -1504198: 0, -1504199: 5, -1504200: 0, -1504201: 0, -1504202: 24, -1504203: 0, -1504204: 16, -1504205: 33, -1504206: 4, -1504207: 104, -1504208: 4, -1504209: 8, -1504210: 33, -1504211: 98, -1504212: 16, -1504213: 65, -1504214: 16, -1504215: 0, -1504216: 8, -1504217: 67, -1504218: 8, -1504219: 34, -1504220: 8, -1504221: 33, -1504222: 4, -1504223: 1, -1504224: 4, -1504225: 32, -1504226: 4, -1504227: 0, -1504228: 0, -1504229: 0, -1504230: 0, -1504231: 0, -1504232: 0, -1504233: 0, -1504234: 0, -1504235: 0, -1504236: 0, -1504237: 100, -1504238: 0, -1504239: 99, -1504240: 12, -1504241: 99, -1504242: 12, -1504243: 0, -1504244: 0, -1504245: 66, -1504246: 8, -1504247: 66, -1504248: 8, -1504249: 33, -1504250: 4, -1504251: 0, -1504252: 0, -1504253: 3, -1504254: 0, -1504255: 2, -1504256: 0, -1504257: 0, -1504258: 12, -1504259: 0, -1504260: 8, -1504261: 0, -1504262: 0, -1504263: 36, -1504264: 0, -1504265: 132, -1504266: 16, -892443: 99, -892449: 56, -455919: 217, -455920: 62, -455921: 87, -455922: 46, -455923: 53, -455924: 42, -455925: 243, -455926: 37, -455927: 210, -455928: 37, -455929: 176, -455930: 29, -455931: 110, -455932: 25, -455933: 46, -455934: 17, -455939: 87, -455940: 46, -455941: 53, -455942: 42, -455943: 243, -455944: 37, -455945: 217, -455946: 62, -455947: 176, -455948: 29, -455949: 110, -455950: 25, -455951: 46, -455952: 17, -455953: 210, -455954: 37, -455959: 53, -455960: 42, -455961: 243, -455962: 37, -455963: 217, -455964: 62, -455965: 87, -455966: 46, -455967: 110, -455968: 25, -455969: 46, -455970: 17, -455971: 210, -455972: 37, -455973: 176, -455974: 29, -455979: 243, -455980: 37, -455981: 217, -455982: 62, -455983: 87, -455984: 46, -455985: 53, -455986: 42, -455987: 46, -455988: 17, -455989: 210, -455990: 37, -455991: 176, -455992: 29, -455993: 110, -455994: 25, -456007: 210, -456008: 37, -456009: 176, -456010: 29, -456011: 110, -456012: 25, -456013: 46, -456014: 17, -456019: 176, -456020: 29, -456021: 110, -456022: 25, -456023: 46, -456024: 17, -456025: 210, -456026: 37, -456031: 110, -456032: 25, -456033: 46, -456034: 17, -456035: 210, -456036: 37, -456037: 176, -456038: 29, -456043: 46, -456044: 17, -456045: 210, -456046: 37, -456047: 176, -456048: 29, -456049: 110, -456050: 25, -456063: 0, -456064: 4, -456065: 34, -456066: 12, -456067: 100, -456068: 24, -456069: 134, -456070: 32, -456071: 201, -456072: 44, -456073: 101, -456074: 28, -456075: 67, -456076: 16, -456077: 33, -456078: 8, -456083: 34, -456084: 12, -456085: 100, -456086: 24, -456087: 134, -456088: 32, -456089: 201, -456090: 44, -456091: 101, -456092: 28, -456093: 67, -456094: 16, -456095: 33, -456096: 8, -456097: 0, -456098: 4, -456103: 100, -456104: 24, -456105: 134, -456106: 32, -456107: 201, -456108: 44, -456109: 101, -456110: 28, -456111: 67, -456112: 16, -456113: 33, -456114: 8, -456115: 0, -456116: 4, -456117: 34, -456118: 12, -456123: 134, -456124: 32, -456125: 201, -456126: 44, -456127: 101, -456128: 28, -456129: 67, -456130: 16, -456131: 33, -456132: 8, -456133: 0, -456134: 4, -456135: 34, -456136: 12, -456137: 100, -456138: 24, -456143: 201, -456144: 44, -456145: 101, -456146: 28, -456147: 67, -456148: 16, -456149: 33, -456150: 8, -456151: 0, -456152: 4, -456153: 34, -456154: 12, -456155: 100, -456156: 24, -456157: 134, -456158: 32, -456163: 101, -456164: 28, -456165: 67, -456166: 16, -456167: 33, -456168: 8, -456169: 0, -456170: 4, -456171: 34, -456172: 12, -456173: 100, -456174: 24, -456175: 134, -456176: 32, -456177: 201, -456178: 44, -456183: 67, -456184: 16, -456185: 33, -456186: 8, -456187: 0, -456188: 4, -456189: 34, -456190: 12, -456191: 100, -456192: 24, -456193: 134, -456194: 32, -456195: 201, -456196: 44, -456197: 101, -456198: 28, -456203: 33, -456204: 8, -456205: 0, -456206: 4, -456207: 34, -456208: 12, -456209: 100, -456210: 24, -456211: 134, -456212: 32, -456213: 201, -456214: 44, -456215: 101, -456216: 28, -456217: 67, -456218: 16, -892461: 87, -456256: 25, -456257: 0, -456258: 18, -456259: 0, -456260: 0, -456261: 92, -456262: 0, -456263: 64, -456264: 132, -456265: 16, -456266: 127, -456267: 25, -456268: 255, -456269: 127, -456278: 22, -456279: 0, -456280: 15, -456281: 0, -456282: 0, -456283: 80, -456284: 0, -456285: 52, -456286: 132, -456287: 16, -456288: 28, -456289: 13, -456290: 156, -456291: 115, -456300: 19, -456301: 0, -456302: 12, -456303: 0, -456304: 0, -456305: 68, -456306: 0, -456307: 40, -456308: 132, -456309: 16, -456310: 185, -456311: 0, -456312: 57, -456313: 103, -456322: 16, -456323: 0, -456324: 9, -456325: 0, -456326: 0, -456327: 56, -456328: 0, -456329: 28, -456330: 132, -456331: 16, -456332: 86, -456333: 0, -456334: 214, -456335: 90, -456344: 13, -456345: 0, -456346: 6, -456347: 0, -456348: 0, -456349: 44, -456350: 0, -456351: 16, -456352: 132, -456353: 16, -456354: 19, -456355: 0, -456356: 115, -456357: 78, -456366: 10, -456367: 0, -456368: 3, -456369: 0, -456370: 0, -456371: 32, -456372: 0, -456373: 4, -456374: 132, -456375: 16, -456376: 16, -456377: 0, -456378: 16, -456379: 66, -456388: 10, -456389: 0, -456390: 3, -456391: 0, -456392: 0, -456393: 32, -456394: 0, -456395: 4, -456396: 132, -456397: 16, -456398: 16, -456399: 0, -456400: 16, -1373905: 255, -1373906: 114, -1373907: 223, -1373908: 44, -1373909: 185, -1373910: 36, -1373911: 175, -1373912: 28, -1373913: 95, -1373914: 94, -1373915: 63, -1373916: 24, -1373917: 20, -1373918: 16, -1373919: 10, -1373920: 8, -1373921: 186, -1373922: 73, -1373923: 26, -1373924: 4, -1373925: 15, -1373926: 0, -1373927: 5, -1373928: 0, -1373929: 21, -1373930: 53, -1373931: 21, -1373932: 0, -1373933: 10, -1373934: 0, -1373935: 5, -456432: 16, -456433: 0, -1373938: 53, -456435: 0, -1373940: 0, -1373941: 10, -1373942: 0, -1373943: 5, -1373944: 0, -1373945: 186, -1373946: 73, -1373947: 26, -1373948: 4, -1373949: 15, -1373950: 0, -1373951: 5, -1373952: 0, -1373953: 95, -1373954: 94, -1373955: 63, -1373956: 24, -1373957: 20, -1373958: 16, -1373959: 10, -1373960: 8, -1373961: 255, -1373962: 114, -1373963: 223, -1373964: 44, -1373965: 185, -1373966: 36, -1373967: 175, -1373968: 28, -1373969: 144, -1373970: 89, -1373971: 112, -1373972: 56, -1373973: 109, -1373974: 52, -1373975: 104, -1373976: 48, -1373977: 77, -1373978: 73, -1373979: 109, -1373980: 48, -1373981: 74, -1373982: 40, -1373983: 70, -1373984: 36, -1373985: 11, -1373986: 61, -1373987: 75, -1373988: 36, -1373989: 72, -1373990: 32, -1373991: 68, -1373992: 24, -1373993: 200, -1373994: 44, -1373995: 72, -1373996: 28, -1373997: 37, -1373998: 20, -1373999: 34, -1374000: 12, -1374001: 200, -1374002: 44, -1374003: 72, -1374004: 28, -1374005: 37, -1374006: 20, -1374007: 34, -1374008: 12, -1374009: 11, -1374010: 61, -1374011: 75, -1374012: 36, -1374013: 72, -1374014: 32, -1374015: 68, -1374016: 24, -1374017: 77, -1374018: 73, -1374019: 109, -1374020: 48, -1374021: 74, -1374022: 40, -1374023: 70, -1374024: 36, -1374025: 144, -1374026: 89, -1374027: 112, -1374028: 56, -1374029: 109, -1374030: 52, -1374031: 104, -1374032: 48, -456623: 148, -456624: 82, -456625: 16, -456626: 66, -456627: 140, -456628: 49, -456629: 8, -456630: 33, -456631: 132, -456632: 16, -456633: 255, -456634: 127, -456639: 117, -456640: 78, -456641: 241, -456642: 61, -456643: 109, -456644: 45, -456645: 232, -456646: 28, -456647: 100, -456648: 12, -456649: 191, -456650: 119, -456655: 85, -456656: 74, -456657: 209, -456658: 57, -456659: 109, -456660: 45, -456661: 232, -456662: 28, -456663: 100, -456664: 12, -456665: 159, -456666: 115, -456671: 54, -456672: 70, -456673: 178, -456674: 53, -456675: 77, -456676: 41, -456677: 201, -456678: 24, -456679: 68, -456680: 8, -456681: 95, -456682: 107, -456687: 246, -456688: 61, -456689: 146, -456690: 49, -456691: 45, -456692: 37, -456693: 169, -456694: 20, -456695: 68, -456696: 8, -456697: 63, -456698: 103, -456703: 215, -456704: 57, -456705: 114, -456706: 45, -456707: 14, -456708: 33, -456709: 137, -456710: 16, -456711: 36, -456712: 4, -456713: 255, -456714: 94, -456719: 183, -456720: 53, -456721: 82, -456722: 41, -456723: 238, -456724: 28, -456725: 137, -456726: 16, -456727: 36, -456728: 4, -456729: 223, -456730: 90, -456735: 152, -456736: 49, -456737: 51, -456738: 37, -456739: 206, -456740: 24, -456741: 105, -456742: 12, -456743: 4, -456744: 0, -456745: 159, -456746: 82, -456751: 183, -456752: 53, -456753: 82, -456754: 41, -456755: 238, -456756: 28, -456757: 137, -456758: 16, -456759: 36, -456760: 4, -456761: 223, -456762: 90, -456767: 215, -456768: 57, -456769: 114, -456770: 45, -456771: 14, -456772: 33, -456773: 137, -456774: 16, -456775: 36, -456776: 4, -456777: 255, -456778: 94, -456783: 246, -456784: 61, -456785: 146, -456786: 49, -456787: 45, -456788: 37, -456789: 169, -456790: 20, -456791: 68, -456792: 8, -456793: 63, -456794: 103, -456799: 54, -456800: 70, -456801: 178, -456802: 53, -456803: 77, -456804: 41, -456805: 201, -456806: 24, -456807: 68, -456808: 8, -456809: 95, -456810: 107, -456815: 85, -456816: 74, -456817: 209, -456818: 57, -456819: 109, -456820: 45, -456821: 232, -456822: 28, -456823: 100, -456824: 12, -456825: 159, -456826: 115, -456831: 117, -456832: 78, -456833: 241, -456834: 61, -456835: 109, -456836: 45, -456837: 232, -456838: 28, -456839: 100, -456840: 12, -456841: 191, -456842: 119, -456855: 26, -456856: 8, -456857: 18, -456858: 8, -456859: 43, -456860: 4, -456861: 35, -456862: 4, -456867: 55, -456868: 12, -456869: 48, -456870: 12, -456871: 42, -456872: 4, -456873: 35, -456874: 4, -456879: 84, -456880: 16, -456881: 46, -456882: 12, -456883: 73, -456884: 8, -456885: 34, -456886: 4, -456891: 113, -456892: 20, -456893: 76, -456894: 16, -456895: 72, -456896: 8, -456897: 34, -456898: 4, -456903: 142, -456904: 20, -456905: 106, -456906: 16, -456907: 102, -456908: 12, -456909: 66, -456910: 8, -456915: 171, -456916: 24, -456917: 136, -456918: 20, -456919: 101, -456920: 12, -456921: 66, -456922: 8, -456927: 200, -456928: 28, -456929: 134, -456930: 20, -456931: 132, -456932: 16, -456933: 65, -1374438: 0, -1374439: 56, -1374440: 184, -1374441: 87, -1374442: 17, -1374443: 11, -1374444: 70, -1374445: 22, -1374446: 227, -1374447: 0, -1374448: 255, -1374449: 114, -1374450: 223, -1374451: 44, -1374452: 185, -1374453: 36, -1374454: 175, -1374455: 28, -1374456: 169, -1374457: 24, -1374458: 159, -1374459: 79, -1374460: 216, -1374461: 62, -456958: 8, -1374463: 46, -1374464: 205, -1374465: 8, -1374466: 255, -1374467: 127, -1374468: 0, -1374469: 0, -456966: 20, -456967: 101, -456968: 12, -456969: 66, -456970: 8, -456975: 142, -456976: 20, -456977: 106, -456978: 16, -456979: 102, -456980: 12, -456981: 66, -456982: 8, -456987: 113, -456988: 20, -456989: 76, -456990: 16, -456991: 72, -456992: 8, -456993: 34, -456994: 4, -456999: 84, -457000: 16, -457001: 46, -457002: 12, -457003: 73, -457004: 8, -457005: 34, -457006: 4, -457011: 55, -457012: 12, -457013: 48, -457014: 12, -457015: 42, -457016: 4, -457017: 35, -457018: 4, -457039: 148, -457040: 82, -457041: 206, -457042: 57, -457043: 8, -457044: 33, -457045: 132, -457046: 16, -457047: 25, -457048: 0, -457049: 18, -457050: 0, -457059: 117, -457060: 78, -457061: 175, -457062: 53, -457063: 232, -457064: 28, -457065: 100, -457066: 12, -457067: 13, -457068: 8, -457069: 9, -457070: 8, -457079: 85, -457080: 74, -457081: 143, -457082: 49, -457083: 233, -457084: 28, -457085: 100, -457086: 12, -457087: 0, -457088: 16, -457089: 0, -457090: 16, -457099: 54, -457100: 70, -457101: 112, -457102: 45, -457103: 201, -457104: 24, -457105: 68, -457106: 8, -457107: 13, -457108: 8, -457109: 9, -457110: 8, -457119: 246, -457120: 61, -457121: 112, -457122: 45, -457123: 202, -457124: 24, -457125: 68, -457126: 8, -457127: 25, -457128: 0, -457129: 18, -457130: 0, -457139: 215, -457140: 57, -457141: 81, -457142: 41, -457143: 170, -457144: 20, -457145: 36, -457146: 4, -457147: 13, -457148: 8, -457149: 9, -457150: 8, -457159: 183, -457160: 53, -457161: 49, -457162: 37, -457163: 171, -457164: 20, -457165: 36, -457166: 4, -457167: 0, -457168: 16, -457169: 0, -457170: 16, -457179: 152, -457180: 49, -457181: 18, -457182: 33, -457183: 139, -457184: 16, -457185: 4, -457186: 0, -457187: 13, -457188: 8, -457189: 9, -457190: 8, -457199: 183, -457200: 53, -457201: 49, -457202: 37, -457203: 171, -457204: 20, -457205: 36, -457206: 4, -457207: 0, -457208: 16, -457209: 0, -457210: 16, -457219: 215, -457220: 57, -457221: 81, -457222: 41, -457223: 170, -457224: 20, -457225: 36, -457226: 4, -457227: 13, -457228: 8, -457229: 9, -457230: 8, -457239: 246, -457240: 61, -457241: 112, -457242: 45, -457243: 202, -457244: 24, -457245: 68, -457246: 8, -457247: 25, -457248: 0, -457249: 18, -457250: 0, -457259: 54, -457260: 70, -457261: 112, -457262: 45, -457263: 201, -457264: 24, -457265: 68, -457266: 8, -457267: 13, -457268: 8, -457269: 9, -457270: 8, -457279: 85, -457280: 74, -457281: 143, -457282: 49, -457283: 233, -457284: 28, -457285: 100, -457286: 12, -457287: 0, -457288: 16, -457289: 0, -457290: 16, -457299: 117, -457300: 78, -457301: 175, -457302: 53, -457303: 232, -457304: 28, -457305: 100, -457306: 12, -457307: 13, -457308: 8, -457309: 9, -457310: 8, -892503: 1, -892509: 21, -457987: 137, -457988: 29, -457989: 6, -457990: 13, -457991: 163, -457992: 12, -457993: 14, -457994: 45, -457995: 9, -457996: 45, -457997: 197, -457998: 40, -457999: 129, -458000: 12, -458005: 138, -458006: 29, -458007: 7, -458008: 13, -458009: 164, -458010: 12, -458011: 14, -458012: 45, -458013: 9, -458014: 45, -458015: 197, -458016: 40, -458017: 33, -458018: 4, -458023: 138, -458024: 29, -458025: 40, -458026: 13, -458027: 196, -458028: 12, -458029: 15, -458030: 45, -458031: 10, -458032: 45, -458033: 198, -458034: 40, -458035: 35, -458036: 4, -458041: 139, -458042: 29, -458043: 41, -458044: 13, -458045: 197, -458046: 12, -458047: 15, -458048: 45, -458049: 10, -458050: 45, -458051: 198, -458052: 40, -458053: 36, -458054: 4, -458059: 171, -458060: 29, -458061: 73, -458062: 17, -458063: 197, -458064: 16, -458065: 15, -458066: 45, -458067: 11, -458068: 45, -458069: 199, -458070: 40, -458071: 69, -458072: 8, -458077: 172, -458078: 29, -458079: 74, -458080: 17, -458081: 198, -458082: 16, -458083: 15, -458084: 45, -458085: 11, -458086: 45, -458087: 199, -458088: 40, -458089: 70, -458090: 8, -458095: 172, -458096: 29, -458097: 107, -458098: 17, -458099: 230, -458100: 16, -458101: 16, -458102: 45, -458103: 12, -458104: 45, -458105: 200, -458106: 40, -458107: 72, -458108: 8, -458113: 173, -458114: 29, -458115: 108, -2175692: 97, -458117: 231, -458118: 16, -458119: 16, -458120: 45, -458121: 12, -458122: 45, -458123: 200, -458124: 40, -458125: 21, -458126: 0, -458131: 172, -458132: 29, -458133: 107, -458134: 17, -458135: 230, -458136: 16, -458137: 16, -458138: 45, -458139: 12, -458140: 45, -458141: 200, -458142: 40, -458143: 72, -458144: 8, -458149: 172, -458150: 29, -458151: 74, -458152: 17, -458153: 198, -458154: 16, -458155: 15, -458156: 45, -458157: 11, -458158: 45, -458159: 199, -458160: 40, -458161: 70, -458162: 8, -458167: 171, -458168: 29, -458169: 73, -458170: 17, -458171: 197, -458172: 16, -458173: 15, -458174: 45, -458175: 11, -458176: 45, -458177: 199, -458178: 40, -458179: 69, -458180: 8, -458185: 139, -458186: 29, -458187: 41, -458188: 13, -458189: 197, -458190: 12, -458191: 15, -458192: 45, -458193: 10, -458194: 45, -458195: 198, -458196: 40, -458197: 36, -458198: 4, -458203: 138, -458204: 29, -458205: 40, -458206: 13, -458207: 196, -458208: 12, -458209: 15, -458210: 45, -458211: 10, -458212: 45, -458213: 198, -458214: 40, -458215: 35, -458216: 4, -458221: 138, -458222: 29, -458223: 7, -458224: 13, -458225: 164, -458226: 12, -458227: 14, -458228: 45, -458229: 9, -458230: 45, -458231: 197, -458232: 40, -458233: 33, -458234: 4, -852394: 29, -852395: 124, -852396: 20, -852397: 88, -852398: 10, -852399: 48, -1321731: 66, -1321737: 79, -1321743: 29, -1321755: 0, -1115783: 0, -1115784: 56, -1640073: 85, -1640074: 87, -1640075: 79, -1640076: 74, -1640077: 228, -1640078: 28, -1640079: 96, -1640080: 12, -1640081: 178, -1640082: 86, -1640083: 13, -1640084: 62, -1640085: 104, -1640086: 45, -1640087: 38, -1640088: 37, -1640089: 187, -1640090: 94, -1640091: 179, -1640092: 61, -1640093: 46, -1640094: 41, -1115807: 0, -1640096: 20, -1640097: 59, -1640098: 3, -1246883: 247, -1640100: 2, -1246885: 0, -1246886: 0, -1640103: 0, -1640104: 56, -1640105: 87, -1640106: 63, -1640107: 77, -1640108: 46, -1640109: 226, -1640110: 0, -1640111: 96, -1640112: 0, -1640113: 176, -1640114: 58, -1640115: 11, -1640116: 34, -1640117: 102, -1640118: 17, -1640119: 36, -1640120: 9, -1640121: 187, -1640122: 94, -1640123: 179, -1640124: 61, -1640125: 46, -1640126: 41, -1640127: 134, -1640128: 20, -1640129: 59, -1640130: 3, -1640131: 22, -1640132: 2, -1640133: 19, -1640134: 1, -1640135: 0, -1640136: 56, -1640137: 191, -1640138: 78, -1640139: 158, -1640140: 77, -1640141: 9, -1640142: 16, -1640143: 4, -1378000: 20, -1640145: 222, -1640146: 73, -1378003: 87, -1640148: 85, -1640149: 176, -1640150: 48, -1640151: 77, -1640152: 28, -1378009: 127, -1640154: 94, -1640155: 179, -1640156: 61, -1640157: 46, -1640158: 41, -1378015: 95, -1378016: 95, -1378017: 26, -1640162: 3, -1378019: 53, -1378020: 93, -1378021: 99, -1378022: 12, -1378023: 0, -1378024: 56, -1378025: 186, -1378026: 74, -1378027: 178, -1378028: 53, -1378029: 71, -1378030: 8, -1378031: 3, -1378032: 0, -1378033: 21, -1378034: 66, -1378035: 112, -1378036: 41, -1378037: 203, -1378038: 24, -1378039: 137, -1378040: 16, -1378041: 58, -1378042: 70, -1640187: 179, -1378044: 40, -1378045: 9, -1378046: 24, -1378047: 127, -1378048: 111, -1378049: 253, -1378050: 81, -1378051: 19, -1378052: 65, -1378053: 99, -1378054: 12, -1378055: 0, -1378056: 56, -1378057: 186, -1378058: 86, -1378059: 178, -1378060: 65, -1378061: 71, -1378062: 20, -1378063: 3, -1378064: 4, -1378065: 21, -1378066: 78, -1378067: 112, -1378068: 53, -1378069: 203, -1378070: 36, -1378071: 104, -1378072: 24, -1378073: 127, -1378074: 111, -1378075: 248, -1378076: 81, -1378077: 14, -1378078: 65, -1378079: 31, -1378080: 3, -1378081: 218, -1378082: 1, -1378083: 245, -1378084: 0, -1378085: 99, -1378086: 12, -1378087: 0, -1378088: 56, -1378089: 21, -1378090: 66, -1378091: 13, -1378092: 45, -1378093: 2, -1378094: 0, -1378095: 0, -1378096: 0, -1378097: 112, -1378098: 57, -1378099: 203, -1378100: 32, -1378101: 38, -1378102: 12, -1378103: 3, -1378104: 4, -1378105: 58, -1378106: 70, -1378107: 179, -1378108: 40, -1378109: 9, -1378110: 24, -1378111: 127, -1378112: 111, -1378113: 253, -1378114: 81, -1378115: 19, -1378116: 65, -1378117: 99, -1378118: 12, -1378119: 0, -1378120: 56, -1378121: 181, -1378122: 106, -1378123: 176, -1378124: 73, -1378125: 69, -1378126: 28, -1378127: 1, -1378128: 12, -1378129: 19, -1378130: 86, -1378131: 109, -1378132: 65, -1378133: 201, -1378134: 44, -1378135: 102, -1378136: 32, -1378137: 20, -1378138: 87, -1378139: 204, -1378140: 49, -1378141: 227, -1378142: 20, -1378143: 48, -1378144: 86, -1378145: 105, -1378146: 53, -1378147: 131, -1378148: 24, -1378149: 102, -1378150: 12, -1378151: 0, -1378152: 56, -1378153: 16, -1378154: 86, -1378155: 11, -1378156: 53, -1378157: 0, -1378158: 8, -1378159: 0, -1378160: 0, -1378161: 110, -1378162: 65, -1378163: 200, -1378164: 44, -1378165: 35, -1378166: 24, -1378167: 1, -1378168: 12, -1378169: 49, -1378170: 106, -1378171: 170, -1378172: 76, -1378173: 6, -1378174: 36, -1378175: 123, -1378176: 127, -1378177: 244, -1378178: 117, -1378179: 16, -1378180: 77, -1378181: 99, -1378182: 12, -1378183: 0, -1378184: 56, -1378185: 190, -1378186: 75, -1378187: 185, -1378188: 6, -1378189: 168, -1378190: 0, -1378191: 0, -1378192: 0, -1378193: 58, -1378194: 23, -1378195: 118, -1378196: 2, -1378197: 242, -1378198: 1, -1378199: 77, -1378200: 1, -1378201: 224, -1378202: 115, -1378203: 32, -1378204: 79, -1378205: 32, -1378206: 42, -1378207: 224, -1378208: 127, -1378209: 160, -1378210: 90, -1378211: 32, -1378212: 89, -1378213: 67, -1378214: 0, -1378215: 0, -1378216: 56, -1378217: 25, -1378218: 55, -1378219: 20, -1378220: 2, -1378221: 3, -1378222: 0, -1378223: 0, -1378224: 0, -1378225: 149, -1378226: 2, -1378227: 209, -1378228: 1, -1378229: 77, -1378230: 1, -1378231: 168, -1378232: 0, -1378233: 64, -1378234: 75, -1378235: 224, -1378236: 37, -1378237: 224, -1378238: 0, -1378239: 64, -1378240: 107, -1378241: 0, -1378242: 70, -1378243: 128, -1378244: 68, -1378245: 0, -1378246: 0, -1117024: 0, -1117025: 56, -1117026: 156, -1117027: 75, -1117028: 148, -1117029: 54, -1117030: 231, -1117031: 8, -1117032: 132, -1117033: 8, -1117034: 247, -1117035: 66, -1117036: 82, -1117037: 42, -1117038: 173, -1117039: 25, -1117040: 41, -1117041: 17, -1117042: 255, -1117043: 127, -1117044: 59, -1117045: 3, -1117046: 22, -1117047: 2, -1117048: 19, -1117049: 1, -1117050: 255, -1117051: 127, -1117052: 255, -1117053: 3, -1117054: 13, -1117055: 0, -1503689: 127, -1120653: 0, -1120654: 56, -1120655: 156, -1120656: 75, -1120657: 16, -1120658: 38, -1120659: 198, -1120660: 12, -1120661: 99, -1120662: 12, -1120663: 247, -1120664: 66, -1120665: 82, -1120666: 42, -1120667: 173, -1120668: 25, -1120669: 41, -1120670: 13, -1120671: 23, -1120672: 86, -1120673: 114, -1120674: 61, -1120675: 72, -1120676: 28, -1120677: 5, -1120678: 12, -1120679: 59, -1120680: 3, -1120681: 22, -1120682: 2, -1120683: 19, -1120684: 1, -1121898: 0, -1121899: 56, -1121900: 87, -1121901: 63, -1121902: 77, -1121903: 46, -1121904: 226, -1121905: 0, -1121906: 96, -1121907: 0, -1121908: 176, -1121909: 58, -1121910: 11, -1121911: 34, -1121912: 102, -1121913: 17, -1121914: 36, -1121915: 9, -1121916: 255, -1121917: 87, -1121918: 247, -1121919: 66, -1121920: 16, -1121921: 38, -1121922: 140, -1121923: 21, -1121924: 127, -1121925: 1, -1121926: 22, -1121927: 0, -1121928: 10, -1121929: 48, -2174963: 115, -2174964: 78, -2174965: 173, -2174966: 53, -2174967: 231, -2174968: 28, -2174969: 99, -2174970: 12, -2174971: 24, -2174972: 99, -2174973: 4, -2174974: 8, -2174975: 255, -2174976: 127, -2174977: 35, -2174978: 0, -2174979: 69, -2174980: 24, -2174981: 99, -2174982: 7, -2174983: 82, -2174984: 25, -2174985: 171, -2174986: 24, -2174987: 103, -2174988: 20, -2174989: 68, -2174990: 16, -2174991: 75, -2174992: 24, -2174993: 99, -2174994: 195, -1123211: 0, -1123212: 56, -1123213: 156, -1123214: 75, -1123215: 16, -1123216: 38, -1123217: 198, -1123218: 12, -1123219: 99, -1123220: 12, -1123221: 247, -1123222: 66, -1123223: 82, -1123224: 42, -1123225: 173, -1123226: 25, -1123227: 41, -1123228: 13, -1123229: 89, -1123230: 94, -1123231: 114, -1123232: 61, -1123233: 238, -1123234: 44, -1123235: 71, -1123236: 20, -1123237: 59, -1123238: 3, -1123239: 22, -1123240: 2, -1123241: 19, -1123242: 1, -1123742: 0, -1123743: 56, -1123744: 149, -1123745: 2, -1123746: 144, -1123747: 1, -1123748: 3, -1123749: 4, -1123750: 1, -1123751: 0, -1123752: 17, -1123753: 2, -1123754: 77, -1123755: 1, -1123756: 201, -1123757: 0, -1123758: 36, -1123759: 0, -1123760: 224, -1123761: 59, -1123762: 128, -1123763: 38, -1123764: 128, -1123765: 21, -1123766: 198, -1123767: 36, -1123768: 66, -1123769: 20, -1123770: 0, -1123771: 0, -1123772: 0, -1123773: 0, -1148551: 0, -1148552: 56, -1148553: 250, -1148554: 114, -1148555: 176, -1148556: 85, -1148557: 69, -1148558: 40, -1148559: 1, -1148560: 24, -1148561: 16, -1148562: 98, -1148563: 107, -1148564: 73, -1148565: 198, -1148566: 56, -1148567: 99, -1148568: 44, -1148569: 31, -1148570: 36, -1148571: 23, -1148572: 28, -1148573: 47, -1148574: 20, -1148575: 71, -1148576: 12, -1148577: 224, -1148578: 59, -1148579: 128, -1148580: 38, -1148581: 128, -1148582: 21, -456464: 185, -456465: 0, -456466: 57, -456467: 103, -2175500: 33, -2175501: 0, -2175502: 173, -2175503: 85, -2175504: 231, -2175505: 56, -2175506: 33, -2175507: 32, -2175508: 0, -2175509: 16, -2175510: 33, -1126935: 2, -2175512: 231, -1126937: 1, -2175514: 82, -2175515: 74, -2175516: 35, -2175517: 0, -2175518: 4, -2175519: 163, -2175520: 1, -2175521: 3, -2175522: 1, -1126947: 29, -1126948: 37, -1126949: 9, -2175526: 1, -2175527: 8, -2175528: 66, -2175529: 34, -2175530: 0, -2175531: 13, -456476: 22, -456477: 0, -456478: 15, -456479: 0, -2174332: 27, -2174333: 0, -2174334: 0, -2174335: 223, -2174336: 2, -2174337: 215, -2174338: 1, -2174339: 172, -2174340: 0, -2174341: 245, -2174342: 93, -2174343: 14, -2174344: 61, -2174345: 170, -2174346: 40, -2174347: 35, -2174348: 20, -2174349: 177, -2174350: 11, -2174351: 251, -2174352: 72, -2174353: 255, -2174354: 127, -2174355: 0, -2174356: 0, -2174357: 255, -2174358: 127, -2174359: 229, -2174360: 68, -2174361: 195, -2174362: 8, -2174363: 21, -2174364: 99, -2174365: 68, -2174366: 177, -2174367: 11, -2174368: 169, -2174369: 30, -2174370: 69, -2174371: 1, -2174372: 187, -2174373: 94, -2174374: 179, -2174375: 61, -2174376: 46, -2174377: 41, -2174378: 134, -2174379: 20, -2174380: 24, -2174381: 99, -2174382: 231, -2174383: 28, -2174384: 132, -2174385: 16, -2174386: 195, -2174387: 32, -2174388: 2, -2174389: 223, -2174390: 2, -2174391: 31, -2174392: 34, -2174393: 0, -2174394: 29, -2174395: 99, -2174396: 68, -2174397: 188, -2174398: 114, -2174399: 251, -2174400: 72, -2174401: 22, -2174402: 24, -2174403: 132, -2174404: 20, -2174405: 99, -2174406: 16, -2174407: 33, -2174408: 8, -2174409: 0, -2174410: 4, -2174411: 239, -2174412: 1, -2174413: 173, -2174414: 1, -2174415: 107, -2174416: 1, -2174417: 41, -2174418: 1, -2174419: 231, -2174420: 4, -2174421: 165, -2174422: 4, -2174423: 99, -2174424: 4, -2174425: 195, -2174426: 32, -2174427: 13, -2174428: 178, -2174429: 114, -2174430: 199, -2174431: 113, -2174432: 3, -2174433: 77, -2174434: 149, -2174435: 82, -2174436: 240, -2174437: 61, -2174438: 108, -2174439: 45, -2174440: 9, -2174441: 33, -2174442: 77, -2174443: 24, -2174444: 99, -2174445: 195, -2174446: 32, -2174447: 224, -2174448: 45, -2174449: 137, -2174450: 29, -2174451: 6, -2174452: 13, -2174453: 163, -2174454: 12, -2174455: 14, -2174456: 45, -2174457: 9, -2174458: 45, -2174459: 197, -2174460: 40, -2174461: 129, -2174462: 12, -2174463: 206, -2174464: 85, -2174465: 104, -2174466: 61, -2174467: 162, -2174468: 36, -2174469: 0, -2174470: 20, -2174471: 12, -2174472: 50, -2174473: 8, -2174474: 33, -2174475: 255, -2174476: 127, -2174477: 32, -2174478: 4, -2174479: 99, -2174480: 68, -2174481: 148, -2174482: 82, -2174483: 206, -2174484: 57, -2174485: 8, -2174486: 33, -2174487: 232, -2174488: 28, -2174489: 199, -2174490: 24, -2174491: 166, -2174492: 20, -2174493: 133, -2174494: 16, -2174495: 67, -2174496: 67, -2174497: 8, -2174498: 13, -2174499: 34, -2174500: 4, -2174501: 33, -2174502: 4, -2174503: 79, -2174504: 45, -2174505: 15, -2174506: 41, -2174507: 255, -2174508: 127, -2174509: 66, -2174510: 0, -2174511: 99, -2174512: 68, -2174513: 69, -2174514: 24, -2174515: 99, -2174516: 15, -2174517: 96, -2174518: 45, -2174519: 32, -2174520: 37, -2174521: 192, -2174522: 24, -2174523: 128, -2174524: 16, -2174525: 12, -2174526: 38, -2174527: 169, -2174528: 25, -2174529: 7, -2174530: 9, -2174531: 130, -2174532: 0, -2174533: 195, -2174534: 70, -2174535: 197, -2174536: 192, -2174537: 69, -2174538: 24, -2174539: 99, -2174540: 7, -2174541: 148, -2174542: 62, -2174543: 206, -2174544: 37, -2174545: 8, -2174546: 13, -2174547: 198, -2174548: 4, -2174549: 199, -2174550: 96, -2174551: 67, -2174552: 24, -2174553: 99, -2174554: 195, -2174555: 32, -2174556: 255, -2174557: 27, -2174558: 99, -2174559: 68, -2174560: 223, -2174561: 2, -2174562: 215, -2174563: 1, -2174564: 172, -2174565: 0, -2174566: 245, -2174567: 93, -2174568: 14, -2174569: 61, -2174570: 170, -2174571: 40, -2174572: 35, -2174573: 20, -2174574: 177, -2174575: 11, -2174576: 251, -2174577: 72, -2174578: 255, -2174579: 127, -2174580: 0, -2174581: 0, -2174582: 255, -2174583: 127, -2174584: 229, -2174585: 68, -2174586: 195, -2174587: 8, -2174588: 21, -2174589: 99, -2174590: 68, -2174591: 177, -2174592: 11, -2174593: 169, -2174594: 30, -2174595: 69, -2174596: 1, -2174597: 187, -2174598: 94, -2174599: 179, -2174600: 61, -2174601: 46, -2174602: 41, -2174603: 134, -2174604: 20, -2174605: 24, -2174606: 99, -2174607: 231, -2174608: 28, -2174609: 132, -2174610: 16, -2174611: 195, -2174612: 32, -2174613: 2, -2174614: 223, -2174615: 2, -2174616: 31, -2174617: 34, -2174618: 0, -2174619: 29, -2174620: 99, -2174621: 68, -2174622: 188, -2174623: 114, -2174624: 251, -2174625: 72, -2174626: 22, -2174627: 24, -2174628: 14, -2174629: 0, -2174630: 6, -2174631: 0, -2174632: 2, -2174633: 0, -2174634: 1, -2174635: 0, -2174636: 239, -2174637: 1, -2174638: 173, -2174639: 1, -2174640: 107, -2174641: 1, -2174642: 41, -2174643: 1, -2174644: 231, -2174645: 4, -2174646: 165, -2174647: 4, -2174648: 99, -2174649: 4, -2174650: 195, -2174651: 32, -2174652: 13, -2174653: 178, -2174654: 114, -2174655: 199, -2174656: 113, -2174657: 3, -2174658: 77, -2174659: 149, -2174660: 82, -2174661: 240, -2174662: 61, -2174663: 108, -2174664: 45, -2174665: 9, -2174666: 33, -2174667: 77, -2174668: 24, -2174669: 99, -2174670: 195, -2174671: 32, -2174672: 10, -2174673: 142, -2174674: 29, -2174675: 11, -2174676: 13, -2174677: 168, -2174678: 12, -2174679: 22, -2174680: 0, -2174681: 15, -2174682: 0, -2174683: 7, -2174684: 194, -2174685: 66, -2174686: 224, -2174687: 36, -2174688: 206, -2174689: 85, -2174690: 104, -2174691: 61, -2174692: 162, -2174693: 36, -2174694: 0, -2174695: 20, -2174696: 17, -2174697: 50, -2174698: 8, -2174699: 33, -2174700: 255, -2174701: 127, -2174702: 32, -2174703: 4, -2174704: 99, -2174705: 68, -2174706: 148, -2174707: 82, -2174708: 206, -2174709: 57, -2174710: 8, -2174711: 33, -2174712: 127, -2174713: 8, -2174714: 123, -2174715: 8, -2174716: 86, -2174717: 4, -2174718: 82, -2174719: 4, -2174720: 45, -2174721: 4, -2174722: 41, -2174723: 4, -2174724: 4, -2174725: 34, -2174726: 0, -2174727: 9, -2174728: 79, -2174729: 45, -2174730: 15, -2174731: 41, -2174732: 255, -2174733: 127, -2174734: 64, -2174735: 0, -2174736: 99, -2174737: 68, -2174738: 69, -2174739: 24, -2174740: 99, -2174741: 19, -2174742: 107, -2174743: 45, -2174744: 43, -2174745: 37, -2174746: 203, -2174747: 24, -2174748: 139, -2174749: 16, -2174750: 17, -2174751: 38, -2174752: 174, -2174753: 25, -2174754: 12, -2174755: 9, -2174756: 135, -2174757: 0, -2174758: 109, -2174759: 61, -2174760: 167, -2174761: 36, -2174762: 197, -2174763: 192, -2174764: 69, -2174765: 24, -2174766: 99, -2174767: 10, -2174768: 153, -2174769: 62, -2174770: 211, -2174771: 37, -2174772: 13, -2174773: 13, -2174774: 198, -2174775: 4, -2174776: 211, -2174777: 85, -2174778: 109, -2174779: 196, -2174780: 96, -2174781: 67, -2174782: 24, -2174783: 99, -2174784: 195, -2174785: 32, -2174786: 255, -2174787: 29, -2174788: 0, -2174789: 0, -2174790: 223, -2174791: 2, -2174792: 215, -2174793: 1, -2174794: 172, -2174795: 0, -2174796: 55, -2174797: 95, -2174798: 16, -2174799: 62, -2174800: 108, -2174801: 41, -2174802: 165, -2174803: 20, -2174804: 177, -2174805: 11, -2174806: 251, -2174807: 72, -2174808: 255, -2174809: 127, -2174810: 0, -2174811: 0, -2174812: 255, -2174813: 127, -2174814: 229, -2174815: 68, -2174816: 255, -2174817: 127, -2174818: 35, -2174819: 0, -2174820: 19, -2174821: 177, -2174822: 11, -2174823: 169, -2174824: 30, -2174825: 69, -2174826: 1, -2174827: 187, -2174828: 94, -2174829: 179, -2174830: 61, -2174831: 46, -2174832: 41, -2174833: 134, -2174834: 20, -2174835: 24, -2174836: 99, -2174837: 231, -2174838: 28, -2174839: 132, -2174840: 16, -2174841: 195, -2174842: 32, -2174843: 2, -2174844: 223, -2174845: 2, -2174846: 31, -2174847: 36, -2174848: 0, -2174849: 5, -2174850: 188, -2174851: 114, -2174852: 251, -2174853: 72, -2174854: 22, -2174855: 24, -2174856: 75, -2174857: 24, -2174858: 99, -2174859: 9, -2174860: 134, -2174861: 44, -2174862: 130, -2174863: 28, -2174864: 72, -2174865: 68, -2174866: 35, -2174867: 44, -2174868: 255, -2174869: 127, -2174870: 35, -2174871: 0, -2174872: 5, -2174873: 178, -2174874: 114, -2174875: 199, -2174876: 113, -2174877: 3, -2174878: 77, -2174879: 83, -2174880: 24, -2174881: 99, -2174882: 1, -2174883: 255, -2174884: 127, -2174885: 35, -2174886: 0, -2174887: 197, -2174888: 88, -2174889: 224, -2174890: 85, -2174891: 111, -2174892: 45, -2174893: 200, -2174894: 40, -2174895: 132, -2174896: 36, -2174897: 97, -2174898: 28, -2174899: 147, -2174900: 92, -2174901: 142, -2174902: 72, -2174903: 6, -2174904: 44, -2174905: 72, -2174906: 68, -2174907: 4, -2174908: 20, -2174909: 2, -2174910: 16, -2174911: 187, -2174912: 94, -2174913: 0, -2174914: 8, -2174915: 0, -2174916: 0, -2174917: 115, -2174918: 90, -2174919: 173, -2174920: 65, -2174921: 231, -2174922: 40, -2174923: 78, -2174924: 37, -2174925: 167, -2174926: 36, -2174927: 99, -2174928: 32, -2174929: 64, -2174930: 24, -2174931: 25, -2174932: 0, -2174933: 18, -2174934: 0, -2174935: 96, -2174936: 52, -2174937: 64, -2174938: 24, -2174939: 68, -2174940: 8, -2174941: 127, -2174942: 25, -2174943: 156, -2174944: 127, -2174945: 0, -2174946: 4, -2174947: 0, -2174948: 0, -2174949: 15, -2174950: 52, -2174951: 11, -2174952: 36, -2174953: 8, -2174954: 24, -2174955: 115, -2174956: 17, -2174957: 204, -2174958: 16, -2174959: 136, -2174960: 12, -2174961: 101, -2174962: 8, -1126387: 0, -1126388: 56, -1126389: 250, -1126390: 114, -1126391: 176, -1126392: 85, -1126393: 69, -1126394: 40, -1126395: 1, -1126396: 24, -1126397: 16, -1126398: 98, -1126399: 107, -1126400: 73, -1126401: 198, -1126402: 56, -1126403: 99, -1126404: 44, -1126405: 239, -1126406: 47, -1126407: 13, -1126408: 3, -1126409: 9, -1126410: 2, -1126411: 69, -1126412: 1, -1126413: 63, -1126414: 24, -1126415: 20, -1126416: 16, -1126417: 10, -1126418: 8, -2174995: 32, -2174996: 255, -2174997: 29, -2174998: 0, -2174999: 0, -2175000: 223, -2175001: 2, -2175002: 215, -2175003: 1, -2175004: 172, -2175005: 0, -2175006: 55, -2175007: 95, -2175008: 16, -2175009: 62, -2175010: 108, -2175011: 41, -2175012: 165, -2175013: 20, -2175014: 177, -2175015: 11, -2175016: 251, -2175017: 72, -2175018: 255, -2175019: 127, -2175020: 0, -2175021: 0, -2175022: 255, -2175023: 127, -2175024: 229, -2175025: 68, -2175026: 255, -2175027: 127, -2175028: 35, -2175029: 0, -2175030: 19, -2175031: 177, -2175032: 11, -2175033: 169, -2175034: 30, -2175035: 69, -2175036: 1, -2175037: 187, -2175038: 94, -2175039: 179, -2175040: 61, -2175041: 46, -2175042: 41, -2175043: 134, -2175044: 20, -2175045: 24, -2175046: 99, -2175047: 231, -2175048: 28, -2175049: 132, -2175050: 16, -2175051: 195, -2175052: 32, -2175053: 2, -2175054: 223, -2175055: 2, -2175056: 31, -2175057: 36, -2175058: 0, -2175059: 5, -2175060: 188, -2175061: 114, -2175062: 251, -2175063: 72, -2175064: 22, -2175065: 24, -2175066: 75, -2175067: 24, -2175068: 99, -2175069: 9, -2175070: 134, -2175071: 44, -2175072: 130, -2175073: 28, -2175074: 72, -2175075: 68, -2175076: 35, -2175077: 44, -2175078: 255, -2175079: 127, -2175080: 35, -2175081: 0, -2175082: 5, -2175083: 178, -2175084: 114, -2175085: 199, -2175086: 113, -2175087: 3, -2175088: 77, -2175089: 83, -2175090: 24, -2175091: 99, -2175092: 1, -2175093: 255, -2175094: 127, -2175095: 35, -2175096: 0, -2175097: 197, -2175098: 88, -2175099: 224, -2175100: 85, -2175101: 111, -2175102: 45, -2175103: 200, -2175104: 40, -2175105: 132, -2175106: 36, -2175107: 97, -2175108: 28, -2175109: 147, -2175110: 92, -2175111: 142, -2175112: 72, -2175113: 6, -2175114: 44, -2175115: 72, -2175116: 68, -2175117: 4, -2175118: 20, -2175119: 2, -2175120: 16, -2175121: 187, -2175122: 94, -2175123: 0, -2175124: 8, -2175125: 0, -2175126: 0, -2175127: 12, -2175128: 33, -2175129: 168, -2175130: 20, -2175131: 2, -2175132: 0, -2175133: 235, -2175134: 24, -2175135: 68, -2175136: 24, -2175137: 33, -2175138: 20, -2175139: 0, -2175140: 12, -2175141: 25, -2175142: 0, -2175143: 18, -2175144: 0, -2175145: 72, -2175146: 68, -2175147: 6, -2175148: 44, -2175149: 68, -2175150: 8, -2175151: 127, -2175152: 25, -2175153: 244, -2175154: 61, -2175155: 0, -2175156: 4, -2175157: 0, -2175158: 0, -2175159: 15, -2175160: 52, -2175161: 11, -2175162: 36, -2175163: 8, -2175164: 24, -2175165: 115, -2175166: 17, -2175167: 204, -2175168: 16, -2175169: 136, -2175170: 12, -2175171: 101, -2175172: 8, -2175173: 115, -2175174: 78, -2175175: 173, -2175176: 53, -2175177: 231, -2175178: 28, -2175179: 99, -2175180: 12, -2175181: 24, -2175182: 99, -2175183: 4, -2175184: 8, -2175185: 255, -2175186: 127, -2175187: 35, -2175188: 0, -2175189: 69, -2175190: 24, -2175191: 99, -2175192: 7, -2175193: 82, -2175194: 25, -2175195: 171, -2175196: 24, -2175197: 103, -2175198: 20, -2175199: 68, -2175200: 16, -2175201: 75, -2175202: 24, -2175203: 99, -2175204: 195, -2175205: 32, -2175206: 255, -2175207: 27, -2175208: 0, -2175209: 0, -2175210: 223, -2175211: 2, -2175212: 215, -2175213: 1, -2175214: 172, -2175215: 0, -2175216: 187, -2175217: 94, -2175218: 179, -2175219: 61, -2175220: 46, -2175221: 41, -2175222: 134, -2175223: 20, -2175224: 177, -2175225: 11, -2175226: 251, -2175227: 72, -2175228: 255, -2175229: 127, -2175230: 0, -2175231: 0, -2175232: 255, -2175233: 127, -2175234: 229, -2175235: 68, -2175236: 195, -2175237: 8, -2175238: 7, -2175239: 3, -2175240: 32, -2175241: 177, -2175242: 11, -2175243: 169, -2175244: 30, -2175245: 69, -2175246: 1, -2175247: 199, -2175248: 32, -2175249: 5, -2175250: 24, -2175251: 99, -2175252: 231, -2175253: 28, -2175254: 132, -2175255: 16, -2175256: 195, -2175257: 32, -2175258: 2, -2175259: 223, -2175260: 2, -2175261: 31, -2175262: 34, -2175263: 0, -2175264: 7, -2175265: 3, -2175266: 32, -2175267: 188, -2175268: 114, -2175269: 251, -2175270: 72, -2175271: 22, -2175272: 24, -2175273: 83, -2175274: 24, -2175275: 99, -2175276: 1, -2175277: 255, -2175278: 127, -2175279: 35, -2175280: 0, -2175281: 5, -2175282: 178, -2175283: 114, -2175284: 199, -2175285: 113, -2175286: 3, -2175287: 77, -2175288: 83, -2175289: 24, -2175290: 99, -2175291: 1, -2175292: 255, -2175293: 127, -2175294: 35, -2175295: 0, -2175296: 27, -2175297: 244, -2175298: 62, -2175299: 46, -2175300: 38, -2175301: 104, -2175302: 13, -2175303: 148, -2175304: 62, -2175305: 206, -2175306: 37, -2175307: 8, -2175308: 13, -2175309: 132, -2175310: 20, -2175311: 148, -2175312: 110, -2175313: 206, -2175314: 85, -2175315: 8, -2175316: 61, -2175317: 132, -2175318: 44, -2175319: 169, -2175320: 30, -2175321: 177, -2175322: 11, -2175323: 255, -2175324: 127, -2175325: 35, -2175326: 0, -2175327: 27, -2175328: 70, -2175329: 18, -2175330: 69, -2175331: 1, -2175332: 192, -2175333: 0, -2175334: 82, -2175335: 54, -2175336: 140, -2175337: 29, -2175338: 198, -2175339: 4, -2175340: 132, -2175341: 4, -2175342: 49, -2175343: 98, -2175344: 107, -2175345: 73, -2175346: 165, -2175347: 48, -2175348: 33, -2175349: 32, -2175350: 21, -2175351: 0, -2175352: 91, -2175353: 2, -2175354: 255, -2175355: 127, -2175356: 35, -2175357: 0, -2175358: 89, -2175359: 24, -2175360: 99, -2175361: 1, -2175362: 255, -2175363: 127, -2175364: 35, -2175365: 0, -2175366: 29, -2175367: 123, -2175368: 71, -2175369: 82, -2175370: 46, -2175371: 198, -2175372: 0, -2175373: 99, -2175374: 0, -2175375: 181, -2175376: 58, -2175377: 16, -2175378: 34, -2175379: 107, -2175380: 17, -2175381: 8, -2175382: 5, -2175383: 255, -2175384: 127, -2175385: 181, -2175386: 54, -2175387: 173, -2175388: 25, -2175389: 41, -2175390: 9, -2175391: 29, -2175392: 56, -2175393: 20, -2175394: 24, -2175395: 10, -2175396: 0, -2175397: 255, -2175398: 27, -2175399: 0, -2175400: 0, -2175401: 223, -2175402: 2, -2175403: 215, -2175404: 1, -2175405: 172, -2175406: 0, -2175407: 187, -2175408: 94, -2175409: 179, -2175410: 61, -2175411: 46, -2175412: 41, -2175413: 134, -2175414: 20, -2175415: 177, -2175416: 11, -2175417: 251, -2175418: 72, -2175419: 255, -2175420: 127, -2175421: 0, -2175422: 0, -2175423: 255, -2175424: 127, -2175425: 229, -2175426: 68, -2175427: 195, -2175428: 8, -2175429: 7, -2175430: 3, -2175431: 32, -2175432: 177, -2175433: 11, -2175434: 169, -2175435: 30, -2175436: 69, -2175437: 1, -2175438: 199, -2175439: 32, -2175440: 5, -2175441: 24, -2175442: 99, -2175443: 231, -2175444: 28, -2175445: 132, -2175446: 16, -2175447: 195, -2175448: 32, -2175449: 2, -2175450: 223, -2175451: 2, -2175452: 31, -2175453: 34, -2175454: 0, -2175455: 7, -2175456: 3, -2175457: 32, -2175458: 188, -2175459: 114, -2175460: 251, -2175461: 72, -2175462: 22, -2175463: 24, -2175464: 83, -2175465: 24, -2175466: 99, -2175467: 1, -2175468: 255, -2175469: 127, -2175470: 35, -2175471: 0, -2175472: 5, -2175473: 178, -2175474: 114, -2175475: 199, -2175476: 113, -2175477: 3, -2175478: 77, -2175479: 83, -2175480: 24, -2175481: 99, -2175482: 1, -2175483: 255, -2175484: 127, -2175485: 35, -2175486: 0, -2175487: 27, -2175488: 170, -2175489: 21, -2175490: 5, -2175491: 1, -2175492: 129, -2175493: 0, -2175494: 74, -2175495: 21, -2175496: 231, -2175497: 12, -2175498: 99, -2175499: 0, -1126924: 0, -1126925: 56, -1126926: 159, -1126927: 3, -1126928: 191, -1126929: 1, -1126930: 15, -1126931: 0, -1126932: 5, -1126933: 0, -1126934: 31, -2175511: 32, -1126936: 91, -2175513: 56, -1126938: 186, -1126939: 0, -1126940: 17, -1126941: 0, -1126942: 114, -1126943: 79, -1126944: 173, -1126945: 54, -1126946: 200, -2175523: 32, -2175524: 195, -2175525: 32, -1126950: 31, -1126951: 126, -1126952: 21, -1126953: 84, -1126954: 10, -1126955: 40, -2175532: 74, -2175533: 69, -2175534: 132, -2175535: 44, -2175536: 0, -2175537: 20, -2175538: 0, -2175539: 0, -2175540: 18, -2175541: 0, -2175542: 248, -2175543: 1, -2175544: 82, -2175545: 74, -2175546: 35, -2175547: 0, -2175548: 89, -2175549: 24, -2175550: 99, -2175551: 1, -2175552: 255, -2175553: 127, -2175554: 35, -2175555: 0, -2175556: 29, -2175557: 123, -2175558: 71, -2175559: 82, -2175560: 46, -2175561: 198, -2175562: 0, -2175563: 99, -2175564: 0, -2175565: 181, -2175566: 58, -2175567: 16, -2175568: 34, -2175569: 107, -2175570: 17, -2175571: 8, -2175572: 5, -2175573: 255, -2175574: 127, -2175575: 181, -2175576: 54, -2175577: 173, -2175578: 25, -2175579: 41, -2175580: 9, -2175581: 29, -2175582: 56, -2175583: 20, -2175584: 24, -2175585: 10, -2175586: 0, -2175587: 255, -1127012: 0, -1127013: 56, -1127014: 250, -1127015: 114, -1127016: 176, -1127017: 85, -1127018: 69, -2175595: 172, -1127020: 1, -2175597: 115, -2175598: 90, -2175599: 173, -2175600: 65, -2175601: 8, -2175602: 45, -2175603: 99, -2175604: 24, -2175605: 177, -2175606: 11, -2175607: 251, -2175608: 72, -2175609: 255, -2175610: 127, -2175611: 0, -2175612: 0, -2175613: 255, -2175614: 127, -2175615: 229, -2175616: 68, -2175617: 195, -2175618: 8, -2175619: 21, -2175620: 3, -2175621: 32, -2175622: 177, -2175623: 11, -2175624: 169, -2175625: 30, -2175626: 69, -2175627: 1, -2175628: 187, -2175629: 94, -2175630: 179, -2175631: 61, -2175632: 46, -2175633: 41, -2175634: 134, -2175635: 20, -2175636: 24, -2175637: 99, -2175638: 82, -2175639: 74, -2175640: 140, -2175641: 49, -2175642: 195, -2175643: 32, -2175644: 2, -2175645: 223, -2175646: 2, -2175647: 31, -2175648: 34, -2175649: 0, -2175650: 29, -2175651: 3, -2175652: 32, -2175653: 188, -2175654: 114, -2175655: 251, -2175656: 72, -2175657: 22, -2175658: 24, -2175659: 144, -2175660: 22, -2175661: 20, -2175662: 14, -2175663: 229, -2175664: 0, -2175665: 65, -2175666: 0, -2175667: 198, -2175668: 48, -2175669: 98, -2175670: 36, -2175671: 33, -2175672: 20, -2175673: 0, -2175674: 8, -2175675: 255, -2175676: 126, -2175677: 64, -2175678: 24, -2175679: 255, -2175680: 127, -2175681: 35, -2175682: 0, -2175683: 21, -2175684: 178, -2175685: 114, -2175686: 199, -2175687: 113, -2175688: 3, -2175689: 77, -2175690: 164, -2175691: 48, -1127116: 17, -2175693: 36, -2175694: 64, -2175695: 24, -2175696: 0, -2175697: 8, -2175698: 82, -2175699: 94, -2175700: 107, -2175701: 65, -2175702: 228, -2175703: 48, -2175704: 96, -2175705: 28, -2175706: 67, -2175707: 24, -2175708: 99, -2175709: 1, -2175710: 156, -2175711: 127, -2175712: 35, -2175713: 0, -2175714: 67, -2175715: 24, -2175716: 99, -2175717: 23, -2175718: 224, -2175719: 32, -2175720: 167, -2175721: 29, -2175722: 33, -2175723: 45, -2175724: 160, -2175725: 40, -2175726: 32, -2175727: 24, -2175728: 169, -2175729: 38, -2175730: 233, -2175731: 37, -2175732: 66, -2175733: 21, -2175734: 32, -2175735: 4, -2175736: 169, -2175737: 38, -2175738: 130, -2175739: 0, -2175740: 113, -2175741: 39, -2175742: 35, -2175743: 0, -2175744: 27, -2175745: 191, -2175746: 2, -2175747: 127, -2175748: 1, -2175749: 21, -2175750: 0, -2175751: 149, -2175752: 79, -2175753: 174, -2175754: 66, -2175755: 6, -2175756: 50, -2175757: 35, -2175758: 37, -2175759: 42, -2175760: 21, -2175761: 199, -2175762: 20, -2175763: 99, -2175764: 20, -2175765: 2, -2175766: 4, -2175767: 161, -2175768: 12, -2175769: 64, -2175770: 24, -2175771: 181, -2175772: 126, -2175773: 35, -2175774: 0, -2175775: 25, -2175776: 186, -2175777: 94, -2175778: 54, -2175779: 78, -2175780: 178, -2175781: 61, -2175782: 46, -2175783: 45, -2175784: 203, -2175785: 32, -2175786: 72, -2175787: 16, -2175788: 2, -2175789: 8, -2175790: 119, -2175791: 26, -2175792: 209, -2175793: 33, -2175794: 199, -2175795: 36, -2175796: 66, -2175797: 0, -2175798: 28, -2175799: 107, -2175800: 24, -2175801: 99, -2175802: 194, -2175803: 32, -2175804: 24, -2175805: 12, -2175806: 0, -2175807: 0, -2175808: 34, -2175809: 93, -2175810: 99, -2175811: 68, -2175812: 64, -2175813: 24, -2175814: 192, -2175815: 36, -2175816: 160, -2175817: 28, -2175818: 128, -2175819: 20, -2175820: 64, -2175821: 16, -2175822: 223, -2175823: 22, -2175824: 215, -2175825: 21, -2175826: 238, -2175827: 20, -2175828: 134, -2175829: 20, -2175830: 67, -2175831: 24, -2175832: 99, -2175833: 3, -2175834: 223, -2175835: 22, -2175836: 0, -2175837: 8, -2175838: 255, -2175839: 27, -2175840: 0, -2175841: 0, -2175842: 223, -2175843: 2, -2175844: 215, -2175845: 1, -2175846: 172, -2175847: 0, -2175848: 115, -2175849: 90, -2175850: 173, -2175851: 65, -2175852: 8, -2175853: 45, -2175854: 99, -2175855: 24, -2175856: 177, -2175857: 11, -2175858: 251, -2175859: 72, -2175860: 255, -2175861: 127, -2175862: 0, -2175863: 0, -2175864: 255, -2175865: 127, -2175866: 229, -2175867: 68, -2175868: 195, -2175869: 8, -2175870: 21, -2175871: 3, -2175872: 32, -2175873: 177, -2175874: 11, -2175875: 169, -2175876: 30, -2175877: 69, -2175878: 1, -2175879: 187, -2175880: 94, -2175881: 179, -2175882: 61, -2175883: 46, -2175884: 41, -2175885: 134, -2175886: 20, -2175887: 24, -2175888: 99, -2175889: 82, -2175890: 74, -2175891: 140, -2175892: 49, -2175893: 195, -2175894: 32, -2175895: 2, -2175896: 223, -2175897: 2, -2175898: 31, -2175899: 34, -2175900: 0, -2175901: 15, -2175902: 3, -2175903: 32, -2175904: 188, -2175905: 114, -2175906: 251, -2175907: 72, -2175908: 22, -2175909: 24, -2175910: 10, -2175911: 29, -2175912: 199, -2175913: 24, -2175914: 69, -2175915: 12, -2175916: 3, -2175917: 4, -2175918: 75, -2175919: 24, -2175920: 99, -2175921: 1, -2175922: 255, -2175923: 127, -2175924: 35, -2175925: 0, -2175926: 13, -2175927: 178, -2175928: 114, -2175929: 199, -2175930: 113, -2175931: 3, -2175932: 77, -2175933: 197, -2175934: 52, -2175935: 130, -2175936: 40, -2175937: 64, -2175938: 32, -2175939: 0, -2175940: 8, -2175941: 71, -2175942: 24, -2175943: 99, -2175944: 5, -2175945: 52, -2175946: 127, -2175947: 0, -2175948: 125, -2175949: 247, -2175950: 91, -2175951: 35, -2175952: 0, -2175953: 27, -2175954: 191, -2175955: 42, -2175956: 127, -2175957: 41, -2175958: 21, -2175959: 40, -2175960: 63, -2175961: 54, -2175962: 124, -2175963: 45, -2175964: 23, -2175965: 33, -2175966: 211, -2175967: 28, -2175968: 176, -2175969: 20, -2175970: 108, -2175971: 16, -2175972: 72, -2175973: 12, -2175974: 4, -2175975: 8, -2175976: 82, -2175977: 74, -2175978: 231, -2175979: 28, -2175980: 255, -2175981: 127, -2175982: 35, -2175983: 0, -2175984: 67, -2175985: 24, -2175986: 99, -2175987: 18, -2175988: 84, -2175989: 42, -2175990: 175, -2175991: 25, -2175992: 41, -2175993: 21, -2175994: 132, -2175995: 8, -2175996: 33, -2175997: 8, -2175998: 176, -2175999: 22, -2176000: 192, -2176001: 29, -2176002: 224, -2176003: 20, -2176004: 96, -2176005: 20, -2176006: 231, -2176007: 194, -2176008: 138, -2176009: 1, -2176010: 255, -2176011: 127, -2176012: 35, -2176013: 0, -2176014: 69, -2176015: 24, -2176016: 99, -2176017: 17, -2176018: 23, -2176019: 76, -2176020: 15, -2176021: 40, -2176022: 9, -2176023: 36, -2176024: 7, -2176025: 28, -2176026: 5, -2176027: 20, -2176028: 3, -2176029: 12, -2176030: 2, -2176031: 8, -2176032: 1, -2176033: 4, -2176034: 59, -2176035: 93, -2176036: 195, -2176037: 32, -2176038: 35, -2176039: 0, -2176040: 22, -2176041: 191, -2176042: 2, -2176043: 127, -2176044: 1, -2176045: 21, -2176046: 0, -2176047: 73, -2176048: 55, -2176049: 192, -2176050: 62, -2176051: 128, -2176052: 29, -2176053: 192, -2176054: 4, -2176055: 0, -2176056: 1, -2176057: 192, -2176058: 0, -2176059: 128, -2176060: 0, -2176061: 64, -2176062: 0, -2176063: 52, -2176064: 68, -2176065: 127, -2176066: 255, -2176067: 1, -2176068: 0, -2176069: 0, -2176070: 255, -2176071: 27, -2176072: 0, -2176073: 0, -2176074: 223, -2176075: 2, -2176076: 215, -2176077: 1, -2176078: 172, -2176079: 0, -2176080: 115, -2176081: 90, -2176082: 173, -2176083: 65, -2176084: 8, -2176085: 45, -2176086: 99, -2176087: 24, -2176088: 177, -2176089: 11, -2176090: 251, -2176091: 72, -2176092: 255, -2176093: 127, -2176094: 0, -2176095: 0, -2176096: 255, -2176097: 127, -2176098: 229, -2176099: 68, -2176100: 195, -2176101: 8, -2176102: 21, -2176103: 3, -2176104: 32, -2176105: 177, -2176106: 11, -2176107: 169, -2176108: 30, -2176109: 69, -2176110: 1, -2176111: 187, -2176112: 94, -2176113: 179, -2176114: 61, -2176115: 46, -2176116: 41, -2176117: 134, -2176118: 20, -2176119: 24, -2176120: 99, -2176121: 82, -2176122: 74, -2176123: 140, -2176124: 49, -2176125: 195, -2176126: 32, -2176127: 2, -2176128: 223, -2176129: 2, -2176130: 31, -2176131: 34, -2176132: 0, -2176133: 15, -2176134: 3, -2176135: 32, -2176136: 188, -2176137: 114, -2176138: 251, -2176139: 72, -2176140: 22, -2176141: 24, -2176142: 10, -2176143: 29, -2176144: 199, -2176145: 24, -2176146: 69, -2176147: 12, -2176148: 3, -2176149: 4, -2176150: 75, -2176151: 24, -2176152: 99, -2176153: 1, -2176154: 255, -2176155: 127, -2176156: 35, -2176157: 0, -2176158: 5, -2176159: 178, -2176160: 114, -2176161: 199, -2176162: 113, -2176163: 3, -2176164: 77, -2176165: 83, -2176166: 24, -2176167: 99, -2176168: 1, -2176169: 255, -2176170: 127, -2176171: 35, -2176172: 0, -2176173: 27, -2176174: 191, -2176175: 2, -2176176: 127, -2176177: 1, -2176178: 21, -2176179: 0, -2176180: 140, -2176181: 69, -2176182: 107, -2176183: 61, -2176184: 41, -2176185: 53, -2176186: 8, -2176187: 45, -2176188: 198, -2176189: 36, -2176190: 165, -2176191: 24, -2176192: 99, -2176193: 16, -2176194: 66, -2176195: 8, -2176196: 19, -2176197: 127, -2176198: 0, -2176199: 125, -2176200: 255, -2176201: 127, -2176202: 35, -2176203: 0, -2176204: 67, -2176205: 24, -2176206: 99, -2176207: 19, -2176208: 250, -2176209: 107, -2176210: 179, -2176211: 110, -2176212: 11, -2176213: 78, -2176214: 6, -2176215: 49, -2176216: 65, -2176217: 12, -2176218: 176, -2176219: 22, -2176220: 192, -2176221: 29, -2176222: 224, -2176223: 20, -2176224: 96, -2176225: 20, -2176226: 72, -2176227: 69, -2176228: 195, -2176229: 64, -2176230: 35, -2176231: 0, -2176232: 69, -2176233: 24, -2176234: 99, -2176235: 17, -2176236: 244, -2176237: 65, -2176238: 145, -2176239: 53, -2176240: 79, -2176241: 45, -2176242: 13, -2176243: 37, -2176244: 203, -2176245: 28, -2176246: 137, -2176247: 20, -2176248: 68, -2176249: 12, -2176250: 33, -2176251: 4, -2176252: 153, -2176253: 86, -2176254: 195, -2176255: 32, -2176256: 35, -2176257: 0, -2176258: 197, -2176259: 96, -2176260: 199, -2176261: 64, -2176262: 73, -2176263: 24, -2176264: 99, -2176265: 67, -2176266: 255, -2176267: 127, -2176268: 1, -2176269: 0, -2176270: 0, -2176271: 255, -2176272: 29, -2176273: 0, -2176274: 0, -2176275: 223, -2176276: 2, -2176277: 215, -2176278: 1, -2176279: 172, -2176280: 0, -2176281: 115, -2176282: 90, -2176283: 173, -2176284: 65, -2176285: 8, -2176286: 45, -2176287: 99, -2176288: 24, -2176289: 177, -2176290: 11, -2176291: 251, -2176292: 72, -2176293: 255, -2176294: 127, -2176295: 0, -2176296: 0, -2176297: 255, -2176298: 127, -2176299: 229, -2176300: 68, -2176301: 255, -2176302: 127, -2176303: 34, -2176304: 0, -2176305: 20, -2176306: 56, -2176307: 177, -2176308: 11, -2176309: 169, -2176310: 30, -2176311: 69, -2176312: 1, -2176313: 187, -2176314: 94, -2176315: 179, -2176316: 61, -2176317: 46, -2176318: 41, -2176319: 134, -2176320: 20, -2176321: 24, -2176322: 99, -2176323: 82, -2176324: 74, -2176325: 140, -2176326: 49, -2176327: 195, -2176328: 32, -2176329: 2, -2176330: 223, -2176331: 2, -2176332: 31, -2176333: 35, -2176334: 0, -2176335: 28, -2176336: 56, -2176337: 188, -2176338: 114, -2176339: 251, -2176340: 72, -2176341: 22, -2176342: 24, -2176343: 144, -2176344: 22, -2176345: 20, -2176346: 14, -2176347: 229, -2176348: 0, -2176349: 65, -2176350: 0, -2176351: 198, -2176352: 48, -2176353: 98, -2176354: 36, -2176355: 33, -2176356: 20, -2176357: 0, -2176358: 8, -2176359: 255, -2176360: 126, -2176361: 24, -2176362: 99, -2176363: 255, -2176364: 127, -2176365: 34, -2176366: 0, -2176367: 14, -2176368: 56, -2176369: 178, -2176370: 114, -2176371: 199, -2176372: 113, -2176373: 3, -2176374: 77, -2176375: 73, -2176376: 61, -2176377: 196, -2176378: 48, -2176379: 64, -2176380: 36, -2176381: 0, -2176382: 16, -2176383: 75, -2176384: 24, -2176385: 99, -2176386: 1, -2176387: 255, -2176388: 127, -2176389: 34, -2176390: 0, -2176391: 0, -2176392: 56, -2176393: 69, -2176394: 24, -2176395: 99, -2176396: 17, -2176397: 46, -2176398: 31, -2176399: 130, -2176400: 21, -2176401: 128, -2176402: 12, -2176403: 0, -2176404: 4, -2176405: 154, -2176406: 103, -2176407: 76, -2176408: 42, -2176409: 163, -2176410: 17, -2176411: 32, -2176412: 4, -2176413: 255, -2176414: 3, -2176415: 195, -2176416: 32, -2176417: 34, -2176418: 0, -2176419: 28, -2176420: 56, -2176421: 191, -2176422: 2, -2176423: 127, -2176424: 1, -2176425: 21, -2176426: 0, -2176427: 73, -2176428: 55, -2176429: 192, -2176430: 62, -2176431: 128, -2176432: 29, -2176433: 192, -2176434: 4, -2176435: 0, -2176436: 1, -2176437: 192, -2176438: 0, -2176439: 128, -2176440: 0, -2176441: 64, -2176442: 0, -2176443: 19, -2176444: 127, -2176445: 0, -2176446: 125, -2176447: 255, -2176448: 127, -2176449: 34, -2176450: 0, -2176451: 0, -2176452: 56, -2176453: 62, -2176454: 0, -2176455: 24, -2176456: 56, -2176457: 157, -2176458: 85, -2176459: 22, -2176460: 24, -2176461: 13, -2176462: 16, -2176463: 159, -2176464: 75, -2176465: 55, -2176466: 63, -2176467: 208, -2176468: 54, -2176469: 105, -2176470: 46, -2176471: 8, -2176472: 38, -2176473: 166, -2176474: 29, -2176475: 37, -2176476: 17, -2176477: 197, -2176478: 8, -2176479: 3, -2176480: 0, -2176481: 197, -2176482: 96, -2176483: 255, -2176484: 27, -2176485: 0, -2176486: 0, -2176487: 223, -2176488: 2, -2176489: 215, -2176490: 1, -2176491: 172, -2176492: 0, -2176493: 187, -2176494: 94, -2176495: 179, -2176496: 61, -2176497: 46, -2176498: 41, -2176499: 134, -2176500: 20, -2176501: 177, -2176502: 11, -2176503: 251, -2176504: 72, -2176505: 255, -2176506: 127, -2176507: 0, -2176508: 0, -2176509: 255, -2176510: 127, -2176511: 229, -2176512: 68, -2176513: 195, -2176514: 8, -2176515: 7, -2176516: 3, -2176517: 32, -2176518: 177, -2176519: 11, -2176520: 169, -2176521: 30, -2176522: 69, -2176523: 1, -2176524: 199, -2176525: 32, -2176526: 5, -2176527: 223, -2176528: 14, -2176529: 63, -2176530: 14, -2176531: 127, -2176532: 13, -2176533: 195, -2176534: 32, -2176535: 2, -2176536: 223, -2176537: 2, -2176538: 31, -2176539: 34, -2176540: 0, -2176541: 7, -2176542: 3, -2176543: 32, -2176544: 188, -2176545: 114, -2176546: 251, -2176547: 72, -2176548: 22, -2176549: 24, -2176550: 205, -2176551: 32, -2176552: 7, -2176553: 159, -2176554: 12, -2176555: 151, -2176556: 54, -2176557: 63, -2176558: 14, -2176559: 255, -2176560: 127, -2176561: 35, -2176562: 0, -2176563: 5, -2176564: 178, -2176565: 114, -2176566: 199, -2176567: 113, -2176568: 3, -2176569: 77, -2176570: 199, -2176571: 24, -2176572: 13, -2176573: 247, -2176574: 94, -2176575: 82, -2176576: 74, -2176577: 107, -2176578: 45, -2176579: 132, -2176580: 16, -2176581: 63, -2176582: 14, -2176583: 82, -2176584: 74, -2176585: 255, -2176586: 127, -2176587: 35, -2176588: 0, -2176589: 197, -2176590: 24, -2176591: 21, -2176592: 236, -2176593: 83, -2176594: 9, -2176595: 67, -2176596: 197, -2176597: 37, -2176598: 194, -2176599: 16, -2176600: 153, -2176601: 12, -2176602: 114, -2176603: 12, -2176604: 73, -2176605: 12, -2176606: 65, -2176607: 12, -2176608: 9, -2176609: 67, -2176610: 119, -2176611: 12, -2176612: 255, -2176613: 127, -2176614: 35, -2176615: 0, -2176616: 197, -2176617: 32, -2176618: 21, -2176619: 58, -2176620: 58, -2176621: 179, -2176622: 45, -2176623: 44, -2176624: 33, -2176625: 132, -2176626: 16, -2176627: 85, -2176628: 85, -2176629: 207, -2176630: 56, -2176631: 105, -2176632: 28, -2176633: 3, -2176634: 4, -2176635: 179, -2176636: 45, -2176637: 207, -2176638: 56, -2176639: 255, -2176640: 127, -2176641: 35, -2176642: 0, -2176643: 16, -2176644: 150, -2176645: 5, -2176646: 214, -2176647: 4, -2176648: 86, -2176649: 4, -2176650: 204, -2176651: 8, -2176652: 168, -2176653: 8, -2176654: 133, -2176655: 8, -2176656: 130, -2176657: 8, -2176658: 39, -2176659: 16, -2176660: 5, -2176661: 67, -2176662: 12, -2176663: 3, -2176664: 194, -2176665: 14, -2176666: 3, -2176667: 5, -2176668: 12, -2176669: 255, -2176670: 127, -2176671: 35, -2176672: 0, -2176673: 197, -2176674: 32, -2176675: 12, -2176676: 12, -2176677: 20, -2176678: 8, -2176679: 12, -2176680: 4, -2176681: 4, -2176682: 0, -2176683: 0, -2176684: 14, -2176685: 8, -2176686: 9, -2176687: 4, -2176688: 4, -2176689: 34, -2176690: 0, -2176691: 3, -2176692: 8, -2176693: 12, -2176694: 9, -2176695: 4, -2176696: 195, -2176697: 32, -2176698: 255, -2176699: 27, -2176700: 0, -2176701: 0, -2176702: 223, -2176703: 2, -2176704: 215, -2176705: 1, -2176706: 172, -2176707: 0, -2176708: 53, -2176709: 107, -2176710: 14, -2176711: 74, -2176712: 106, -2176713: 53, -2176714: 163, -2176715: 32, -2176716: 177, -2176717: 11, -2176718: 251, -2176719: 72, -2176720: 255, -2176721: 127, -2176722: 0, -2176723: 0, -2176724: 255, -2176725: 127, -2176726: 229, -2176727: 68, -2176728: 195, -2176729: 8, -2176730: 0, -2176731: 3, -2176732: 194, -2176733: 18, -2176734: 17, -2176735: 169, -2176736: 30, -2176737: 69, -2176738: 1, -2176739: 187, -2176740: 94, -2176741: 179, -2176742: 61, -2176743: 46, -2176744: 41, -2176745: 134, -2176746: 20, -2176747: 223, -2176748: 14, -2176749: 63, -2176750: 14, -2176751: 127, -2176752: 13, -2176753: 195, -2176754: 32, -2176755: 2, -2176756: 223, -2176757: 2, -2176758: 31, -2176759: 34, -2176760: 0, -2176761: 7, -2176762: 3, -2176763: 32, -2176764: 188, -2176765: 114, -2176766: 251, -2176767: 72, -2176768: 22, -2176769: 24, -2176770: 205, -2176771: 32, -2176772: 7, -2176773: 159, -2176774: 12, -2176775: 151, -2176776: 54, -2176777: 63, -2176778: 14, -2176779: 255, -2176780: 127, -2176781: 35, -2176782: 0, -2176783: 5, -2176784: 178, -2176785: 114, -2176786: 199, -2176787: 113, -2176788: 3, -2176789: 77, -2176790: 199, -2176791: 24, -2176792: 13, -2176793: 247, -2176794: 94, -2176795: 82, -2176796: 74, -2176797: 107, -2176798: 45, -2176799: 132, -2176800: 16, -2176801: 63, -2176802: 14, -2176803: 82, -2176804: 74, -2176805: 255, -2176806: 127, -2176807: 35, -2176808: 0, -2176809: 197, -2176810: 24, -2176811: 21, -2176812: 236, -2176813: 83, -2176814: 9, -2176815: 67, -2176816: 197, -2176817: 37, -2176818: 194, -2176819: 16, -2176820: 121, -2176821: 78, -2176822: 212, -2176823: 57, -2176824: 237, -2176825: 28, -2176826: 65, -2176827: 12, -2176828: 9, -2176829: 67, -2176830: 119, -2176831: 12, -2176832: 255, -2176833: 127, -2176834: 35, -2176835: 0, -2176836: 197, -2176837: 32, -2176838: 21, -2176839: 58, -2176840: 58, -2176841: 179, -2176842: 45, -2176843: 44, -2176844: 33, -2176845: 99, -2176846: 12, -2176847: 85, -2176848: 85, -2176849: 207, -2176850: 56, -2176851: 105, -2176852: 28, -2176853: 3, -2176854: 4, -2176855: 179, -2176856: 45, -2176857: 207, -2176858: 56, -2176859: 255, -2176860: 127, -2176861: 35, -2176862: 0, -2176863: 16, -2176864: 150, -2176865: 5, -2176866: 214, -2176867: 4, -2176868: 86, -2176869: 4, -2176870: 206, -2176871: 32, -2176872: 137, -2176873: 28, -2176874: 101, -2176875: 20, -2176876: 32, -2176877: 20, -2176878: 39, -2176879: 16, -2176880: 5, -2176881: 67, -2176882: 12, -2176883: 3, -2176884: 6, -2176885: 8, -2176886: 168, -2176887: 8, -2176888: 5, -2176889: 12, -2176890: 255, -2176891: 127, -2176892: 35, -2176893: 0, -2176894: 197, -2176895: 32, -2176896: 19, -2176897: 136, -2176898: 20, -2176899: 70, -2176900: 12, -2176901: 35, -2176902: 4, -2176903: 0, -2176904: 0, -2176905: 14, -2176906: 33, -2176907: 201, -2176908: 24, -2176909: 100, -2176910: 12, -2176911: 0, -2176912: 0, -2176913: 8, -2176914: 12, -2176915: 9, -2176916: 4, -2176917: 195, -2176918: 32, -2176919: 255, -2176920: 27, -2176921: 0, -2176922: 0, -2176923: 223, -2176924: 2, -2176925: 215, -2176926: 1, -2176927: 172, -2176928: 0, -2176929: 115, -2176930: 90, -2176931: 173, -2176932: 65, -2176933: 8, -2176934: 45, -2176935: 99, -2176936: 24, -2176937: 177, -2176938: 11, -2176939: 251, -2176940: 72, -2176941: 255, -2176942: 127, -2176943: 0, -2176944: 0, -2176945: 255, -2176946: 127, -2176947: 229, -2176948: 68, -2176949: 195, -2176950: 8, -2176951: 27, -2176952: 3, -2176953: 32, -2176954: 177, -2176955: 11, -2176956: 169, -2176957: 30, -2176958: 69, -2176959: 1, -2176960: 187, -2176961: 94, -2176962: 179, -2176963: 61, -2176964: 46, -2176965: 41, -2176966: 134, -2176967: 20, -2176968: 24, -2176969: 99, -2176970: 82, -2176971: 74, -2176972: 140, -2176973: 49, -2176974: 0, -2176975: 0, -2176976: 24, -2176977: 99, -2176978: 223, -2176979: 2, -2176980: 197, -2176981: 32, -2176982: 5, -2176983: 188, -2176984: 114, -2176985: 157, -2176986: 85, -2176987: 22, -2176988: 24, -2176989: 81, -2176990: 24, -2176991: 99, -2176992: 1, -2176993: 0, -2176994: 0, -2176995: 67, -2176996: 24, -2176997: 99, -2176998: 7, -2176999: 0, -2177000: 0, -2177001: 178, -2177002: 114, -2177003: 199, -2177004: 113, -2177005: 99, -2177006: 68, -2177007: 81, -2177008: 24, -2177009: 99, -2177010: 1, -2177011: 0, -2177012: 0, -2177013: 67, -2177014: 24, -2177015: 99, -2177016: 1, -2177017: 0, -2177018: 0, -2177019: 93, -2177020: 24, -2177021: 99, -2177022: 1, -2177023: 0, -2177024: 0, -2177025: 93, -2177026: 24, -2177027: 99, -2177028: 195, -2177029: 170, -2177030: 25, -2177031: 255, -2177032: 45, -2177033: 191, -2177034: 40, -2177035: 144, -2177036: 28, -2177037: 105, -2177038: 20, -2177039: 36, -2177040: 20, -2177041: 123, -2177042: 74, -2177043: 27, -2177044: 54, -2177045: 85, -2177046: 25, -2177047: 110, -2177048: 0, -2177049: 8, -2177050: 0, -2177051: 159, -2177052: 3, -2177053: 58, -2177054: 2, -2177055: 118, -2177056: 1, -2177057: 35, -2177058: 0, -2177059: 194, -2177060: 32, -2177061: 18, -2177062: 13, -2177063: 191, -2177064: 8, -2177065: 149, -2177066: 8, -2177067: 108, -2177068: 8, -2177069: 71, -2177070: 4, -2177071: 126, -2177072: 107, -2177073: 30, -2177074: 87, -2177075: 88, -2177076: 58, -2177077: 113, -2177078: 33, -2177079: 203, -2177080: 12, -2177081: 199, -2177082: 32, -2177083: 255, -2177084: 27, -2177085: 0, -2177086: 0, -2177087: 223, -2177088: 2, -2177089: 215, -2177090: 1, -2177091: 172, -2177092: 0, -2177093: 25, -2177094: 106, -2177095: 51, -2177096: 81, -2177097: 171, -2177098: 52, -2177099: 102, -2177100: 32, -2177101: 177, -2177102: 11, -2177103: 251, -2177104: 72, -2177105: 255, -2177106: 127, -2177107: 0, -2177108: 0, -2177109: 255, -2177110: 127, -2177111: 229, -2177112: 68, -2177113: 195, -2177114: 8, -2177115: 0, -2177116: 3, -2177117: 194, -2177118: 18, -2177119: 17, -2177120: 169, -2177121: 30, -2177122: 69, -2177123: 1, -2177124: 187, -2177125: 94, -2177126: 179, -2177127: 61, -2177128: 46, -2177129: 41, -2177130: 134, -2177131: 20, -2177132: 16, -2177133: 66, -2177134: 0, -2177135: 4, -2177136: 129, -2177137: 20, -2177138: 195, -2177139: 32, -2177140: 2, -2177141: 223, -2177142: 2, -2177143: 31, -2177144: 34, -2177145: 0, -2177146: 224, -2177147: 61, -2177148: 3, -2177149: 32, -2177150: 188, -2177151: 114, -2177152: 251, -2177153: 72, -2177154: 22, -2177155: 24, -2177156: 157, -2177157: 42, -2177158: 214, -2177159: 25, -2177160: 16, -2177161: 13, -2177162: 72, -2177163: 0, -2177164: 133, -2177165: 33, -2177166: 2, -2177167: 21, -2177168: 193, -2177169: 16, -2177170: 96, -2177171: 4, -2177172: 72, -2177173: 66, -2177174: 181, -2177175: 26, -2177176: 255, -2177177: 127, -2177178: 33, -2177179: 8, -2177180: 0, -2177181: 0, -2177182: 178, -2177183: 114, -2177184: 199, -2177185: 113, -2177186: 3, -2177187: 77, -2177188: 170, -2177189: 33, -2177190: 71, -2177191: 21, -2177192: 229, -2177193: 16, -2177194: 130, -2177195: 4, -2177196: 201, -2177197: 16, -2177198: 135, -2177199: 12, -2177200: 36, -2177201: 4, -2177202: 1, -2177203: 0, -2177204: 110, -2177205: 33, -2177206: 128, -2177207: 8, -2177208: 185, -2177209: 54, -2177210: 35, -2177211: 0, -2177212: 28, -2177213: 102, -2177214: 69, -2177215: 227, -2177216: 40, -2177217: 96, -2177218: 28, -2177219: 176, -2177220: 54, -2177221: 11, -2177222: 38, -2177223: 104, -2177224: 25, -2177225: 226, -2177226: 12, -2177227: 202, -2177228: 90, -2177229: 72, -2177230: 62, -2177231: 99, -2177232: 33, -2177233: 163, -2177234: 4, -2177235: 124, -2177236: 81, -2177237: 23, -2177238: 127, -2177239: 255, -2177240: 127, -2177241: 32, -2177242: 34, -2177243: 0, -2177244: 27, -2177245: 236, -2177246: 41, -2177247: 104, -2177248: 29, -2177249: 6, -2177250: 21, -2177251: 236, -2177252: 82, -2177253: 71, -2177254: 62, -2177255: 68, -2177256: 33, -2177257: 193, -2177258: 16, -2177259: 135, -2177260: 0, -2177261: 233, -2177262: 12, -2177263: 76, -2177264: 17, -2177265: 242, -2177266: 25, -2177267: 2, -2177268: 0, -2177269: 36, -2177270: 0, -2177271: 255, -2177272: 127, -2177273: 35, -2177274: 0, -2177275: 5, -2177276: 49, -2177277: 49, -2177278: 238, -2177279: 40, -2177280: 167, -2177281: 24, -2177282: 197, -2177283: 62, -2177284: 15, -2177285: 128, -2177286: 4, -2177287: 10, -2177288: 13, -2177289: 168, -2177290: 8, -2177291: 102, -2177292: 4, -2177293: 68, -2177294: 0, -2177295: 236, -2177296: 8, -2177297: 215, -2177298: 22, -2177299: 16, -2177300: 66, -2177301: 35, -2177302: 0, -2177303: 69, -2177304: 16, -2177305: 66, -2177306: 199, -2177307: 96, -2177308: 15, -2177309: 87, -2177310: 119, -2177311: 114, -2177312: 110, -2177313: 238, -2177314: 89, -2177315: 106, -2177316: 69, -2177317: 40, -2177318: 53, -2177319: 5, -2177320: 37, -2177321: 156, -2177322: 127, -2177323: 0, -2177324: 0, -2177325: 255, -2177326: 27, -2177327: 0, -2177328: 0, -2177329: 223, -2177330: 2, -2177331: 215, -2177332: 1, -2177333: 172, -2177334: 0, -2177335: 250, -2177336: 105, -2177337: 52, -2177338: 73, -2177339: 173, -2177340: 44, -2177341: 103, -2177342: 20, -2177343: 177, -2177344: 11, -2177345: 251, -2177346: 72, -2177347: 255, -2177348: 127, -2177349: 0, -2177350: 0, -2177351: 255, -2177352: 127, -2177353: 229, -2177354: 68, -2177355: 195, -2177356: 8, -2177357: 21, -2177358: 3, -2177359: 32, -2177360: 177, -2177361: 11, -2177362: 169, -2177363: 30, -2177364: 69, -2177365: 1, -2177366: 187, -2177367: 94, -2177368: 179, -2177369: 61, -2177370: 46, -2177371: 41, -2177372: 134, -2177373: 20, -2177374: 16, -2177375: 66, -2177376: 0, -2177377: 4, -2177378: 35, -2177379: 20, -2177380: 195, -2177381: 32, -2177382: 2, -2177383: 223, -2177384: 2, -2177385: 31, -2177386: 34, -2177387: 0, -2177388: 23, -2177389: 3, -2177390: 32, -2177391: 188, -2177392: 114, -2177393: 251, -2177394: 72, -2177395: 22, -2177396: 24, -2177397: 217, -2177398: 62, -2177399: 87, -2177400: 46, -2177401: 53, -2177402: 42, -2177403: 243, -2177404: 37, -2177405: 210, -2177406: 37, -2177407: 176, -2177408: 29, -2177409: 110, -2177410: 25, -2177411: 46, -2177412: 17, -2177413: 195, -2177414: 80, -2177415: 1, -2177416: 255, -2177417: 127, -2177418: 35, -2177419: 0, -2177420: 27, -2177421: 178, -2177422: 114, -2177423: 199, -2177424: 113, -2177425: 3, -2177426: 77, -2177427: 32, -2177428: 0, -2177429: 97, -2177430: 4, -2177431: 162, -2177432: 8, -2177433: 227, -2177434: 12, -2177435: 36, -2177436: 17, -2177437: 101, -2177438: 21, -2177439: 166, -2177440: 25, -2177441: 9, -2177442: 38, -2177443: 102, -2177444: 4, -2177445: 36, -2177446: 0, -2177447: 255, -2177448: 127, -2177449: 35, -2177450: 0, -2177451: 224, -2177452: 59, -2177453: 106, -2177454: 61, -2177455: 40, -2177456: 53, -2177457: 197, -2177458: 40, -2177459: 209, -2177460: 29, -2177461: 110, -2177462: 21, -2177463: 11, -2177464: 17, -2177465: 135, -2177466: 4, -2177467: 100, -2177468: 37, -2177469: 3, -2177470: 29, -2177471: 194, -2177472: 20, -2177473: 129, -2177474: 12, -2177475: 255, -2177476: 127, -2177477: 36, -2177478: 0, -2177479: 40, -2177480: 54, -2177481: 96, -2177482: 8, -2177483: 0, -2177484: 0, -2177485: 191, -2177486: 99, -2177487: 93, -2177488: 83, -2177489: 251, -2177490: 66, -2177491: 239, -2177492: 69, -2177493: 107, -2177494: 53, -2177495: 231, -2177496: 36, -2177497: 132, -2177498: 24, -2177499: 135, -2177500: 0, -2177501: 233, -2177502: 12, -2177503: 76, -2177504: 17, -2177505: 242, -2177506: 25, -2177507: 1, -2177508: 0, -2177509: 38, -2177510: 0, -2177511: 90, -2177512: 115, -2177513: 35, -2177514: 0, -2177515: 224, -2177516: 61, -2177517: 36, -2177518: 29, -2177519: 161, -2177520: 16, -2177521: 96, -2177522: 8, -2177523: 2, -2177524: 0, -2177525: 35, -2177526: 0, -2177527: 69, -2177528: 4, -2177529: 102, -2177530: 4, -2177531: 103, -2177532: 8, -2177533: 136, -2177534: 8, -2177535: 170, -2177536: 12, -2177537: 203, -2177538: 12, -2177539: 16, -2177540: 66, -2177541: 255, -2177542: 127, -2177543: 35, -2177544: 0, -2177545: 64, -2177546: 4, -2177547: 0, -2177548: 0, -2177549: 21, -2177550: 38, -2177551: 113, -2177552: 25, -2177553: 235, -2177554: 12, -2177555: 154, -2177556: 101, -2177557: 51, -2177558: 77, -2177559: 238, -2177560: 60, -2177561: 137, -2177562: 44, -2177563: 91, -2177564: 123, -2177565: 182, -2177566: 102, -2177567: 50, -2177568: 86, -2177569: 207, -2177570: 73, -2177571: 45, -2177572: 61, -2177573: 201, -2177574: 40, -2177575: 101, -2177576: 28, -2177577: 1, -2177578: 8, -2177579: 255, -2177580: 29, -2177581: 0, -2177582: 0, -2177583: 223, -2177584: 2, -2177585: 215, -2177586: 1, -2177587: 172, -2177588: 0, -2177589: 244, -2177590: 106, -2177591: 111, -2177592: 90, -2177593: 170, -2177594: 61, -2177595: 229, -2177596: 32, -2177597: 177, -2177598: 11, -2177599: 251, -2177600: 72, -2177601: 255, -2177602: 127, -2177603: 0, -2177604: 0, -2177605: 255, -2177606: 127, -2177607: 229, -2177608: 68, -2177609: 255, -2177610: 127, -2177611: 34, -2177612: 0, -2177613: 20, -2177614: 56, -2177615: 177, -2177616: 11, -2177617: 169, -2177618: 30, -2177619: 69, -2177620: 1, -2177621: 187, -2177622: 94, -2177623: 179, -2177624: 61, -2177625: 46, -2177626: 41, -2177627: 134, -2177628: 20, -2177629: 24, -2177630: 99, -2177631: 82, -2177632: 74, -2177633: 140, -2177634: 49, -2177635: 195, -2177636: 32, -2177637: 2, -2177638: 223, -2177639: 2, -2177640: 31, -2177641: 35, -2177642: 0, -2177643: 6, -2177644: 56, -2177645: 188, -2177646: 114, -2177647: 251, -2177648: 72, -2177649: 22, -2177650: 24, -2177651: 83, -2177652: 128, -2177653: 21, -2177654: 1, -2177655: 255, -2177656: 127, -2177657: 34, -2177658: 0, -2177659: 6, -2177660: 56, -2177661: 178, -2177662: 114, -2177663: 199, -2177664: 113, -2177665: 3, -2177666: 77, -2177667: 83, -2177668: 128, -2177669: 21, -2177670: 1, -2177671: 255, -2177672: 127, -2177673: 34, -1129098: 2, -2177675: 8, -2177676: 56, -2177677: 255, -2177678: 127, -2177679: 247, -2177680: 94, -2177681: 16, -2177682: 66, -2177683: 8, -2177684: 33, -2177685: 199, -2177686: 8, -2177687: 77, -2177688: 128, -2177689: 21, -2177690: 31, -2177691: 0, -2177692: 56, -2177693: 87, -2177694: 63, -2177695: 77, -2177696: 46, -2177697: 226, -2177698: 0, -2177699: 96, -2177700: 0, -2177701: 176, -2177702: 58, -2177703: 11, -2177704: 34, -2177705: 102, -2177706: 17, -2177707: 36, -2177708: 9, -2177709: 25, -2177710: 3, -2177711: 84, -2177712: 2, -2177713: 143, -2177714: 1, -2177715: 202, -2177716: 0, -2177717: 27, -2177718: 88, -2177719: 146, -2177720: 24, -2177721: 69, -2177722: 1, -2177723: 194, -2177724: 64, -2177725: 14, -2177726: 87, -2177727: 247, -2177728: 66, -2177729: 140, -2177730: 21, -2177731: 165, -2177732: 0, -2177733: 90, -2177734: 79, -2177735: 181, -2177736: 54, -2177737: 16, -2177738: 38, -2177739: 206, -2177740: 29, -2177741: 205, -2177742: 48, -2177743: 21, -2177744: 0, -2177745: 56, -2177746: 156, -2177747: 75, -2177748: 148, -2177749: 54, -2177750: 41, -2177751: 9, -2177752: 66, -2177753: 0, -2177754: 247, -2177755: 66, -2177756: 82, -2177757: 42, -2177758: 173, -2177759: 25, -2177760: 107, -2177761: 17, -2177762: 231, -2177763: 0, -2177764: 132, -2177765: 0, -2177766: 71, -2177767: 128, -2177768: 21, -2177769: 1, -2177770: 224, -2177771: 127, -2177772: 255, -2177773: 29, -2177774: 0, -2177775: 0, -2177776: 223, -2177777: 2, -2177778: 215, -2177779: 1, -2177780: 172, -2177781: 0, -2177782: 115, -2177783: 90, -2177784: 173, -2177785: 65, -2177786: 8, -2177787: 45, -2177788: 99, -2177789: 24, -2177790: 177, -2177791: 11, -2177792: 251, -2177793: 72, -2177794: 255, -2177795: 127, -2177796: 0, -2177797: 0, -2177798: 255, -2177799: 127, -2177800: 229, -2177801: 68, -2177802: 255, -2177803: 127, -2177804: 35, -2177805: 0, -2177806: 19, -2177807: 177, -2177808: 11, -2177809: 169, -2177810: 30, -2177811: 69, -2177812: 1, -2177813: 187, -2177814: 94, -2177815: 179, -2177816: 61, -2177817: 46, -2177818: 41, -2177819: 134, -2177820: 20, -2177821: 24, -2177822: 99, -2177823: 82, -2177824: 74, -2177825: 140, -2177826: 49, -2177827: 195, -2177828: 32, -2177829: 2, -2177830: 223, -2177831: 2, -2177832: 31, -2177833: 36, -2177834: 0, -2177835: 21, -2177836: 188, -2177837: 114, -2177838: 251, -2177839: 72, -2177840: 22, -2177841: 24, -2177842: 84, -2177843: 47, -2177844: 114, -2177845: 2, -2177846: 110, -2177847: 1, -2177848: 71, -2177849: 0, -2177850: 138, -2177851: 24, -2177852: 39, -2177853: 12, -2177854: 36, -2177855: 4, -2177856: 2, -2177857: 4, -2177858: 67, -2177859: 69, -2177860: 1, -2177861: 1, -2177862: 255, -2177863: 127, -2177864: 35, -2177865: 0, -2177866: 21, -2177867: 178, -2177868: 114, -2177869: 199, -2177870: 113, -2177871: 3, -2177872: 77, -2177873: 30, -2177874: 103, -2177875: 22, -2177876: 74, -2177877: 145, -2177878: 53, -2177879: 233, -2177880: 32, -2177881: 237, -2177882: 65, -2177883: 39, -2177884: 45, -2177885: 193, -2177886: 24, -2177887: 96, -2177888: 16, -2177889: 35, -2177890: 0, -2177891: 1, -2177892: 223, -2177893: 2, -2177894: 35, -2177895: 0, -2177896: 15, -2177897: 249, -2177898: 70, -2177899: 18, -2177900: 54, -2177901: 233, -2177902: 16, -2177903: 165, -2177904: 0, -2177905: 117, -2177906: 66, -2177907: 208, -2177908: 41, -2177909: 43, -2177910: 25, -2177911: 200, -2177912: 12, -2177913: 199, -2177914: 74, -2177915: 7, -2177916: 188, -2177917: 114, -2177918: 157, -2177919: 85, -2177920: 22, -2177921: 24, -2177922: 0, -2177923: 0, -2177924: 67, -2177925: 24, -2177926: 99, -2177927: 23, -2177928: 2, -2177929: 8, -2177930: 148, -2177931: 82, -2177932: 206, -2177933: 57, -2177934: 8, -2177935: 33, -2177936: 132, -2177937: 16, -2177938: 25, -2177939: 0, -2177940: 18, -2177941: 0, -2177942: 0, -2177943: 92, -2177944: 0, -2177945: 64, -2177946: 132, -2177947: 16, -2177948: 127, -2177949: 25, -2177950: 255, -2177951: 127, -2177952: 35, -2177953: 0, -2177954: 15, -2177955: 156, -2177956: 75, -2177957: 148, -2177958: 54, -2177959: 41, -2177960: 9, -2177961: 66, -2177962: 0, -2177963: 247, -2177964: 66, -2177965: 82, -2177966: 42, -2177967: 173, -2177968: 25, -2177969: 107, -2177970: 17, -2177971: 75, -2177972: 69, -2177973: 1, -2177974: 3, -2177975: 2, -2177976: 4, -2177977: 0, -2177978: 0, -2177979: 67, -2177980: 24, -2177981: 99, -2177982: 217, -2177983: 64, -2177984: 255, -2177985: 27, -2177986: 0, -2177987: 0, -2177988: 223, -2177989: 2, -2177990: 215, -2177991: 1, -2177992: 172, -2177993: 0, -2177994: 115, -2177995: 90, -2177996: 173, -2177997: 65, -2177998: 8, -2177999: 45, -2178000: 99, -2178001: 24, -2178002: 177, -2178003: 11, -2178004: 251, -2178005: 72, -2178006: 255, -2178007: 127, -2178008: 0, -2178009: 0, -2178010: 255, -2178011: 127, -2178012: 229, -2178013: 68, -2178014: 195, -2178015: 8, -2178016: 21, -2178017: 3, -2178018: 32, -2178019: 177, -2178020: 11, -2178021: 169, -2178022: 30, -2178023: 69, -2178024: 1, -2178025: 187, -2178026: 94, -2178027: 179, -2178028: 61, -2178029: 46, -2178030: 41, -2178031: 134, -2178032: 20, -2178033: 24, -2178034: 99, -2178035: 63, -2178036: 14, -2178037: 127, -2178038: 13, -2178039: 195, -2178040: 32, -2178041: 2, -2178042: 223, -2178043: 2, -2178044: 31, -2178045: 34, -2178046: 0, -2178047: 23, -2178048: 3, -2178049: 32, -2178050: 188, -2178051: 114, -2178052: 251, -2178053: 72, -2178054: 22, -2178055: 24, -2178056: 84, -2178057: 47, -2178058: 114, -2178059: 2, -2178060: 110, -2178061: 1, -2178062: 71, -2178063: 0, -2178064: 138, -2178065: 24, -2178066: 39, -2178067: 12, -2178068: 36, -2178069: 4, -2178070: 2, -2178071: 4, -2178072: 67, -2178073: 69, -2178074: 1, -2178075: 1, -2178076: 255, -2178077: 127, -2178078: 35, -2178079: 0, -2178080: 21, -2178081: 178, -2178082: 114, -2178083: 199, -2178084: 113, -2178085: 3, -2178086: 77, -2178087: 22, -2178088: 74, -2178089: 145, -2178090: 57, -2178091: 44, -2178092: 45, -2178093: 167, -2178094: 28, -2178095: 229, -2178096: 32, -2178097: 164, -2178098: 24, -2178099: 131, -2178100: 16, -2178101: 65, -2178102: 8, -2178103: 35, -2178104: 0, -2178105: 1, -2178106: 223, -2178107: 2, -2178108: 35, -2178109: 0, -2178110: 27, -2178111: 191, -2178112: 4, -2178113: 150, -2178114: 4, -2178115: 77, -2178116: 0, -2178117: 36, -2178118: 0, -2178119: 57, -2178120: 87, -2178121: 115, -2178122: 66, -2178123: 173, -2178124: 45, -2178125: 198, -2178126: 20, -2178127: 127, -2178128: 54, -2178129: 249, -2178130: 41, -2178131: 115, -2178132: 33, -2178133: 12, -2178134: 21, -2178135: 134, -2178136: 12, -2178137: 255, -2178138: 127, -2178139: 35, -2178140: 0, -2178141: 67, -2178142: 24, -2178143: 99, -2178144: 23, -2178145: 2, -2178146: 8, -2178147: 148, -2178148: 82, -2178149: 206, -2178150: 57, -2178151: 8, -2178152: 33, -2178153: 132, -2178154: 16, -2178155: 25, -2178156: 0, -2178157: 18, -2178158: 0, -2178159: 0, -2178160: 92, -2178161: 0, -2178162: 64, -2178163: 132, -2178164: 16, -2178165: 127, -2178166: 25, -2178167: 255, -2178168: 127, -2178169: 35, -2178170: 0, -2178171: 197, -2178172: 26, -2178173: 21, -2178174: 139, -2178175: 127, -2178176: 10, -2178177: 111, -2178178: 136, -2178179: 94, -2178180: 7, -2178181: 78, -2178182: 134, -2178183: 57, -2178184: 5, -2178185: 41, -2178186: 131, -2178187: 24, -2178188: 2, -2178189: 8, -2178190: 132, -2178191: 16, -2178192: 24, -2178193: 99, -2178194: 255, -2178195: 127, -2178196: 35, -2178197: 0, -2178198: 67, -2178199: 24, -2178200: 99, -2178201: 217, -2178202: 64, -2178203: 255, -2178204: 27, -2178205: 0, -2178206: 0, -2178207: 223, -2178208: 2, -2178209: 215, -2178210: 1, -2178211: 172, -2178212: 0, -2178213: 115, -2178214: 90, -2178215: 173, -2178216: 65, -2178217: 8, -2178218: 45, -2178219: 99, -2178220: 24, -2178221: 177, -2178222: 11, -2178223: 251, -2178224: 72, -2178225: 255, -2178226: 127, -2178227: 0, -2178228: 0, -2178229: 255, -2178230: 127, -2178231: 229, -2178232: 68, -2178233: 195, -2178234: 8, -2178235: 21, -2178236: 3, -2178237: 32, -2178238: 177, -2178239: 11, -2178240: 169, -2178241: 30, -2178242: 69, -2178243: 1, -2178244: 187, -2178245: 94, -2178246: 179, -2178247: 61, -2178248: 46, -2178249: 41, -2178250: 134, -2178251: 20, -2178252: 24, -2178253: 99, -2178254: 227, -2178255: 40, -2178256: 96, -2178257: 28, -2178258: 195, -2178259: 32, -2178260: 2, -2178261: 223, -2178262: 2, -2178263: 31, -2178264: 34, -2178265: 0, -2178266: 29, -2178267: 3, -2178268: 32, -2178269: 188, -2178270: 114, -2178271: 251, -2178272: 72, -2178273: 22, -2178274: 24, -2178275: 255, -2178276: 107, -2178277: 243, -2178278: 66, -2178279: 12, -2178280: 22, -2178281: 4, -2178282: 9, -2178283: 146, -2178284: 42, -2178285: 171, -2178286: 9, -2178287: 38, -2178288: 1, -2178289: 33, -2178290: 0, -2178291: 131, -2178292: 0, -2178293: 16, -2178294: 26, -2178295: 255, -2178296: 127, -2178297: 35, -2178298: 0, -2178299: 19, -2178300: 178, -2178301: 114, -2178302: 199, -2178303: 113, -2178304: 3, -2178305: 77, -2178306: 73, -2178307: 12, -2178308: 40, -2178309: 20, -2178310: 39, -2178311: 16, -2178312: 6, -2178313: 12, -2178314: 5, -2178315: 16, -2178316: 4, -2178317: 12, -2178318: 3, -2178319: 12, -2178320: 69, -2178321: 130, -2178322: 21, -2178323: 1, -2178324: 255, -2178325: 127, -2178326: 35, -2178327: 0, -2178328: 21, -2178329: 88, -2178330: 82, -2178331: 244, -2178332: 69, -2178333: 177, -2178334: 57, -2178335: 78, -2178336: 49, -2178337: 11, -2178338: 37, -2178339: 200, -2178340: 24, -2178341: 134, -2178342: 20, -2178343: 57, -2178344: 79, -2178345: 115, -2178346: 54, -2178347: 173, -2178348: 29, -2178349: 41, -2178350: 13, -2178351: 67, -2178352: 130, -2178353: 21, -2178354: 1, -2178355: 255, -2178356: 127, -2178357: 35, -2178358: 0, -2178359: 13, -2178360: 182, -2178361: 11, -2178362: 174, -2178363: 30, -2178364: 74, -2178365: 1, -2178366: 88, -2178367: 82, -2178368: 80, -2178369: 49, -2178370: 203, -2178371: 28, -2178372: 35, -2178373: 8, -2178374: 199, -2178375: 136, -2178376: 67, -2178377: 130, -2178378: 21, -2178379: 1, -2178380: 255, -2178381: 127, -2178382: 35, -2178383: 0, -2178384: 21, -2178385: 255, -2178386: 87, -2178387: 255, -2178388: 43, -2178389: 60, -2178390: 31, -2178391: 120, -2178392: 2, -2178393: 176, -2178394: 1, -2178395: 11, -2178396: 1, -2178397: 135, -2178398: 0, -2178399: 68, -2178400: 0, -2178401: 224, -2178402: 127, -2178403: 64, -2178404: 74, -2178405: 32, -2178406: 37, -2178407: 69, -2178408: 255, -2178409: 127, -2178410: 35, -2178411: 0, -2178412: 207, -2178413: 32, -2178414: 69, -2178415: 255, -2178416: 127, -2178417: 7, -2178418: 224, -2178419: 3, -2178420: 64, -2178421: 2, -2178422: 32, -2178423: 1, -2178424: 0, -2178425: 0, -2178426: 255, -2178427: 27, -2178428: 0, -2178429: 0, -2178430: 223, -2178431: 2, -2178432: 215, -2178433: 1, -2178434: 172, -2178435: 0, -2178436: 187, -2178437: 94, -2178438: 179, -2178439: 61, -2178440: 46, -2178441: 41, -2178442: 134, -2178443: 20, -2178444: 177, -2178445: 11, -2178446: 251, -2178447: 72, -2178448: 255, -2178449: 127, -2178450: 0, -2178451: 0, -2178452: 255, -2178453: 127, -2178454: 229, -2178455: 68, -2178456: 195, -2178457: 8, -2178458: 7, -2178459: 3, -2178460: 32, -2178461: 177, -2178462: 11, -2178463: 169, -2178464: 30, -2178465: 69, -2178466: 1, -2178467: 199, -2178468: 32, -2178469: 5, -2178470: 24, -2178471: 99, -2178472: 227, -2178473: 40, -2178474: 96, -2178475: 28, -2178476: 195, -2178477: 32, -2178478: 2, -2178479: 223, -2178480: 2, -2178481: 31, -2178482: 34, -2178483: 0, -2178484: 29, -2178485: 3, -2178486: 32, -2178487: 213, -2178488: 85, -2178489: 182, -2178490: 56, -2178491: 15, -2178492: 0, -2178493: 255, -2178494: 107, -2178495: 243, -2178496: 66, -2178497: 12, -2178498: 22, -2178499: 4, -2178500: 9, -2178501: 146, -2178502: 42, -2178503: 171, -2178504: 9, -2178505: 38, -2178506: 1, -2178507: 33, -2178508: 0, -2178509: 131, -2178510: 0, -2178511: 16, -2178512: 26, -2178513: 255, -2178514: 127, -2178515: 35, -2178516: 0, -2178517: 6, -2178518: 203, -2178519: 85, -2178520: 224, -2178521: 84, -2178522: 0, -2178523: 40, -2178524: 4, -2178525: 68, -2178526: 0, -2178527: 3, -2178528: 0, -2178529: 2, -2178530: 67, -2178531: 0, -2178532: 1, -2178533: 34, -2178534: 0, -2178535: 69, -2178536: 130, -2178537: 21, -2178538: 1, -2178539: 255, -2178540: 127, -2178541: 35, -2178542: 0, -2178543: 21, -2178544: 138, -2178545: 24, -2178546: 105, -2178547: 20, -2178548: 103, -2178549: 16, -2178550: 70, -2178551: 16, -2178552: 37, -2178553: 12, -2178554: 35, -2178555: 8, -2178556: 2, -2178557: 4, -2178558: 48, -2178559: 46, -2178560: 140, -2178561: 29, -2178562: 7, -2178563: 17, -2178564: 99, -2178565: 0, -2178566: 67, -2178567: 130, -2178568: 21, -2178569: 1, -2178570: 255, -2178571: 127, -2178572: 35, -2178573: 0, -2178574: 13, -2178575: 41, -2178576: 2, -2178577: 135, -2178578: 1, -2178579: 197, -2178580: 0, -2178581: 46, -2178582: 45, -2178583: 202, -2178584: 32, -2178585: 102, -2178586: 16, -2178587: 2, -2178588: 4, -2178589: 199, -2178590: 136, -2178591: 67, -2178592: 130, -2178593: 21, -2178594: 1, -2178595: 181, -2178596: 86, -2178597: 35, -2178598: 0, -2178599: 27, -1130024: 19, -1130025: 0, -2178602: 255, -2178603: 43, -1130028: 11, -1130029: 0, -2178606: 120, -1130031: 127, -1130032: 156, -2178609: 1, -2178610: 11, -1130035: 2, -2178612: 135, -2178613: 0, -2178614: 68, -2178615: 0, -2178616: 31, -2178617: 0, -2178618: 18, -2178619: 0, -2178620: 9, -2178621: 0, -2178622: 255, -2178623: 3, -2178624: 82, -2178625: 2, -2178626: 41, -2178627: 1, -2178628: 35, -2178629: 0, -2178630: 207, -2178631: 32, -2178632: 13, -2178633: 224, -2178634: 127, -2178635: 64, -2178636: 74, -2178637: 32, -2178638: 37, -2178639: 224, -2178640: 3, -2178641: 64, -2178642: 2, -2178643: 32, -2178644: 1, -2178645: 0, -2178646: 0, -2178647: 255, -2178648: 27, -2178649: 0, -2178650: 0, -2178651: 223, -2178652: 2, -2178653: 215, -2178654: 1, -2178655: 172, -2178656: 0, -2178657: 115, -2178658: 90, -2178659: 173, -2178660: 65, -2178661: 8, -2178662: 45, -2178663: 99, -2178664: 24, -2178665: 177, -2178666: 11, -2178667: 251, -2178668: 72, -2178669: 255, -2178670: 127, -2178671: 0, -2178672: 0, -2178673: 255, -2178674: 127, -2178675: 229, -2178676: 68, -2178677: 195, -2178678: 8, -2178679: 21, -2178680: 3, -2178681: 32, -2178682: 177, -2178683: 11, -2178684: 169, -2178685: 30, -2178686: 69, -2178687: 1, -2178688: 187, -2178689: 94, -2178690: 179, -2178691: 61, -2178692: 46, -2178693: 41, -2178694: 134, -2178695: 20, -2178696: 24, -2178697: 99, -2178698: 214, -2178699: 90, -2178700: 82, -2178701: 74, -2178702: 195, -2178703: 32, -2178704: 2, -2178705: 223, -2178706: 2, -2178707: 31, -2178708: 34, -2178709: 0, -2178710: 29, -2178711: 3, -2178712: 32, -2178713: 188, -2178714: 114, -2178715: 251, -2178716: 72, -2178717: 22, -2178718: 24, -2178719: 255, -2178720: 107, -2178721: 243, -2178722: 66, -2178723: 12, -2178724: 22, -2178725: 4, -2178726: 9, -2178727: 146, -2178728: 42, -2178729: 171, -2178730: 9, -2178731: 38, -2178732: 1, -2178733: 33, -2178734: 0, -2178735: 131, -2178736: 0, -2178737: 16, -2178738: 26, -2178739: 255, -2178740: 127, -2178741: 35, -2178742: 0, -2178743: 19, -2178744: 178, -2178745: 114, -2178746: 199, -2178747: 113, -2178748: 3, -2178749: 77, -2178750: 66, -2178751: 60, -2178752: 66, -2178753: 52, -2178754: 33, -2178755: 40, -2178756: 33, -2178757: 32, -2178758: 33, -2178759: 24, -2178760: 0, -2178761: 12, -2178762: 0, -2178763: 4, -2178764: 69, -2178765: 130, -2178766: 21, -2178767: 1, -2178768: 255, -2178769: 127, -2178770: 35, -2178771: 0, -2178772: 21, -2178773: 199, -2178774: 113, -2178775: 134, -2178776: 97, -2178777: 69, -2178778: 81, -2178779: 4, -2178780: 65, -2178781: 163, -2178782: 44, -2178783: 98, -2178784: 28, -2178785: 33, -2178786: 12, -2178787: 86, -2178788: 123, -2178789: 176, -2178790: 110, -2178791: 224, -2178792: 117, -2178793: 35, -2178794: 85, -2178795: 67, -2178796: 130, -2178797: 21, -2178798: 1, -2178799: 255, -2178800: 127, -2178801: 35, -2178802: 0, -2178803: 13, -2178804: 182, -2178805: 11, -2178806: 174, -2178807: 30, -2178808: 74, -2178809: 1, -2178810: 145, -2178811: 110, -2178812: 199, -2178813: 113, -2178814: 66, -2178815: 40, -2178816: 33, -2178817: 20, -2178818: 199, -2178819: 32, -2178820: 67, -2178821: 130, -2178822: 21, -2178823: 1, -2178824: 255, -2178825: 127, -2178826: 35, -2178827: 0, -2178828: 27, -2178829: 255, -2178830: 87, -2178831: 255, -2178832: 43, -2178833: 60, -2178834: 31, -2178835: 120, -2178836: 2, -2178837: 176, -2178838: 1, -2178839: 11, -2178840: 1, -2178841: 135, -2178842: 0, -2178843: 68, -2178844: 0, -2178845: 31, -2178846: 0, -2178847: 18, -2178848: 0, -2178849: 9, -2178850: 0, -2178851: 255, -2178852: 3, -2178853: 82, -2178854: 2, -2178855: 41, -2178856: 1, -2178857: 35, -2178858: 0, -2178859: 207, -2178860: 32, -2178861: 13, -2178862: 224, -2178863: 127, -2178864: 64, -2178865: 74, -2178866: 32, -2178867: 37, -2178868: 224, -2178869: 3, -2178870: 64, -2178871: 2, -2178872: 32, -2178873: 1, -2178874: 0, -2178875: 0, -2178876: 255, -2178877: 27, -2178878: 0, -2178879: 0, -2178880: 223, -2178881: 2, -2178882: 215, -2178883: 1, -2178884: 172, -2178885: 0, -2178886: 115, -2178887: 90, -2178888: 173, -2178889: 65, -2178890: 8, -2178891: 45, -2178892: 99, -2178893: 24, -2178894: 177, -2178895: 11, -2178896: 251, -2178897: 72, -2178898: 255, -2178899: 127, -2178900: 0, -2178901: 0, -2178902: 255, -2178903: 127, -2178904: 229, -2178905: 68, -2178906: 195, -2178907: 8, -2178908: 21, -2178909: 3, -2178910: 32, -2178911: 177, -2178912: 11, -2178913: 169, -2178914: 30, -2178915: 69, -2178916: 1, -2178917: 187, -2178918: 94, -2178919: 179, -2178920: 61, -2178921: 46, -2178922: 41, -2178923: 134, -2178924: 20, -2178925: 24, -2178926: 99, -2178927: 214, -2178928: 90, -2178929: 82, -2178930: 74, -2178931: 195, -2178932: 32, -2178933: 2, -2178934: 223, -2178935: 2, -2178936: 31, -2178937: 34, -2178938: 0, -2178939: 29, -2178940: 3, -2178941: 32, -2178942: 188, -2178943: 114, -2178944: 251, -2178945: 72, -2178946: 22, -2178947: 24, -2178948: 255, -2178949: 107, -2178950: 243, -2178951: 66, -2178952: 12, -2178953: 22, -2178954: 4, -2178955: 9, -2178956: 146, -2178957: 42, -2178958: 171, -2178959: 9, -2178960: 38, -2178961: 1, -2178962: 33, -2178963: 0, -2178964: 131, -2178965: 0, -2178966: 16, -2178967: 26, -2178968: 255, -2178969: 127, -2178970: 35, -2178971: 0, -2178972: 19, -2178973: 178, -2178974: 114, -2178975: 199, -2178976: 113, -2178977: 3, -2178978: 77, -2178979: 226, -2178980: 12, -2178981: 194, -2178982: 12, -2178983: 161, -2178984: 8, -2178985: 129, -2178986: 8, -2178987: 65, -2178988: 4, -2178989: 32, -2178990: 4, -2178991: 0, -2178992: 0, -2178993: 69, -2178994: 130, -2178995: 21, -2178996: 1, -2178997: 255, -2178998: 127, -2178999: 35, -2179000: 0, -2179001: 21, -2179002: 56, -2179003: 55, -2179004: 180, -2179005: 46, -2179006: 48, -2179007: 38, -2179008: 205, -2179009: 29, -2179010: 73, -2179011: 17, -2179012: 197, -2179013: 8, -2179014: 65, -2179015: 0, -2179016: 221, -2179017: 75, -2179018: 52, -2179019: 51, -2179020: 106, -2179021: 22, -2179022: 99, -2179023: 1, -2179024: 67, -2179025: 130, -2179026: 21, -2179027: 1, -2179028: 255, -2179029: 127, -2179030: 35, -2179031: 0, -2179032: 12, -2179033: 182, -2179034: 11, -2179035: 174, -2179036: 30, -2179037: 74, -2179038: 1, -2179039: 101, -2179040: 35, -2179041: 160, -2179042: 21, -2179043: 224, -2179044: 4, -2179045: 32, -2179046: 200, -2179047: 32, -2179048: 67, -2179049: 130, -2179050: 21, -2179051: 1, -2179052: 255, -2179053: 127, -2179054: 35, -2179055: 0, -2179056: 27, -2179057: 255, -2179058: 87, -2179059: 255, -2179060: 43, -2179061: 60, -2179062: 31, -2179063: 120, -2179064: 2, -2179065: 176, -2179066: 1, -2179067: 11, -2179068: 1, -2179069: 135, -2179070: 0, -2179071: 68, -2179072: 0, -2179073: 31, -2179074: 0, -2179075: 18, -2179076: 0, -2179077: 9, -2179078: 0, -2179079: 255, -2179080: 3, -2179081: 82, -2179082: 2, -2179083: 41, -2179084: 1, -2179085: 35, -2179086: 0, -2179087: 207, -2179088: 32, -2179089: 13, -2179090: 224, -2179091: 127, -2179092: 64, -2179093: 74, -2179094: 32, -2179095: 37, -2179096: 224, -2179097: 3, -2179098: 64, -2179099: 2, -2179100: 32, -2179101: 1, -2179102: 0, -2179103: 0, -2179104: 255, -2179105: 27, -2179106: 0, -2179107: 0, -2179108: 223, -2179109: 2, -2179110: 215, -2179111: 1, -2179112: 172, -2179113: 0, -2179114: 115, -2179115: 90, -2179116: 173, -2179117: 65, -2179118: 8, -2179119: 45, -2179120: 99, -2179121: 24, -2179122: 177, -2179123: 11, -2179124: 251, -2179125: 72, -2179126: 255, -2179127: 127, -2179128: 0, -2179129: 0, -2179130: 255, -2179131: 127, -2179132: 229, -2179133: 68, -2179134: 195, -2179135: 8, -2179136: 21, -2179137: 3, -2179138: 32, -2179139: 177, -2179140: 11, -2179141: 169, -2179142: 30, -2179143: 69, -2179144: 1, -2179145: 187, -2179146: 94, -2179147: 179, -2179148: 61, -2179149: 46, -2179150: 41, -2179151: 134, -2179152: 20, -2179153: 24, -2179154: 99, -2179155: 214, -2179156: 90, -2179157: 82, -2179158: 74, -2179159: 195, -2179160: 32, -2179161: 2, -2179162: 223, -2179163: 2, -2179164: 31, -2179165: 34, -2179166: 0, -2179167: 29, -2179168: 3, -2179169: 32, -2179170: 188, -2179171: 114, -2179172: 251, -2179173: 72, -2179174: 22, -2179175: 24, -2179176: 255, -2179177: 107, -2179178: 243, -2179179: 66, -2179180: 12, -2179181: 22, -2179182: 4, -2179183: 9, -2179184: 146, -2179185: 42, -2179186: 171, -2179187: 9, -2179188: 38, -2179189: 1, -2179190: 33, -2179191: 0, -2179192: 131, -2179193: 0, -2179194: 16, -2179195: 26, -2179196: 255, -2179197: 127, -2179198: 35, -2179199: 0, -2179200: 16, -2179201: 178, -2179202: 114, -2179203: 199, -2179204: 113, -2179205: 3, -2179206: 77, -2179207: 135, -2179208: 0, -2179209: 102, -2179210: 0, -2179211: 69, -2179212: 0, -2179213: 36, -2179214: 0, -2179215: 34, -2179216: 0, -2179217: 1, -2179218: 34, -2179219: 0, -2179220: 69, -2179221: 130, -2179222: 21, -2179223: 1, -2179224: 255, -2179225: 127, -2179226: 35, -2179227: 0, -2179228: 8, -2179229: 212, -2179230: 1, -2179231: 144, -2179232: 1, -2179233: 77, -2179234: 1, -2179235: 233, -2179236: 0, -2179237: 137, -2179238: 194, -2179239: 30, -2179240: 9, -2179241: 1, -2179242: 0, -2179243: 152, -2179244: 31, -2179245: 174, -2179246: 30, -2179247: 168, -2179248: 1, -2179249: 161, -2179250: 0, -2179251: 67, -2179252: 130, -2179253: 21, -2179254: 1, -2179255: 255, -2179256: 127, -2179257: 35, -2179258: 0, -2179259: 12, -2179260: 186, -2179261: 55, -2179262: 174, -2179263: 30, -2179264: 74, -2179265: 1, -2179266: 82, -2179267: 41, -2179268: 174, -2179269: 16, -2179270: 72, -2179271: 4, -2179272: 34, -2179273: 200, -2179274: 32, -2179275: 67, -2179276: 130, -2179277: 21, -2179278: 1, -2179279: 255, -2179280: 127, -1130705: 0, -1130706: 56, -1130707: 127, -1130708: 78, -1130709: 117, -1130710: 57, -1130711: 10, -1130712: 12, -1130713: 6, -1130714: 0, -1130715: 216, -1130716: 69, -1130717: 51, -1130718: 45, -1130719: 142, -1130720: 28, -1130721: 43, -1130722: 16, -1130723: 71, -1130724: 83, -1130725: 36, -2179302: 18, -1130727: 98, -1130728: 89, -1130729: 160, -1130730: 92, -1130731: 189, -1130732: 27, -1130733: 253, -1130734: 13, -1130735: 157, -1130736: 0, -2179313: 0, -2179314: 207, -2179315: 32, -2179316: 13, -2179317: 224, -2179318: 127, -2179319: 64, -2179320: 74, -2179321: 32, -2179322: 37, -2179323: 224, -2179324: 3, -2179325: 64, -2179326: 2, -2179327: 32, -2179328: 1, -2179329: 0, -2179330: 0, -2179331: 255, -2179332: 29, -2179333: 0, -2179334: 0, -2179335: 223, -2179336: 2, -2179337: 215, -2179338: 1, -2179339: 172, -2179340: 0, -2179341: 78, -2179342: 94, -2179343: 70, -2179344: 61, -2179345: 193, -2179346: 40, -2179347: 32, -2179348: 20, -2179349: 177, -2179350: 11, -2179351: 251, -2179352: 72, -2179353: 255, -2179354: 127, -2179355: 0, -2179356: 0, -2179357: 255, -2179358: 127, -2179359: 229, -2179360: 68, -2179361: 255, -2179362: 127, -2179363: 35, -2179364: 0, -2179365: 5, -2179366: 177, -2179367: 11, -2179368: 169, -2179369: 30, -2179370: 69, -2179371: 1, -2179372: 199, -2179373: 32, -2179374: 5, -2179375: 24, -2179376: 99, -2179377: 82, -2179378: 74, -2179379: 140, -2179380: 49, -2179381: 195, -2179382: 32, -2179383: 2, -2179384: 223, -2179385: 2, -2179386: 31, -2179387: 36, -2179388: 0, -2179389: 224, -2179390: 61, -2179391: 32, -2179392: 126, -2179393: 96, -2179394: 101, -2179395: 96, -2179396: 32, -2179397: 0, -2179398: 16, -2179399: 64, -2179400: 121, -2179401: 0, -2179402: 93, -2179403: 160, -2179404: 76, -2179405: 160, -2179406: 60, -2179407: 255, -2179408: 127, -2179409: 19, -2179410: 1, -2179411: 15, -2179412: 0, -2179413: 92, -2179414: 23, -2179415: 153, -2179416: 2, -2179417: 214, -2179418: 1, -2179419: 32, -2179420: 12, -2179421: 0, -2179422: 0, -2179423: 245, -2179424: 107, -2179425: 225, -2179426: 6, -2179427: 65, -2179428: 6, -2179429: 161, -2179430: 5, -2179431: 95, -2179432: 94, -2179433: 63, -2179434: 24, -2179435: 20, -2179436: 16, -2179437: 10, -2179438: 8, -2179439: 4, -2179440: 4, -2179441: 159, -2179442: 79, -2179443: 216, -2179444: 62, -2179445: 18, -2179446: 46, -2179447: 112, -2179448: 111, -2179449: 255, -2179450: 127, -2179451: 224, -2179452: 94, -2179453: 34, -2179454: 0, -2179455: 14, -2179456: 124, -2179457: 0, -2179458: 88, -2179459: 0, -2179460: 36, -2179461: 0, -2179462: 16, -2179463: 0, -2179464: 108, -2179465: 0, -2179466: 72, -2179467: 0, -2179468: 64, -2179469: 0, -2179470: 56, -2179471: 47, -2179472: 0, -2179473: 26, -2179474: 96, -2179475: 53, -2179476: 192, -2179477: 36, -2179478: 0, -2179479: 8, -2179480: 0, -2179481: 4, -2179482: 32, -2179483: 49, -2179484: 128, -2179485: 24, -2179486: 64, -2179487: 20, -2179488: 0, -2179489: 28, -2179490: 255, -2179491: 67, -2179492: 24, -2179493: 1, -2179494: 20, -2179495: 0, -2179496: 255, -2179497: 22, -2179498: 62, -2179499: 2, -2179500: 123, -2179501: 196, -2179502: 96, -2179503: 7, -2179504: 242, -2179505: 114, -2179506: 77, -2179507: 106, -2179508: 36, -2179509: 69, -2179510: 0, -2179511: 20, -2179512: 194, -2179513: 96, -2179514: 17, -2179515: 44, -2179516: 20, -2179517: 36, -2179518: 10, -2179519: 28, -2179520: 94, -2179521: 107, -2179522: 120, -2179523: 78, -2179524: 145, -2179525: 41, -2179526: 153, -2179527: 106, -2179528: 116, -2179529: 65, -2179530: 15, -2179531: 64, -2179532: 64, -2179533: 34, -2179534: 0, -2179535: 6, -2179536: 32, -2179537: 126, -2179538: 31, -2179539: 64, -2179540: 0, -2179541: 93, -2179542: 10, -2179543: 195, -2179544: 214, -2179545: 194, -2179546: 98, -2179547: 1, -2179548: 64, -2179549: 121, -2179550: 41, -2179551: 0, -2179552: 195, -2179553: 224, -2179554: 255, -2179555: 29, -2179556: 0, -2179557: 0, -2179558: 223, -2179559: 2, -2179560: 215, -2179561: 1, -2179562: 172, -2179563: 0, -2179564: 85, -2179565: 87, -2179566: 13, -2179567: 62, -2179568: 38, -2179569: 37, -2179570: 96, -2179571: 12, -2179572: 177, -2179573: 11, -2179574: 251, -2179575: 72, -2179576: 255, -2179577: 127, -2179578: 0, -2179579: 0, -2179580: 255, -2179581: 127, -2179582: 229, -2179583: 68, -2179584: 255, -2179585: 127, -2179586: 35, -2179587: 0, -2179588: 5, -2179589: 177, -2179590: 11, -2179591: 169, -2179592: 30, -2179593: 69, -2179594: 1, -2179595: 199, -2179596: 32, -2179597: 5, -2179598: 24, -2179599: 99, -2179600: 82, -2179601: 74, -2179602: 140, -2179603: 49, -2179604: 195, -2179605: 32, -2179606: 2, -2179607: 223, -2179608: 2, -2179609: 31, -2179610: 36, -2179611: 0, -2179612: 27, -2179613: 85, -2179614: 87, -2179615: 79, -2179616: 74, -2179617: 228, -2179618: 28, -2179619: 96, -2179620: 12, -2179621: 178, -2179622: 86, -2179623: 13, -2179624: 62, -2179625: 104, -2179626: 45, -2179627: 38, -2179628: 37, -2179629: 255, -2179630: 71, -2179631: 19, -2179632: 1, -2179633: 15, -2179634: 0, -2179635: 92, -2179636: 23, -2179637: 153, -2179638: 2, -2179639: 214, -2179640: 1, -2179641: 35, -2179642: 0, -2179643: 224, -2179644: 46, -2179645: 245, -2179646: 107, -2179647: 225, -2179648: 6, -2179649: 65, -2179650: 6, -2179651: 161, -2179652: 5, -2179653: 95, -2179654: 94, -2179655: 63, -2179656: 24, -2179657: 20, -2179658: 16, -2179659: 10, -2179660: 8, -2179661: 4, -2179662: 4, -2179663: 159, -2179664: 79, -2179665: 216, -2179666: 62, -2179667: 18, -2179668: 46, -2179669: 112, -2179670: 111, -2179671: 255, -2179672: 127, -2179673: 224, -2179674: 94, -2179675: 0, -2179676: 0, -2179677: 145, -2179678: 38, -2179679: 102, -2179680: 17, -2179681: 128, -2179682: 0, -2179683: 32, -2179684: 0, -2179685: 201, -2179686: 29, -2179687: 102, -2179688: 13, -2179689: 3, -2179690: 5, -2179691: 193, -2179692: 48, -2179693: 0, -2179694: 224, -2179695: 39, -2179696: 112, -2179697: 53, -2179698: 203, -2179699: 36, -2179700: 2, -2179701: 4, -2179702: 1, -2179703: 4, -2179704: 46, -2179705: 49, -2179706: 137, -2179707: 24, -2179708: 38, -2179709: 16, -2179710: 4, -2179711: 12, -2179712: 255, -2179713: 67, -2179714: 24, -2179715: 1, -2179716: 20, -2179717: 0, -2179718: 255, -2179719: 22, -2179720: 62, -2179721: 2, -2179722: 123, -2179723: 1, -2179724: 32, -2179725: 12, -2179726: 0, -2179727: 0, -2179728: 151, -2179729: 95, -2179730: 242, -2179731: 86, -2179732: 201, -2179733: 49, -2179734: 160, -2179735: 0, -2179736: 199, -2179737: 96, -2179738: 5, -2179739: 94, -2179740: 107, -2179741: 120, -2179742: 78, -2179743: 145, -2179744: 41, -2179745: 197, -2179746: 128, -2179747: 35, -2179748: 0, -2179749: 15, -2179750: 59, -2179751: 3, -2179752: 31, -2179753: 0, -2179754: 116, -2179755: 1, -2179756: 10, -2179757: 8, -2179758: 255, -2179759: 127, -2179760: 102, -2179761: 13, -2179762: 226, -2179763: 0, -2179764: 153, -2179765: 2, -2179766: 41, -2179767: 0, -2179768: 195, -2179769: 224, -2179770: 255, -1132026: 0, -1132027: 56, -1132028: 250, -1132029: 114, -1132030: 176, -1132031: 85, -1132032: 69, -1132033: 40, -1132034: 1, -1132035: 24, -1132036: 16, -1132037: 98, -1132038: 107, -1132039: 73, -1132040: 198, -1132041: 56, -1132042: 99, -1132043: 44, -1132044: 127, -1132045: 125, -1132046: 213, -1132047: 84, -1132048: 77, -1132049: 56, -1132050: 7, -1132051: 32, -1132052: 31, -1132053: 2, -1132054: 20, -1132055: 16, -1132056: 10, -1132057: 8, -452331: 1, -1133099: 0, -1133100: 56, -1133101: 87, -1133102: 63, -1133103: 77, -1133104: 46, -1133105: 226, -1133106: 0, -1133107: 96, -1133108: 0, -1133109: 176, -1133110: 58, -1133111: 11, -1133112: 34, -1133113: 102, -1133114: 17, -1133115: 36, -1133116: 9, -1133117: 185, -1133118: 33, -1133119: 51, -1133120: 21, -1133121: 206, -1133122: 12, -1133123: 72, -1133124: 4, -1133125: 224, -1133126: 3, -1133127: 160, -1133128: 2, -1133129: 64, -1133130: 1, -1115811: 0, -1115813: 0, -1115814: 0, -1398597: 0, -1398598: 56, -1398599: 255, -1398600: 87, -1398601: 255, -1398602: 43, -1398603: 60, -1398604: 31, -1398605: 120, -1398606: 2, -1398607: 176, -1398608: 1, -1398609: 11, -1398610: 1, -1398611: 135, -1398612: 0, -1398613: 68, -1398614: 0, -1398615: 31, -1398616: 0, -1398617: 18, -1398618: 0, -1398619: 9, -1398620: 0, -1398621: 255, -1398622: 127, -1398623: 255, -1398624: 127, -1398625: 255, -1398626: 127, -1398627: 0, -1398628: 0, -1398629: 0, -1398630: 56, -1398631: 255, -1398632: 87, -1398633: 255, -1398634: 43, -1398635: 60, -1398636: 31, -1398637: 120, -1398638: 2, -1398639: 176, -1398640: 1, -1398641: 11, -1398642: 1, -1398643: 135, -1398644: 0, -1398645: 68, -1398646: 0, -1398647: 255, -1398648: 127, -1398649: 255, -1398650: 127, -1398651: 255, -1398652: 127, -1398653: 255, -1398654: 3, -1398655: 82, -1398656: 2, -1398657: 41, -1398658: 1, -1398659: 0, -1398660: 0, -1398661: 0, -1398662: 56, -1398663: 249, -1398664: 39, -1398665: 117, -1398666: 35, -1398667: 210, -1398668: 26, -1398669: 78, -1398670: 22, -1398671: 171, -1398672: 17, -1398673: 39, -1398674: 13, -1398675: 132, -1398676: 4, -1398677: 0, -1398678: 0, -1398679: 95, -1398680: 127, -1398681: 31, -1398682: 124, -1398683: 22, -1398684: 88, -1398685: 12, -1398686: 48, -1398687: 148, -1398688: 82, -1398689: 206, -1398690: 57, -1398691: 8, -1398692: 33, -1136652: 0, -1136653: 56, -1136654: 156, -1136655: 75, -1136656: 16, -1136657: 38, -1136658: 198, -1136659: 12, -1136660: 99, -1136661: 12, -1136662: 247, -1136663: 66, -1136664: 82, -1136665: 42, -1136666: 173, -1136667: 25, -1136668: 41, -1136669: 13, -1136670: 89, -1136671: 94, -1136672: 114, -1136673: 61, -1136674: 238, -1136675: 44, -1136676: 71, -1136677: 20, -1136678: 59, -1136679: 3, -1136680: 22, -1136681: 2, -1136682: 19, -1136683: 1, -1399057: 0, -1399058: 56, -1399059: 85, -1399060: 87, -1399061: 79, -1399062: 74, -1399063: 228, -1399064: 28, -1399065: 96, -1399066: 12, -1399067: 178, -1399068: 86, -1399069: 13, -1399070: 62, -1399071: 104, -1399072: 45, -1399073: 38, -1399074: 37, -1399075: 255, -1399076: 3, -1399077: 247, -1399078: 2, -1399079: 16, -1399080: 2, -1399081: 8, -1399082: 1, -1399083: 31, -1399084: 0, -1399085: 24, -1399086: 0, -1399087: 14, -1399088: 0, -1500262: 10, -451687: 0, -451688: 8, -451689: 1, -451690: 189, -451691: 3, -1500268: 42, -451693: 20, -451694: 224, -451695: 59, -451696: 168, -451697: 33, -451698: 159, -451699: 87, -1500276: 170, -451701: 74, -451702: 78, -1500279: 8, -1500280: 240, -451705: 0, -451706: 181, -451707: 2, -451708: 107, -1500285: 12, -1500286: 105, -451711: 2, -1500288: 71, -451713: 17, -451714: 116, -451715: 0, -451716: 13, -451717: 0, -1500296: 249, -451721: 0, -451722: 9, -451723: 1, -451724: 189, -1500301: 0, -451726: 6, -451727: 20, -1500304: 55, -1500305: 46, -1500306: 179, -1500307: 29, -1500308: 46, -1500309: 13, -1500310: 63, -1500311: 38, -1500312: 251, -1500313: 29, -1500314: 151, -1500315: 21, -1500316: 82, -1500317: 13, -1500318: 238, -1500319: 8, -1500320: 63, -1500321: 87, -1500322: 170, -1500323: 0, -1500324: 144, -1500325: 4, -1500326: 141, -1500327: 4, -1500330: 103, -1500331: 0, -1500332: 110, -1246855: 0, -1500333: 29, -1500334: 44, -1500335: 25, -451760: 6, -1500337: 16, -1246856: 56, -1500338: 167, -1500339: 8, -1500340: 48, -1500341: 21, -1500342: 14, -1246857: 239, -1500343: 17, -1500344: 204, -1500345: 12, -1500346: 169, -1500347: 8, -1246858: 73, -1500348: 135, -1500349: 4, -1500350: 176, -1500351: 45, -1500352: 101, -1246859: 206, -1500353: 0, -1500354: 223, -1500355: 5, -1500356: 185, -1500357: 5, -1246860: 69, -1500358: 147, -1500359: 1, -1500360: 109, -1500361: 1, -1246861: 140, -1500364: 247, -1500365: 46, -1500366: 115, -1500367: 30, -1246862: 61, -1500368: 206, -1500369: 13, -1500370: 255, -1500371: 38, -1500372: 155, -1246863: 107, -1500373: 30, -1500374: 87, -1500375: 22, -1500376: 18, -1500377: 14, -1246864: 57, -1500378: 174, -1500379: 9, -1500380: 255, -1500381: 87, -1500382: 74, -1246865: 41, -1500383: 1, -1500384: 240, -1500385: 4, -1500386: 237, -1500387: 4, -1246866: 53, -1500388: 202, -1500389: 0, -1500390: 199, -1500391: 0, -1500392: 206, -1246867: 8, -1500393: 29, -1500394: 140, -1500395: 25, -1246868: 45, -1500398: 231, -1500399: 8, -1500400: 144, -1500401: 21, -1500402: 78, -1246869: 198, -1500403: 17, -1500404: 44, -1500405: 13, -1500406: 9, -1500407: 9, -1246870: 40, -1500408: 231, -1500409: 4, -1500410: 16, -1500411: 46, -1500412: 165, -1246871: 165, -1500413: 0, -1500414: 213, -1500415: 5, -1500416: 175, -1500417: 5, -1246872: 32, -1500418: 137, -1500419: 1, -1500420: 99, -1500421: 1, -1129051: 0, -1500422: 113, -1246873: 99, -1129052: 56, -1500423: 59, -1129053: 57, -1500424: 237, -1129054: 87, -1500425: 46, -1129055: 115, -1500426: 105, -1129056: 66, -1500427: 30, -1246874: 28, -1129057: 173, -1500428: 196, -1129058: 45, -1500429: 13, -1129059: 198, -1129060: 20, -1129061: 218, -1500432: 145, -1246875: 6, -1129062: 25, -1500433: 30, -1129063: 116, -1500434: 77, -1129064: 17, -1500435: 22, -1129065: 15, -1500436: 8, -1129066: 13, -1500437: 14, -1246876: 37, -1129067: 170, -1500438: 164, -1129068: 8, -1500439: 9, -1129069: 222, -1500440: 245, -1129070: 15, -1500441: 87, -1129071: 223, -1500442: 64, -1246877: 196, -1129072: 2, -1500443: 1, -1129073: 159, -1500444: 235, -1129074: 1, -1500445: 4, -1129075: 95, -1500446: 232, -1129076: 0, -1500447: 4, -1246878: 28, -1129077: 55, -1500448: 197, -1129078: 0, -1500449: 0, -1129079: 223, -1500450: 194, -1129080: 111, -1500451: 0, -1129081: 6, -1500452: 201, -1246879: 131, -1129082: 0, -1500453: 29, -1129083: 0, -1500454: 135, -1129084: 56, -1500455: 25, -1129085: 255, -451880: 85, -1129086: 47, -1500457: 17, -1246880: 16, -1500458: 226, -1129088: 26, -1500459: 8, -1129089: 74, -1500460: 139, -1129090: 1, -1500461: 21, -1129091: 99, -1500462: 73, -1129092: 0, -1500463: 17, -1129093: 90, -1129094: 39, -1129095: 181, -1500466: 4, -1129096: 14, -1500467: 9, -1129097: 16, -1500468: 226, -2177674: 0, -451893: 1, -1129099: 206, -1500470: 11, -1129100: 1, -1500471: 46, -1129101: 224, -1500472: 160, -1129102: 3, -451897: 20, -1129103: 224, -1500474: 149, -1129104: 2, -451899: 59, -1129105: 0, -1500476: 111, -1129106: 2, -1500477: 13, -1129107: 0, -1500478: 73, -1129108: 1, -451903: 87, -1129109: 0, -1500480: 35, -1129110: 127, -1500481: 9, -1129111: 224, -1500482: 49, -1129112: 109, -1500483: 67, -1129113: 224, -451908: 189, -1129114: 84, -1500485: 54, -451910: 184, -1500487: 38, -1500488: 132, -1500489: 21, -1500490: 181, -1500491: 46, -1500492: 81, -1500493: 38, -1500494: 13, -1500495: 30, -1500496: 200, -1500497: 21, -1500500: 181, -451925: 0, -1500502: 0, -1500503: 9, -1500504: 203, -1500505: 8, -1500506: 200, -1500507: 8, -1500508: 165, -1500509: 4, -1500510: 162, -451935: 46, -1500512: 169, -1500513: 33, -1500514: 103, -1150989: 114, -1500515: 29, -451940: 83, -1500517: 21, -1500518: 194, -1500519: 12, -1500520: 107, -1500521: 25, -1500522: 41, -1500523: 21, -1500524: 7, -1500525: 17, -1500526: 228, -1500527: 12, -1500528: 194, -1500529: 8, -1500530: 235, -1500531: 49, -1500534: 21, -1500535: 25, -1500536: 15, -1500537: 25, -1500538: 201, -1500539: 24, -1500540: 195, -1500541: 24, -1500542: 209, -1500543: 82, -1500544: 77, -1500545: 66, -1500546: 201, -1500547: 53, -1500548: 36, -1500549: 37, -1500550: 85, -1500551: 58, -1500552: 241, -1500553: 49, -1500554: 141, -1500555: 45, -1500556: 72, -1500557: 37, -1500558: 4, -1500559: 33, -1500560: 85, -451985: 17, -1500562: 160, -1500563: 24, -1500564: 139, -1139067: 0, -1139068: 56, -1139069: 156, -1139070: 75, -1139071: 148, -1139072: 54, -1139073: 41, -1139074: 9, -1139075: 66, -1139076: 0, -1139077: 24, -1139078: 59, -1139079: 82, -1139080: 42, -1139081: 173, -1139082: 25, -1139083: 107, -1139084: 17, -1139085: 90, -1139086: 127, -1139087: 192, -1139088: 126, -1139089: 224, -1139090: 109, -1139091: 224, -1139092: 84, -1139093: 29, -1139094: 0, -1139095: 20, -1139096: 0, -1139097: 10, -1139098: 0, -1500570: 98, -451995: 1, -1500572: 105, -1500573: 41, -1500574: 39, -1500575: 33, -452000: 227, -1500577: 28, -1500578: 162, -1500579: 20, -452004: 159, -1500581: 29, -1500582: 9, -1500583: 25, -1500584: 199, -1500585: 24, -1500586: 164, -1500587: 20, -1500588: 130, -1500589: 16, -1500590: 171, -1500591: 53, -1500592: 96, -1500593: 12, -1500594: 117, -1500595: 44, -1500596: 111, -1500597: 44, -1500598: 41, -1500599: 40, -1500602: 49, -1500603: 98, -1500604: 173, -1500605: 85, -452030: 190, -1500607: 73, -1500608: 132, -1500609: 56, -1500610: 181, -1500611: 77, -1500612: 81, -1500613: 69, -1500614: 237, -1270442: 31, -1270443: 3, -1270444: 218, -1270445: 1, -1270446: 245, -1270447: 0, -1270448: 221, -1270449: 2, -1270450: 184, -1270451: 1, -1270452: 211, -1270453: 0, -1270454: 154, -1270455: 2, -1270456: 150, -1270457: 1, -1270458: 210, -1270459: 0, -1270460: 120, -1270461: 2, -1270462: 116, -1270463: 1, -1139392: 0, -1139393: 56, -1139394: 29, -1139395: 2, -1139396: 21, -1139397: 0, -1139398: 8, -1139399: 0, -1139400: 3, -1139401: 0, -1139402: 189, -1139403: 0, -1139404: 19, -1139405: 0, -1139406: 14, -1139407: 0, -1139408: 11, -1139409: 0, -1139410: 190, -1139411: 23, -1139412: 159, -1139413: 26, -1139414: 83, -1139415: 12, -1139416: 75, -1139417: 8, -1139418: 192, -1139419: 126, -1139420: 224, -1139421: 109, -1139422: 224, -1139423: 84, -1270496: 44, -1270497: 1, -1270498: 170, -1270499: 0, -1270500: 72, -1270501: 0, -1270502: 10, -1270503: 1, -1270504: 136, -1270505: 0, -1270506: 71, -1270507: 0, -1270508: 200, -1270509: 0, -1270510: 103, -1270511: 0, -1270512: 37, -1270513: 0, -1270514: 134, -1270515: 0, -1270516: 69, -1270517: 0, -1270518: 36, -1270519: 0, -1270520: 100, -1270521: 0, -1270522: 35, -1270523: 0, -1270524: 34, -1270525: 0, -1270526: 34, -1270527: 0, -1270528: 33, -1270529: 0, -1270530: 1, -1270531: 0, -1270532: 0, -1270533: 0, -1270534: 0, -1270535: 0, -1270536: 0, -1270537: 0, -1500631: 20, -1500632: 41, -1500633: 49, -1270554: 0, -1270555: 0, -1270556: 0, -1270557: 0, -1270558: 0, -1270559: 0, -1270560: 33, -1270561: 4, -1270562: 1, -1270563: 4, -1270564: 0, -1270565: 0, -1270566: 0, -1270567: 0, -1270568: 33, -452060: 6, -1270570: 1, -1270571: 0, -1270572: 0, -1270573: 0, -1270574: 0, -1270575: 0, -1270576: 34, -1270577: 4, -1270578: 2, -1270579: 0, -1270580: 1, -1270581: 0, -1270582: 67, -1270583: 8, -1270584: 34, -1270585: 8, -1270586: 1, -1270587: 0, -1270588: 0, -1270589: 0, -1270590: 66, -1270591: 8, -1270592: 34, -1270593: 4, -1270594: 1, -1270595: 4, -1270596: 1, -1270597: 0, -1270598: 68, -1270599: 12, -1270600: 4, -1270601: 0, -1270602: 2, -1270603: 0, -1270604: 133, -1270605: 16, -1270606: 67, -1270607: 12, -1270608: 1, -1270609: 4, -1270610: 0, -1270611: 0, -1270612: 100, -1270613: 12, -1270614: 67, -1270615: 8, -1270616: 34, -1270617: 4, -1270618: 1, -1270619: 4, -1270620: 102, -1270621: 16, -1270622: 6, -1270623: 4, -1270624: 4, -1270625: 0, -1270626: 167, -1270627: 20, -1270628: 100, -1270629: 16, -1270630: 1, -1270631: 4, -1270632: 0, -1270633: 0, -1270634: 133, -1270635: 20, -1270636: 100, -1270637: 12, -1270638: 35, -1270639: 8, -1270640: 2, -1270641: 4, -1270642: 136, -1270643: 24, -1270644: 8, -1270645: 4, -1270646: 5, -1270647: 4, -1270648: 232, -1270649: 28, -1270650: 134, -1270651: 20, -1270652: 2, -1270653: 4, -1270654: 1, -1270655: 0, -1270656: 167, -1270657: 24, -1270658: 101, -1270659: 16, -1270660: 67, -1270661: 12, -1270662: 34, -1270663: 8, -1270664: 202, -1270665: 28, -1270666: 10, -1270667: 8, -1270668: 6, -1270669: 4, -1270670: 10, -1270671: 33, -1270672: 167, -1270673: 24, -1270674: 2, -1270675: 8, -1270676: 1, -1270677: 0, -1270678: 200, -1270679: 28, -1270680: 134, -1270681: 20, -1270682: 68, -1270683: 12, -1270684: 35, -1270685: 8, -1270686: 236, -1270687: 36, -1270688: 12, -1270689: 8, -1270690: 8, -1270691: 4, -1270692: 44, -1270693: 37, -1270694: 200, -1270695: 28, -1270696: 35, -1270697: 8, -1270698: 1, -1270699: 0, -1270700: 233, -1270701: 32, -1270702: 167, -1270703: 24, -1270704: 69, -1270705: 16, -1270706: 35, -1270707: 8, -1270708: 14, -1270709: 41, -1270710: 14, -1270711: 8, -1270712: 9, -1270713: 4, -1270714: 109, -1270715: 45, -1270716: 233, -1270717: 32, -1270718: 35, -1270719: 8, -1270720: 1, -1270721: 0, -1270722: 11, -1270723: 41, -1270724: 168, -1270725: 28, -1270726: 101, -1270727: 16, -1270728: 36, -1270729: 12, -1270730: 48, -1270731: 49, -1270732: 16, -1270733: 12, -1270734: 10, -1270735: 8, -1270736: 143, -1270737: 49, -1270738: 234, -1270739: 36, -1270740: 36, -1270741: 12, -1270742: 1, -1270743: 0, -1270744: 44, -1270745: 45, -1270746: 201, -1270747: 28, -1270748: 102, -1500666: 46, -1270750: 36, -1270751: 12, -1270752: 82, -1270753: 53, -1270754: 18, -1270755: 12, -1270756: 12, -1270757: 8, -1270758: 209, -1270759: 57, -1270760: 12, -1270761: 41, -1270762: 36, -1270763: 12, -1270764: 2, -1270765: 0, -1270766: 78, -1270767: 49, -1270768: 234, -1270769: 32, -1270770: 135, -1270771: 24, -1270772: 69, -1270773: 16, -1270774: 148, -1270775: 61, -1270776: 20, -1270777: 16, -1270778: 13, -1270779: 8, -1270780: 243, -1270781: 61, -1270782: 45, -1270783: 45, -1270784: 37, -1270785: 12, -1270786: 2, -1270787: 0, -1270788: 111, -1270789: 57, -1270790: 11, -1270791: 37, -1270792: 136, -1270793: 24, -1270794: 69, -1270795: 16, -1270796: 182, -1270797: 65, -1270798: 22, -1270799: 16, -1270800: 14, -1270801: 12, -1270802: 20, -1270803: 66, -1270804: 78, -1270805: 49, -1270806: 37, -1270807: 16, -1270808: 2, -1270809: 0, -1270810: 144, -1270811: 61, -1270812: 12, -1270813: 41, -1270814: 136, -1270815: 28, -1270816: 70, -1270817: 16, -1270818: 216, -1270819: 73, -1270820: 24, -1270821: 16, -1270822: 16, -1270823: 12, -1270824: 86, -1270825: 74, -1270826: 111, -1270827: 53, -1270828: 38, -1270829: 16, -1270830: 2, -1270831: 0, -1270832: 178, -1270833: 65, -1270834: 45, -1270835: 45, -1270836: 169, -1270837: 28, -1270838: 71, -1270839: 20, -1270840: 250, -1270841: 81, -1270842: 26, -1270843: 20, -1270844: 17, -1270845: 12, -1270846: 120, -1270847: 78, -1270848: 144, -1270849: 61, -1270850: 38, -1270851: 16, -1270852: 2, -1270853: 4, -1270854: 243, -1270855: 69, -1270856: 79, -1270857: 49, -1270858: 170, -1270859: 32, -1270860: 71, -1270861: 20, -1270862: 29, -1270863: 86, -1270864: 61, -1270865: 20, -1270866: 18, -1270867: 12, -1270868: 186, -1270869: 86, -1270870: 178, -1139799: 0, -1139800: 56, -1139801: 31, -1139802: 47, -1139803: 50, -1139804: 9, -1139805: 106, -1139806: 0, -1139807: 3, -1139808: 0, -1139809: 28, -1139810: 34, -1139811: 182, -1139812: 25, -1139813: 81, -1139814: 21, -1139815: 236, -1139816: 16, -1139817: 190, -1139818: 23, -1139819: 159, -1139820: 24, -1139821: 83, -1139822: 12, -1139823: 75, -1139824: 8, -1139825: 224, -1139826: 59, -1139827: 128, -1139828: 38, -1139829: 128, -1139830: 21, -1270903: 32, -1270904: 74, -1270905: 24, -1270906: 31, -1270907: 86, -1270908: 63, -1270909: 24, -1270910: 21, -1270911: 16, -1270912: 191, -1270913: 2, -1270914: 154, -1270915: 1, -1270916: 214, -1270917: 0, -1270918: 90, -1270919: 74, -1270920: 115, -1270921: 57, -1270922: 42, -1270923: 20, -1270924: 7, -1270925: 4, -1270926: 182, -1270927: 69, -1270928: 50, -1270929: 49, -1270930: 174, -1270931: 32, -1270932: 75, -1270933: 24, -1270934: 255, -1270935: 81, -1270936: 63, -1270937: 24, -1270938: 21, -1270939: 16, -1270940: 159, -1270941: 2, -1270942: 154, -1270943: 1, -1270944: 214, -1270945: 0, -1270946: 59, -1270947: 74, -1270948: 84, -1270949: 57, -1270950: 43, -1270951: 20, -1270952: 8, -1270953: 8, -1270954: 183, -1270955: 65, -1270956: 51, -1270957: 45, -1270958: 175, -1270959: 32, -1270960: 76, -1270961: 24, -1270962: 223, -1270963: 77, -1270964: 63, -1270965: 24, -1270966: 22, -1270967: 16, -1270968: 127, -1270969: 6, -1270970: 123, -1129087: 247, -1270972: 183, -1270973: 4, -1500704: 137, -1500705: 32, -1500706: 103, -1500707: 28, -1500708: 69, -452133: 3, -1500710: 109, -1500711: 65, -1500712: 3, -1500713: 20, -452138: 202, -1500715: 44, -1500716: 121, -452141: 87, -1500718: 51, -1500719: 40, -1500720: 45, -1500721: 40, -1500722: 59, -1500723: 98, -1500724: 183, -1500725: 85, -452150: 108, -452151: 1, -452152: 83, -452153: 2, -452154: 5, -452155: 17, -452156: 117, -1500733: 69, -452158: 14, -1140091: 0, -1140092: 56, -1140093: 255, -1140094: 2, -1140095: 191, -1140096: 1, -1140097: 15, -1140098: 0, -1140099: 8, -1140100: 0, -1140101: 191, -1140102: 1, -1140103: 27, -1140104: 1, -1140105: 186, -1140106: 0, -1140107: 17, -1140108: 0, -1500738: 110, -1140110: 90, -1140111: 180, -1140112: 65, -1140113: 13, -1140114: 41, -1500739: 48, -1140116: 16, -1140117: 255, -1140118: 3, -1140119: 55, -1140120: 2, -1140121: 209, -1140122: 0, -1500741: 126, -1500742: 10, -1500743: 40, -1500744: 80, -1500745: 24, -1500746: 77, -1500747: 24, -1500748: 42, -1500749: 20, -1500750: 39, -1500751: 20, -1500752: 46, -1500753: 49, -1500754: 236, -1500755: 44, -1500756: 170, -1500757: 36, -1500758: 71, -1500759: 28, -1500760: 240, -1500761: 40, -1500762: 174, -1500763: 36, -1500764: 140, -1500765: 32, -1500766: 105, -1500767: 28, -1500768: 71, -1500769: 24, -452196: 3, -1500773: 20, -1500774: 127, -1500775: 24, -1500776: 121, -1500777: 24, -452202: 6, -452203: 20, -1500780: 45, -1500781: 20, -1500782: 59, -1500783: 78, -1500784: 183, -452209: 87, -1500786: 51, -1500787: 49, -1500788: 142, -1500789: 32, -1500790: 191, -1500791: 57, -1500792: 91, -1500793: 49, -1500794: 247, -1500795: 40, -1500796: 178, -1500797: 32, -1500798: 110, -1500799: 28, -1500800: 191, -1500801: 106, -1500802: 10, -1500803: 20, -1500818: 71, -452243: 0, -1500820: 240, -1500821: 28, -1500822: 174, -452247: 2, -452248: 5, -1500825: 20, -1500826: 105, -1500827: 16, -1500828: 71, -1500829: 16, -1500830: 112, -1500831: 53, -1500832: 5, -1500833: 12, -1500844: 159, -1500845: 38, -1500846: 89, -452271: 0, -1500848: 76, -1500849: 0, -1500852: 57, -1500853: 87, -1500854: 115, -1500855: 66, -1500856: 173, -1500857: 45, -1500858: 198, -1500859: 20, -1500860: 127, -1500861: 54, -1500862: 249, -452287: 37, -452288: 159, -452289: 87, -452290: 211, -452291: 74, -1500868: 134, -1500869: 12, -1500870: 255, -1500871: 127, -1500872: 0, -1500873: 0, -1500874: 95, -1500875: 34, -1500876: 57, -1500877: 1, -1500878: 45, -1500879: 0, -1500880: 5, -452305: 0, -452306: 14, -1500883: 78, -1500886: 142, -1500887: 41, -1500888: 167, -1500889: 16, -1500890: 63, -1500891: 50, -1500892: 217, -1500893: 37, -452318: 225, -1500895: 29, -1500896: 237, -1500897: 16, -1500898: 103, -1500899: 8, -1500900: 191, -1500901: 119, -1500902: 2, -1500903: 0, -1500904: 31, -1500905: 30, -1141112: 0, -1141113: 56, -1141114: 31, -1141115: 77, -1141116: 182, -1141117: 56, -1141118: 110, -1141119: 36, -1141120: 72, -1141121: 20, -1141122: 255, -1141123: 71, -1141124: 250, -1141125: 46, -1141126: 22, -1141127: 22, -1141128: 50, -1141129: 1, -1141130: 57, -1141131: 111, -1141132: 115, -1141133: 90, -1141134: 173, -452333: 0, -1141136: 8, -1141137: 45, -1141138: 99, -1141139: 24, -1141140: 255, -1141141: 127, -1141142: 65, -1141143: 0, -1500911: 0, -1500912: 154, -1500913: 66, -1500914: 245, -1500915: 49, -1500916: 80, -1500917: 33, -1500920: 255, -1500921: 41, -1500922: 154, -1500923: 33, -1500924: 21, -1500925: 25, -452350: 7, -452351: 20, -452352: 226, -452353: 59, -1500930: 31, -1500931: 99, -1500932: 6, -1500933: 0, -1500934: 159, -1500935: 21, -452360: 80, -1500937: 0, -1500938: 51, -1500939: 0, -1500940: 14, -452365: 1, -1500942: 251, -1500943: 49, -1500944: 119, -1500945: 37, -452370: 6, -1500947: 24, -452372: 118, -452373: 0, -452374: 15, -1500951: 29, -452378: 6, -452379: 0, -1500956: 147, -1500957: 12, -1500958: 80, -1500959: 4, -1500960: 95, -1500961: 74, -1500962: 12, -1500963: 0, -1500964: 159, -1500965: 21, -1500966: 219, -1500967: 0, -1500968: 51, -1500969: 0, -1500970: 14, -1500971: 0, -1500972: 251, -1500973: 49, -1500974: 119, -1500975: 37, -1500976: 244, -1500977: 24, -1500978: 112, -1500979: 12, -1500980: 127, -1500981: 29, -1500982: 59, -1500983: 25, -1500984: 215, -1500985: 16, -1500988: 80, -1500989: 4, -1500990: 95, -452415: 1, -1500992: 12, -1500993: 0, -1501004: 0, -1501005: 0, -452430: 189, -1501007: 0, -1501008: 0, -1501009: 0, -1501010: 36, -1501011: 0, -1501012: 173, -1501013: 41, -1501014: 74, -1501015: 33, -1501016: 231, -1501017: 20, -1501018: 99, -1501019: 12, -1501022: 0, -1501023: 0, -1501024: 0, -1501025: 0, -1501026: 0, -1501027: 0, -1501028: 0, -1501029: 0, -1501030: 173, -1501031: 41, -1501032: 0, -1501033: 0, -1501034: 4, -452459: 87, -1501036: 4, -1501037: 0, -1501038: 4, -452463: 58, -1501040: 7, -1501041: 0, -452466: 190, -1501043: 33, -1501044: 12, -1501045: 29, -1501046: 202, -1501047: 16, -1501048: 70, -1501049: 8, -1501050: 4, -1501051: 0, -1501052: 4, -1501053: 0, -1501056: 4, -1501057: 0, -1501058: 4, -1501059: 0, -1501060: 111, -1501061: 33, -1501062: 4, -1501063: 0, -1501064: 8, -1501065: 0, -1501066: 8, -1501067: 0, -1501068: 8, -1501069: 0, -1501070: 11, -1501071: 0, -1501072: 49, -1501073: 29, -1501074: 239, -1501075: 20, -1501076: 173, -1501077: 12, -1501078: 74, -452503: 0, -452504: 87, -1501081: 0, -452506: 9, -452507: 17, -1501084: 8, -452509: 0, -1501086: 8, -1501087: 0, -1142215: 0, -1142216: 56, -1142217: 90, -1142218: 127, -1142219: 224, -1142220: 59, -1142221: 128, -1142222: 38, -1142223: 32, -1142224: 9, -1142225: 90, -1142226: 79, -1501091: 29, -1142228: 54, -1142229: 16, -1142230: 38, -1142231: 206, -1142232: 29, -1142233: 148, -1142234: 82, -1142235: 206, -1142236: 57, -1142237: 8, -1142238: 33, -1142239: 132, -1142240: 16, -1142241: 59, -1142242: 3, -1142243: 22, -1142244: 2, -1142245: 19, -1142246: 1, -1501095: 0, -1501096: 14, -1501097: 0, -1501098: 14, -1501099: 0, -1501100: 16, -1501101: 0, -452526: 159, -452527: 87, -1501104: 179, -1501105: 16, -1501106: 114, -1501107: 8, -1501108: 48, -452533: 0, -1501110: 14, -1501111: 0, -1501112: 14, -1501113: 0, -1501114: 14, -1501115: 0, -1501116: 14, -1501117: 0, -1501118: 14, -1501119: 0, -1501120: 245, -1501121: 20, -1501124: 14, -1501125: 0, -1501126: 14, -1501127: 0, -1501128: 14, -1501129: 0, -1501130: 16, -1501131: 0, -1501132: 245, -1501133: 20, -1501134: 179, -1501135: 16, -1501136: 114, -1501137: 8, -1501138: 48, -1501139: 4, -1501140: 14, -1501141: 0, -1501142: 14, -1501143: 0, -1501144: 14, -1501145: 0, -1501146: 14, -1501147: 0, -452572: 85, -1501149: 0, -452574: 7, -1501151: 20, -1501152: 14, -1501153: 0, -1501170: 184, -452595: 87, -452596: 213, -452597: 74, -1501174: 70, -1501175: 22, -1501176: 227, -452601: 0, -1501178: 159, -1501179: 79, -1501180: 216, -1501181: 62, -1501182: 18, -1501183: 46, -1501184: 205, -1501185: 8, -1501186: 255, -452611: 0, -1501188: 184, -1501189: 79, -1501192: 39, -1501193: 18, -1501194: 228, -452619: 1, -1501196: 126, -1501197: 75, -452622: 7, -452623: 20, -1501200: 241, -1501201: 41, -1501202: 205, -1501203: 8, -1501204: 255, -1501205: 123, -1501206: 185, -1501207: 71, -1501208: 242, -1501209: 10, -1501210: 40, -1501211: 18, -1501212: 228, -1501213: 0, -1501214: 60, -452639: 0, -452640: 84, -452641: 2, -1501218: 241, -1501219: 41, -452644: 118, -1501221: 8, -1501222: 255, -452647: 0, -452650: 5, -452651: 0, -452652: 10, -452653: 1, -452654: 255, -1501231: 0, -1501232: 27, -1501233: 67, -1501234: 117, -1501235: 54, -1501236: 208, -1501237: 37, -452662: 159, -1501239: 8, -1501240: 255, -1501241: 111, -1501242: 153, -1501243: 51, -1501244: 243, -1501245: 6, -1501246: 9, -1501247: 14, -1501248: 229, -1501249: 0, -1501250: 250, -1501251: 66, -452676: 6, -452677: 17, -452678: 118, -1501255: 37, -1501256: 171, -452681: 0, -1501260: 153, -1501261: 43, -1501262: 244, -1501263: 6, -1501264: 234, -1501265: 9, -1501266: 230, -1501267: 0, -1501268: 217, -1501269: 62, -1501270: 51, -452695: 37, -1501272: 142, -452697: 87, -1501274: 171, -1501275: 4, -1501276: 222, -1501277: 99, -452702: 188, -452703: 0, -1501280: 212, -452705: 1, -1501282: 235, -452707: 0, -1501284: 230, -1501285: 0, -452710: 5, -452711: 17, -452712: 117, -452713: 0, -1501290: 142, -1501291: 33, -1501294: 222, -452719: 0, -1501296: 154, -1501297: 27, -452722: 255, -1501299: 6, -452724: 6, -452725: 20, -1501302: 231, -452727: 59, -1501304: 118, -452729: 37, -1501306: 241, -1501307: 41, -452732: 211, -1501309: 29, -1501310: 138, -1501311: 4, -1501312: 222, -1501313: 87, -452754: 8, -1501331: 114, -1501332: 223, -1501333: 44, -1501334: 185, -1501335: 36, -452760: 224, -452761: 59, -1501338: 169, -452763: 33, -1501340: 189, -1501341: 110, -1501342: 221, -452767: 74, -1501344: 183, -452769: 58, -1501346: 174, -452771: 0, -452772: 190, -452773: 1, -452774: 142, -452775: 0, -1501352: 187, -452777: 2, -452778: 4, -452779: 17, -452780: 116, -1501357: 32, -452782: 13, -452783: 0, -452798: 16, -452799: 0, -452800: 8, -452801: 1, -452802: 31, -452803: 66, -452804: 5, -452805: 20, -452806: 224, -1130012: 0, -452807: 59, -1130013: 56, -452808: 168, -1130014: 29, -452809: 33, -1130015: 2, -452810: 159, -1130016: 21, -452811: 87, -1130017: 0, -452812: 210, -1130018: 8, -452813: 74, -1130019: 0, -452814: 78, -1130020: 3, -452815: 58, -1130021: 0, -452816: 187, -1130022: 189, -452817: 0, -1130023: 0, -452818: 20, -2178600: 255, -452819: 89, -2178601: 87, -452820: 170, -1130026: 14, -452821: 48, -1130027: 0, -452822: 116, -2178604: 60, -452823: 2, -2178605: 31, -452824: 4, -1130030: 255, -452825: 17, -2178607: 2, -452826: 116, -2178608: 176, -452827: 0, -1130033: 3, -452828: 13, -1130034: 55, -452829: 0, -2178611: 1, -1130036: 209, -1130037: 0, -452832: 4, -1130038: 160, -452833: 0, -1130039: 3, -1130040: 192, -1130041: 2, -1130042: 224, -1130043: 5, -452848: 79, -452849: 58, -452850: 188, -452851: 0, -452852: 21, -452853: 89, -452854: 171, -452855: 48, -452856: 83, -452857: 2, -452858: 5, -452859: 17, -452860: 117, -452861: 0, -452862: 14, -452863: 0, -452866: 4, -452867: 0, -452868: 9, -452869: 1, -452870: 31, -452871: 66, -452872: 6, -452873: 20, -452874: 225, -452875: 59, -452876: 202, -452877: 37, -452878: 159, -452879: 87, -452880: 211, -452881: 74, -452882: 79, -452883: 58, -452884: 188, -452885: 0, -452886: 21, -452887: 89, -452888: 171, -452889: 48, -452890: 83, -452891: 2, -452892: 5, -452893: 17, -452894: 117, -452895: 0, -452896: 14, -452897: 0, -452900: 5, -452901: 0, -452902: 10, -452903: 1, -452904: 31, -452905: 66, -452906: 7, -452907: 20, -452908: 226, -452909: 59, -452910: 204, -452911: 37, -452912: 159, -452913: 87, -452914: 212, -452915: 74, -452916: 80, -452917: 58, -452918: 188, -452919: 0, -452920: 22, -452921: 89, -452922: 172, -452923: 48, -452924: 84, -452925: 2, -452926: 6, -452927: 17, -452928: 118, -452929: 0, -452930: 15, -452931: 0, -452934: 6, -452935: 0, -452936: 10, -452937: 1, -452938: 31, -452939: 66, -452940: 7, -452941: 20, -452942: 226, -452943: 59, -452944: 204, -452945: 37, -452946: 159, -452947: 87, -452948: 212, -452949: 74, -452950: 80, -452951: 58, -452952: 188, -452953: 0, -452954: 22, -452955: 89, -452956: 172, -452957: 48, -452958: 84, -452959: 2, -452960: 6, -452961: 17, -452962: 118, -452963: 0, -452964: 15, -452965: 0, -452968: 7, -452969: 0, -452970: 11, -452971: 1, -452972: 31, -452973: 66, -452974: 8, -452975: 20, -452976: 227, -452977: 59, -452978: 238, -452979: 41, -452980: 159, -452981: 87, -452982: 213, -452983: 74, -452984: 81, -452985: 58, -452986: 189, -452987: 0, -452988: 23, -452989: 89, -452990: 173, -452991: 48, -452992: 85, -452993: 2, -452994: 7, -452995: 17, -452996: 119, -452997: 0, -452998: 16, -452999: 0, -453002: 8, -453003: 0, -453004: 11, -453005: 1, -453006: 31, -453007: 66, -453008: 8, -453009: 20, -453010: 227, -453011: 59, -453012: 238, -453013: 41, -453014: 159, -453015: 87, -453016: 213, -453017: 74, -453018: 81, -453019: 58, -453020: 189, -453021: 0, -453022: 23, -453023: 89, -453024: 173, -453025: 48, -453026: 85, -453027: 2, -453028: 7, -453029: 17, -453030: 119, -453031: 0, -453032: 16, -453033: 0, -453036: 8, -453037: 0, -453038: 13, -453039: 1, -453040: 31, -453041: 66, -456480: 0, -453042: 10, -453043: 20, -452116: 109, -453128: 85, -453129: 2, -453130: 7, -453131: 17, -453132: 119, -453133: 0, -453134: 16, -453135: 0, -453138: 7, -453139: 0, -453140: 11, -453141: 1, -453142: 31, -453143: 66, -453144: 8, -453145: 20, -453146: 227, -453147: 59, -453148: 238, -453149: 41, -453150: 159, -453151: 87, -453152: 213, -453153: 74, -453154: 81, -453155: 58, -453156: 189, -453157: 0, -453158: 23, -453159: 89, -453160: 173, -453161: 48, -453162: 85, -453163: 2, -453164: 7, -453165: 17, -453166: 119, -456481: 80, -453167: 0, -453168: 16, -453169: 0, -453172: 6, -453173: 0, -453174: 10, -453175: 1, -453176: 31, -453177: 66, -453178: 7, -453179: 20, -453180: 226, -453181: 59, -453182: 204, -453183: 37, -453184: 159, -1277319: 95, -1277320: 0, -1277321: 55, -1277322: 0, -1277323: 63, -1277324: 0, -1277325: 25, -1277326: 0, -1277327: 63, -1277328: 0, -1277329: 27, -1277330: 0, -1277331: 31, -1277332: 0, -1277333: 29, -1277334: 0, -1277335: 31, -1277336: 0, -1277337: 31, -1277338: 0, -1277339: 31, -1277340: 0, -1277341: 29, -1277342: 0, -1277343: 63, -1277344: 0, -1277345: 27, -1277346: 0, -1277347: 63, -1277348: 0, -1277349: 25, -1277350: 0, -453191: 0, -453192: 22, -453193: 89, -453194: 172, -453195: 48, -453196: 84, -453197: 2, -453198: 6, -453199: 17, -453200: 118, -453201: 0, -453202: 15, -453203: 0, -453206: 5, -453207: 0, -453208: 10, -453209: 1, -453210: 31, -453211: 66, -453212: 7, -453213: 20, -453214: 226, -453215: 59, -453216: 204, -453217: 37, -453218: 159, -453219: 87, -453220: 212, -453221: 74, -453222: 80, -453223: 58, -453224: 188, -453225: 0, -453226: 22, -453227: 89, -453228: 172, -453229: 48, -453230: 84, -453231: 2, -453232: 6, -453233: 17, -453234: 118, -453235: 0, -453236: 15, -453237: 0, -453240: 4, -453241: 0, -453242: 9, -453243: 1, -453244: 31, -453245: 66, -453246: 6, -453247: 20, -453248: 225, -453249: 59, -453250: 202, -453251: 37, -453252: 159, -453253: 87, -453254: 211, -453255: 74, -453256: 79, -453257: 58, -453258: 188, -453259: 0, -453260: 21, -453261: 89, -453262: 171, -453263: 48, -453264: 83, -453265: 2, -453266: 5, -453267: 17, -453268: 117, -453269: 0, -453270: 14, -453271: 0, -453274: 4, -453275: 0, -453276: 9, -453277: 1, -453278: 31, -453279: 66, -453280: 6, -453281: 20, -453282: 225, -453283: 59, -453284: 202, -453285: 37, -453286: 159, -453287: 87, -453288: 211, -453289: 74, -453290: 79, -453291: 58, -453292: 188, -453293: 0, -453294: 21, -453295: 89, -453296: 171, -453297: 48, -453298: 83, -453299: 2, -453300: 5, -453301: 17, -453302: 117, -453303: 0, -453304: 14, -453305: 0, -453308: 16, -453309: 0, -453310: 8, -453311: 1, -453312: 31, -453313: 66, -453314: 5, -453315: 20, -453316: 224, -453317: 59, -453318: 168, -453319: 33, -453320: 159, -453321: 87, -453322: 210, -453323: 74, -453324: 78, -453325: 58, -453326: 187, -453327: 0, -453328: 20, -453329: 89, -453330: 170, -453331: 48, -453332: 116, -453333: 2, -453334: 4, -453335: 17, -453336: 116, -453337: 0, -453338: 13, -453339: 0, -453352: 169, -453353: 30, -453354: 177, -453355: 11, -453360: 103, -453361: 22, -453362: 78, -453363: 3, -453368: 37, -453369: 14, -453370: 235, -453371: 2, -453376: 227, -453377: 5, -453378: 136, -453379: 2, -453384: 161, -453385: 1, -453386: 37, -453387: 2, -453392: 227, -453393: 5, -453394: 136, -453395: 2, -453400: 37, -453401: 14, -453402: 235, -453403: 2, -453408: 103, -453409: 22, -453410: 78, -453411: 3, -230300: 0, -230301: 56, -230302: 88, -230303: 127, -230304: 213, -230305: 110, -230306: 113, -230307: 90, -230308: 238, -230309: 73, -230310: 106, -230311: 53, -230312: 231, -230313: 36, -230314: 131, -230315: 16, -2179281: 35, -2179282: 0, -2179283: 27, -2179284: 255, -2179285: 87, -2179286: 255, -2179287: 43, -2179288: 60, -2179289: 31, -2179290: 120, -2179291: 2, -2179292: 176, -2179293: 1, -2179294: 11, -2179295: 1, -2179296: 135, -2179297: 0, -2179298: 68, -2179299: 0, -2179300: 31, -2179301: 0, -1130726: 86, -2179303: 0, -2179304: 9, -2179305: 0, -2179306: 255, -2179307: 3, -2179308: 82, -2179309: 2, -2179310: 41, -2179311: 1, -2179312: 35, -1152603: 0, -890460: 116, -1152605: 223, -1152606: 62, -890463: 0, -1152608: 0, -1152609: 15, -890466: 14, -1152611: 5, -1152612: 0, -890469: 67, -1152614: 1, -1152615: 24, -1152616: 1, -1152617: 147, -1152618: 0, -890475: 34, -1279623: 0, -1279624: 56, -1279625: 157, -1279626: 85, -1279627: 22, -1279628: 24, -1279629: 13, -1279630: 16, -1279631: 159, -1279632: 75, -1279633: 55, -1279634: 63, -1279635: 208, -1279636: 54, -1279637: 105, -1279638: 46, -1279639: 8, -1279640: 38, -1279641: 166, -1279642: 29, -1279643: 37, -1279644: 17, -1279645: 197, -1279646: 8, -1279647: 3, -1279648: 0, -1279649: 24, -1279650: 99, -1279651: 255, -1279652: 127, -1279653: 0, -1279654: 0, -1279655: 0, -1279656: 56, -1279657: 157, -1279658: 85, -1279659: 22, -1279660: 24, -1279661: 13, -1279662: 16, -1279663: 159, -1279664: 75, -1279665: 55, -1279666: 63, -1279667: 208, -1279668: 54, -1279669: 105, -1279670: 46, -1279671: 8, -1279672: 38, -1279673: 166, -1279674: 29, -1279675: 37, -1279676: 17, -1279677: 197, -1279678: 8, -1279679: 3, -1279680: 0, -1279681: 24, -1279682: 99, -1279683: 255, -1279684: 127, -1279685: 0, -1279686: 0, -1279687: 0, -1279688: 4, -1279689: 107, -1279690: 45, -1279691: 107, -1279692: 45, -1279693: 107, -1279694: 45, -1279695: 134, -1279696: 25, -1279697: 194, -1279698: 20, -1279699: 64, -1279700: 8, -1279701: 0, -1279702: 4, -1279703: 237, -1279704: 49, -1279705: 198, -1279706: 24, -1279707: 195, -1279708: 16, -1279709: 32, -1279710: 4, -1279711: 82, -1279712: 2, -1279713: 107, -1279714: 45, -1279715: 82, -1279716: 74, -1279717: 0, -1279718: 0, -1673099: 0, -1673100: 56, -1673101: 29, -1673102: 2, -1673103: 21, -1673104: 0, -1673105: 8, -1673106: 0, -1673107: 3, -1673108: 0, -1673109: 189, -1673110: 0, -1673111: 19, -1673112: 0, -1673113: 14, -1673114: 0, -1673115: 11, -1673116: 0, -1673117: 90, -1673118: 127, -1673119: 192, -1673120: 126, -1673121: 224, -1673122: 109, -1673123: 224, -1673124: 84, -1673125: 224, -1673126: 3, -1673127: 160, -1673128: 2, -1673129: 64, -1673130: 1, -1149168: 0, -1149169: 56, -1149170: 31, -1149171: 77, -1149172: 182, -1149173: 56, -1149174: 110, -1149175: 36, -1149176: 72, -1149177: 20, -1149178: 255, -1149179: 71, -1149180: 250, -1149181: 46, -1149182: 22, -1149183: 22, -1149184: 50, -1149185: 1, -1149186: 57, -1149187: 111, -1149188: 115, -1149189: 90, -1149190: 173, -1149191: 65, -1149192: 8, -1149193: 45, -1149194: 99, -1149195: 24, -1149196: 255, -1149197: 127, -1149198: 65, -1149199: 0, -1673725: 0, -1673726: 56, -1673727: 149, -1673728: 63, -1673729: 139, -1673730: 46, -1673731: 32, -1673732: 1, -1673733: 96, -1673734: 0, -1673735: 238, -1673736: 58, -1673737: 73, -1673738: 34, -1673739: 164, -1673740: 17, -1673741: 98, -1673742: 9, -1673743: 187, -1673744: 57, -1673745: 245, -1673746: 48, -1673747: 110, -1673748: 44, -1673749: 39, -1673750: 40, -1673751: 147, -1673752: 127, -1673753: 206, -1673754: 110, -1673755: 41, -1673756: 98, -1673921: 0, -1673922: 56, -1673923: 29, -1673924: 2, -1673925: 21, -1673926: 0, -1673927: 8, -1673928: 0, -1673929: 3, -1673930: 0, -1673931: 95, -1673932: 1, -1673933: 118, -1673934: 0, -1673935: 80, -1673936: 0, -1673937: 11, -1673938: 0, -1673939: 255, -1673940: 127, -1673941: 224, -1673942: 86, -1673943: 128, -1673944: 49, -1673945: 192, -1673946: 24, -1673947: 255, -1673948: 67, -1673949: 220, -1673950: 66, -1673951: 118, -1673952: 65, -1149967: 0, -1149968: 56, -1149969: 255, -1149970: 2, -1149971: 191, -1149972: 1, -1149973: 15, -1149974: 0, -1149975: 8, -1149976: 0, -1149977: 191, -1149978: 1, -1149979: 27, -1149980: 1, -1149981: 186, -1149982: 0, -1149983: 17, -1149984: 0, -1149985: 90, -1149986: 127, -1149987: 69, -1149988: 93, -1149989: 195, -1149990: 64, -1149991: 98, -1149992: 36, -1149993: 233, -1149994: 83, -1149995: 198, -1149996: 58, -1149997: 97, -1149998: 25, -1677184: 22, -1677185: 234, -1677186: 17, -1677187: 6, -1677188: 9, -1677189: 189, -1677190: 54, -1677191: 249, -1677192: 41, -1677193: 21, -1677194: 29, -890763: 33, -1677196: 16, -1677197: 223, -1677198: 17, -1677199: 123, -890768: 78, -1677201: 22, -1677202: 17, -1677203: 179, -1677204: 16, -890773: 89, -1677206: 16, -1677207: 222, -1677208: 103, -1677209: 8, -890778: 4, -1677211: 0, -1677212: 0, -1677213: 242, -890782: 13, -890783: 0, -1677216: 18, -1677217: 236, -1677218: 17, -1677219: 8, -890788: 191, -1677221: 125, -1677222: 50, -1677223: 185, -1677224: 37, -1677225: 214, -1677226: 24, -1677227: 83, -1677228: 20, -1677229: 191, -1677230: 17, -1677231: 92, -1677232: 17, -1677233: 247, -1677234: 16, -1677235: 147, -1677236: 16, -1677237: 46, -1677238: 16, -890807: 69, -890808: 185, -1677241: 8, -1677242: 4, -1677243: 0, -1677244: 0, -890813: 21, -1677246: 35, -1677247: 241, -1677248: 18, -1677249: 14, -890818: 57, -1677251: 41, -1677252: 9, -1677253: 61, -1677254: 46, -1677255: 122, -456487: 13, -1677256: 37, -1677257: 183, -1677258: 24, -1677259: 52, -890828: 223, -1677261: 159, -1677262: 21, -1677263: 60, -1674972: 0, -1674973: 56, -1674974: 255, -1674975: 87, -1674976: 247, -1674977: 66, -1674978: 140, -1674979: 21, -1674980: 165, -1674981: 0, -1674982: 90, -1674983: 79, -1674984: 181, -1674985: 54, -1674986: 16, -1674987: 38, -1674988: 206, -1674989: 29, -1674990: 224, -1674991: 63, -1674992: 224, -1674993: 46, -1674994: 0, -1674995: 34, -1674996: 0, -1674997: 17, -1674998: 91, -1674999: 126, -1675000: 47, -1675001: 85, -1675002: 135, -1675003: 40, -1677269: 46, -890838: 21, -1677271: 222, -1677272: 87, -1677273: 9, -1677274: 4, -890843: 62, -1677276: 0, -1677277: 246, -1677278: 31, -1677279: 243, -1677280: 14, -1677281: 47, -1677282: 14, -1677283: 74, -1677284: 5, -890853: 127, -1677286: 45, -1677287: 90, -890856: 240, -890857: 123, -1677290: 24, -1677291: 54, -1677292: 20, -890861: 127, -890862: 255, -890863: 127, -890864: 254, -890865: 123, -1677298: 20, -1677299: 116, -1677300: 20, -1677301: 47, -1677302: 20, -1677303: 255, -1677304: 79, -1677305: 10, -1677306: 0, -1677307: 0, -890876: 127, -1677309: 248, -1677310: 31, -1677311: 21, -1677312: 15, -1677313: 81, -1150986: 0, -1150987: 56, -1150988: 250, -1677314: 14, -1150990: 176, -1150991: 85, -1150992: 69, -1150993: 40, -1150994: 1, -1150995: 24, -1150996: 16, -1150997: 98, -1150998: 107, -1150999: 73, -1151000: 198, -1151001: 56, -1151002: 99, -1151003: 44, -1151004: 31, -1151005: 36, -1151006: 23, -1151007: 28, -1151008: 47, -1151009: 20, -1151010: 71, -1151011: 12, -1151012: 255, -1151013: 3, -1151014: 55, -1151015: 2, -1151016: 209, -1151017: 0, -1677319: 27, -1677320: 33, -1677321: 121, -1677322: 24, -1677323: 23, -1677324: 20, -1677325: 63, -1677326: 25, -1677327: 252, -1153040: 74, -1677329: 183, -1153042: 17, -1677331: 116, -1677332: 24, -1153045: 82, -1677334: 24, -1677335: 255, -1153048: 62, -1677337: 11, -1677338: 0, -1153051: 198, -1153052: 37, -890909: 0, -1153054: 3, -890911: 0, -1153056: 2, -890913: 0, -890914: 8, -890915: 1, -890916: 189, -890917: 3, -890918: 5, -890919: 20, -890920: 224, -890921: 59, -890922: 168, -454060: 64, -454061: 32, -454062: 20, -454067: 224, -454068: 84, -454069: 33, -454070: 60, -454071: 0, -454072: 16, -454077: 192, -454078: 80, -454079: 33, -454080: 60, -454081: 0, -454082: 16, -454087: 160, -454088: 76, -454089: 0, -454090: 56, -454091: 0, -454092: 12, -454097: 128, -454098: 72, -454099: 0, -454100: 56, -454101: 0, -454102: 12, -1676057: 0, -1676058: 0, -1676059: 233, -1676060: 39, -1676061: 102, -1676062: 26, -1676063: 133, -1676064: 21, -1676065: 163, -1676066: 12, -1676067: 156, -1676068: 63, -1676069: 151, -1676070: 46, -1676071: 114, -1676072: 29, -1676073: 142, -1676074: 16, -1676075: 95, -1676076: 10, -1676077: 219, -1676078: 9, -1676079: 86, -1676080: 9, -1676081: 210, -1676082: 8, -1676083: 44, -1676084: 8, -1676085: 189, -1676086: 127, -1676087: 5, -1676088: 12, -454111: 0, -454112: 8, -454117: 64, -454118: 64, -454119: 0, -454120: 52, -454121: 0, -454122: 8, -454127: 96, -454128: 68, -454129: 0, -454130: 52, -454131: 0, -454132: 8, -454137: 128, -454138: 72, -454139: 0, -454140: 56, -454141: 0, -454142: 12, -889856: 0, -889857: 56, -889858: 8, -889859: 1, -889860: 189, -889861: 3, -889862: 5, -889863: 20, -889864: 224, -889865: 59, -889866: 168, -889867: 33, -889868: 159, -889869: 87, -889870: 210, -889871: 74, -889872: 78, -889873: 58, -889874: 187, -889875: 0, -889876: 181, -889877: 2, -889878: 107, -889879: 1, -889880: 82, -889881: 2, -889882: 4, -889883: 17, -889884: 116, -889885: 0, -889886: 13, -889887: 0, -454150: 56, -454151: 0, -454152: 12, -454157: 192, -454158: 80, -454159: 33, -454160: 60, -454161: 0, -454162: 16, -454167: 224, -454168: 84, -454169: 33, -454170: 60, -454171: 0, -454172: 16, -454177: 1, -454178: 89, -454179: 66, -454180: 64, -454181: 32, -454182: 20, -890144: 0, -890145: 0, -890146: 8, -890147: 1, -890148: 255, -890149: 2, -890150: 5, -890151: 20, -890152: 224, -890153: 59, -890154: 168, -890155: 33, -890156: 159, -890157: 87, -890158: 210, -890159: 74, -890160: 78, -890161: 58, -890162: 187, -890163: 0, -890164: 190, -890165: 1, -890166: 142, -890167: 0, -890168: 82, -890169: 2, -890170: 4, -890171: 17, -890172: 116, -890173: 0, -890174: 13, -890175: 0, -890176: 224, -890177: 20, -890178: 206, -890179: 0, -890180: 31, -890181: 66, -890182: 5, -890183: 20, -890184: 224, -890185: 59, -890186: 168, -890187: 33, -890188: 159, -890189: 87, -890190: 210, -890191: 74, -890192: 78, -890193: 58, -890194: 187, -890195: 0, -890196: 20, -890197: 89, -890198: 170, -890199: 48, -890200: 22, -890201: 2, -890202: 4, -890203: 17, -890204: 116, -890205: 0, -890206: 13, -890207: 0, -890208: 3, -890209: 32, -890210: 206, -890211: 40, -890212: 31, -890213: 110, -890214: 5, -890215: 60, -890216: 224, -890217: 99, -890218: 168, -890219: 73, -890220: 159, -890221: 127, -890222: 210, -890223: 114, -890224: 78, -890225: 98, -890226: 187, -890227: 40, -890228: 20, -890229: 125, -890230: 170, -890231: 88, -890232: 22, -890233: 42, -890234: 4, -890235: 57, -890236: 116, -890237: 40, -890238: 13, -890239: 40, -890240: 3, -890241: 32, -890242: 110, -890243: 81, -890244: 191, -890245: 126, -890246: 165, -890247: 100, -890248: 224, -890249: 127, -890250: 72, -890251: 114, -890252: 255, -890253: 127, -890254: 114, -890255: 127, -890256: 238, -890257: 126, -890258: 91, -890259: 85, -890260: 180, -890261: 125, -890262: 74, -890263: 125, -890264: 182, -890265: 82, -890266: 164, -890267: 97, -890268: 20, -890269: 81, -890270: 173, -890271: 80, -890272: 0, -890273: 0, -890274: 174, -890275: 82, -890276: 255, -890277: 127, -890278: 229, -890279: 101, -890280: 224, -890281: 127, -890282: 136, -890283: 115, -890284: 255, -890285: 127, -890286: 242, -890287: 127, -890288: 238, -890289: 127, -890290: 155, -890291: 86, -890292: 244, -890293: 126, -890294: 138, -890295: 126, -890296: 246, -890297: 83, -890298: 228, -890299: 98, -890300: 84, -890301: 82, -890302: 237, -890303: 81, -890304: 0, -890305: 56, -890306: 206, -890307: 0, -890308: 31, -890309: 66, -890310: 5, -890311: 20, -890312: 224, -890313: 59, -890314: 168, -890315: 33, -890316: 159, -890317: 87, -890318: 210, -890319: 74, -890320: 78, -890321: 58, -890322: 187, -890323: 0, -890324: 20, -890325: 89, -890326: 170, -890327: 48, -890328: 22, -890329: 2, -890330: 4, -890331: 17, -890332: 116, -890333: 0, -890334: 13, -890335: 0, -890336: 0, -890337: 56, -890338: 24, -890339: 22, -890340: 95, -890341: 87, -890342: 79, -890343: 41, -890344: 234, -890345: 79, -890346: 242, -890347: 54, -890348: 255, -890349: 107, -890350: 252, -890351: 95, -890352: 152, -890353: 71, -890354: 255, -890355: 21, -890356: 94, -890357: 110, -890358: 244, -890359: 69, -890360: 95, -890361: 23, -890362: 78, -890363: 38, -890364: 190, -890365: 21, -890366: 87, -890367: 21, -890368: 0, -890369: 56, -890370: 222, -890371: 2, -890372: 255, -890373: 67, -890374: 21, -890375: 22, -890376: 240, -890377: 59, -890378: 184, -890379: 35, -890380: 255, -890381: 87, -890382: 255, -890383: 75, -890384: 254, -890385: 59, -890386: 191, -890387: 2, -890388: 31, -890389: 91, -890390: 186, -890391: 50, -890392: 255, -890393: 3, -890394: 20, -890395: 19, -890396: 127, -890397: 2, -890398: 29, -890399: 2, -890400: 0, -890401: 56, -890402: 255, -890403: 43, -890404: 255, -890405: 107, -890406: 95, -890407: 63, -890408: 250, -890409: 99, -890410: 255, -890411: 75, -890412: 255, -890413: 127, -890414: 255, -890415: 115, -890416: 255, -890417: 99, -890418: 255, -890419: 43, -890420: 255, -890421: 127, -890422: 255, -890423: 91, -890424: 255, -890425: 43, -890426: 254, -890427: 59, -890428: 191, -890429: 43, -890430: 95, -890431: 43, -890432: 0, -890433: 56, -890434: 206, -890435: 0, -890436: 31, -890437: 66, -890438: 5, -890439: 20, -890440: 224, -890441: 59, -890442: 168, -890443: 33, -890444: 159, -890445: 87, -890446: 210, -890447: 74, -890448: 78, -890449: 58, -890450: 187, -890451: 0, -890452: 20, -890453: 89, -890454: 170, -890455: 48, -890456: 22, -890457: 2, -890458: 4, -890459: 17, -1152604: 56, -890461: 0, -890462: 13, -1152607: 24, -890464: 0, -890465: 56, -1152610: 0, -890467: 2, -890468: 95, -1152613: 221, -890470: 69, -890471: 21, -890472: 224, -890473: 67, -890474: 232, -1152619: 47, -890476: 255, -890477: 87, -890478: 242, -890479: 75, -890480: 142, -890481: 59, -1152626: 72, -890483: 1, -890484: 84, -890485: 90, -890486: 234, -890487: 49, -890488: 86, -890489: 3, -890490: 68, -890491: 18, -890492: 180, -890493: 1, -890494: 77, -890495: 1, -890496: 0, -890497: 56, -890498: 78, -890499: 3, -890500: 255, -890501: 67, -890502: 133, -890503: 22, -890504: 224, -890505: 79, -890506: 232, -890507: 35, -890508: 255, -890509: 87, -890510: 242, -890511: 75, -890512: 238, -890513: 59, -890514: 59, -890515: 3, -890516: 148, -890517: 91, -890518: 42, -890519: 51, -890520: 246, -890521: 3, -890522: 132, -890523: 19, -890524: 244, -890525: 2, -890526: 141, -890527: 2, -890528: 0, -890529: 56, -890530: 238, -890531: 43, -890532: 255, -890533: 107, -890534: 197, -890535: 63, -890536: 224, -890537: 99, -890538: 232, -890539: 75, -890540: 255, -890541: 127, -890542: 242, -890543: 115, -890544: 238, -890545: 99, -890546: 251, -890547: 43, -890548: 244, -890549: 127, -890550: 234, -890551: 91, -890552: 246, -890553: 43, -890554: 228, -890555: 59, -890556: 244, -890557: 43, -890558: 237, -890559: 43, -1677083: 0, -1677084: 0, -1677085: 233, -1677086: 39, -1677087: 102, -1677088: 26, -1677089: 133, -1677090: 21, -1677091: 163, -1677092: 12, -1677093: 156, -1677094: 63, -1677095: 151, -1677096: 46, -1677097: 114, -1677098: 29, -1677099: 142, -1677100: 16, -1677101: 95, -1677102: 10, -1677103: 219, -1677104: 9, -1677105: 86, -1677106: 9, -1677107: 210, -1677108: 8, -1677109: 44, -1677110: 8, -1677111: 189, -1677112: 127, -1677113: 5, -1677114: 12, -1677115: 3, -1677116: 32, -1677117: 235, -1677118: 39, -1677119: 136, -1677120: 26, -1677121: 167, -1677122: 21, -1677123: 196, -1677124: 12, -1677125: 92, -1677126: 59, -1677127: 88, -1677128: 46, -1677129: 83, -1677130: 29, -1677131: 111, -1677132: 16, -1677133: 63, -1677134: 14, -1677135: 187, -1677136: 13, -1677137: 54, -1677138: 13, -1677139: 210, -1677140: 12, -1677141: 44, -1677142: 12, -1677143: 189, -1677144: 119, -1677145: 6, -1677146: 12, -1677147: 3, -1677148: 32, -1677149: 237, -1677150: 35, -1677151: 138, -1677152: 22, -1677153: 200, -1677154: 17, -1677155: 229, -1677156: 8, -1677157: 253, -1677158: 58, -1677159: 56, -1677160: 42, -1677161: 52, -1677162: 29, -1677163: 113, -1677164: 16, -1677165: 255, -1677166: 13, -1677167: 155, -1677168: 13, -1677169: 54, -1677170: 13, -1677171: 179, -1677172: 12, -1677173: 45, -1677174: 12, -1677175: 222, -1677176: 111, -1677177: 7, -1677178: 8, -1677179: 0, -1677180: 0, -1677181: 239, -1677182: 35, -1677183: 172, -890752: 0, -890753: 0, -890754: 206, -890755: 0, -890756: 31, -890757: 66, -890758: 5, -890759: 20, -890760: 224, -890761: 59, -890762: 168, -1677195: 82, -890764: 159, -890765: 87, -890766: 210, -890767: 74, -1677200: 17, -890769: 58, -890770: 187, -890771: 0, -890772: 20, -1677205: 45, -890774: 170, -890775: 48, -890776: 22, -890777: 2, -1677210: 8, -890779: 17, -890780: 116, -890781: 0, -1677214: 35, -1677215: 207, -890784: 0, -890785: 0, -890786: 214, -890787: 33, -1677220: 9, -890789: 86, -890790: 170, -890791: 40, -890792: 229, -890793: 79, -890794: 77, -890795: 54, -890796: 191, -890797: 99, -890798: 54, -890799: 91, -890800: 212, -890801: 82, -890802: 92, -890803: 21, -890804: 184, -890805: 101, -890806: 79, -1677239: 222, -1677240: 95, -890809: 22, -890810: 169, -890811: 37, -890812: 24, -1677245: 244, -890814: 178, -890815: 20, -890816: 0, -890817: 0, -1677250: 18, -890819: 46, -890820: 95, -890821: 107, -890822: 112, -890823: 65, -890824: 235, -890825: 103, -890826: 19, -890827: 79, -1677260: 20, -890829: 115, -890830: 155, -890831: 111, -1677264: 21, -1677265: 215, -890834: 30, -1677267: 147, -1677268: 20, -890837: 114, -1677270: 20, -890839: 94, -890840: 92, -890841: 47, -890842: 111, -1677275: 0, -890844: 219, -890845: 45, -890846: 120, -890847: 45, -890848: 0, -890849: 0, -890850: 222, -890851: 66, -890852: 255, -1677285: 222, -890854: 21, -890855: 86, -1677288: 33, -1677289: 152, -890858: 184, -890859: 99, -890860: 255, -1677293: 95, -1677294: 21, -1677295: 28, -1677296: 21, -1677297: 215, -890866: 191, -890867: 66, -890868: 31, -890869: 127, -890870: 186, -890871: 114, -890872: 255, -890873: 67, -890874: 20, -890875: 83, -1677308: 0, -890877: 66, -890878: 29, -890879: 66, -890880: 0, -890881: 56, -890882: 8, -890883: 1, -1677316: 5, -890885: 66, -1677318: 41, -890887: 20, -890888: 224, -890889: 59, -890890: 168, -1153035: 0, -1153036: 56, -1153037: 245, -1153038: 87, -1153039: 239, -890896: 78, -1153041: 130, -890898: 187, -1153043: 192, -1153044: 0, -890901: 89, -1153046: 87, -1153047: 173, -890904: 116, -890905: 2, -1153050: 46, -890907: 17, -890908: 116, -1153053: 253, -890910: 13, -1153055: 213, -890912: 0, -1153057: 15, -1153058: 2, -1153059: 73, -1153060: 1, -1153061: 223, -1153062: 62, -1153063: 24, -1153064: 0, -1153065: 15, -1153066: 0, -890923: 33, -890924: 159, -890925: 87, -890926: 210, -890927: 74, -890928: 78, -890929: 58, -890930: 187, -890931: 0, -890932: 181, -890933: 2, -890934: 107, -890935: 1, -890936: 82, -890937: 2, -890938: 4, -890939: 17, -890940: 116, -890941: 0, -890942: 13, -890943: 0, -890944: 99, -890945: 12, -890946: 74, -890947: 13, -890948: 189, -890949: 15, -890950: 104, -890951: 32, -890952: 227, -890953: 67, -890954: 234, -890955: 41, -890956: 159, -890957: 91, -890958: 243, -890959: 78, -890960: 112, -890961: 66, -890962: 27, -890963: 13, -890964: 214, -890965: 14, -890966: 173, -890967: 13, -890968: 115, -890969: 14, -890970: 71, -890971: 29, -890972: 213, -890973: 12, -890974: 111, -890975: 12, -890976: 231, -890977: 28, -890978: 173, -890979: 29, -890980: 189, -890981: 31, -890982: 235, -890983: 44, -890984: 231, -890985: 75, -890986: 45, -890987: 54, -890988: 159, -890989: 95, -890990: 21, -890991: 87, -890992: 178, -890993: 74, -890994: 124, -890995: 29, -890996: 247, -890997: 30, -890998: 16, -890999: 30, -891000: 181, -891001: 30, -891002: 170, -891003: 41, -891004: 86, -891005: 29, -891006: 241, -891007: 28, -891008: 107, -891009: 45, -891010: 16, -891011: 46, -891012: 189, -891013: 47, -891014: 110, -891015: 57, -891016: 235, -891017: 83, -891018: 112, -891019: 66, -891020: 191, -891021: 99, -891022: 54, -891023: 91, -891024: 212, -891025: 82, -891026: 220, -891027: 45, -891028: 24, -891029: 47, -891030: 82, -891031: 46, -891032: 214, -891033: 46, -891034: 14, -891035: 58, -891036: 184, -891037: 45, -891038: 115, -891039: 45, -891040: 239, -891041: 61, -891042: 115, -891043: 62, -891044: 222, -891045: 63, -891046: 242, -891047: 73, -891048: 239, -891049: 91, -891050: 211, -891051: 78, -891052: 191, -891053: 107, -891054: 88, -891055: 99, -891056: 22, -891057: 91, -891058: 93, -891059: 62, -891060: 90, -891061: 63, -891062: 181, -891063: 62, -891064: 24, -891065: 63, -891066: 113, -891067: 70, -891068: 57, -891069: 62, -891070: 246, -891071: 61, -891072: 115, -891073: 78, -891074: 214, -891075: 78, -891076: 222, -891077: 79, -891078: 117, -891079: 86, -891080: 243, -891081: 99, -891082: 22, -891083: 91, -891084: 191, -891085: 111, -891086: 122, -891087: 107, -891088: 88, -891089: 99, -891090: 189, -891091: 78, -891092: 123, -891093: 79, -891094: 247, -891095: 78, -891096: 90, -891097: 79, -891098: 212, -891099: 82, -891100: 154, -891101: 78, -891102: 120, -891103: 78, -891104: 247, -891105: 94, -891106: 57, -891107: 95, -891108: 222, -891109: 95, -891110: 248, -891111: 98, -891112: 247, -891113: 107, -891114: 89, -891115: 103, -891116: 223, -891117: 115, -891118: 155, -891119: 111, -891120: 122, -891121: 107, -891122: 30, -891123: 95, -891124: 156, -891125: 95, -891126: 90, -891127: 95, -891128: 123, -891129: 95, -891130: 56, -891131: 99, -891132: 28, -891133: 95, -891134: 250, -891135: 94, -891136: 123, -891137: 111, -891138: 156, -891139: 111, -891140: 222, -891141: 111, -891142: 123, -891143: 111, -891144: 251, -891145: 115, -891146: 156, -891147: 115, -891148: 223, -891149: 119, -891150: 189, -891151: 119, -891152: 188, -891153: 115, -891154: 126, -891155: 111, -891156: 189, -891157: 111, -891158: 156, -891159: 111, -891160: 189, -891161: 111, -891162: 155, -891163: 111, -891164: 125, -891165: 111, -891166: 124, -891167: 111, -891168: 0, -891169: 0, -891170: 8, -891171: 1, -891172: 255, -891173: 2, -891174: 5, -891175: 20, -891176: 224, -891177: 59, -891178: 168, -891179: 33, -891180: 159, -891181: 87, -891182: 210, -891183: 74, -891184: 78, -891185: 58, -891186: 187, -891187: 0, -891188: 190, -891189: 1, -891190: 142, -891191: 0, -891192: 82, -891193: 2, -891194: 4, -891195: 17, -891196: 116, -891197: 0, -891198: 13, -891199: 0, -891200: 99, -891201: 12, -891202: 74, -891203: 13, -891204: 31, -891205: 15, -891206: 104, -891207: 32, -891208: 227, -891209: 67, -891210: 234, -891211: 41, -891212: 159, -891213: 91, -891214: 243, -891215: 78, -891216: 112, -891217: 66, -891218: 27, -891219: 13, -891220: 254, -891221: 13, -891222: 240, -891223: 12, -891224: 115, -891225: 14, -891226: 71, -891227: 29, -891228: 213, -891229: 12, -891230: 111, -891231: 12, -891232: 231, -891233: 28, -891234: 173, -891235: 29, -891236: 63, -891237: 31, -891238: 235, -891239: 44, -891240: 231, -891241: 75, -891242: 45, -891243: 54, -891244: 159, -891245: 95, -891246: 21, -891247: 87, -891248: 178, -891249: 74, -891250: 124, -891251: 29, -891252: 62, -891253: 30, -891254: 82, -891255: 29, -891256: 181, -891257: 30, -891258: 170, -891259: 41, -891260: 86, -891261: 29, -891262: 241, -891263: 28, -891264: 107, -891265: 45, -891266: 16, -891267: 46, -891268: 95, -891269: 47, -891270: 110, -891271: 57, -891272: 235, -891273: 83, -891274: 112, -891275: 66, -891276: 191, -891277: 99, -891278: 54, -891279: 91, -891280: 212, -891281: 82, -891282: 220, -891283: 45, -891284: 126, -891285: 46, -891286: 212, -891287: 45, -891288: 214, -891289: 46, -891290: 14, -891291: 58, -891292: 184, -891293: 45, -891294: 115, -891295: 45, -891296: 239, -891297: 61, -891298: 115, -891299: 62, -891300: 127, -891301: 63, -891302: 242, -891303: 73, -891304: 239, -891305: 91, -891306: 211, -891307: 78, -891308: 191, -891309: 107, -891310: 88, -891311: 99, -891312: 22, -891313: 91, -891314: 93, -891315: 62, -891316: 222, -891317: 62, -891318: 54, -891319: 62, -891320: 24, -891321: 63, -891322: 113, -891323: 70, -891324: 57, -891325: 62, -891326: 246, -891327: 61, -891328: 115, -891329: 78, -891330: 214, -891331: 78, -891332: 159, -891333: 79, -891334: 117, -891335: 86, -891336: 243, -891337: 99, -891338: 22, -891339: 91, -891340: 191, -891341: 111, -891342: 122, -891343: 107, -891344: 88, -891345: 99, -891346: 189, -891347: 78, -891348: 30, -891349: 79, -891350: 152, -891351: 78, -891352: 90, -891353: 79, -891354: 212, -891355: 82, -891356: 154, -891357: 78, -891358: 120, -891359: 78, -891360: 247, -891361: 94, -891362: 57, -891363: 95, -891364: 191, -891365: 95, -891366: 248, -891367: 98, -891368: 247, -891369: 107, -891370: 89, -891371: 103, -891372: 223, -891373: 115, -891374: 155, -891375: 111, -891376: 122, -891377: 107, -891378: 30, -891379: 95, -891380: 94, -891381: 95, -891382: 26, -891383: 95, -891384: 123, -891385: 95, -891386: 56, -891387: 99, -891388: 28, -891389: 95, -891390: 250, -891391: 94, -891392: 123, -891393: 111, -891394: 156, -891395: 111, -891396: 223, -891397: 111, -891398: 123, -891399: 111, -891400: 251, -891401: 115, -891402: 156, -891403: 115, -891404: 223, -891405: 119, -891406: 189, -891407: 119, -891408: 188, -891409: 115, -891410: 126, -891411: 111, -891412: 158, -891413: 111, -891414: 124, -891415: 111, -891416: 189, -891417: 111, -891418: 155, -891419: 111, -891420: 125, -891421: 111, -891422: 124, -891423: 111, -891424: 224, -891425: 20, -891426: 206, -891427: 0, -891428: 31, -891429: 66, -891430: 5, -891431: 20, -891432: 224, -891433: 59, -891434: 168, -891435: 33, -891436: 159, -891437: 87, -891438: 210, -891439: 74, -891440: 78, -891441: 58, -891442: 187, -891443: 0, -891444: 20, -891445: 89, -891446: 170, -891447: 48, -891448: 22, -891449: 2, -891450: 4, -891451: 17, -891452: 116, -891453: 0, -891454: 13, -891455: 0, -891456: 67, -891457: 33, -891458: 48, -891459: 13, -891460: 63, -891461: 70, -891462: 104, -891463: 32, -891464: 227, -891465: 67, -891466: 234, -891467: 41, -891468: 159, -891469: 91, -891470: 243, -891471: 78, -891472: 112, -891473: 66, -891474: 27, -891475: 13, -891476: 85, -891477: 93, -891478: 12, -891479: 57, -891480: 55, -891481: 14, -891482: 71, -891483: 29, -891484: 213, -891485: 12, -891486: 111, -891487: 12, -891488: 167, -891489: 45, -891490: 146, -891491: 29, -891492: 127, -891493: 78, -891494: 235, -891495: 44, -891496: 231, -891497: 75, -891498: 45, -891499: 54, -891500: 159, -891501: 95, -891502: 21, -891503: 87, -891504: 178, -891505: 74, -891506: 124, -891507: 29, -891508: 182, -891509: 97, -891510: 111, -891511: 65, -891512: 120, -891513: 30, -891514: 170, -891515: 41, -891516: 86, -891517: 29, -891518: 241, -891519: 28, -891520: 11, -891521: 58, -891522: 244, -891523: 45, -891524: 191, -891525: 86, -891526: 110, -891527: 57, -891528: 235, -891529: 83, -891530: 112, -891531: 66, -891532: 191, -891533: 99, -891534: 54, -891535: 91, -891536: 212, -891537: 82, -891538: 220, -891539: 45, -891540: 24, -891541: 102, -891542: 209, -891543: 77, -891544: 185, -891545: 46, -891546: 14, -891547: 58, -891548: 184, -891549: 45, -891550: 115, -891551: 45, -891552: 111, -891553: 74, -891554: 86, -891555: 62, -891556: 255, -891557: 94, -891558: 242, -891559: 73, -891560: 239, -891561: 91, -891562: 211, -891563: 78, -891564: 191, -891565: 107, -891566: 88, -891567: 99, -891568: 22, -891569: 91, -891570: 93, -891571: 62, -891572: 121, -891573: 106, -891574: 84, -891575: 86, -891576: 250, -891577: 62, -891578: 113, -891579: 70, -891580: 57, -891581: 62, -891582: 246, -891583: 61, -891584: 211, -891585: 86, -891586: 184, -891587: 78, -891588: 63, -891589: 103, -891590: 117, -891591: 86, -891592: 243, -891593: 99, -891594: 22, -891595: 91, -891596: 191, -891597: 111, -891598: 122, -891599: 107, -891600: 88, -891601: 99, -891602: 189, -891603: 78, -891604: 218, -891605: 110, -891606: 183, -891607: 94, -891608: 59, -891609: 79, -891610: 212, -891611: 82, -891612: 154, -891613: 78, -891614: 120, -891615: 78, -891616: 55, -891617: 99, -891618: 26, -891619: 95, -891620: 127, -891621: 111, -891622: 248, -891623: 98, -891624: 247, -891625: 107, -891626: 89, -891627: 103, -891628: 223, -891629: 115, -891630: 155, -891631: 111, -891632: 122, -891633: 107, -891634: 30, -891635: 95, -891636: 60, -891637: 115, -891638: 25, -891639: 107, -891640: 124, -891641: 95, -891642: 56, -891643: 99, -891644: 28, -891645: 95, -891646: 250, -891647: 94, -891648: 155, -891649: 111, -891650: 124, -891651: 111, -891652: 191, -891653: 119, -891654: 123, -891655: 111, -891656: 251, -891657: 115, -891658: 156, -891659: 115, -891660: 223, -891661: 119, -891662: 189, -891663: 119, -891664: 188, -891665: 115, -891666: 126, -891667: 111, -891668: 157, -891669: 119, -891670: 124, -891671: 115, -891672: 189, -891673: 111, -891674: 155, -891675: 111, -891676: 125, -891677: 111, -891678: 124, -891679: 111, -891680: 0, -891681: 0, -891682: 8, -891683: 1, -891684: 189, -891685: 3, -891686: 5, -891687: 20, -891688: 224, -891689: 59, -891690: 168, -891691: 33, -891692: 159, -891693: 87, -891694: 210, -891695: 74, -891696: 78, -891697: 58, -891698: 187, -891699: 0, -891700: 181, -891701: 2, -891702: 107, -891703: 1, -891704: 82, -891705: 2, -891706: 4, -891707: 17, -891708: 116, -891709: 0, -891710: 13, -891711: 0, -891712: 3, -891713: 32, -891714: 206, -891715: 40, -891716: 189, -891717: 55, -891718: 5, -891719: 60, -891720: 224, -891721: 99, -891722: 168, -891723: 73, -891724: 159, -891725: 127, -891726: 210, -891727: 114, -891728: 78, -891729: 98, -891730: 187, -891731: 40, -891732: 181, -891733: 54, -891734: 107, -891735: 53, -891736: 22, -891737: 42, -891738: 4, -891739: 57, -891740: 116, -891741: 40, -891742: 13, -891743: 40, -891744: 3, -891745: 32, -891746: 110, -891747: 81, -891748: 189, -891749: 75, -891750: 165, -891751: 100, -891752: 224, -891753: 127, -891754: 72, -891755: 114, -891756: 255, -891757: 127, -891758: 114, -891759: 127, -891760: 238, -891761: 126, -891762: 91, -891763: 85, -891764: 181, -891765: 74, -891766: 107, -891767: 73, -891768: 182, -891769: 82, -891770: 164, -1153915: 0, -1153916: 56, -1153917: 87, -1153918: 63, -1153919: 77, -891776: 0, -1153921: 226, -1153922: 0, -1153923: 96, -1153924: 0, -1153925: 176, -1153926: 58, -1153927: 11, -1153928: 34, -1153929: 102, -1153930: 17, -1153931: 36, -1153932: 9, -1153933: 90, -1153934: 127, -1153935: 192, -1153936: 126, -1153937: 224, -1153938: 109, -1153939: 224, -1153940: 84, -1153941: 255, -1153942: 3, -1153943: 55, -1153944: 2, -1153945: 209, -1153946: 0, -1153947: 0, -1153948: 56, -1153949: 90, -891806: 237, -1153951: 224, -1153952: 59, -1153953: 128, -1153954: 38, -1153955: 32, -1153956: 9, -1153957: 90, -1153958: 79, -1153959: 181, -1153960: 54, -1153961: 16, -1153962: 38, -1153963: 206, -1153964: 29, -1153965: 148, -1153966: 82, -1153967: 206, -1153968: 57, -1153969: 8, -1153970: 33, -1153971: 132, -1153972: 16, -1153973: 59, -1153974: 3, -1153975: 22, -1153976: 2, -1153977: 19, -1153978: 1, -891835: 17, -891836: 116, -891837: 0, -891838: 13, -891839: 0, -891840: 0, -891841: 0, -891842: 173, -891843: 29, -891844: 189, -891845: 31, -891846: 235, -891847: 44, -891848: 231, -891849: 75, -891850: 45, -891851: 54, -891852: 159, -891853: 95, -891854: 21, -891855: 87, -891856: 178, -891857: 74, -891858: 124, -891859: 29, -891860: 247, -891861: 30, -891862: 16, -891863: 30, -891864: 181, -891865: 30, -891866: 170, -891867: 41, -891868: 86, -891869: 29, -891870: 241, -891871: 28, -891872: 0, -891873: 0, -891874: 115, -891875: 62, -891876: 222, -891877: 63, -891878: 242, -891879: 73, -891880: 239, -891881: 91, -891882: 211, -891883: 78, -891884: 191, -891885: 107, -891886: 88, -891887: 99, -891888: 22, -891889: 91, -891890: 93, -891891: 62, -891892: 90, -891893: 63, -891894: 181, -891895: 62, -891896: 24, -891897: 63, -891898: 113, -891899: 70, -891900: 57, -891901: 62, -891902: 246, -891903: 61, -891904: 0, -891905: 0, -891906: 57, -891907: 95, -891908: 222, -891909: 95, -891910: 248, -891911: 98, -891912: 247, -891913: 107, -891914: 89, -891915: 103, -891916: 223, -891917: 115, -891918: 155, -891919: 111, -891920: 122, -891921: 107, -891922: 30, -891923: 95, -891924: 156, -891925: 95, -891926: 90, -891927: 95, -891928: 123, -891929: 95, -891930: 56, -891931: 99, -891932: 28, -891933: 95, -891934: 250, -891935: 94, -891936: 0, -891937: 56, -891938: 8, -891939: 1, -891940: 189, -891941: 3, -891942: 5, -891943: 20, -891944: 224, -891945: 59, -891946: 168, -891947: 33, -891948: 159, -891949: 87, -891950: 210, -891951: 74, -891952: 78, -891953: 58, -891954: 187, -891955: 0, -891956: 181, -891957: 2, -891958: 107, -891959: 1, -891960: 82, -891961: 2, -891962: 4, -891963: 17, -891964: 116, -891965: 0, -891966: 13, -891967: 0, -891968: 0, -891969: 56, -891970: 173, -891971: 1, -891972: 255, -891973: 3, -891974: 79, -891975: 41, -891976: 234, -891977: 79, -891978: 242, -891979: 54, -891980: 255, -891981: 107, -891982: 252, -891983: 95, -891984: 152, -891985: 71, -891986: 124, -891987: 1, -891988: 24, -891989: 3, -891990: 16, -891991: 2, -891992: 181, -891993: 2, -891994: 78, -891995: 38, -891996: 190, -891997: 21, -891998: 87, -891999: 21, -892000: 0, -892001: 56, -892002: 115, -892003: 2, -892004: 255, -892005: 23, -892006: 21, -892007: 22, -892008: 240, -892009: 59, -892010: 184, -892011: 35, -892012: 255, -892013: 87, -892014: 255, -892015: 75, -892016: 254, -892017: 59, -892018: 93, -892019: 2, -892020: 123, -892021: 3, -892022: 181, -892023: 2, -892024: 24, -892025: 3, -892026: 20, -892027: 19, -892028: 127, -892029: 2, -892030: 29, -892031: 2, -892032: 0, -892033: 56, -892034: 57, -892035: 3, -892036: 255, -892037: 35, -892038: 95, -892039: 63, -892040: 250, -892041: 99, -892042: 255, -892043: 75, -892044: 255, -892045: 127, -892046: 255, -892047: 115, -892048: 255, -892049: 99, -892050: 30, -892051: 3, -892052: 222, -892053: 3, -892054: 90, -892055: 3, -892056: 123, -892057: 3, -892058: 254, -892059: 59, -892060: 191, -892061: 43, -892062: 95, -892063: 43, -892064: 0, -892065: 56, -892066: 8, -892067: 1, -892068: 189, -892069: 3, -892070: 5, -892071: 20, -892072: 224, -892073: 59, -892074: 168, -892075: 33, -892076: 159, -892077: 87, -892078: 210, -892079: 74, -892080: 78, -892081: 58, -892082: 187, -892083: 0, -892084: 181, -892085: 2, -892086: 107, -892087: 1, -892088: 82, -892089: 2, -892090: 4, -892091: 17, -892092: 116, -892093: 0, -892094: 13, -892095: 0, -892096: 0, -892097: 56, -892098: 168, -892099: 1, -892100: 248, -892101: 3, -892102: 69, -892103: 21, -892104: 224, -892105: 67, -892106: 232, -892107: 34, -892108: 255, -892109: 87, -892110: 242, -892111: 75, -892112: 142, -892113: 59, -892114: 251, -892115: 1, -892116: 85, -892117: 3, -892118: 11, -892119: 2, -892120: 86, -892121: 3, -892122: 68, -892123: 18, -892124: 180, -892125: 1, -892126: 77, -892127: 1, -892128: 0, -892129: 56, -892130: 72, -892131: 2, -892132: 243, -892133: 3, -892134: 133, -892135: 22, -892136: 224, -892137: 79, -892138: 232, -892139: 35, -892140: 255, -892141: 87, -892142: 242, -892143: 75, -892144: 238, -892145: 59, -892146: 59, -892147: 3, -892148: 245, -892149: 3, -892150: 171, -892151: 2, -892152: 246, -892153: 3, -892154: 132, -892155: 19, -892156: 244, -892157: 2, -892158: 141, -892159: 2, -892160: 0, -892161: 56, -892162: 232, -892163: 2, -892164: 238, -892165: 3, -892166: 197, -892167: 63, -892168: 224, -892169: 99, -892170: 232, -892171: 75, -892172: 255, -892173: 127, -892174: 242, -892175: 115, -892176: 238, -892177: 99, -892178: 251, -892179: 43, -892180: 240, -892181: 3, -892182: 75, -892183: 3, -892184: 246, -892185: 43, -892186: 228, -892187: 59, -892188: 244, -892189: 43, -892190: 237, -892191: 43, -892192: 0, -892193: 0, -892194: 8, -892195: 1, -892196: 255, -892197: 2, -892198: 5, -892199: 20, -892200: 224, -892201: 59, -892202: 168, -892203: 33, -892204: 159, -892205: 87, -892206: 210, -892207: 74, -892208: 78, -892209: 58, -892210: 187, -892211: 0, -892212: 190, -892213: 1, -892214: 142, -892215: 0, -892216: 82, -892217: 2, -892218: 4, -892219: 17, -892220: 116, -892221: 0, -892222: 13, -892223: 0, -892224: 3, -892225: 32, -892226: 8, -892227: 41, -892228: 255, -892229: 42, -892230: 5, -892231: 60, -892232: 224, -892233: 99, -892234: 168, -892235: 73, -892236: 159, -892237: 127, -892238: 210, -892239: 114, -892240: 78, -892241: 98, -892242: 187, -892243: 40, -892244: 190, -892245: 41, -892246: 142, -892247: 40, -892248: 77, -892249: 42, -892250: 4, -892251: 57, -892252: 116, -892253: 40, -892254: 13, -892255: 40, -892256: 3, -892257: 32, -892258: 8, -892259: 81, -892260: 255, -892261: 82, -892262: 165, -892263: 100, -892264: 224, -892265: 127, -892266: 72, -892267: 114, -892268: 255, -892269: 127, -892270: 114, -892271: 127, -892272: 238, -892273: 126, -892274: 91, -892275: 85, -892276: 190, -892277: 81, -892278: 142, -892279: 80, -892280: 77, -892281: 62, -892282: 164, -892283: 97, -892284: 20, -892285: 81, -892286: 173, -892287: 80, -892288: 0, -892289: 0, -892290: 168, -892291: 121, -892292: 159, -892293: 123, -892294: 229, -892295: 101, -892296: 224, -892297: 127, -892298: 136, -892299: 115, -892300: 255, -892301: 127, -892302: 242, -892303: 127, -892304: 238, -892305: 127, -892306: 155, -892307: 86, -892308: 94, -892309: 118, -892310: 46, -454553: 0, -892312: 77, -892313: 82, -892314: 228, -892315: 98, -892316: 84, -454554: 0, -892318: 237, -892319: 81, -892320: 0, -892321: 0, -892322: 8, -892323: 1, -892324: 255, -892325: 2, -892326: 5, -892327: 20, -892328: 224, -892329: 59, -892330: 168, -892331: 33, -892332: 159, -892333: 87, -892334: 210, -892335: 74, -892336: 78, -892337: 58, -892338: 187, -892339: 0, -892340: 190, -892341: 1, -892342: 142, -892343: 0, -892344: 82, -892345: 2, -892346: 4, -892347: 17, -892348: 116, -892349: 0, -892350: 13, -892351: 0, -892352: 0, -892353: 0, -892354: 173, -892355: 29, -892356: 63, -892357: 31, -892358: 235, -892359: 44, -892360: 231, -892361: 75, -892362: 45, -892363: 54, -892364: 159, -892365: 95, -892366: 21, -892367: 87, -892368: 178, -892369: 74, -892370: 124, -892371: 29, -892372: 62, -892373: 30, -892374: 82, -892375: 29, -892376: 181, -892377: 30, -892378: 170, -892379: 41, -892380: 86, -892381: 29, -892382: 241, -892383: 28, -892384: 0, -892385: 0, -892386: 115, -892387: 62, -892388: 127, -892389: 63, -892390: 242, -892391: 73, -892392: 239, -892393: 91, -892394: 211, -892395: 78, -892396: 191, -892397: 107, -892398: 88, -892399: 99, -892400: 22, -892401: 91, -892402: 93, -892403: 62, -892404: 222, -892405: 62, -892406: 54, -454569: 5, -892408: 24, -892409: 63, -892410: 113, -892411: 70, -892412: 57, -892413: 62, -892414: 246, -892415: 61, -892416: 0, -892417: 0, -892418: 57, -892419: 95, -892420: 191, -892421: 95, -892422: 248, -892423: 98, -892424: 247, -892425: 107, -892426: 89, -892427: 103, -892428: 223, -892429: 115, -892430: 155, -892431: 111, -892432: 122, -892433: 107, -892434: 30, -892435: 95, -892436: 94, -892437: 95, -892438: 26, -892439: 95, -892440: 123, -892441: 95, -892442: 56, -454575: 0, -892444: 28, -892445: 95, -892446: 250, -892447: 94, -892448: 0, -454576: 0, -892450: 8, -892451: 1, -892452: 255, -892453: 2, -892454: 5, -892455: 20, -892456: 224, -892457: 59, -892458: 168, -892459: 33, -892460: 159, -454578: 0, -892462: 210, -892463: 74, -892464: 78, -892465: 58, -892466: 187, -892467: 0, -892468: 190, -892469: 1, -892470: 142, -892471: 0, -892472: 82, -892473: 2, -892474: 4, -892475: 17, -892476: 116, -892477: 0, -892478: 13, -892479: 0, -892480: 0, -892481: 56, -892482: 173, -892483: 1, -892484: 159, -892485: 3, -892486: 79, -892487: 41, -892488: 234, -892489: 79, -892490: 242, -892491: 54, -892492: 255, -892493: 107, -892494: 252, -892495: 95, -892496: 152, -892497: 71, -892498: 255, -892499: 21, -892500: 94, -892501: 2, -892502: 51, -454585: 20, -892504: 247, -892505: 2, -892506: 78, -892507: 38, -892508: 190, -454586: 64, -892510: 87, -892511: 21, -892512: 0, -892513: 56, -892514: 82, -454587: 12, -892516: 255, -892517: 3, -892518: 21, -892519: 22, -892520: 240, -454588: 28, -892522: 184, -892523: 35, -892524: 255, -892525: 87, -892526: 255, -454589: 6, -892528: 254, -892529: 59, -892530: 191, -892531: 2, -892532: 254, -454590: 24, -892534: 216, -892535: 1, -892536: 156, -892537: 3, -892538: 20, -892539: 19, -892540: 127, -892541: 2, -892542: 29, -892543: 2, -892544: 0, -892545: 56, -892546: 247, -892547: 2, -892548: 255, -892549: 23, -892550: 95, -892551: 63, -892552: 250, -892553: 99, -892554: 255, -892555: 75, -892556: 255, -892557: 127, -892558: 255, -892559: 115, -892560: 255, -892561: 99, -892562: 255, -892563: 43, -892564: 158, -892565: 3, -892566: 125, -892567: 2, -892568: 255, -892569: 3, -892570: 254, -892571: 59, -892572: 191, -892573: 43, -892574: 95, -892575: 43, -892576: 0, -892577: 56, -892578: 8, -892579: 1, -892580: 255, -892581: 2, -892582: 5, -892583: 20, -892584: 224, -892585: 59, -892586: 168, -892587: 33, -892588: 159, -892589: 87, -892590: 210, -892591: 74, -892592: 78, -892593: 58, -892594: 187, -892595: 0, -892596: 190, -892597: 1, -892598: 142, -892599: 0, -892600: 82, -892601: 2, -892602: 4, -892603: 17, -892604: 116, -892605: 0, -892606: 13, -892607: 0, -892608: 0, -892609: 56, -892610: 168, -892611: 1, -892612: 159, -892613: 3, -892614: 69, -892615: 21, -892616: 224, -892617: 67, -892618: 232, -892619: 34, -892620: 255, -892621: 87, -892622: 242, -892623: 75, -892624: 142, -892625: 59, -892626: 251, -892627: 1, -892628: 94, -892629: 2, -892630: 46, -892631: 1, -892632: 242, -892633: 2, -892634: 68, -892635: 18, -892636: 180, -892637: 1, -892638: 77, -892639: 1, -892640: 0, -892641: 56, -892642: 72, -892643: 2, -892644: 154, -892645: 3, -892646: 133, -892647: 22, -892648: 224, -892649: 79, -892650: 232, -892651: 35, -892652: 255, -892653: 87, -892654: 242, -892655: 75, -892656: 238, -892657: 59, -892658: 59, -892659: 3, -892660: 254, -892661: 2, -892662: 206, -892663: 1, -892664: 146, -892665: 3, -892666: 132, -892667: 19, -892668: 244, -892669: 2, -892670: 141, -892671: 2, -892672: 0, -892673: 56, -892674: 232, -892675: 2, -892676: 148, -892677: 3, -892678: 197, -892679: 63, -892680: 224, -892681: 99, -892682: 232, -892683: 75, -892684: 255, -892685: 127, -892686: 242, -892687: 115, -892688: 238, -892689: 99, -892690: 251, -892691: 43, -892692: 158, -892693: 3, -892694: 110, -892695: 2, -892696: 242, -892697: 3, -892698: 228, -892699: 59, -892700: 244, -892701: 43, -892702: 237, -892703: 43, -1500569: 12, -892705: 20, -892706: 206, -454619: 0, -892708: 31, -892709: 66, -892710: 5, -892711: 20, -892712: 224, -454620: 0, -892714: 168, -892715: 33, -892716: 159, -892717: 87, -892718: 210, -892719: 74, -892720: 78, -892721: 58, -892722: 187, -892723: 0, -892724: 20, -892725: 89, -892726: 170, -892727: 48, -892728: 22, -892729: 2, -892730: 4, -892731: 17, -892732: 116, -892733: 0, -892734: 13, -892735: 0, -892736: 3, -892737: 32, -892738: 206, -892739: 40, -892740: 31, -892741: 110, -892742: 5, -454625: 22, -892744: 224, -892745: 99, -892746: 168, -892747: 73, -892748: 159, -454626: 72, -892750: 210, -892751: 114, -892752: 78, -892753: 98, -892754: 187, -454627: 14, -892756: 20, -892757: 125, -892758: 170, -892759: 88, -892760: 22, -454628: 36, -892762: 4, -892763: 57, -892764: 116, -892765: 40, -892766: 13, -454629: 8, -892768: 3, -892769: 32, -892770: 110, -892771: 81, -892772: 191, -454630: 32, -892774: 165, -892775: 100, -892776: 224, -892777: 127, -892778: 72, -454631: 6, -892780: 255, -892781: 127, -892782: 114, -892783: 127, -892784: 238, -454632: 24, -892786: 91, -892787: 85, -892788: 180, -892789: 125, -892790: 74, -454633: 4, -892792: 182, -892793: 82, -892794: 164, -892795: 97, -892796: 20, -454634: 16, -892798: 173, -892799: 80, -892800: 0, -892801: 0, -892802: 174, -454635: 2, -892804: 255, -892805: 127, -892806: 229, -892807: 101, -892808: 224, -454636: 8, -892810: 136, -892811: 115, -892812: 255, -892813: 127, -892814: 242, -454637: 1, -892816: 238, -892817: 127, -892818: 155, -892819: 86, -892820: 244, -454638: 4, -892822: 138, -892823: 126, -892824: 246, -892825: 83, -892826: 228, -454639: 0, -892828: 84, -892829: 82, -892830: 237, -892831: 81, -892832: 0, -454640: 0, -892834: 206, -1640238: 0, -892836: 31, -892837: 66, -892838: 5, -892839: 20, -892840: 224, -892841: 59, -892842: 168, -892843: 33, -892844: 159, -892845: 87, -892846: 210, -892847: 74, -892848: 78, -892849: 58, -892850: 187, -892851: 0, -892852: 20, -892853: 89, -892854: 170, -892855: 48, -892856: 22, -892857: 2, -892858: 4, -892859: 17, -892860: 116, -892861: 0, -892862: 13, -892863: 0, -892864: 0, -892865: 0, -892866: 146, -892867: 29, -892868: 127, -892869: 78, -892870: 235, -892871: 44, -892872: 231, -892873: 75, -892874: 45, -892875: 54, -892876: 159, -892877: 95, -892878: 21, -892879: 87, -892880: 178, -892881: 74, -892882: 124, -892883: 29, -892884: 182, -892885: 97, -892886: 111, -892887: 65, -892888: 120, -892889: 30, -892890: 170, -892891: 41, -892892: 86, -892893: 29, -892894: 241, -892895: 28, -892896: 0, -892897: 0, -892898: 86, -892899: 62, -892900: 255, -892901: 94, -892902: 242, -892903: 73, -892904: 239, -892905: 91, -892906: 211, -892907: 78, -892908: 191, -892909: 107, -892910: 88, -454653: 191, -892912: 22, -892913: 91, -892914: 93, -892915: 62, -892916: 121, -454654: 2, -892918: 84, -892919: 86, -892920: 250, -892921: 62, -892922: 113, -454655: 127, -892924: 57, -892925: 62, -892926: 246, -892927: 61, -892928: 0, -454656: 1, -892930: 26, -892931: 95, -892932: 127, -892933: 111, -892934: 248, -454657: 21, -892936: 247, -892937: 107, -892938: 89, -892939: 103, -892940: 223, -454658: 0, -892942: 155, -892943: 111, -892944: 122, -892945: 107, -892946: 30, -892947: 95, -892948: 60, -892949: 115, -892950: 25, -892951: 107, -892952: 124, -892953: 95, -892954: 56, -892955: 99, -892956: 28, -892957: 95, -892958: 250, -892959: 94, -892960: 0, -892961: 56, -892962: 206, -892963: 0, -892964: 31, -892965: 66, -892966: 5, -892967: 20, -892968: 224, -892969: 59, -892970: 168, -892971: 33, -892972: 159, -892973: 87, -892974: 210, -892975: 74, -892976: 78, -892977: 58, -892978: 187, -892979: 0, -892980: 20, -892981: 89, -892982: 170, -892983: 48, -892984: 22, -892985: 2, -892986: 4, -892987: 17, -892988: 116, -892989: 0, -892990: 13, -892991: 0, -892992: 0, -892993: 56, -892994: 24, -454667: 59, -892996: 95, -892997: 87, -892998: 79, -892999: 41, -893000: 234, -454668: 2, -893002: 242, -893003: 54, -893004: 255, -893005: 107, -893006: 252, -454669: 251, -893008: 152, -1155153: 0, -1155154: 56, -1155155: 87, -1155156: 63, -1155157: 77, -1155158: 46, -1155159: 226, -1155160: 0, -1155161: 96, -1155162: 0, -1155163: 176, -1155164: 58, -1155165: 11, -1155166: 34, -1155167: 102, -1155168: 17, -1155169: 36, -1155170: 9, -1155171: 90, -1155172: 67, -1155173: 148, -1155174: 54, -1155175: 173, -1155176: 21, -1155177: 8, -1155178: 5, -1155179: 255, -1155180: 3, -1155181: 55, -1155182: 2, -1155183: 209, -1155184: 0, -893041: 59, -893042: 191, -893043: 2, -893044: 31, -893045: 91, -893046: 186, -893047: 50, -893048: 255, -893049: 3, -893050: 20, -893051: 19, -893052: 127, -893053: 2, -893054: 29, -893055: 2, -893056: 0, -893057: 56, -893058: 255, -893059: 43, -893060: 255, -893061: 107, -893062: 95, -893063: 63, -893064: 250, -893065: 99, -893066: 255, -893067: 75, -893068: 255, -893069: 127, -893070: 255, -893071: 115, -893072: 255, -893073: 99, -893074: 255, -893075: 43, -893076: 255, -893077: 127, -893078: 255, -454681: 216, -893080: 255, -893081: 43, -893082: 254, -893083: 59, -893084: 191, -454682: 1, -893086: 95, -893087: 43, -893088: 0, -893089: 56, -893090: 206, -454683: 152, -893092: 31, -893093: 66, -893094: 5, -893095: 20, -893096: 224, -454684: 0, -893098: 168, -893099: 33, -893100: 159, -893101: 87, -893102: 210, -454685: 14, -893104: 78, -893105: 58, -893106: 187, -893107: 0, -893108: 20, -454686: 0, -893110: 170, -893111: 48, -893112: 22, -893113: 2, -893114: 4, -893115: 17, -893116: 116, -893117: 0, -893118: 13, -893119: 0, -893120: 0, -893121: 56, -893122: 14, -893123: 2, -893124: 95, -893125: 67, -893126: 69, -893127: 21, -893128: 224, -893129: 67, -893130: 232, -893131: 34, -893132: 255, -893133: 87, -893134: 242, -893135: 75, -893136: 142, -893137: 59, -893138: 251, -893139: 1, -893140: 84, -893141: 90, -893142: 234, -893143: 49, -893144: 86, -893145: 3, -893146: 68, -893147: 18, -893148: 180, -893149: 1, -893150: 77, -893151: 1, -893152: 0, -893153: 56, -893154: 78, -893155: 3, -893156: 255, -893157: 67, -893158: 133, -893159: 22, -893160: 224, -893161: 79, -893162: 232, -454695: 84, -893164: 255, -893165: 87, -893166: 242, -893167: 75, -893168: 238, -454696: 1, -893170: 59, -893171: 3, -893172: 148, -893173: 91, -893174: 42, -454697: 85, -893176: 246, -893177: 3, -893178: 132, -893179: 19, -893180: 244, -454698: 0, -893182: 141, -893183: 2, -893184: 0, -893185: 56, -893186: 238, -454699: 11, -893188: 255, -893189: 107, -893190: 197, -893191: 63, -893192: 224, -454700: 0, -893194: 232, -893195: 75, -893196: 255, -893197: 127, -893198: 242, -893199: 115, -893200: 238, -893201: 99, -893202: 251, -893203: 43, -893204: 244, -893205: 127, -893206: 234, -893207: 91, -893208: 246, -893209: 43, -893210: 228, -893211: 59, -893212: 244, -893213: 43, -893214: 237, -893215: 43, -454709: 208, -454710: 0, -454711: 16, -454712: 0, -454713: 7, -454714: 0, -454723: 170, -454724: 0, -454725: 11, -454726: 0, -454727: 4, -454728: 0, -454740: 208, -454741: 0, -454742: 16, -454743: 0, -454744: 7, -454745: 0, -1503330: 33, -454755: 1, -454756: 85, -454757: 0, -1503334: 24, -454759: 0, -454768: 216, -454769: 1, -454770: 152, -454771: 0, -454772: 14, -454773: 0, -454782: 59, -454783: 2, -454784: 251, -454785: 0, -454786: 17, -454787: 0, -1500615: 64, -1500616: 168, -1500617: 56, -1500618: 100, -891771: 97, -891772: 20, -891773: 81, -891774: 173, -891775: 80, -1500619: 48, -1153920: 46, -891777: 0, -891778: 174, -891779: 82, -891780: 189, -1500620: 181, -891781: 95, -891782: 229, -891783: 101, -891784: 224, -891785: 127, -1500621: 126, -891786: 136, -891787: 115, -891788: 255, -891789: 127, -891790: 242, -1500622: 0, -891791: 127, -891792: 238, -891793: 127, -891794: 155, -891795: 86, -1500623: 40, -891796: 85, -891797: 95, -891798: 11, -891799: 94, -891800: 246, -1500624: 75, -891801: 83, -891802: 228, -891803: 98, -891804: 84, -891805: 82, -1500625: 24, -1153950: 127, -891807: 81, -891808: 0, -891809: 0, -891810: 8, -1500626: 72, -891811: 1, -891812: 189, -891813: 3, -891814: 5, -891815: 20, -1500627: 24, -891816: 224, -891817: 59, -891818: 168, -891819: 33, -891820: 159, -1500628: 37, -891821: 87, -891822: 210, -891823: 74, -891824: 78, -891825: 58, -1500629: 20, -891826: 187, -891827: 0, -891828: 181, -891829: 2, -891830: 107, -1500630: 34, -891831: 1, -891832: 82, -891833: 2, -891834: 4, -1500637: 36, -1156901: 0, -1156902: 56, -1156903: 183, -1156904: 63, -1156905: 173, -1156906: 46, -1156907: 66, -1156908: 1, -1156909: 96, -1156910: 0, -1156911: 16, -1156912: 59, -1156913: 107, -1156914: 34, -1156915: 198, -1156916: 17, -1156917: 132, -1156918: 9, -1156919: 255, -1156920: 114, -1156921: 223, -1156922: 44, -1156923: 185, -1156924: 36, -1156925: 175, -1156926: 28, -1156927: 157, -1156928: 42, -1156929: 214, -1156930: 25, -1156931: 16, -1156932: 13, -1500639: 28, -1500640: 235, -1500641: 40, -1500642: 169, -1500643: 36, -1503569: 159, -1500644: 135, -1500645: 32, -1500646: 100, -1500647: 28, -1500648: 66, -1500649: 24, -1503599: 223, -1500650: 107, -1500651: 65, -1500652: 0, -1500653: 20, -1500654: 154, -1500655: 44, -1503629: 31, -1500656: 116, -1500657: 44, -1500658: 78, -1500659: 40, -1500660: 40, -1500661: 40, -1503659: 63, -1500662: 54, -1500663: 98, -1500664: 178, -1157704: 0, -1157705: 56, -1157706: 255, -1157707: 87, -1157708: 247, -1157709: 66, -1157710: 74, -1157711: 13, -1157712: 99, -1157713: 0, -1157714: 90, -1157715: 79, -1157716: 181, -1157717: 54, -1157718: 16, -1157719: 38, -1157720: 206, -1157721: 29, -1157722: 218, -1157723: 78, -1157724: 53, -1157725: 58, -1157726: 144, -1157727: 41, -1157728: 235, -1157729: 20, -1157730: 224, -1500665: 85, -1157732: 160, -1157733: 50, -1157734: 64, -1157735: 25, -1500667: 73, -1288880: 246, -1288881: 13, -1288882: 146, -1288883: 13, -1288884: 236, -1288885: 12, -1288886: 169, -1288887: 12, -453185: 87, -1500670: 186, -453186: 212, -1500671: 77, -453187: 74, -1500672: 86, -453188: 80, -1500673: 69, -1503719: 191, -453189: 58, -1500674: 18, -453190: 188, -1500675: 65, -1500676: 205, -1500677: 56, -1500678: 105, -1500679: 48, -1503749: 255, -1500680: 186, -1500681: 126, -1500682: 5, -1500683: 40, -1500684: 77, -1500685: 24, -1500686: 74, -1500687: 24, -1500688: 39, -1500689: 20, -1500690: 36, -1500691: 20, -1500694: 233, -1500696: 167, -1500697: 36, -1500698: 69, -1500699: 28, -1500700: 237, -1500701: 40, -1159045: 0, -1159046: 56, -1159047: 250, -1159048: 114, -1159049: 176, -1159050: 85, -1159051: 69, -1159052: 40, -1159053: 1, -1159054: 24, -1159055: 16, -1159056: 98, -1159057: 107, -1159058: 73, -1159059: 198, -1159060: 56, -1159061: 99, -1159062: 44, -1159063: 31, -1159064: 36, -1159065: 23, -1159066: 28, -1159067: 47, -1159068: 20, -1159069: 71, -1159070: 12, -1159071: 224, -1159072: 3, -1159073: 160, -1159074: 2, -1159075: 64, -1159076: 1, -1159301: 0, -1159302: 56, -1159303: 255, -1159304: 87, -1159305: 247, -1159306: 66, -1159307: 74, -1159308: 13, -1159309: 99, -1159310: 0, -1159311: 90, -1159312: 79, -1159313: 181, -1159314: 54, -1159315: 16, -1159316: 38, -1159317: 206, -1159318: 29, -1159319: 218, -1159320: 78, -1159321: 53, -1159322: 58, -1159323: 144, -1159324: 41, -1159325: 235, -1159326: 20, -1159327: 224, -1159328: 95, -1159329: 160, -1159330: 50, -1159331: 64, -1159332: 25, -1159333: 0, -1159334: 56, -1159335: 255, -1159336: 127, -1159337: 224, -1159338: 86, -1159339: 128, -1159340: 49, -1159341: 192, -1159342: 24, -1159343: 192, -1159344: 107, -1159345: 192, -1159346: 94, -1159347: 32, -1159348: 74, -1159349: 160, -1159350: 53, -1159351: 255, -1159352: 127, -1159353: 156, -1159354: 3, -1159355: 55, -1159356: 2, -1159357: 209, -1159358: 0, -1159359: 255, -1159360: 3, -1159361: 55, -1159362: 2, -1159363: 209, -1159364: 0, -892311: 117, -892317: 82, -1159703: 0, -1159704: 56, -1159705: 250, -1159706: 114, -1159707: 176, -1159708: 85, -1159709: 69, -1159710: 40, -1159711: 1, -1159712: 24, -1159713: 16, -1159714: 98, -1159715: 107, -1159716: 73, -1159717: 198, -1159718: 56, -1159719: 99, -1159720: 44, -1159721: 31, -1159722: 36, -1159723: 23, -1159724: 28, -1159725: 47, -1159726: 20, -1159727: 71, -1159728: 12, -1159729: 224, -1159730: 3, -1159731: 160, -1159732: 2, -1159733: 64, -1159734: 1, -1500740: 191, -1160097: 0, -1160098: 56, -1160099: 250, -1160100: 114, -1160101: 176, -1160102: 85, -1160103: 69, -1160104: 40, -1160105: 1, -1160106: 24, -1160107: 16, -1160108: 98, -1160109: 107, -1160110: 73, -1160111: 198, -1160112: 56, -1160113: 99, -1160114: 44, -1160115: 121, -1160116: 29, -1160117: 51, -1160118: 25, -1160119: 14, -1160120: 21, -1160121: 200, -1160122: 16, -1160123: 224, -1160124: 59, -1160125: 128, -1160126: 38, -1160127: 128, -1160128: 21, -892407: 62, -1291251: 0, -1291252: 0, -1291253: 157, -1291254: 85, -1291255: 22, -1291256: 24, -1291257: 13, -1291258: 16, -1291259: 122, -1291260: 18, -1291261: 23, -1291262: 14, -1291263: 211, -1291264: 13, -1291265: 112, -1291266: 9, -1291267: 45, -1291268: 9, -1291269: 202, -1291270: 4, -1291271: 134, -1291272: 4, -1291273: 35, -1291274: 0, -1291275: 67, -1291276: 4, -1291277: 82, -1291278: 62, -1291279: 213, -1291280: 74, -1291281: 0, -1291282: 0, -1291283: 0, -1291284: 56, -1291285: 157, -1291286: 85, -1291287: 22, -1291288: 24, -1291289: 13, -1291290: 16, -1291291: 155, -1291292: 26, -1291293: 55, -1291294: 22, -1291295: 243, -1291296: 17, -1291297: 143, -1291298: 13, -1291299: 76, -1291300: 13, -1291301: 233, -1291302: 8, -1291303: 166, -1291304: 4, -1291305: 67, -1291306: 0, -1291307: 67, -1291308: 4, -1291309: 115, -1291310: 66, -1291311: 246, -1291312: 82, -1291313: 0, -1291314: 0, -1291315: 0, -1291316: 56, -1291317: 157, -1291318: 85, -1291319: 22, -1291320: 24, -1291321: 13, -1291322: 16, -1291323: 219, -1291324: 34, -1291325: 119, -1291326: 26, -1291327: 18, -1291328: 26, -1291329: 174, -1291330: 21, -1291331: 108, -1291332: 17, -1291333: 9, -1291334: 13, -1291335: 166, -1291336: 8, -1291337: 68, -1291338: 4, -1291339: 35, -1291340: 4, -1291341: 148, -1291342: 74, -1291343: 56, -1291344: 91, -1291345: 0, -1291346: 0, -1291347: 0, -1291348: 56, -1291349: 157, -1291350: 85, -1291351: 22, -1291352: 24, -1291353: 13, -1291354: 16, -1291355: 252, -1291356: 42, -1291357: 151, -1291358: 34, -1291359: 50, -1291360: 30, -1291361: 205, -1291362: 25, -1291363: 139, -1291364: 21, -1291365: 40, -1291366: 17, -1291367: 198, -1291368: 8, -1291369: 100, -1291370: 4, -1291371: 35, -1291372: 4, -1291373: 181, -1291374: 78, -1291375: 89, -1291376: 99, -1291377: 0, -1291378: 0, -1291379: 0, -1291380: 56, -1291381: 157, -1291382: 85, -1291383: 22, -1291384: 24, -1291385: 13, -1291386: 16, -1291387: 29, -1291388: 51, -1291389: 183, -1291390: 42, -1291391: 145, -1291392: 42, -1291393: 12, -1291394: 30, -1291395: 170, -1291396: 25, -1291397: 72, -1291398: 17, -1291399: 229, -1291400: 12, -1291401: 132, -1291402: 4, -1291403: 35, -1291404: 0, -1291405: 181, -1291406: 82, -1291407: 123, -1291408: 103, -1291409: 0, -1291410: 0, -1291411: 0, -1291412: 56, -1291413: 157, -1291414: 85, -1291415: 22, -1291416: 24, -1291417: 13, -1291418: 16, -1291419: 62, -1291420: 59, -1291421: 215, -1291422: 50, -1291423: 176, -1291424: 50, -1291425: 43, -1291426: 34, -1291427: 201, -1291428: 29, -1291429: 103, -1291430: 21, -1291431: 5, -1291432: 13, -1291433: 164, -1291434: 4, -1291435: 35, -1291436: 0, -1291437: 214, -1291438: 86, -1291439: 156, -1291440: 111, -1291441: 0, -1291442: 0, -1291443: 0, -1291444: 56, -1291445: 157, -1291446: 85, -1291447: 22, -1291448: 24, -1291449: 13, -1291450: 16, -1291451: 126, -1291452: 67, -1291453: 23, -1291454: 55, -1291455: 176, -1291456: 50, -1291457: 74, -1291458: 42, -1291459: 233, -1291460: 33, -1291461: 135, -1291462: 25, -1291463: 5, -1291464: 17, -1291465: 165, -1291466: 8, -1291467: 3, -1291468: 0, -1291469: 247, -1291470: 94, -1291471: 222, -1291472: 119, -1291473: 0, -1291474: 0, -1291475: 0, -1291476: 56, -1291477: 157, -1291478: 85, -1291479: 22, -1291480: 24, -1291481: 13, -1291482: 16, -1291483: 159, -1291484: 75, -1291485: 55, -1291486: 63, -1291487: 208, -1291488: 54, -1291489: 105, -1291490: 46, -1291491: 8, -1291492: 38, -1291493: 166, -1291494: 29, -1291495: 37, -1291496: 17, -1291497: 197, -1291498: 8, -1291499: 3, -1291500: 0, -1291501: 24, -1291502: 99, -1291503: 255, -1291504: 127, -1291505: 0, -1291506: 0, -1291507: 0, -1291508: 56, -1291509: 7, -1291510: 8, -1291511: 4, -1291512: 4, -1291513: 0, -1291514: 0, -1291515: 122, -1291516: 18, -1291517: 23, -1291518: 14, -1291519: 211, -1291520: 13, -1291521: 112, -1291522: 9, -1291523: 45, -1291524: 9, -1291525: 202, -1291526: 4, -1291527: 134, -1291528: 4, -1291529: 35, -1291530: 0, -1291531: 67, -1291532: 4, -1291533: 82, -1291534: 62, -1291535: 213, -1291536: 74, -1291537: 0, -1291538: 0, -1291571: 0, -1291572: 0, -1291573: 157, -1291574: 85, -1291575: 22, -1291576: 24, -1291577: 13, -1291578: 16, -1291579: 122, -1291580: 18, -1291581: 23, -1291582: 14, -1291583: 211, -1291584: 13, -1291585: 112, -1291586: 9, -1291587: 45, -1291588: 9, -1291589: 202, -1291590: 4, -1291591: 134, -1291592: 4, -1291593: 35, -1291594: 0, -1291595: 67, -1291596: 4, -1291597: 82, -1291598: 62, -1291599: 213, -1291600: 74, -1291601: 0, -1291602: 0, -1291603: 0, -1291604: 56, -1291605: 157, -1291606: 85, -1291607: 22, -1291608: 24, -1291609: 13, -1291610: 16, -1291611: 155, -1291612: 26, -1291613: 55, -1291614: 22, -1291615: 243, -1291616: 17, -1291617: 143, -1291618: 13, -1291619: 76, -1291620: 13, -1291621: 233, -1291622: 8, -1291623: 166, -1291624: 4, -1291625: 67, -1291626: 0, -1291627: 67, -1291628: 4, -1291629: 115, -1291630: 66, -1291631: 246, -1291632: 82, -1291633: 0, -1291634: 0, -1291635: 0, -1291636: 56, -1291637: 157, -1291638: 85, -1291639: 22, -1291640: 24, -1291641: 13, -1291642: 16, -1291643: 219, -1291644: 34, -1291645: 119, -1291646: 26, -1291647: 18, -1291648: 26, -1291649: 174, -1291650: 21, -1291651: 108, -1291652: 17, -1291653: 9, -1291654: 13, -1291655: 166, -1291656: 8, -1291657: 68, -1291658: 4, -1291659: 35, -1291660: 4, -1291661: 148, -1291662: 74, -1291663: 56, -1291664: 91, -1291665: 0, -1291666: 0, -1291667: 0, -1291668: 56, -1291669: 157, -1291670: 85, -1291671: 22, -1291672: 24, -1291673: 13, -1291674: 16, -1291675: 252, -1291676: 42, -1291677: 151, -1291678: 34, -1291679: 50, -1291680: 30, -1291681: 205, -1291682: 25, -1291683: 139, -1291684: 21, -1291685: 40, -1291686: 17, -1291687: 198, -1291688: 8, -1291689: 100, -1291690: 4, -1291691: 35, -1291692: 4, -1291693: 181, -1291694: 78, -1291695: 89, -1291696: 99, -1291697: 0, -1291698: 0, -1160627: 0, -1160628: 56, -1160629: 255, -1160630: 127, -1160631: 224, -1160632: 86, -1160633: 128, -1160634: 49, -1160635: 192, -1160636: 24, -1160637: 192, -1160638: 107, -1160639: 192, -1160640: 94, -1160641: 32, -1160642: 74, -1160643: 160, -1160644: 53, -1160645: 31, -1291718: 17, -1160647: 23, -1160648: 28, -1160649: 47, -1291722: 4, -1291723: 35, -1160652: 12, -1291725: 181, -1291726: 82, -1291727: 123, -1291728: 103, -1160657: 64, -1291730: 0, -1291731: 0, -1291732: 56, -1291733: 157, -1291734: 85, -1291735: 22, -1291736: 24, -1291737: 13, -1291738: 16, -1291739: 62, -1291740: 59, -1291741: 215, -1291742: 50, -1291743: 176, -1291744: 50, -1291745: 43, -1291746: 34, -1291747: 201, -1291748: 29, -1291749: 103, -1291750: 21, -1291751: 5, -1291752: 13, -1291753: 164, -1291754: 4, -1291755: 35, -1291756: 0, -1291757: 214, -1291758: 86, -1291759: 156, -1291760: 111, -1291761: 0, -1291762: 0, -1291763: 0, -1291764: 56, -1291765: 157, -1291766: 85, -1291767: 22, -1291768: 24, -1291769: 13, -1291770: 16, -1291771: 126, -1291772: 67, -1291773: 23, -1291774: 55, -1291775: 176, -1291776: 50, -1291777: 74, -1291778: 42, -1291779: 233, -1291780: 33, -1291781: 135, -1291782: 25, -1291783: 5, -1291784: 17, -1291785: 165, -1291786: 8, -1291787: 3, -1291788: 0, -1291789: 247, -1291790: 94, -1291791: 222, -1291792: 119, -1291793: 0, -1291794: 0, -1291795: 0, -1291796: 56, -1291797: 157, -1291798: 85, -1291799: 22, -1291800: 24, -1291801: 13, -1291802: 16, -1291803: 159, -1291804: 75, -1291805: 55, -1291806: 63, -1291807: 208, -1291808: 54, -1291809: 105, -1291810: 46, -1291811: 8, -1291812: 38, -1291813: 166, -1291814: 29, -1291815: 37, -1291816: 17, -1291817: 197, -1291818: 8, -1291819: 3, -1291820: 0, -1291821: 24, -1291822: 99, -1291823: 255, -1291824: 127, -1291825: 0, -1291826: 0, -892515: 2, -892521: 59, -892527: 75, -892533: 2, -892704: 224, -892707: 0, -892713: 59, -892743: 60, -892749: 127, -892755: 40, -892761: 42, -892767: 40, -892773: 126, -892779: 114, -892785: 126, -892791: 125, -892797: 81, -892803: 82, -892809: 127, -892815: 127, -892821: 126, -892827: 98, -892833: 0, -892835: 0, -892911: 99, -892917: 106, -892923: 70, -892929: 0, -892935: 98, -892941: 115, -892995: 22, -893001: 79, -893007: 95, -893009: 71, -893010: 255, -893011: 21, -893012: 94, -893013: 110, -893014: 244, -893015: 69, -893016: 95, -893017: 23, -893018: 78, -893019: 38, -893020: 190, -893021: 21, -893022: 87, -893023: 21, -893024: 0, -893025: 56, -893026: 222, -893027: 2, -893028: 255, -893029: 67, -893030: 21, -893031: 22, -893032: 240, -893033: 59, -893034: 184, -893035: 35, -893036: 255, -893037: 87, -893038: 255, -893039: 75, -893040: 254, -893079: 91, -893085: 43, -893091: 0, -893097: 59, -893103: 74, -893109: 89, -893163: 35, -893169: 59, -893175: 51, -893181: 2, -893187: 43, -893193: 99, -1164862: 0, -1164863: 56, -1164864: 87, -1164865: 63, -1164866: 77, -1164867: 46, -1164868: 226, -1164869: 0, -1164870: 96, -1164871: 0, -1164872: 176, -1164873: 58, -1164874: 11, -1164875: 34, -1164876: 102, -1164877: 17, -1164878: 36, -1164879: 9, -1164880: 255, -1164881: 127, -1164882: 156, -1164883: 3, -1164884: 55, -1164885: 2, -1164886: 209, -1164887: 0, -1164888: 191, -1164889: 97, -1164890: 182, -1164891: 56, -1164892: 72, -1164893: 20, -902791: 145, -902792: 127, -1500906: 26, -1500908: 47, -1500910: 9, -1165478: 0, -1165479: 56, -1165480: 255, -1165481: 87, -1165482: 247, -1165483: 66, -1165484: 140, -1165485: 21, -1165486: 165, -1165487: 0, -1165488: 90, -1165489: 79, -1165490: 181, -1165491: 54, -1165492: 16, -1165493: 38, -1165494: 206, -1165495: 29, -1165496: 255, -1165497: 119, -1165498: 181, -1165499: 98, -1165500: 74, -1165501: 49, -1165502: 99, -1165503: 16, -1165504: 31, -1165505: 77, -1165506: 182, -1165507: 56, -1165508: 110, -1165509: 36, -456401: 66, -456410: 13, -456411: 0, -456412: 6, -456413: 0, -456414: 0, -456415: 44, -456416: 0, -456417: 16, -456418: 132, -456419: 16, -456420: 19, -456421: 0, -456422: 115, -456423: 78, -1373936: 0, -1373937: 21, -456434: 9, -1373939: 21, -456436: 0, -456437: 56, -456438: 0, -456439: 28, -456440: 132, -456441: 16, -456442: 86, -456443: 0, -456444: 214, -456445: 90, -1296897: 0, -1296898: 56, -1296899: 255, -1296900: 127, -1296901: 192, -1296902: 126, -1296903: 224, -1296904: 109, -1296905: 224, -1296906: 84, -1296907: 0, -1296908: 0, -1296909: 0, -1296910: 0, -1296911: 0, -1296912: 0, -1296913: 0, -1296914: 0, -1296915: 0, -1296916: 0, -1296917: 0, -1296918: 0, -1296919: 0, -1296920: 0, -1296921: 0, -1296922: 0, -1296923: 0, -1296924: 0, -1296925: 0, -1296926: 0, -1296927: 0, -1296928: 0, -456454: 19, -456455: 0, -456456: 12, -456457: 0, -456458: 0, -456459: 68, -456460: 0, -456461: 40, -456462: 132, -456463: 16, -1296993: 0, -1296994: 0, -1296995: 223, -1296996: 2, -1296997: 215, -1296998: 1, -1296999: 172, -1297000: 0, -1297001: 187, -1297002: 94, -1297003: 179, -1297004: 61, -1297005: 46, -1297006: 41, -1297007: 134, -1297008: 20, -1297009: 177, -1297010: 11, -1297011: 251, -1297012: 72, -1297013: 255, -1297014: 127, -1297015: 0, -1297016: 0, -1297017: 255, -1297018: 127, -1297019: 229, -1297020: 68, -1297021: 255, -1297022: 127, -1297023: 0, -1297024: 0, -1297025: 3, -1297026: 32, -1297027: 177, -1297028: 11, -1297029: 169, -1297030: 30, -1297031: 69, -1297032: 1, -1297033: 187, -1297034: 94, -1297035: 179, -1297036: 61, -1297037: 46, -1297038: 41, -1297039: 134, -1297040: 20, -1297041: 24, -1297042: 99, -1297043: 231, -1297044: 28, -1297045: 132, -1297046: 16, -1297047: 0, -1297048: 0, -1297049: 255, -1297050: 127, -1297051: 223, -1297052: 2, -1297053: 31, -1297054: 0, -1297055: 0, -1297056: 0, -1297057: 3, -1297058: 32, -1297059: 188, -1297060: 114, -1297061: 251, -1297062: 72, -1297063: 22, -1297064: 24, -1297065: 24, -1297066: 99, -1297067: 24, -1297068: 99, -1297069: 24, -1297070: 99, -1297071: 24, -1297072: 99, -1297073: 24, -1297074: 99, -1297075: 24, -1297076: 99, -1297077: 24, -1297078: 99, -1297079: 24, -1297080: 99, -1297081: 24, -1297082: 99, -1297083: 24, -1297084: 99, -1297085: 255, -1297086: 127, -1297087: 0, -1297088: 0, -1297089: 0, -1297090: 0, -1297091: 178, -1297092: 114, -1297093: 199, -1297094: 113, -1297095: 3, -1297096: 77, -1297097: 24, -1297098: 99, -1297099: 24, -1297100: 99, -1297101: 24, -1297102: 99, -1297103: 24, -1297104: 99, -1297105: 24, -1297106: 99, -1297107: 24, -1297108: 99, -1297109: 24, -1297110: 99, -1297111: 24, -1297112: 99, -1297113: 24, -1297114: 99, -1297115: 24, -1297116: 99, -1297117: 255, -1297118: 127, -1297119: 0, -1297120: 0, -1297121: 0, -1297122: 0, -1297123: 244, -1297124: 62, -1297125: 46, -1297126: 38, -1297127: 104, -1297128: 13, -1297129: 148, -1297130: 62, -1297131: 206, -1297132: 37, -1297133: 8, -1297134: 13, -1297135: 132, -1297136: 20, -1297137: 148, -1297138: 110, -1297139: 206, -1297140: 85, -1297141: 8, -1297142: 61, -1297143: 132, -1297144: 44, -1297145: 169, -1297146: 30, -1297147: 177, -1297148: 11, -1297149: 255, -1297150: 127, -1297151: 0, -1297152: 0, -1297153: 0, -1297154: 0, -1297155: 70, -1297156: 18, -1297157: 69, -1297158: 1, -1297159: 192, -1297160: 0, -1297161: 82, -1297162: 54, -1297163: 140, -1297164: 29, -1297165: 198, -1297166: 4, -1297167: 132, -1297168: 4, -1297169: 49, -1297170: 98, -1297171: 107, -1297172: 73, -1297173: 165, -1297174: 48, -1297175: 33, -1297176: 32, -1297177: 21, -1297178: 0, -1297179: 91, -1297180: 2, -1297181: 255, -1297182: 127, -1297183: 0, -1297184: 0, -1297185: 0, -1297186: 0, -1297187: 24, -1297188: 99, -1297189: 24, -1297190: 99, -1297191: 24, -1297192: 99, -1297193: 24, -1297194: 99, -1297195: 24, -1297196: 99, -1297197: 24, -1297198: 99, -1297199: 24, -1297200: 99, -1297201: 24, -1297202: 99, -1297203: 24, -1297204: 99, -1297205: 24, -1297206: 99, -1297207: 24, -1297208: 99, -1297209: 24, -1297210: 99, -1297211: 24, -1297212: 99, -1297213: 255, -1297214: 127, -1297215: 0, -1297216: 0, -1297217: 14, -1297218: 0, -1297219: 220, -1297220: 37, -1297221: 56, -1297222: 21, -1297223: 113, -1297224: 0, -1297225: 48, -1297226: 0, -1297227: 121, -1297228: 29, -1297229: 23, -1297230: 17, -1297231: 180, -1297232: 8, -1297233: 146, -1297234: 0, -1297235: 31, -1297236: 66, -1297237: 121, -1297238: 29, -1297239: 245, -1297240: 12, -1297241: 147, -1297242: 4, -1297243: 29, -1297244: 28, -1297245: 25, -1297246: 12, -1297247: 19, -1297248: 0, -1297249: 12, -1297250: 0, -1297251: 28, -1297252: 42, -1297253: 87, -1297254: 25, -1297255: 112, -1297256: 0, -1297257: 46, -1297258: 0, -1297259: 153, -1297260: 33, -1297261: 54, -1297262: 17, -1297263: 211, -1297264: 8, -1297265: 145, -1297266: 0, -1297267: 95, -1297268: 74, -1297269: 153, -1297270: 29, -1297271: 244, -1297272: 12, -1297273: 177, -1297274: 4, -1297275: 29, -1297276: 32, -1297277: 24, -1297278: 12, -1297279: 18, -1297280: 0, -1297281: 10, -1297282: 0, -1297283: 92, -1297284: 46, -1297285: 150, -1297286: 29, -1297287: 142, -1297288: 0, -1297289: 76, -1297290: 0, -1297291: 216, -1297292: 37, -1297293: 85, -1297294: 21, -1297295: 241, -1297296: 8, -1297297: 175, -1297298: 0, -1297299: 159, -1297300: 82, -1297301: 216, -1297302: 33, -1297303: 19, -1297304: 17, -1297305: 208, -1297306: 4, -1297307: 29, -1297308: 36, -1297309: 23, -1297310: 16, -1297311: 17, -1297312: 0, -1297313: 8, -1297314: 0, -1297315: 124, -1297316: 50, -1297317: 181, -1297318: 33, -1297319: 140, -1297320: 0, -1297321: 74, -1297322: 0, -1297323: 247, -1297324: 41, -1297325: 116, -1297326: 21, -1297327: 16, -1297328: 13, -1297329: 174, -1297330: 0, -1297331: 223, -1297332: 90, -1297333: 247, -1297334: 37, -1297335: 49, -1297336: 17, -1297337: 206, -1297338: 4, -1297339: 29, -1297340: 40, -1297341: 22, -1297342: 16, -1297343: 15, -1297344: 0, -1297345: 6, -1297346: 0, -1297347: 187, -1297348: 54, -1297349: 212, -1297350: 33, -1297351: 139, -1297352: 0, -1297353: 72, -1297354: 0, -1297355: 23, -1297356: 46, -1297357: 147, -1297358: 25, -1297359: 15, -1297360: 13, -1297361: 204, -1297362: 0, -1297363: 31, -1297364: 99, -1297365: 23, -1297366: 42, -1297367: 80, -1297368: 17, -1297369: 237, -1297370: 4, -1297371: 29, -1297372: 44, -1297373: 22, -1297374: 16, -1297375: 14, -1297376: 0, -1297377: 4, -1297378: 0, -1297379: 251, -1297380: 58, -1297381: 243, -1297382: 37, -1297383: 169, -1297384: 0, -1297385: 70, -1297386: 0, -1297387: 86, -1297388: 50, -1297389: 178, -1297390: 29, -1297391: 45, -1297392: 13, -1297393: 235, -1297394: 0, -1297395: 95, -1297396: 107, -1297397: 86, -1297398: 46, -1297399: 111, -1297400: 21, -1297401: 235, -1297402: 4, -1297403: 29, -1297404: 48, -1297405: 21, -1297406: 20, -1297407: 12, -1297408: 0, -1297409: 2, -1297410: 0, -1297411: 59, -1297412: 63, -1297413: 18, -1297414: 42, -1297415: 167, -1297416: 0, -1297417: 68, -1297418: 0, -1297419: 117, -1297420: 54, -1297421: 241, -1297422: 29, -1297423: 76, -1297424: 13, -1297425: 233, -1297426: 4, -1297427: 191, -1297428: 119, -1297429: 117, -1297430: 50, -1297431: 142, -1297432: 21, -1297433: 10, -1297434: 5, -1297435: 29, -1297436: 52, -1297437: 20, -1297438: 20, -1297439: 11, -1297440: 0, -1297441: 0, -1297442: 0, -1297443: 123, -1297444: 71, -1297445: 82, -1297446: 46, -1297447: 198, -1297448: 0, -1297449: 99, -1297450: 0, -1297451: 181, -1297452: 58, -1297453: 16, -1297454: 34, -1297455: 107, -1297456: 17, -1297457: 8, -1297458: 5, -1297459: 255, -1297460: 127, -1297461: 181, -1297462: 54, -1297463: 173, -1297464: 25, -1297465: 41, -1297466: 9, -1297467: 29, -1297468: 56, -1297469: 20, -1297470: 24, -1297471: 10, -1297472: 0, -456934: 8, -456939: 229, -456940: 32, -456941: 164, -456942: 24, -456943: 131, -456944: 16, -456945: 65, -456946: 8, -456951: 200, -456952: 28, -456953: 134, -456954: 20, -456955: 132, -456956: 16, -456957: 65, -1374462: 18, -456963: 171, -456964: 24, -456965: 136, -1501090: 49, -1501092: 8, -1501093: 0, -1501094: 14, -1171362: 0, -1171363: 56, -1171364: 90, -1171365: 127, -1171366: 224, -1171367: 59, -1171368: 128, -1171369: 38, -1171370: 32, -1171371: 9, -1171372: 90, -1171373: 79, -1171374: 181, -1171375: 54, -1171376: 16, -1171377: 38, -1171378: 206, -1171379: 29, -1171380: 148, -1171381: 82, -1171382: 206, -1171383: 57, -1171384: 8, -1171385: 33, -1171386: 132, -1171387: 16, -1171388: 59, -1171389: 3, -1171390: 22, -1171391: 2, -1171392: 19, -1171393: 1, -1172028: 0, -1172029: 56, -1172030: 90, -1172031: 63, -1172032: 80, -1172033: 46, -1172034: 229, -1172035: 0, -1172036: 99, -1172037: 0, -1172038: 179, -1172039: 58, -1172040: 14, -1172041: 34, -1172042: 105, -1172043: 17, -1172044: 39, -1172045: 9, -1172046: 255, -1172047: 3, -1172048: 247, -1172049: 2, -1172050: 16, -1172051: 2, -1172052: 8, -1172053: 1, -1172054: 255, -1172055: 127, -1172056: 153, -1172057: 2, -1172058: 112, -1172059: 1, -1697061: 0, -1697062: 56, -1697063: 151, -1697064: 95, -1697065: 242, -1697066: 86, -1697067: 135, -1697068: 41, -1697069: 160, -1697070: 0, -1697071: 85, -1697072: 99, -1697073: 176, -1697074: 74, -1697075: 11, -1697076: 58, -1697077: 201, -1697078: 49, -1697079: 255, -1697080: 87, -1697081: 247, -1697082: 66, -1697083: 16, -1697084: 38, -1697085: 140, -1697086: 21, -1697087: 255, -1697088: 3, -1697089: 121, -1697090: 2, -1697091: 41, -1697092: 0, -1172860: 0, -1172861: 56, -1172862: 255, -1172863: 2, -1172864: 191, -1172865: 1, -1172866: 15, -1172867: 0, -1172868: 8, -1172869: 0, -1172870: 191, -1172871: 1, -1172872: 27, -1172873: 1, -1172874: 186, -1172875: 0, -1172876: 17, -1172877: 0, -1172878: 255, -1172879: 127, -1172880: 156, -1172881: 3, -1172882: 55, -1172883: 2, -1172884: 209, -1172885: 0, -1172886: 224, -1172887: 59, -1172888: 128, -1172889: 38, -1172890: 128, -1172891: 21, -1172912: 0, -1172913: 56, -1172914: 250, -1172915: 114, -1172916: 176, -1172917: 85, -1172918: 69, -1172919: 40, -1172920: 1, -1172921: 24, -1172922: 16, -1172923: 98, -1172924: 107, -1172925: 73, -1172926: 198, -1172927: 56, -1172928: 99, -1172929: 44, -1172930: 224, -1172931: 127, -1172932: 160, -1172933: 125, -1172934: 224, -1172935: 72, -1172936: 160, -1172937: 48, -1172938: 255, -1172939: 3, -1172940: 55, -1172941: 2, -1172942: 209, -1172943: 0, -1172944: 0, -1172945: 56, -1172946: 255, -1172947: 127, -1172948: 211, -1172949: 78, -1172950: 38, -1172951: 25, -1172952: 129, -1172953: 4, -1172954: 87, -1172955: 95, -1172956: 145, -1172957: 70, -1172958: 13, -1172959: 54, -1172960: 203, -1172961: 45, -1172962: 186, -1172963: 107, -1172964: 178, -1172965: 86, -1172966: 71, -1172967: 41, -1172968: 195, -1172969: 24, -1172970: 255, -1172971: 127, -1172972: 211, -1172973: 78, -1172974: 38, -1172975: 25, -452117: 1, -456482: 0, -456483: 52, -452119: 2, -456484: 132, -1304574: 0, -1304575: 56, -1304576: 151, -1304577: 95, -1304578: 242, -1304579: 86, -1304580: 135, -1304581: 41, -1304582: 160, -1304583: 0, -1304584: 85, -1304585: 99, -1304586: 176, -1304587: 74, -1304588: 11, -1304589: 58, -1304590: 201, -1304591: 49, -1304592: 255, -1304593: 87, -1304594: 247, -1304595: 66, -1304596: 16, -1304597: 38, -1304598: 140, -1304599: 21, -1304600: 255, -1304601: 3, -1304602: 121, -1304603: 2, -1304604: 41, -1304605: 0, -456485: 16, -456486: 28, -1698116: 0, -1698117: 56, -1698118: 87, -1698119: 63, -1698120: 77, -1698121: 46, -1698122: 226, -1698123: 0, -1698124: 96, -1698125: 0, -1698126: 176, -1698127: 58, -1698128: 11, -1698129: 34, -1698130: 102, -1698131: 17, -1698132: 36, -1698133: 9, -1698134: 255, -1698135: 87, -1698136: 247, -1698137: 66, -1698138: 16, -1698139: 38, -1698140: 140, -1698141: 21, -1698142: 31, -1698143: 124, -1698144: 24, -1698145: 96, -1698146: 10, -1698147: 48, -1173935: 0, -1173936: 56, -1173937: 245, -1173938: 107, -1173939: 225, -1173940: 6, -1173941: 65, -1173942: 6, -1173943: 161, -1173944: 5, -1173945: 95, -1173946: 94, -1173947: 63, -1173948: 24, -1173949: 20, -1173950: 16, -1173951: 10, -1173952: 8, -1173953: 4, -1173954: 4, -1173955: 159, -1173956: 79, -1173957: 216, -1173958: 62, -1173959: 18, -1173960: 46, -1173961: 112, -1173962: 111, -1173963: 255, -1173964: 127, -1173965: 224, -1173966: 94, -456488: 156, -456489: 115, -3141200: 255, -3141201: 255, -3141202: 255, -3141203: 255, -3141204: 255, -3141205: 255, -3141206: 255, -3141207: 255, -3141208: 255, -3141209: 255, -3141210: 255, -3141211: 255, -3141212: 255, -3141213: 255, -3141214: 255, -3141215: 255, -3141216: 255, -3141217: 255, -3141218: 255, -3141219: 255, -3141220: 255, -3141221: 255, -3141222: 255, -3141223: 255, -3141224: 255, -3141225: 255, -3141226: 255, -3141227: 255, -3141228: 255, -3141229: 255, -3141230: 255, -3141231: 255, -3141232: 255, -3141233: 255, -3141234: 255, -3141235: 255, -3141236: 255, -3141237: 255, -3141238: 255, -3141239: 255, -3141240: 255, -3141241: 255, -3141242: 255, -3141243: 255, -3141244: 255, -3141245: 255, -3141246: 255, -3141247: 255, -3141248: 255, -3141249: 255, -3141250: 255, -3141251: 255, -3141252: 255, -3141253: 255, -3141254: 255, -3141255: 255, -3141256: 255, -3141257: 255, -3141258: 255, -3141259: 255, -3141260: 255, -3141261: 255, -3141262: 255, -3141263: 255, -3141264: 255, -3141265: 255, -3141266: 255, -3141267: 255, -3141268: 255, -3141269: 255, -3141270: 255, -3141271: 255, -3141272: 255, -3141273: 255, -3141274: 255, -3141275: 255, -3141276: 255, -3141277: 255, -3141278: 255, -3141279: 255, -3141280: 255, -3141281: 255, -3141282: 255, -3141283: 255, -3141284: 255, -3141285: 255, -3141286: 255, -3141287: 255, -3141288: 255, -3141289: 255, -3141290: 255, -3141291: 255, -3141292: 255, -3141293: 255, -3141294: 255, -3141295: 255, -3141296: 255, -3141297: 255, -3141298: 255, -3141299: 255, -3141300: 255, -3141301: 255, -3141302: 255, -3141303: 255, -3141304: 255, -3141305: 255, -3141306: 255, -3141307: 255, -3141308: 255, -3141309: 255, -3141310: 255, -3141311: 255, -3141312: 255, -3141313: 255, -3141314: 255, -3141315: 255, -3141316: 255, -3141317: 255, -3141318: 255, -3141319: 255, -3141320: 255, -3141321: 255, -3141322: 255, -3141323: 255, -3141324: 255, -3141325: 255, -3141326: 255, -3141327: 255, -3141328: 255, -3141329: 255, -3141330: 255, -3141331: 255, -3141332: 255, -3141333: 255, -3141334: 255, -3141335: 255, -3141336: 255, -3141337: 255, -3141338: 255, -3141339: 255, -3141340: 255, -3141341: 255, -3141342: 255, -3141343: 255, -3141344: 255, -3141345: 255, -3141346: 255, -3141347: 255, -3141348: 255, -3141349: 255, -3141350: 255, -3141351: 255, -3141352: 255, -3141353: 255, -3141354: 255, -3141355: 255, -3141356: 255, -3141357: 255, -3141358: 255, -3141359: 255, -3141360: 255, -3141361: 255, -3141362: 255, -3141363: 255, -3141364: 255, -3141365: 255, -3141366: 255, -3141367: 255, -3141368: 255, -3141369: 255, -3141370: 255, -3141371: 255, -3141372: 255, -3141373: 255, -3141374: 255, -3141375: 255, -3141376: 255, -3141377: 255, -3141378: 255, -3141379: 255, -3141380: 255, -3141381: 255, -3141382: 255, -3141383: 255, -3141384: 255, -3141385: 255, -3141386: 255, -3141387: 255, -3141388: 255, -3141389: 255, -3141390: 255, -3141391: 255, -3141392: 255, -3141393: 255, -3141394: 255, -3141395: 255, -3141396: 255, -3141397: 255, -3141398: 255, -3141399: 255, -3141400: 255, -3141401: 255, -3141402: 255, -3141403: 255, -3141404: 255, -3141405: 255, -3141406: 255, -3141407: 255, -3141408: 255, -458116: 17, -1307173: 0, -1307174: 56, -1307175: 87, -1307176: 63, -1307177: 77, -1307178: 46, -1307179: 226, -1307180: 0, -1307181: 96, -1307182: 0, -1307183: 176, -1307184: 58, -1307185: 11, -1307186: 34, -1307187: 102, -1307188: 17, -1307189: 36, -1307190: 9, -1307191: 255, -1307192: 87, -1307193: 247, -1307194: 66, -1307195: 16, -1307196: 38, -1307197: 140, -1307198: 21, -1307199: 31, -1307200: 124, -1307201: 24, -1307202: 96, -1307203: 10, -1307204: 48, -3143248: 255, -3143249: 255, -3143250: 255, -3143251: 255, -3143252: 255, -3143253: 255, -3143254: 255, -3143255: 255, -3143256: 255, -3143257: 255, -3143258: 255, -3143259: 255, -3143260: 255, -3143261: 255, -3143262: 255, -3143263: 255, -3143264: 255, -3143265: 255, -3143266: 255, -3143267: 255, -3143268: 255, -3143269: 255, -3143270: 255, -3143271: 255, -3143272: 255, -3143273: 255, -3143274: 255, -3143275: 255, -3143276: 255, -3143277: 255, -3143278: 255, -3143279: 255, -3143280: 255, -3143281: 255, -3143282: 255, -3143283: 255, -3143284: 255, -3143285: 255, -3143286: 255, -3143287: 255, -3143288: 255, -3143289: 255, -3143290: 255, -3143291: 255, -3143292: 255, -3143293: 255, -3143294: 255, -3143295: 255, -3143296: 255, -3143297: 255, -3143298: 255, -3143299: 255, -3143300: 255, -3143301: 255, -3143302: 255, -3143303: 255, -3143304: 255, -3143305: 255, -3143306: 255, -3143307: 255, -3143308: 255, -3143309: 255, -3143310: 255, -3143311: 255, -3143312: 255, -3143313: 255, -3143314: 255, -3143315: 255, -3143316: 255, -3143317: 255, -3143318: 255, -3143319: 255, -3143320: 255, -3143321: 255, -3143322: 255, -3143323: 255, -3143324: 255, -3143325: 255, -3143326: 255, -3143327: 255, -3143328: 255, -3143329: 255, -3143330: 255, -3143331: 255, -3143332: 255, -3143333: 255, -3143334: 255, -3143335: 255, -3143336: 255, -3143337: 255, -3143338: 255, -3143339: 255, -3143340: 255, -3143341: 255, -3143342: 255, -3143343: 255, -3143344: 255, -3143345: 255, -3143346: 255, -3143347: 255, -3143348: 255, -3143349: 255, -3143350: 255, -3143351: 255, -3143352: 255, -3143353: 255, -3143354: 255, -3143355: 255, -3143356: 255, -3143357: 255, -3143358: 255, -3143359: 255, -3143360: 255, -3143361: 255, -3143362: 255, -3143363: 255, -3143364: 255, -3143365: 255, -3143366: 255, -3143367: 255, -3143368: 255, -3143369: 255, -3143370: 255, -3143371: 255, -3143372: 255, -3143373: 255, -3143374: 255, -3143375: 255, -3143376: 255, -3143377: 255, -3143378: 255, -3143379: 255, -3143380: 255, -3143381: 255, -3143382: 255, -3143383: 255, -3143384: 255, -3143385: 255, -3143386: 255, -3143387: 255, -3143388: 255, -3143389: 255, -3143390: 255, -3143391: 255, -3143392: 255, -3143393: 255, -3143394: 255, -3143395: 255, -3143396: 255, -3143397: 255, -3143398: 255, -3143399: 255, -3143400: 255, -3143401: 255, -3143402: 255, -3143403: 255, -3143404: 255, -3143405: 255, -3143406: 255, -3143407: 255, -3143408: 255, -3143409: 255, -3143410: 255, -3143411: 255, -3143412: 255, -3143413: 255, -3143414: 255, -3143415: 255, -3143416: 255, -3143417: 255, -3143418: 255, -3143419: 255, -3143420: 255, -3143421: 255, -3143422: 255, -3143423: 255, -3143424: 255, -3143425: 255, -3143426: 255, -3143427: 255, -3143428: 255, -3143429: 255, -3143430: 255, -3143431: 255, -3143432: 255, -3143433: 255, -3143434: 255, -3143435: 255, -3143436: 255, -3143437: 255, -3143438: 255, -3143439: 255, -3143440: 255, -3143441: 255, -3143442: 255, -3143443: 255, -3143760: 255, -3143761: 255, -3143762: 255, -3143763: 255, -3143764: 255, -3143765: 255, -3143766: 255, -3143767: 255, -3143768: 255, -3143769: 255, -3143770: 255, -3143771: 255, -3143772: 255, -3143773: 255, -3143774: 255, -3143775: 255, -3143776: 255, -3143777: 255, -3143778: 255, -3143779: 255, -3143780: 255, -3143781: 255, -3143782: 255, -3143783: 255, -3143784: 255, -3143785: 255, -3143786: 255, -3143787: 255, -3143788: 255, -3143789: 255, -3143790: 255, -3143791: 255, -3143792: 255, -3143793: 255, -3143794: 255, -3143795: 255, -3143796: 255, -3143797: 255, -3143798: 255, -3143799: 255, -3143800: 255, -3143801: 255, -3143802: 255, -3143803: 255, -3143804: 255, -3143805: 255, -3143806: 255, -3143807: 255, -3143808: 255, -3143809: 255, -3143810: 255, -3143811: 255, -3143812: 255, -3143813: 255, -3143814: 255, -3143815: 255, -3143816: 255, -3143817: 255, -3143818: 255, -3143819: 255, -3143820: 255, -3143821: 255, -3143822: 255, -3143823: 255, -3143824: 255, -3143825: 255, -3143826: 255, -3143827: 255, -3143828: 255, -3143829: 255, -3143830: 255, -3143831: 255, -3143832: 255, -3143833: 255, -3143834: 255, -3143835: 255, -3143836: 255, -3143837: 255, -3143838: 255, -3143839: 255, -3143840: 255, -3143841: 255, -3143842: 255, -3143843: 255, -3143844: 255, -3143845: 255, -3143846: 255, -3143847: 255, -3143848: 255, -3143849: 255, -3143850: 255, -3143851: 255, -3143852: 255, -3143853: 255, -3143854: 255, -3143855: 255, -3143856: 255, -3143857: 255, -3143858: 255, -3143859: 255, -3143860: 255, -3143861: 255, -3143862: 255, -3143863: 255, -3143864: 255, -3143865: 255, -3143866: 255, -3143867: 255, -3143868: 255, -3143869: 255, -3143870: 255, -3143871: 255, -3143872: 255, -3143873: 255, -3143874: 255, -3143875: 255, -3143876: 255, -3143877: 255, -3143878: 255, -3143879: 255, -3143880: 255, -3143881: 255, -3143882: 255, -3143883: 255, -3143884: 255, -3143885: 255, -3143886: 255, -3143887: 255, -3143888: 255, -3143889: 255, -3143890: 255, -3143891: 255, -3143892: 255, -3143893: 255, -3143894: 255, -3143895: 255, -3143896: 255, -3143897: 255, -3143898: 255, -3143899: 255, -3143900: 255, -3143901: 255, -3143902: 255, -3143903: 255, -3143904: 255, -3143905: 255, -3143906: 255, -3143907: 255, -3143908: 255, -3143909: 255, -3143910: 255, -3143911: 255, -3143912: 255, -3143913: 255, -3143914: 255, -3143915: 255, -3143916: 255, -3143917: 255, -3143918: 255, -3143919: 255, -3143920: 255, -3143921: 255, -3143922: 255, -3143923: 255, -3143924: 255, -3143925: 255, -3143926: 255, -3143927: 255, -3143928: 255, -3143929: 255, -3143930: 255, -3143931: 255, -3143932: 255, -3143933: 255, -3143934: 255, -3143935: 255, -3143936: 255, -3143937: 255, -3143938: 255, -3143939: 255, -3143940: 255, -3143941: 255, -3143942: 255, -3143943: 255, -3143944: 255, -3143945: 255, -3143946: 255, -3143947: 255, -3143948: 255, -3143949: 255, -3143950: 255, -3143951: 255, -3143952: 255, -3143953: 255, -3143954: 255, -3143955: 255, -3143956: 255, -1157731: 95, -452159: 0, -852400: 224, -852401: 59, -852402: 128, -852403: 38, -852404: 128, -852405: 21, -1237977: 1, -1237978: 255, -1237979: 0, -1237980: 254, -1237981: 1, -1237982: 255, -1237983: 1, -1237984: 254, -1237985: 0, -1237986: 255, -1237987: 1, -1237988: 254, -1237989: 0, -1237990: 255, -1237991: 1, -1237992: 254, -1237993: 0, -1237994: 255, -1237995: 0, -1237996: 254, -1237997: 1, -1237998: 255, -1237999: 0, -1238000: 254, -1238001: 0, -1238002: 254, -1238003: 0, -1238004: 255, -1238005: 0, -1238006: 254, -1238007: 0, -1238008: 255, -1238009: 0, -1238010: 254, -1238011: 255, -1238012: 254, -1238013: 0, -1238014: 255, -1238015: 0, -1238016: 254, -1238017: 255, -1238018: 254, -1238019: 0, -1238020: 254, -1238021: 255, -1238022: 255, -1238023: 255, -1238024: 254, -1238025: 255, -1238026: 254, -1238027: 255, -1238028: 255, -1238029: 255, -1238030: 254, -1238031: 255, -1238032: 254, -1238033: 255, -1238034: 255, -1238035: 255, -1238036: 254, -1238037: 255, -1238038: 255, -1238039: 255, -1238040: 255, -1238041: 255, -1238042: 254, -1238043: 254, -1238044: 255, -1238045: 255, -1238046: 255, -1238047: 255, -1238048: 255, -1238049: 254, -1238050: 255, -1238051: 255, -1238052: 0, -1238053: 255, -1238054: 255, -1238055: 254, -1238056: 255, -1238057: 255, -1238058: 0, -1238059: 255, -1238060: 0, -1238061: 254, -1238062: 0, -1238063: 255, -1238064: 0, -1238065: 255, -1238066: 1, -1238067: 255, -1238068: 0, -1238069: 254, -1238070: 1, -1238071: 255, -1238072: 0, -1238073: 255, -1238074: 1, -1238075: 255, -1238076: 1, -1238077: 255, -1238078: 1, -1238079: 254, -1238080: 1, -1238081: 255, -1238082: 2, -1238083: 255, -1238084: 1, -1238085: 255, -1238086: 2, -1238087: 255, -1238088: 1, -1238089: 255, -1238090: 2, -1238091: 255, -1238092: 1, -1238093: 255, -1238094: 2, -1238095: 0, -1238096: 2, -1238097: 255, -1238098: 1, -1238099: 255, -1238100: 2, -1238101: 255, -1238102: 2, -1238103: 0, -1238104: 2, -1238105: 255, -1238106: 2, -1238107: 255, -1238108: 2, -1238109: 0, -1238110: 1, -1238111: 255, -1238112: 2, -1238113: 0, -1238114: 2, -1238115: 0, -1238116: 2, -1238117: 255, -1238118: 1, -1238119: 0, -1238120: 2, -1238121: 0, -1238122: 2, -1238123: 0, -1238124: 1, -1238125: 0, -1238126: 2, -1238127: 0, -1238128: 2, -1238129: 0, -1238130: 1, -1238131: 0, -1238132: 2, -1238133: 0, -1238134: 1, -1238135: 0, -1238136: 2, -1238137: 0, -1238138: 2, -1238139: 0, -1238140: 1, -1238141: 0, -1238142: 2, -1238143: 1, -1238144: 1, -1238145: 0, -1238146: 2, -1238147: 0, -1238148: 1, -1238149: 1, -1238150: 2, -1238151: 0, -1238152: 2, -1238153: 1, -1238154: 1, -1238155: 0, -1238156: 2, -1238157: 1, -1238158: 1, -1238159: 0, -1238160: 2, -1238161: 1, -1238162: 1, -1238163: 0, -1238164: 2, -1238165: 1, -1238166: 1, -1238167: 0, -1238168: 2, -1238169: 1, -1238170: 1, -1238171: 1, -1238172: 2, -1238173: 0, -1238174: 2, -1238175: 1, -1238176: 1, -1238177: 1, -1238178: 2, -1238179: 1, -1238180: 1, -1238181: 0, -1238182: 2, -1238183: 1, -1238184: 1, -1238185: 1, -1238186: 2, -1238187: 1, -1238188: 1, -1238189: 1, -1238190: 2, -1238191: 1, -1238192: 1, -1238193: 1, -1238194: 1, -1238195: 1, -1238196: 2, -1238197: 1, -1238198: 1, -1238199: 1, -1238200: 1, -1238201: 1, -1238202: 1, -1238203: 1, -1238204: 1, -1238205: 1, -1238206: 1, -1238207: 1, -1238208: 1, -1238209: 2, -1238210: 1, -1238211: 1, -1238212: 1, -1238213: 1, -1238214: 1, -1238215: 1, -1238216: 1, -1238217: 2, -1238218: 0, -1238219: 1, -1238220: 1, -1238221: 2, -1238222: 1, -1238223: 1, -1238224: 0, -1238225: 2, -1238226: 1, -1238227: 1, -1238228: 0, -1238229: 2, -1238230: 0, -1238231: 1, -1238232: 1, -1238233: 2, -1238234: 0, -1238235: 1, -1238236: 0, -1238237: 2, -1238238: 0, -1238239: 2, -1238240: 0, -1238241: 1, -1238242: 0, -1238243: 2, -1238244: 0, -1238245: 2, -1238246: 0, -1238247: 2, -1238248: 0, -1238249: 1, -1238250: 0, -1238251: 2, -1238252: 0, -1238253: 2, -1238254: 0, -1238255: 1, -1238256: 0, -1238257: 2, -1238258: 0, -1238259: 2, -1238260: 0, -1238261: 1, -1238262: 255, -1238263: 2, -1238264: 0 -} diff --git a/worlds/sm/variaRandomizer/randomizer.py b/worlds/sm/variaRandomizer/randomizer.py index 332c333c03..924b4c5d77 100644 --- a/worlds/sm/variaRandomizer/randomizer.py +++ b/worlds/sm/variaRandomizer/randomizer.py @@ -13,6 +13,8 @@ from .utils.utils import PresetLoader, loadRandoPreset, getDefaultMultiValues, g from .utils.version import displayedVersion from .utils.doorsmanager import DoorsManager from .logic.logic import Logic +from .utils.objectives import Objectives +from .utils.utils import dumpErrorMsg from .utils import log from ..Options import StartLocation @@ -33,6 +35,9 @@ progDiffs = defaultMultiValues['progressionDifficulty'] morphPlacements = defaultMultiValues['morphPlacement'] majorsSplits = defaultMultiValues['majorsSplit'] gravityBehaviours = defaultMultiValues['gravityBehaviour'] +objectives = defaultMultiValues['objective'] +tourians = defaultMultiValues['tourian'] +areaRandomizations = defaultMultiValues['areaRandomization'] def randomMulti(args, param, defaultMultiValues): value = args[param] @@ -40,7 +45,7 @@ def randomMulti(args, param, defaultMultiValues): isRandom = False if value == "random": isRandom = True - if args[param+"List"] != None: + if args[param+"List"] is not None: # use provided list choices = args[param+"List"].split(',') value = random.choice(choices) @@ -50,12 +55,6 @@ def randomMulti(args, param, defaultMultiValues): return (isRandom, value) -def dumpErrorMsg(outFileName, msg): - print("DIAG: " + msg) - if outFileName is not None: - with open(outFileName, 'w') as jsonFile: - json.dump({"errorMsg": msg}, jsonFile) - def dumpErrorMsgs(outFileName, msgs): dumpErrorMsg(outFileName, joinErrorMsgs(msgs)) @@ -74,9 +73,6 @@ def to_pascal_case_with_space(snake_str): class VariaRandomizer: parser = argparse.ArgumentParser(description="Random Metroid Randomizer") - parser.add_argument('--patchOnly', - help="only apply patches, do not perform any randomization", action='store_true', - dest='patchOnly', default=False) parser.add_argument('--param', '-p', help="the input parameters", default=None, dest='paramsFileName') parser.add_argument('--dir', @@ -86,12 +82,12 @@ class VariaRandomizer: help="generate dot file with area graph", action='store_true',dest='dot', default=False) parser.add_argument('--area', help="area mode", - dest='area', nargs='?', const=True, default=False) + dest='area', nargs='?', const=True, choices=["random"]+areaRandomizations, default='off') + parser.add_argument('--areaList', help="list to choose from when random", + dest='areaList', nargs='?', default=None) parser.add_argument('--areaLayoutBase', help="use simple layout patch for area mode", action='store_true', dest='areaLayoutBase', default=False) - parser.add_argument('--lightArea', help="keep number of transitions between vanilla areas", action='store_true', - dest='lightArea', default=False) parser.add_argument('--escapeRando', help="Randomize the escape sequence", dest='escapeRando', nargs='?', const=True, default=False) @@ -103,9 +99,6 @@ class VariaRandomizer: parser.add_argument('--minimizer', help="minimizer mode: area and boss mixed together. arg is number of non boss locations", dest='minimizerN', nargs='?', const=35, default=None, choices=[str(i) for i in range(30,101)]+["random"]) - parser.add_argument('--minimizerTourian', - help="Tourian speedup in minimizer mode", - dest='minimizerTourian', nargs='?', const=True, default=False) parser.add_argument('--startLocation', help="Name of the Access Point to start from", dest='startLocation', nargs='?', default="Landing Site", choices=['random'] + GraphUtils.getStartAccessPointNames()) @@ -135,11 +128,11 @@ class VariaRandomizer: parser.add_argument('--patch', '-c', help="optional patches to add", dest='patches', nargs='?', default=[], action='append', - choices=['itemsounds.ips', 'elevators_doors_speed.ips', 'random_music.ips', - 'spinjumprestart.ips', 'rando_speed.ips', 'No_Music', 'AimAnyButton.ips', - 'max_ammo_display.ips', 'supermetroid_msu1.ips', 'Infinite_Space_Jump', - 'refill_before_save.ips', 'remove_elevators_doors_speed.ips', - 'remove_itemsounds.ips', 'vanilla_music.ips']) + choices=['itemsounds.ips', 'random_music.ips', + 'fast_doors.ips', 'elevators_speed.ips', + 'spinjumprestart.ips', 'rando_speed.ips', 'No_Music', 'AimAnyButton.ips', + 'max_ammo_display.ips', 'supermetroid_msu1.ips', 'Infinite_Space_Jump', + 'refill_before_save.ips', 'relaxed_round_robin_cf.ips']) parser.add_argument('--missileQty', '-m', help="quantity of missiles", dest='missileQty', nargs='?', default=3, @@ -177,9 +170,6 @@ class VariaRandomizer: parser.add_argument('--scavRandomized', help="For Scavenger split, decide whether mandatory major locs will have non-vanilla items", dest='scavRandomized', nargs='?', const=True, default=False) - parser.add_argument('--scavEscape', - help="For Scavenger split, decide whether escape sequence shall be triggered as soon as the hunt is over", - dest='scavEscape', nargs='?', const=True, default=False) parser.add_argument('--suitsRestriction', help="no suits in early game", dest='suitsRestriction', nargs='?', const=True, default=False) @@ -235,46 +225,11 @@ class VariaRandomizer: parser.add_argument('--race', help="Race mode magic number, between 1 and 65535", dest='raceMagic', type=int) parser.add_argument('--vcr', help="Generate VCR output file", dest='vcr', action='store_true') - parser.add_argument('--palette', help="Randomize the palettes", dest='palette', action='store_true') - parser.add_argument('--individual_suit_shift', help="palette param", action='store_true', - dest='individual_suit_shift', default=False) - parser.add_argument('--individual_tileset_shift', help="palette param", action='store_true', - dest='individual_tileset_shift', default=False) - parser.add_argument('--no_match_ship_and_power', help="palette param", action='store_false', - dest='match_ship_and_power', default=True) - parser.add_argument('--seperate_enemy_palette_groups', help="palette param", action='store_true', - dest='seperate_enemy_palette_groups', default=False) - parser.add_argument('--no_match_room_shift_with_boss', help="palette param", action='store_false', - dest='match_room_shift_with_boss', default=True) - parser.add_argument('--no_shift_tileset_palette', help="palette param", action='store_false', - dest='shift_tileset_palette', default=True) - parser.add_argument('--no_shift_boss_palettes', help="palette param", action='store_false', - dest='shift_boss_palettes', default=True) - parser.add_argument('--no_shift_suit_palettes', help="palette param", action='store_false', - dest='shift_suit_palettes', default=True) - parser.add_argument('--no_shift_enemy_palettes', help="palette param", action='store_false', - dest='shift_enemy_palettes', default=True) - parser.add_argument('--no_shift_beam_palettes', help="palette param", action='store_false', - dest='shift_beam_palettes', default=True) - parser.add_argument('--no_shift_ship_palette', help="palette param", action='store_false', - dest='shift_ship_palette', default=True) - parser.add_argument('--min_degree', help="min hue shift", dest='min_degree', nargs='?', default=-180, type=int) - parser.add_argument('--max_degree', help="max hue shift", dest='max_degree', nargs='?', default=180, type=int) - parser.add_argument('--no_global_shift', help="", action='store_false', dest='global_shift', default=True) - parser.add_argument('--invert', help="invert color range", dest='invert', action='store_true', default=False) - parser.add_argument('--no_blue_door_palette', help="palette param", action='store_true', - dest='no_blue_door_palette', default=False) parser.add_argument('--ext_stats', help="dump extended stats SQL", nargs='?', default=None, dest='extStatsFilename') parser.add_argument('--randoPreset', help="rando preset file", dest="randoPreset", nargs='?', default=None) parser.add_argument('--fakeRandoPreset', help="for prog speed stats", dest="fakeRandoPreset", nargs='?', default=None) parser.add_argument('--plandoRando', help="json string with already placed items/locs", dest="plandoRando", nargs='?', default=None) - parser.add_argument('--sprite', help='use a custom sprite for Samus', dest='sprite', default=None) - parser.add_argument('--no_spin_attack', help='when using a custom sprite, use the same animation for screw attack with or without Space Jump', dest='noSpinAttack', action='store_true', default=False) - parser.add_argument('--customItemNames', help='add custom item names for some of them, related to the custom sprite', - dest='customItemNames', action='store_true', default=False) - parser.add_argument('--ship', help='use a custom sprite for Samus ship', dest='ship', default=None) - parser.add_argument('--seedIps', help='ips generated from previous seed', dest='seedIps', default=None) parser.add_argument('--jm,', help="display data used by jm for its stats", dest='jm', action='store_true', default=False) parser.add_argument('--doorsColorsRando', help='randomize color of colored doors', dest='doorsColorsRando', nargs='?', const=True, default=False) @@ -283,6 +238,17 @@ class VariaRandomizer: parser.add_argument('--logic', help='logic to use', dest='logic', nargs='?', default="varia", choices=["varia", "rotation"]) parser.add_argument('--hud', help='Enable VARIA hud', dest='hud', nargs='?', const=True, default=False) + parser.add_argument('--objective', + help="objectives to open G4", + dest='objective', nargs='?', default=[], action='append', + choices=Objectives.getAllGoals()+["random"]+[str(i) for i in range(6)]) + parser.add_argument('--objectiveList', help="list to choose from when random", + dest='objectiveList', nargs='?', default=None) + parser.add_argument('--tourian', help="Tourian mode", + dest='tourian', nargs='?', default='Vanilla', + choices=tourians+['random']) + parser.add_argument('--tourianList', help="list to choose from when random", + dest='tourianList', nargs='?', default=None) def __init__(self, world, rom, player): # parse args @@ -293,15 +259,13 @@ class VariaRandomizer: # args.startLocation = to_pascal_case_with_space(world.startLocation[player].current_key) if args.output is None and args.rom is None: - print("Need --output or --rom parameter") - sys.exit(-1) - elif args.output is not None and args.rom is not None: - print("Can't have both --output and --rom parameters") - sys.exit(-1) + raise Exception("Need --output or --rom parameter") - if args.plandoRando != None and args.output == None: - print("plandoRando param requires output param") - sys.exit(-1) + elif args.output is not None and args.rom is not None: + raise Exception("Can't have both --output and --rom parameters") + + if args.plandoRando is not None and args.output is None: + raise Exception("plandoRando param requires output param") log.init(args.debug) logger = log.get('Rando') @@ -320,7 +284,7 @@ class VariaRandomizer: if argDict[arg] not in okValues: argDict[arg] = value - self.forcedArgs[webArg if webArg != None else arg] = webValue if webValue != None else value + self.forcedArgs[webArg if webArg is not None else arg] = webValue if webValue is not None else value # print(msg) # optErrMsgs.append(msg) @@ -368,8 +332,7 @@ class VariaRandomizer: if args.raceMagic is not None: if args.raceMagic <= 0 or args.raceMagic >= 0x10000: - print("Invalid magic") - sys.exit(-1) + raise Exception("Invalid magic") # if no max diff, set it very high if args.maxDifficulty: @@ -401,6 +364,11 @@ class VariaRandomizer: (_, progDiff) = randomMulti(args.__dict__, "progressionDifficulty", progDiffs) (majorsSplitRandom, args.majorsSplit) = randomMulti(args.__dict__, "majorsSplit", majorsSplits) (_, self.gravityBehaviour) = randomMulti(args.__dict__, "gravityBehaviour", gravityBehaviours) + (_, args.tourian) = randomMulti(args.__dict__, "tourian", tourians) + (areaRandom, args.area) = randomMulti(args.__dict__, "area", areaRandomizations) + areaRandomization = args.area in ['light', 'full'] + lightArea = args.area == 'light' + if args.minDifficulty: minDifficulty = text2diff[args.minDifficulty] if progSpeed != "speedrun": @@ -408,7 +376,7 @@ class VariaRandomizer: else: minDifficulty = 0 - if args.area == True and args.bosses == True and args.minimizerN is not None: + if areaRandomization == True and args.bosses == True and args.minimizerN is not None: forceArg('majorsSplit', 'Full', "'Majors Split' forced to Full", altValue='FullWithHUD') if args.minimizerN == "random": self.minimizerN = random.randint(30, 60) @@ -417,11 +385,6 @@ class VariaRandomizer: self.minimizerN = int(args.minimizerN) else: self.minimizerN = None - areaRandom = False - if args.area == 'random': - areaRandom = True - args.area = bool(random.getrandbits(1)) - logger.debug("area: {}".format(args.area)) doorsColorsRandom = False if args.doorsColorsRando == 'random': @@ -443,7 +406,7 @@ class VariaRandomizer: forceArg('suitsRestriction', False, "'Suits restriction' forced to off", webValue='off') if args.suitsRestriction == 'random': - if args.morphPlacement == 'late' and args.area == True: + if args.morphPlacement == 'late' and areaRandomization == True: forceArg('suitsRestriction', False, "'Suits restriction' forced to off", webValue='off') else: args.suitsRestriction = bool(random.getrandbits(1)) @@ -453,7 +416,7 @@ class VariaRandomizer: args.hideItems = bool(random.getrandbits(1)) if args.morphPlacement == 'random': - if args.morphPlacementList != None: + if args.morphPlacementList is not None: morphPlacements = args.morphPlacementList.split(',') args.morphPlacement = random.choice(morphPlacements) # Scavenger Hunt constraints @@ -465,9 +428,10 @@ class VariaRandomizer: forceArg('startLocation', "Landing Site", "Start Location forced to Landing Site because of Scavenger mode") if args.morphPlacement == 'late': forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal instead of late") - if args.scavEscape == True: - forceArg('escapeRando', True, "'Escape randomization' forced to on", webValue='on') - forceArg('noRemoveEscapeEnemies', True, "Enemies enabled during escape sequence", webArg='removeEscapeEnemies', webValue='off') + # use escape rando for auto escape trigger + if args.tourian == 'Disabled': + forceArg('escapeRando', True, "'Escape randomization' forced to on", webValue='on') + forceArg('noRemoveEscapeEnemies', True, "Enemies enabled during escape sequence", webArg='removeEscapeEnemies', webValue='off') # random fill makes certain options unavailable if (progSpeed == 'speedrun' or progSpeed == 'basic') and args.majorsSplit != 'Scavenger': forceArg('progressionDifficulty', 'normal', "'Progression difficulty' forced to normal") @@ -485,19 +449,17 @@ class VariaRandomizer: forceArg('noLayout', False, "'Anti-softlock layout patches' forced to on", webValue='on') forceArg('suitsRestriction', False, "'Suits restriction' forced to off", webValue='off') forceArg('areaLayoutBase', False, "'Additional layout patches for easier navigation' forced to on", webValue='on') - possibleStartAPs, reasons = GraphUtils.getPossibleStartAPs(args.area, self.maxDifficulty, args.morphPlacement, self.player) + possibleStartAPs, reasons = GraphUtils.getPossibleStartAPs(areaRandomization, self.maxDifficulty, args.morphPlacement, self.player) if args.startLocation == 'random': - if args.startLocationList != None: - # to be able to give the list in jm we had to replace ' ' with '_', do the opposite operation - startLocationList = args.startLocationList.replace('_', ' ') - startLocationList = startLocationList.split(',') + if args.startLocationList is not None: + startLocationList = args.startLocationList.split(',') # intersection between user whishes and reality possibleStartAPs = sorted(list(set(possibleStartAPs).intersection(set(startLocationList)))) if len(possibleStartAPs) == 0: - optErrMsgs += ["%s : %s" % (apName, cause) for apName, cause in reasons.items() if apName in startLocationList] - optErrMsgs.append('Invalid start locations list with your settings.') - dumpErrorMsgs(args.output, optErrMsgs) - sys.exit(-1) + #optErrMsgs += ["%s : %s" % (apName, cause) for apName, cause in reasons.items() if apName in startLocationList] + raise Exception("Invalid start locations list with your settings." + + "%s : %s" % (apName, cause) for apName, cause in reasons.items() if apName in startLocationList) + #dumpErrorMsgs(args.output, optErrMsgs) args.startLocation = random.choice(possibleStartAPs) elif args.startLocation not in possibleStartAPs: args.startLocation = 'Landing Site' @@ -505,7 +467,6 @@ class VariaRandomizer: #optErrMsgs.append('Invalid start location: {}. {}'.format(args.startLocation, reasons[args.startLocation])) #optErrMsgs.append('Possible start locations with these settings: {}'.format(possibleStartAPs)) #dumpErrorMsgs(args.output, optErrMsgs) - #sys.exit(-1) ap = getAccessPoint(args.startLocation) if 'forcedEarlyMorph' in ap.Start and ap.Start['forcedEarlyMorph'] == True: forceArg('morphPlacement', 'early', "'Morph Placement' forced to early for custom start location") @@ -517,8 +478,7 @@ class VariaRandomizer: forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal for custom start location") if args.majorsSplit == 'Chozo' and args.morphPlacement == "late": forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal for Chozo") - #if args.patchOnly == False: - # print("SEED: " + str(self.seed)) + #print("SEED: " + str(self.seed)) # fill restrictions dict restrictions = { 'Suits' : args.suitsRestriction, 'Morph' : args.morphPlacement, "doors": "normal" if not args.doorsColorsRando else "late" } @@ -527,7 +487,8 @@ class VariaRandomizer: scavNumLocs = int(args.scavNumLocs) if scavNumLocs == 0: scavNumLocs = random.randint(4,16) - restrictions["ScavengerParams"] = {'numLocs':scavNumLocs, 'vanillaItems':not args.scavRandomized, 'escape': args.scavEscape} + restrictions["ScavengerParams"] = {'numLocs':scavNumLocs, 'vanillaItems':not args.scavRandomized} + restrictions["EscapeTrigger"] = args.tourian == 'Disabled' seedCode = 'X' if majorsSplitRandom == False: if restrictions['MajorMinor'] == 'Full': @@ -542,16 +503,12 @@ class VariaRandomizer: seedCode = 'B'+seedCode if args.doorsColorsRando == True and doorsColorsRandom == False: seedCode = 'D'+seedCode - if args.area == True and areaRandom == False: + if areaRandomization == True and areaRandom == False: seedCode = 'A'+seedCode - # output ROM name - #if args.patchOnly == False: - # self.fileName = 'VARIA_Randomizer_' + seedCode + str(self.seed) + '_' + preset - # if args.progressionSpeed != "random": - # self.fileName += "_" + args.progressionSpeed - #else: - # self.fileName = 'VARIA' # TODO : find better way to name the file (argument?) + #fileName = 'VARIA_Randomizer_' + seedCode + str(seed) + '_' + preset + #if args.progressionSpeed != "random": + # fileName += "_" + args.progressionSpeed self.fileName = output_path seedName = self.fileName if args.directory != '.': @@ -572,8 +529,12 @@ class VariaRandomizer: RomPatches.ActivePatches[self.player] += RomPatches.VariaTweaks if self.minimizerN is not None: RomPatches.ActivePatches[self.player].append(RomPatches.NoGadoras) - if args.minimizerTourian == True: - RomPatches.ActivePatches[self.player] += RomPatches.MinimizerTourian + if args.tourian == 'Fast': + RomPatches.ActivePatches[self.player] += RomPatches.MinimizerTourian + elif args.tourian == 'Disabled': + RomPatches.ActivePatches[self.player].append(RomPatches.NoTourian) + if 'relaxed_round_robin_cf.ips' in args.patches: + RomPatches.ActivePatches[self.player].append(RomPatches.RoundRobinCF) missileQty = float(args.missileQty) superQty = float(args.superQty) powerBombQty = float(args.powerBombQty) @@ -588,10 +549,8 @@ class VariaRandomizer: if minorQty < 1: minorQty = random.randint(25, 100) if self.energyQty == 'random': - if args.energyQtyList != None: - # with jm can't have a list with space in it - energyQtyList = args.energyQtyList.replace('_', ' ') - energyQties = energyQtyList.split(',') + if args.energyQtyList is not None: + energyQties = args.energyQtyList.split(',') self.energyQty = random.choice(energyQties) if self.energyQty == 'ultra sparse': # add nerfed rainbow beam patch @@ -620,31 +579,24 @@ class VariaRandomizer: self.ctrlDict = { getattr(ctrl, button) : button for button in ctrlButton } args.moonWalk = ctrl.Moonwalk - PlandoOptions = None + plandoSettings = None if args.plandoRando is not None: + plandoRando = json.loads(args.plandoRando) forceArg('progressionSpeed', 'speedrun', "'Progression Speed' forced to speedrun") progSpeed = 'speedrun' forceArg('majorsSplit', 'Full', "'Majors Split' forced to Full") forceArg('morphPlacement', 'normal', "'Morph Placement' forced to normal") forceArg('progressionDifficulty', 'normal', "'Progression difficulty' forced to normal") progDiff = 'normal' - args.plandoRando = json.loads(args.plandoRando) - RomPatches.ActivePatches[self.player] = args.plandoRando["patches"] - DoorsManager.unserialize(args.plandoRando["doors"]) - PlandoOptions = {"locsItems": args.plandoRando['locsItems'], "forbiddenItems": args.plandoRando['forbiddenItems']} + RomPatches.ActivePatches = plandoRando["patches"] + DoorsManager.unserialize(plandoRando["doors"]) + plandoSettings = {"locsItems": plandoRando['locsItems'], "forbiddenItems": plandoRando['forbiddenItems']} randoSettings = RandoSettings(self.maxDifficulty, progSpeed, progDiff, qty, restrictions, args.superFun, args.runtimeLimit_s, - PlandoOptions, minDifficulty) - - # print some parameters for jm's stats - if args.jm == True: - print("startLocation:{}".format(args.startLocation)) - print("progressionSpeed:{}".format(progSpeed)) - print("majorsSplit:{}".format(args.majorsSplit)) - print("morphPlacement:{}".format(args.morphPlacement)) + plandoSettings, minDifficulty) dotFile = None - if args.area == True: + if areaRandomization == True: if args.dot == True: dotFile = args.directory + '/' + seedName + '.dot' RomPatches.ActivePatches[self.player] += RomPatches.AreaBaseSet @@ -652,49 +604,84 @@ class VariaRandomizer: RomPatches.ActivePatches[self.player] += RomPatches.AreaComfortSet if args.doorsColorsRando == True: RomPatches.ActivePatches[self.player].append(RomPatches.RedDoorsMissileOnly) - graphSettings = GraphSettings(args.startLocation, args.area, args.lightArea, args.bosses, - args.escapeRando, self.minimizerN, dotFile, args.doorsColorsRando, args.allowGreyDoors, - args.plandoRando["transitions"] if args.plandoRando != None else None) + graphSettings = GraphSettings(self.player, args.startLocation, areaRandomization, lightArea, args.bosses, + args.escapeRando, self.minimizerN, dotFile, + args.doorsColorsRando, args.allowGreyDoors, args.tourian, + plandoRando["transitions"] if plandoSettings is not None else None) - if args.plandoRando is None: + if plandoSettings is None: DoorsManager.setDoorsColor(self.player) self.escapeAttr = None - if args.patchOnly == False: - try: - self.randoExec = RandoExec(seedName, args.vcr, randoSettings, graphSettings, self.player) - self.container = self.randoExec.randomize() - # if we couldn't find an area layout then the escape graph is not created either - # and getDoorConnections will crash if random escape is activated. - stuck = False - if not stuck or args.vcr == True: - self.doors = GraphUtils.getDoorConnections(self.randoExec.areaGraph, - args.area, args.bosses, - args.escapeRando) - escapeAttr = self.randoExec.areaGraph.EscapeAttributes if args.escapeRando else None - if escapeAttr is not None: - escapeAttr['patches'] = [] - if args.noRemoveEscapeEnemies == True: - escapeAttr['patches'].append("Escape_Rando_Enable_Enemies") - if args.scavEscape == True: - escapeAttr['patches'].append('Escape_Scavenger') - except Exception as e: - import traceback - traceback.print_exc(file=sys.stdout) - dumpErrorMsg(args.output, "Error: {}".format(e)) - sys.exit(-1) + if plandoSettings is None: + self.objectivesManager = Objectives(self.player, args.tourian != 'Disabled', randoSettings) + addedObjectives = 0 + if args.majorsSplit == "Scavenger": + self.objectivesManager.setScavengerHunt() + addedObjectives = 1 + + if args.objective: + try: + nbObjectives = int(args.objective[0]) + except: + nbObjectives = 0 if "random" in args.objective else None + if nbObjectives is not None: + availableObjectives = args.objectiveList.split(',') if args.objectiveList is not None else objectives + if nbObjectives == 0: + nbObjectives = random.randint(1, min(Objectives.maxActiveGoals, len(availableObjectives))) + self.objectivesManager.setRandom(nbObjectives, availableObjectives) + else: + maxActiveGoals = Objectives.maxActiveGoals - addedObjectives + if len(args.objective) > maxActiveGoals: + args.objective = args.objective[0:maxActiveGoals] + for goal in args.objective: + self.objectivesManager.addGoal(goal) + self.objectivesManager.expandGoals() + else: + if not (args.majorsSplit == "Scavenger" and args.tourian == 'Disabled'): + self.objectivesManager.setVanilla() + if len(self.objectivesManager.activeGoals) == 0: + self.objectivesManager.addGoal('nothing') + if any(goal for goal in self.objectivesManager.activeGoals if goal.area is not None): + forceArg('hud', True, "'VARIA HUD' forced to on", webValue='on') else: - stuck = False - itemLocs = [] - progItemLocs = None + args.tourian = plandoRando["tourian"] + self.objectivesManager = Objectives(self.player, args.tourian != 'Disabled') + for goal in plandoRando["objectives"]: + self.objectivesManager.addGoal(goal) + + # print some parameters for jm's stats + #if args.jm == True: + # print("startLocation:{}".format(args.startLocation)) + # print("progressionSpeed:{}".format(progSpeed)) + # print("majorsSplit:{}".format(args.majorsSplit)) + # print("morphPlacement:{}".format(args.morphPlacement)) + # print("gravity:{}".format(gravityBehaviour)) + # print("maxDifficulty:{}".format(maxDifficulty)) + # print("tourian:{}".format(args.tourian)) + # print("objectives:{}".format([g.name for g in Objectives.activeGoals])) + # print("energyQty:{}".format(energyQty)) + + #try: + self.randoExec = RandoExec(seedName, args.vcr, randoSettings, graphSettings, self.player) + self.container = self.randoExec.randomize() + # if we couldn't find an area layout then the escape graph is not created either + # and getDoorConnections will crash if random escape is activated. + stuck = False + if not stuck or args.vcr == True: + self.escapeAttr = self.randoExec.areaGraph.EscapeAttributes if args.escapeRando else None + if self.escapeAttr is not None: + self.escapeAttr['patches'] = [] + if args.noRemoveEscapeEnemies == True: + self.escapeAttr['patches'].append("Escape_Rando_Enable_Enemies") + #except Exception as e: + # import traceback + # traceback.print_exc(file=sys.stdout) + # dumpErrorMsg(args.output, "Error: {}".format(e)) + if stuck == True: - dumpErrorMsg(args.output, self.randoExec.errorMsg) - print("Can't generate " + self.fileName + " with the given parameters: {}".format(self.randoExec.errorMsg)) - # in vcr mode we still want the seed to be generated to analyze it - if args.vcr == False: - sys.exit(-1) - #if args.patchOnly == False: - # randoExec.postProcessItemLocs(itemLocs, args.hideItems) + #dumpErrorMsg(args.output, self.randoExec.errorMsg) + raise Exception("Can't generate " + self.fileName + " with the given parameters: {}".format(self.randoExec.errorMsg)) def PatchRom(self, outputFilename, customPrePatchApply = None, customPostPatchApply = None): args = self.args @@ -722,13 +709,12 @@ class VariaRandomizer: # for loc in sorted(locsItems.keys()): # print('{:>50}: {:>16} '.format(loc, locsItems[loc])) - # if args.plandoRando != None: + # if plandoSettings is not None: # with open(args.output, 'w') as jsonFile: # json.dump({"itemLocs": [il.json() for il in itemLocs], "errorMsg": randoExec.errorMsg}, jsonFile) - # sys.exit(0) # # generate extended stats - # if args.extStatsFilename != None: + # if args.extStatsFilename is not None: # with open(args.extStatsFilename, 'a') as extStatsFile: # skillPreset = os.path.splitext(os.path.basename(args.paramsFileName))[0] # if args.fakeRandoPreset is not None: @@ -738,96 +724,64 @@ class VariaRandomizer: # db.DB.dumpExtStatsItems(skillPreset, randoPreset, locsItems, extStatsFile) try: + if args.hud == True or args.majorsSplit == "FullWithHUD": + args.patches.append("varia_hud.ips") + if args.debug == True: + args.patches.append("Disable_Clear_Save_Boot") + + patcherSettings = { + "isPlando": False, + "majorsSplit": args.majorsSplit, + "startLocation": args.startLocation, + "optionalPatches": args.patches, + "layout": not args.noLayout, + "suitsMode": args.gravityBehaviour, + "area": args.area in ['light', 'full'], + "boss": args.bosses, + "areaLayout": not args.areaLayoutBase, + "variaTweaks": not args.noVariaTweaks, + "nerfedCharge": args.nerfedCharge, + "nerfedRainbowBeam": args.energyQty == 'ultra sparse', + "escapeAttr": self.escapeAttr, + "minimizerN": None, #minimizerN, + "tourian": args.tourian, + "doorsColorsRando": args.doorsColorsRando, + "vanillaObjectives": self.objectivesManager.isVanilla(), + "ctrlDict": self.ctrlDict, + "moonWalk": args.moonWalk, + "seed": self.seed, + "randoSettings": self.randoExec.randoSettings, + "doors": self.doors, + "displayedVersion": displayedVersion, + #"itemLocs": itemLocs, + #"progItemLocs": progItemLocs, + } + # args.rom is not None: generate local rom named filename.sfc with args.rom as source # args.output is not None: generate local json named args.output if args.rom is not None: # patch local rom romFileName = args.rom shutil.copyfile(romFileName, outputFilename) - romPatcher = RomPatcher(outputFilename, args.raceMagic, False, self.player) + romPatcher = RomPatcher(settings=patcherSettings, romFileName=outputFilename, magic=args.raceMagic, player=self.player) else: - romPatcher = RomPatcher(magic=args.raceMagic) + romPatcher = RomPatcher(settings=patcherSettings, magic=args.raceMagic) if customPrePatchApply != None: customPrePatchApply(romPatcher) - if args.hud == True or args.majorsSplit == "FullWithHUD": - args.patches.append("varia_hud.ips") - if args.patchOnly == False: - romPatcher.applyIPSPatches(args.startLocation, args.patches, - args.noLayout, self.gravityBehaviour, - args.area, args.bosses, args.areaLayoutBase, - args.noVariaTweaks, args.nerfedCharge, self.energyQty == 'ultra sparse', - self.escapeAttr, self.minimizerN, args.minimizerTourian, - args.doorsColorsRando) - else: - # from customizer permalink, apply previously generated seed ips first - if args.seedIps != None: - romPatcher.applyIPSPatch(args.seedIps) - - romPatcher.addIPSPatches(args.patches) - # don't color randomize custom ships - args.shift_ship_palette = False + romPatcher.patchRom() if customPostPatchApply != None: customPostPatchApply(romPatcher) - # we have to write ips to ROM before doing our direct modifications which will rewrite some parts (like in credits), - # but in web mode we only want to generate a global ips at the end -# if args.rom != None: -# romPatcher.commitIPS() - if args.patchOnly == False: -# romPatcher.writeItemsLocs(itemLocs) -# romPatcher.writeSplitLocs(args.majorsSplit, itemLocs, progItemLocs) - romPatcher.writeItemsNumber() - romPatcher.writeSeed(self.seed) # lol if race mode -# romPatcher.writeSpoiler(itemLocs, progItemLocs) -# romPatcher.writeRandoSettings(self.randoExec.randoSettings, itemLocs) - romPatcher.writeDoorConnections(self.doors) - romPatcher.writeVersion(displayedVersion) - if self.ctrlDict is not None: - romPatcher.writeControls(self.ctrlDict) - if args.moonWalk == True: - romPatcher.enableMoonWalk() - if args.patchOnly == False: - romPatcher.writeMagic() - romPatcher.writeMajorsSplit(args.majorsSplit) - # if args.palette == True: - # paletteSettings = { - # "global_shift": None, - # "individual_suit_shift": None, - # "individual_tileset_shift": None, - # "match_ship_and_power": None, - # "seperate_enemy_palette_groups": None, - # "match_room_shift_with_boss": None, - # "shift_tileset_palette": None, - # "shift_boss_palettes": None, - # "shift_suit_palettes": None, - # "shift_enemy_palettes": None, - # "shift_beam_palettes": None, - # "shift_ship_palette": None, - # "min_degree": None, - # "max_degree": None, - # "invert": None, - # "no_blue_door_palette": None - # } - # for param in paletteSettings: - # paletteSettings[param] = getattr(args, param) - # PaletteRando(romPatcher, paletteSettings, args.sprite).randomize() - - # web mode, generate only one ips at the end - if args.rom == None: - romPatcher.commitIPS() - romPatcher.end() - if args.patchOnly == False: - if len(optErrMsgs) > 0: -# optErrMsgs.append(randoExec.errorMsg) - msg = joinErrorMsgs(optErrMsgs) - else: -# msg = randoExec.errorMsg - msg = '' + if len(optErrMsgs) > 0: + #optErrMsgs.append(randoExec.errorMsg) + msg = joinErrorMsgs(optErrMsgs) else: + #msg = randoExec.errorMsg msg = '' + if args.rom is None: # web mode data = romPatcher.romFile.data self.fileName = '{}.sfc'.format(self.fileName) @@ -845,9 +799,8 @@ class VariaRandomizer: except Exception as e: import traceback traceback.print_exc(file=sys.stdout) - msg = "Error patching {}: ({}: {})".format(outputFilename, type(e).__name__, e) - dumpErrorMsg(args.output, msg) - sys.exit(-1) + raise Exception("Error patching {}: ({}: {})".format(outputFilename, type(e).__name__, e)) + #dumpErrorMsg(args.output, msg) # if stuck == True: # print("Rom generated for debug purpose: {}".format(self.fileName)) diff --git a/worlds/sm/variaRandomizer/rom/addressTypes.py b/worlds/sm/variaRandomizer/rom/addressTypes.py new file mode 100644 index 0000000000..f735919d34 --- /dev/null +++ b/worlds/sm/variaRandomizer/rom/addressTypes.py @@ -0,0 +1,72 @@ +from .rom import snes_to_pc, pc_to_snes + +class Byte(object): + def __init__(self, value): + self.value = value + + def expand(self): + return [self.value] + +class Word(object): + def __init__(self, value): + self.value = value + + def expand(self): + return [self.value, self.value+1] + +class Long(object): + def __init__(self, value): + self.value = value + + def expand(self): + return [self.value, self.value+1, self.value+2] + +class ValueSingle(object): + def __init__(self, value, storage=Word): + self.value = snes_to_pc(value) + self.storage = storage + + def getOne(self): + return self.value + + def getAll(self): + return [self.value] + + def getWeb(self): + return self.storage(self.value).expand() + +class ValueList(object): + def __init__(self, values, storage=Word): + self.values = [snes_to_pc(value) for value in values] + self.storage = storage + + def getOne(self): + return self.values[0] + + def getAll(self): + return self.values + + def getWeb(self): + out = [] + for value in self.values: + out += self.storage(value).expand() + return out + +class ValueRange(object): + def __init__(self, start, length=-1, end=-1): + self.start = snes_to_pc(start) + if length != -1: + self.end = self.start + length + self.length = length + else: + self.end = snes_to_pc(end) + self.length = self.end - self.start + + def getOne(self): + return self.start + + def getAll(self): + return [self.start+i for i in range(self.length)] + + def getWeb(self): + return [self.start, self.end] diff --git a/worlds/sm/variaRandomizer/rom/addresses.py b/worlds/sm/variaRandomizer/rom/addresses.py new file mode 100644 index 0000000000..76f149e10e --- /dev/null +++ b/worlds/sm/variaRandomizer/rom/addresses.py @@ -0,0 +1,51 @@ +from .addressTypes import ValueList, ValueSingle, ValueRange, Byte, Word, Long +from .objectivesAddresses import objectivesAddr + +# TODO::add patches + + +class Addresses(object): + @staticmethod + def getOne(key): + value = Addresses.addresses[key] + return value.getOne() + + @staticmethod + def getAll(key): + value = Addresses.addresses[key] + return value.getAll() + + @staticmethod + def getWeb(key): + value = Addresses.addresses[key] + return value.getWeb() + + @staticmethod + def getRange(key): + value = Addresses.addresses[key] + return value.getWeb() + + addresses = { + 'totalItems': ValueList([0x8BE656, 0x8BE6B3], storage=Byte), + 'majorsSplit': ValueSingle(0x82fb6c, storage=Byte), + # scavenger hunt items list (17 prog items (including ridley) + hunt over + terminator, each is a word) + 'scavengerOrder': ValueRange(0xA1F5D8, length=(17+1+1)*2), + 'plandoAddresses': ValueRange(0xdee000, length=128), + 'plandoTransitions': ValueSingle(0xdee100), + 'escapeTimer': ValueSingle(0x809e21), + 'escapeTimerTable': ValueSingle(0xA1F0AA), + 'startAP': ValueSingle(0xa1f200), + 'customDoorsAsm': ValueSingle(0x8ff800), + 'locIdsByArea': ValueRange(0xA1F568, end=0xA1F5D7), + 'plmSpawnTable': ValueSingle(0x8fe9a0), + 'plmSpawnRoomTable': ValueSingle(0x8ff000), + 'moonwalk': ValueSingle(0x81b35d), + 'additionalETanks': ValueSingle(0xA1F470, storage=Byte), + 'hellrunRate': ValueSingle(0x8DE387), + 'BTtweaksHack1': ValueSingle(0x84ba6f+3), + 'BTtweaksHack2': ValueSingle(0x84d33b+3), + # in intro_text.ips + 'introText': ValueSingle(0x8cc389) + } + +Addresses.addresses.update(objectivesAddr) diff --git a/worlds/sm/variaRandomizer/rom/ips.py b/worlds/sm/variaRandomizer/rom/ips.py index 23c5281372..dd3f30a3ac 100644 --- a/worlds/sm/variaRandomizer/rom/ips.py +++ b/worlds/sm/variaRandomizer/rom/ips.py @@ -1,4 +1,4 @@ -import itertools +import itertools, math from ..utils.utils import range_union, openFile @@ -9,9 +9,14 @@ class IPS_Patch(object): self.truncate_length = None self.max_size = 0 if patchDict is not None: + recMaxSize = 0xffff for addr, data in patchDict.items(): - byteData = bytearray(data) - self.add_record(addr, byteData) + nrecs = int(math.ceil(float(len(data))/recMaxSize)) + for i in range(nrecs): + start = i*recMaxSize + end = min((i+1)*recMaxSize, len(data)) + byteData = bytearray(data[start:end]) + self.add_record(addr+start, byteData) def toDict(self): ret = {} diff --git a/worlds/sm/variaRandomizer/rom/objectivesAddresses.py b/worlds/sm/variaRandomizer/rom/objectivesAddresses.py new file mode 100644 index 0000000000..058d4b6c1a --- /dev/null +++ b/worlds/sm/variaRandomizer/rom/objectivesAddresses.py @@ -0,0 +1,64 @@ +from .addressTypes import ValueList, ValueSingle, ValueRange +# generated from asar output +# A1 start: A1FA80 +objectivesAddr = { + # --- objectives checker functions: A1FA80 --- + 'objectivesList': ValueSingle(0xA1FA80), + 'objectiveEventsArray': ValueRange(0xA1FB1A, length=2*5), + 'objective[kraid_is_dead]': ValueSingle(0xA1FBCE), + 'objective[phantoon_is_dead]': ValueSingle(0xA1FBD6), + 'objective[draygon_is_dead]': ValueSingle(0xA1FBDE), + 'objective[ridley_is_dead]': ValueSingle(0xA1FBE6), + 'objective[all_g4_dead]': ValueSingle(0xA1FBEE), + 'objective[spore_spawn_is_dead]': ValueSingle(0xA1FC04), + 'objective[botwoon_is_dead]': ValueSingle(0xA1FC0C), + 'objective[crocomire_is_dead]': ValueSingle(0xA1FC14), + 'objective[golden_torizo_is_dead]': ValueSingle(0xA1FC1C), + 'objective[all_mini_bosses_dead]': ValueSingle(0xA1FC24), + 'objective[scavenger_hunt_completed]': ValueSingle(0xA1FC3A), + 'objective[boss_1_killed]': ValueSingle(0xA1FC7A), + 'objective[boss_2_killed]': ValueSingle(0xA1FC83), + 'objective[boss_3_killed]': ValueSingle(0xA1FC8C), + 'objective[miniboss_1_killed]': ValueSingle(0xA1FC95), + 'objective[miniboss_2_killed]': ValueSingle(0xA1FC9E), + 'objective[miniboss_3_killed]': ValueSingle(0xA1FCA7), + 'objective[collect_25_items]': ValueSingle(0xA1FCB0), + '__pct25': 0xA1FCB5, + 'objective[collect_50_items]': ValueSingle(0xA1FCB8), + '__pct50': 0xA1FCBD, + 'objective[collect_75_items]': ValueSingle(0xA1FCC0), + '__pct75': 0xA1FCC5, + 'objective[collect_100_items]': ValueSingle(0xA1FCC8), + '__pct100': 0xA1FCCD, + 'objective[nothing_objective]': ValueSingle(0xA1FCD0), + 'objective[fish_tickled]': ValueSingle(0xA1FCF8), + 'objective[orange_geemer]': ValueSingle(0xA1FD00), + 'objective[shak_dead]': ValueSingle(0xA1FD08), + 'itemsMask': ValueSingle(0xA1FD10), + 'beamsMask': ValueSingle(0xA1FD12), + 'objective[all_major_items]': ValueSingle(0xA1FD14), + 'objective[crateria_cleared]': ValueSingle(0xA1FD2B), + 'objective[green_brin_cleared]': ValueSingle(0xA1FD33), + 'objective[red_brin_cleared]': ValueSingle(0xA1FD3B), + 'objective[ws_cleared]': ValueSingle(0xA1FD43), + 'objective[kraid_cleared]': ValueSingle(0xA1FD4B), + 'objective[upper_norfair_cleared]': ValueSingle(0xA1FD53), + 'objective[croc_cleared]': ValueSingle(0xA1FD5B), + 'objective[lower_norfair_cleared]': ValueSingle(0xA1FD63), + 'objective[west_maridia_cleared]': ValueSingle(0xA1FD6B), + 'objective[east_maridia_cleared]': ValueSingle(0xA1FD73), + 'objective[all_chozo_robots]': ValueSingle(0xA1FD7B), + 'objective[visited_animals]': ValueSingle(0xA1FD9A), + 'objective[king_cac_dead]': ValueSingle(0xA1FDE6), + # A1 end: A1FDEE + # Pause stuff: 82FB6D + # *** completed spritemaps: 82FE83 + 'objectivesSpritesOAM': ValueSingle(0x82FE83), + # 82 end: 82FEB0 + 'objectivesText': ValueSingle(0xB6F200), +} +_pctList = [] +for pct in [25,50,75,100]: + _pctList.append(objectivesAddr['__pct%d' % pct]) + del objectivesAddr['__pct%d' % pct] +objectivesAddr['totalItemsPercent'] = ValueList(_pctList) diff --git a/worlds/sm/variaRandomizer/rom/rom.py b/worlds/sm/variaRandomizer/rom/rom.py index d173f7ab54..37c15698a2 100644 --- a/worlds/sm/variaRandomizer/rom/rom.py +++ b/worlds/sm/variaRandomizer/rom/rom.py @@ -18,17 +18,50 @@ def snes_to_pc(B): return (A_1 << 16) | A_2 +VANILLA_ROM_SIZE = 3145728 +BANK_SIZE = 0x8000 + class ROM(object): + def __init__(self, data={}): + self.address = 0 + self.maxAddress = VANILLA_ROM_SIZE + + def close(self): + pass + + def seek(self, address): + if address > self.maxAddress: + self.maxAddress = address + self.address = address + + def tell(self): + if self.address > self.maxAddress: + self.maxAddress = self.address + return self.address + + def inc(self, n=1): + self.address += n + self.tell() + + def read(self, byteCount): + pass + def readWord(self, address=None): return self.readBytes(2, address) def readByte(self, address=None): return self.readBytes(1, address) + def readLong(self, address=None): + return self.readBytes(3, address) + def readBytes(self, size, address=None): if address != None: self.seek(address) return int.from_bytes(self.read(size), byteorder='little') + + def write(self, bytes): + pass def writeWord(self, word, address=None): self.writeBytes(word, 2, address) @@ -36,25 +69,45 @@ class ROM(object): def writeByte(self, byte, address=None): self.writeBytes(byte, 1, address) + def writeLong(self, lng, address=None): + self.writeBytes(lng, 3, address) + def writeBytes(self, value, size, address=None): if address != None: self.seek(address) self.write(value.to_bytes(size, byteorder='little')) + + def ipsPatch(self, ipsPatches): + pass + + def fillToNextBank(self): + off = self.maxAddress % BANK_SIZE + if off > 0: + self.seek(self.maxAddress + BANK_SIZE - off - 1) + self.writeByte(0xff) + assert (self.maxAddress % BANK_SIZE) == 0 class RealROM(ROM): def __init__(self, name): + super(RealROM, self).__init__() self.romFile = open(name, "rb+") - self.address = 0 def seek(self, address): - self.address = address + super(RealROM, self).seek(address) self.romFile.seek(address) + def tell(self): + self.address = self.romFile.tell() + return super(RealROM, self).tell() + def write(self, bytes): self.romFile.write(bytes) + self.tell() def read(self, byteCount): - return self.romFile.read(byteCount) + ret = self.romFile.read(byteCount) + self.tell() + return ret def close(self): self.romFile.close() diff --git a/worlds/sm/variaRandomizer/rom/rom_patches.py b/worlds/sm/variaRandomizer/rom/rom_patches.py index ff5e2aa823..f108f2624e 100644 --- a/worlds/sm/variaRandomizer/rom/rom_patches.py +++ b/worlds/sm/variaRandomizer/rom/rom_patches.py @@ -61,6 +61,8 @@ class RomPatches: CrabShaftBlueDoor = 107 # wrap door from sand halls left to under botwoon MaridiaSandWarp = 108 + # Replace PB blocks at Aqueduct entrance with bomb blocks + AqueductBombBlocks = 109 ## Minimizer Patches NoGadoras = 200 TourianSpeedup = 201 @@ -81,6 +83,12 @@ class RomPatches: NerfedRainbowBeam = 1005 # Red doors open with one missile, and don't react to supers: part of door color rando RedDoorsMissileOnly = 1006 + # Escape auto-trigger on objectives completion (no Tourian) + NoTourian = 1007 + # BT wakes up on its item instead of bombs + BombTorizoWake = 1008 + # Round-Robin Crystal Flash patch + RoundRobinCF = 1009 ### Hacks # rotation hack @@ -103,11 +111,11 @@ class RomPatches: AreaBaseSet = [ SingleChamberNoCrumble, AreaRandoGatesBase, AreaRandoBlueDoors, AreaRandoMoreBlueDoors, CrocBlueDoors, CrabShaftBlueDoor, MaridiaSandWarp ] - AreaComfortSet = [ AreaRandoGatesOther, SpongeBathBlueDoor, EastOceanPlatforms ] + AreaComfortSet = [ AreaRandoGatesOther, SpongeBathBlueDoor, EastOceanPlatforms, AqueductBombBlocks ] AreaSet = AreaBaseSet + AreaComfortSet # VARIA specific patch set - VariaTweaks = [ WsEtankPhantoonAlive, LNChozoSJCheckDisabled ] + VariaTweaks = [ WsEtankPhantoonAlive, LNChozoSJCheckDisabled, BombTorizoWake ] # Tourian speedup in minimizer mode MinimizerTourian = [ TourianSpeedup, OpenZebetites ] @@ -125,6 +133,6 @@ class RomPatches: @staticmethod def setDefaultPatches(startLocation): # called by the isolver in seedless mode. - # activate only layout patch (the most common one), red tower blue doors and the startLocation's patches. + # activate only layout patch (the most common one), red tower blue doors, startLocation's patches and balanced suits. from graph.graph_utils import GraphUtils - RomPatches.ActivePatches[0] = [RomPatches.RedTowerBlueDoors] + RomPatches.TotalLayout + GraphUtils.getGraphPatches(startLocation) + RomPatches.ActivePatches[0] = [RomPatches.RedTowerBlueDoors] + RomPatches.TotalLayout + GraphUtils.getGraphPatches(startLocation) + [RomPatches.NoGravityEnvProtection] diff --git a/worlds/sm/variaRandomizer/rom/rompatcher.py b/worlds/sm/variaRandomizer/rom/rompatcher.py index 0e052f20c1..fedb89e854 100644 --- a/worlds/sm/variaRandomizer/rom/rompatcher.py +++ b/worlds/sm/variaRandomizer/rom/rompatcher.py @@ -1,10 +1,15 @@ -import os, random, re +import os, random, re, json +from math import ceil +from enum import IntFlag from ..rando.Items import ItemManager from ..rom.ips import IPS_Patch -from ..utils.doorsmanager import DoorsManager -from ..graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses +from ..utils.doorsmanager import DoorsManager, IndicatorFlag +from ..utils.objectives import Objectives +from ..graph.graph_utils import GraphUtils, getAccessPoint, locIdsByAreaAddresses, graphAreas from ..logic.logic import Logic from ..rom.rom import RealROM, snes_to_pc, pc_to_snes +from ..rom.addresses import Addresses +from ..rom.rom_patches import RomPatches from ..patches.patchaccess import PatchAccess from ..utils.parameters import appDir from ..utils import log @@ -20,9 +25,7 @@ class RomPatcher: # faster MB cutscene transitions 'Mother_Brain_Cutscene_Edits', # "Balanced" suit mode - 'Removes_Gravity_Suit_heat_protection', - # door ASM to skip G4 cutscene when all 4 bosses are dead - 'g4_skip.ips', + 'Removes_Gravity_Suit_heat_protection' ], # VARIA tweaks 'VariaTweaks' : ['WS_Etank', 'LN_Chozo_SpaceJump_Check_Disable', 'ln_chozo_platform.ips', 'bomb_torizo.ips'], @@ -30,20 +33,9 @@ class RomPatcher: 'Layout': ['dachora.ips', 'early_super_bridge.ips', 'high_jump.ips', 'moat.ips', 'spospo_save.ips', 'nova_boost_platform.ips', 'red_tower.ips', 'spazer.ips', 'brinstar_map_room.ips', 'kraid_save.ips', 'mission_impossible.ips'], - # comfort patches - 'Optional': ['rando_speed.ips', 'Infinite_Space_Jump', 'refill_before_save.ips', - 'spinjumprestart.ips', 'elevators_doors_speed.ips', 'No_Music', 'random_music.ips', - # animals - 'animal_enemies.ips', 'animals.ips', 'draygonimals.ips', - 'escapimals.ips', 'gameend.ips', 'grey_door_animals.ips', - 'low_timer.ips', 'metalimals.ips', 'phantoonimals.ips', 'ridleyimals.ips', - 'Escape_Animals_Change_Event', # ...end animals - # vanilla behaviour restore - 'remove_elevators_doors_speed.ips', - 'varia_hud.ips'], # base patchset+optional layout for area rando 'Area': ['area_rando_layout.ips', 'door_transition.ips', 'area_rando_doors.ips', - 'Sponge_Bath_Blinking_Door', 'east_ocean.ips', 'area_rando_warp_door.ips', + 'Sponge_Bath_Blinking_Door', 'east_ocean.ips', 'area_rando_warp_door.ips', 'aqueduct_bomb_blocks.ips', 'crab_shaft.ips', 'Save_Crab_Shaft', 'Save_Main_Street', 'no_demo.ips'], # patches for boss rando 'Bosses': ['door_transition.ips', 'no_demo.ips'], @@ -55,14 +47,16 @@ class RomPatcher: 'DoorsColors': ['beam_doors_plms.ips', 'beam_doors_gfx.ips', 'red_doors.ips'] } - def __init__(self, romFileName=None, magic=None, plando=False, player=0): + def __init__(self, settings=None, romFileName=None, magic=None, player=0): self.log = log.get('RomPatcher') + self.settings = settings self.romFileName = romFileName + self.patchAccess = PatchAccess() self.race = None self.romFile = RealROM(romFileName) #if magic is not None: # from rom.race_mode import RaceModePatcher - # self.race = RaceModePatcher(self, magic, plando) + # self.race = RaceModePatcher(self, magic) # IPS_Patch objects list self.ipsPatches = [] # loc name to alternate address. we still write to original @@ -79,18 +73,25 @@ class RomPatcher: # get out of croc room: reload CRE 0x93ea: self.forceRoomCRE } - self.patchAccess = PatchAccess() self.player = player + def patchRom(self): + self.applyIPSPatches() + self.commitIPS() + def end(self): + self.romFile.fillToNextBank() self.romFile.close() def writeItemCode(self, item, visibility, address): itemCode = ItemManager.getItemTypeCode(item, visibility) + self.writePlmWord(itemCode, address) + + def writePlmWord(self, word, address): if self.race is None: - self.romFile.writeWord(itemCode, address) + self.romFile.writeWord(word, address) else: - self.race.writeItemCode(itemCode, address) + self.race.writePlmWord(word, address) def getLocAddresses(self, loc): ret = [loc.Address] @@ -115,23 +116,24 @@ class RomPatcher: continue self.writeItem(itemLoc) if item.Category != 'Nothing': - self.nItems += 1 + if not loc.restricted: + self.nItems += 1 if loc.Name == 'Morphing Ball': self.patchMorphBallEye(item) def writeSplitLocs(self, split, itemLocs, progItemLocs): majChozoCheck = lambda itemLoc: itemLoc.Item.Class == split and itemLoc.Location.isClass(split) - fullCheck = lambda itemLoc: itemLoc.Location.Id is not None + fullCheck = lambda itemLoc: itemLoc.Location.Id is not None and itemLoc.Location.BossItemType is None splitChecks = { 'Full': fullCheck, 'Scavenger': fullCheck, 'Major': majChozoCheck, 'Chozo': majChozoCheck, - 'FullWithHUD': lambda itemLoc: itemLoc.Item.Category not in ['Energy', 'Ammo', 'Boss'] + 'FullWithHUD': lambda itemLoc: itemLoc.Item.Category not in ['Energy', 'Ammo', 'Boss', 'MiniBoss'] } itemLocCheck = lambda itemLoc: itemLoc.Item.Category != "Nothing" and splitChecks[split](itemLoc) for area,addr in locIdsByAreaAddresses.items(): - locs = [il.Location for il in itemLocs if itemLocCheck(il) and il.Location.GraphArea == area] + locs = [il.Location for il in itemLocs if itemLocCheck(il) and il.Location.GraphArea == area and not il.Location.restricted] self.log.debug("writeSplitLocs. area="+area) self.log.debug(str([loc.Name for loc in locs])) self.romFile.seek(addr) @@ -140,11 +142,14 @@ class RomPatcher: self.romFile.writeByte(0xff) if split == "Scavenger": # write required major item order - self.romFile.seek(snes_to_pc(0xA1F5D8)) + self.romFile.seek(Addresses.getOne('scavengerOrder')) for itemLoc in progItemLocs: self.romFile.writeWord((itemLoc.Location.Id << 8) | itemLoc.Location.HUD) # bogus loc ID | "HUNT OVER" index - self.romFile.writeWord(0xff10) + self.romFile.writeWord(0xff11) + # fill remaining list with 0xFFFF to avoid issue with plandomizer having less items than in the base seed + for i in range(18-len(progItemLocs)): + self.romFile.writeWord(0xffff) # trigger morph eye enemy on whatever item we put there, # not just morph ball @@ -183,8 +188,8 @@ class RomPatcher: operand = item.BeamBits else: operand = item.ItemBits - self.patchMorphBallCheck(0x1410E6, cat, comp, operand, branch) # eye main AI - self.patchMorphBallCheck(0x1468B2, cat, comp, operand, branch) # head main AI + self.patchMorphBallCheck(snes_to_pc(0xa890e6), cat, comp, operand, branch) # eye main AI + self.patchMorphBallCheck(snes_to_pc(0xa8e8b2), cat, comp, operand, branch) # head main AI def patchMorphBallCheck(self, offset, cat, comp, operand, branch): # actually patch enemy AI @@ -195,113 +200,111 @@ class RomPatcher: def writeItemsNumber(self): # write total number of actual items for item percentage patch (patch the patch) - for addr in [0x5E64E, 0x5E6AB]: + for addr in Addresses.getAll('totalItems'): self.romFile.writeByte(self.nItems, addr) + # for X% collected items objectives, precompute values and write them in objectives functions + for percent, addr in zip([25, 50, 75, 100], Addresses.getAll('totalItemsPercent')): + self.romFile.writeWord(ceil((self.nItems * percent)/100), addr) + def addIPSPatches(self, patches): for patchName in patches: self.applyIPSPatch(patchName) - def writePlmTable(self, plms, area, bosses, startLocation): - # called when saving a plando - try: - if bosses == True or area == True: - plms.append('WS_Save_Blinking_Door') - - doors = self.getStartDoors(plms, area, None) - self.writeDoorsColor(doors, self.player) - self.applyStartAP(startLocation, plms, doors) - - self.applyPLMs(plms) - except Exception as e: - raise Exception("Error patching {}. ({})".format(self.romFileName, e)) - - def applyIPSPatches(self, startLocation="Landing Site", - optionalPatches=[], noLayout=False, suitsMode="Balanced", - area=False, bosses=False, areaLayoutBase=False, - noVariaTweaks=False, nerfedCharge=False, nerfedRainbowBeam=False, - escapeAttr=None, minimizerN=None, minimizerTourian=True, - doorsColorsRando=False): + def applyIPSPatches(self): try: # apply standard patches stdPatches = [] plms = [] - # apply race mode first because it fills the rom with a bunch of crap - if self.race is not None: - stdPatches.append('race_mode.ips') + stdPatches += RomPatcher.IPSPatches['Standard'][:] + if not self.settings["layout"]: + # when disabling anti softlock protection also disable doors indicators + stdPatches.remove('door_indicators_plms.ips') if self.race is not None: - stdPatches.append('race_mode_credits.ips') - if suitsMode != "Balanced": + stdPatches.append('race_mode_post.ips') + if self.settings["suitsMode"] != "Balanced": stdPatches.remove('Removes_Gravity_Suit_heat_protection') - if suitsMode == "Progressive": + if self.settings["suitsMode"] == "Progressive": stdPatches.append('progressive_suits.ips') - if nerfedCharge == True: + if self.settings["nerfedCharge"] == True: stdPatches.append('nerfed_charge.ips') - if nerfedRainbowBeam == True: + if self.settings["nerfedRainbowBeam"] == True: stdPatches.append('nerfed_rainbow_beam.ips') - if bosses == True or area == True: + if self.settings["boss"] == True or self.settings["area"] == True: stdPatches += ["WS_Main_Open_Grey", "WS_Save_Active"] plms.append('WS_Save_Blinking_Door') - if bosses == True: + if self.settings["boss"] == True: stdPatches.append("Phantoon_Eye_Door") - if area == True or doorsColorsRando == True: + if (self.settings["area"] == True + or self.settings["doorsColorsRando"] == True + or not GraphUtils.isStandardStart(self.settings["startLocation"])): stdPatches.append("Enable_Backup_Saves") - if 'varia_hud.ips' in optionalPatches: - # varia hud has its own variant of g4_skip for scavenger mode, - # it can also make demos glitch out - stdPatches.remove("g4_skip.ips") + if 'varia_hud.ips' in self.settings["optionalPatches"]: + # varia hud can make demos glitch out self.applyIPSPatch("no_demo.ips") for patchName in stdPatches: self.applyIPSPatch(patchName) - if noLayout == False: + if not self.settings["vanillaObjectives"]: + self.applyIPSPatch("Objectives_sfx") + # show objectives and Tourian status in a shortened intro sequence + # if not full vanilla objectives+tourian + if not self.settings["vanillaObjectives"] or self.settings["tourian"] != "Vanilla": + self.applyIPSPatch("Restore_Intro") # important to apply this after new_game.ips + self.applyIPSPatch("intro_text.ips") + if self.settings["layout"]: # apply layout patches for patchName in RomPatcher.IPSPatches['Layout']: self.applyIPSPatch(patchName) - if noVariaTweaks == False: + if self.settings["variaTweaks"]: # VARIA tweaks for patchName in RomPatcher.IPSPatches['VariaTweaks']: self.applyIPSPatch(patchName) + if (self.settings["majorsSplit"] == 'Scavenger' + and any(il for il in self.settings["progItemLocs"] if il.Location.Name == "Ridley")): + # ridley as scav loc + self.applyIPSPatch("Blinking[RidleyRoomIn]") # apply optional patches - for patchName in optionalPatches: - if patchName in RomPatcher.IPSPatches['Optional']: - self.applyIPSPatch(patchName) + for patchName in self.settings["optionalPatches"]: + self.applyIPSPatch(patchName) # random escape - if escapeAttr is not None: + if self.settings["escapeAttr"] is not None: for patchName in RomPatcher.IPSPatches['Escape']: self.applyIPSPatch(patchName) # animals and timer - self.applyEscapeAttributes(escapeAttr, plms) + self.applyEscapeAttributes(self.settings["escapeAttr"], plms) # apply area patches - if area == True: + if self.settings["area"] == True: + if not self.settings["areaLayout"]: + for p in ['area_rando_layout.ips', 'Sponge_Bath_Blinking_Door', 'east_ocean.ips', 'aqueduct_bomb_blocks.ips']: + RomPatcher.IPSPatches['Area'].remove(p) + RomPatcher.IPSPatches['Area'].append('area_rando_layout_base.ips') for patchName in RomPatcher.IPSPatches['Area']: - if areaLayoutBase == True and patchName in ['area_rando_layout.ips', 'Sponge_Bath_Blinking_Door', 'east_ocean.ips']: - continue self.applyIPSPatch(patchName) - if areaLayoutBase == True: - self.applyIPSPatch('area_rando_layout_base.ips') - - else: self.applyIPSPatch('area_ids_alt.ips') - if bosses == True: + if self.settings["boss"] == True: for patchName in RomPatcher.IPSPatches['Bosses']: self.applyIPSPatch(patchName) - if minimizerN is not None: + if self.settings["minimizerN"] is not None: self.applyIPSPatch('minimizer_bosses.ips') - if minimizerTourian == True: - for patchName in RomPatcher.IPSPatches['MinimizerTourian']: - self.applyIPSPatch(patchName) - doors = self.getStartDoors(plms, area, minimizerN) - if doorsColorsRando == True: + if self.settings["tourian"] == "Fast": + for patchName in RomPatcher.IPSPatches['MinimizerTourian']: + self.applyIPSPatch(patchName) + elif self.settings["tourian"] == "Disabled": + self.applyIPSPatch("Escape_Trigger") + doors = self.getStartDoors(plms, self.settings["area"], self.settings["minimizerN"]) + if self.settings["doorsColorsRando"] == True: for patchName in RomPatcher.IPSPatches['DoorsColors']: self.applyIPSPatch(patchName) self.writeDoorsColor(doors, self.player) - self.applyStartAP(startLocation, plms, doors) + if self.settings["layout"]: + self.writeDoorIndicators(plms, self.settings["area"], self.settings["doorsColorsRando"]) + self.applyStartAP(self.settings["startLocation"], plms, doors) self.applyPLMs(plms) except Exception as e: raise Exception("Error patching {}. ({})".format(self.romFileName, e)) @@ -359,9 +362,9 @@ class RomPatcher: if 'doors' in ap.Start: doors += ap.Start['doors'] doors.append(0x0) - addr = 0x10F200 + addr = Addresses.getOne('startAP') patch = [w0, w1] + doors - assert (addr + len(patch)) < 0x10F210, "Stopped before new_game overwrite" + assert (addr + len(patch)) < addr + 0x10, "Stopped before new_game overwrite" patchDict = { 'StartAP': { addr: patch @@ -381,11 +384,22 @@ class RomPatcher: # timer escapeTimer = escapeAttr['Timer'] if escapeTimer is not None: - minute = int(escapeTimer / 60) - second = escapeTimer % 60 - minute = int(minute / 10) * 16 + minute % 10 - second = int(second / 10) * 16 + second % 10 - patchDict = {'Escape_Timer': {0x1E21:[second, minute]}} + patchDict = { 'Escape_Timer': {} } + timerPatch = patchDict["Escape_Timer"] + def getTimerBytes(t): + minute = int(t / 60) + second = t % 60 + minute = int(minute / 10) * 16 + minute % 10 + second = int(second / 10) * 16 + second % 10 + return [second, minute] + timerPatch[Addresses.getOne('escapeTimer')] = getTimerBytes(escapeTimer) + # timer table for Disabled Tourian escape + if 'TimerTable' in escapeAttr: + tableBytes = [] + timerPatch[Addresses.getOne('escapeTimerTable')] = tableBytes + for area in graphAreas[1:-1]: # no Ceres or Tourian + t = escapeAttr['TimerTable'][area] + tableBytes += getTimerBytes(t) self.applyIPSPatch('Escape_Timer', patchDict) # animals door to open if escapeAttr['Animals'] is not None: @@ -431,9 +445,9 @@ class RomPatcher: for locName, locIndex in locList: plmLocs[(k, locIndex)] = locName # make two patches out of this dict - plmTblAddr = 0x7E9A0 # moves downwards + plmTblAddr = Addresses.getOne('plmSpawnTable') # moves downwards plmPatchData = [] - roomTblAddr = 0x7EC00 # moves upwards + roomTblAddr = Addresses.getOne('plmSpawnRoomTable') # moves upwards roomPatchData = [] plmTblOffset = plmTblAddr def appendPlmBytes(bytez): @@ -463,7 +477,7 @@ class RomPatcher: addRoomPatchData(roomData) # write room table terminator addRoomPatchData([0x0] * 8) - assert plmTblOffset < roomTblAddr, "Spawn PLM table overlap" + assert plmTblOffset < roomTblAddr, "Spawn PLM table overlap. PLM table offset is 0x%x, Room table address is 0x%x" % (plmTblOffset,roomTblAddr) patchDict = { "PLM_Spawn_Tables" : { plmTblAddr: plmPatchData, @@ -479,7 +493,7 @@ class RomPatcher: random.seed(seed) seedInfo = random.randint(0, 0xFFFF) seedInfo2 = random.randint(0, 0xFFFF) - self.romFile.writeWord(seedInfo, 0x2FFF00) + self.romFile.writeWord(seedInfo, snes_to_pc(0xdfff00)) self.romFile.writeWord(seedInfo2) def writeMagic(self): @@ -487,7 +501,7 @@ class RomPatcher: self.race.writeMagic() def writeMajorsSplit(self, majorsSplit): - address = 0x17B6C + address = Addresses.getOne('majorsSplit') splits = { 'Chozo': 'Z', 'Major': 'M', @@ -528,7 +542,7 @@ class RomPatcher: totalNothing = sum(1 for il in itemLocs if il.Accessible and il.Item.Category == 'Nothing') totalEnergy = self.getItemQty(itemLocs, 'ETank')+self.getItemQty(itemLocs, 'Reserve') totalMajors = max(totalItemLocs - totalEnergy - totalAmmo - totalNothing, 0) - address = 0x2736C0 + address = snes_to_pc(0xceb6c0) value = "{:>2}".format(totalItemLocs) line = " ITEM LOCATIONS %s " % value self.writeCreditsStringBig(address, line, top=True) @@ -606,7 +620,7 @@ class RomPatcher: address += 0x40 # write ammo/energy pct - address = 0x273C40 + address = snes_to_pc(0xcebc40) (ammoPct, energyPct) = (int(self.getAmmoPct(dist)), int(100*totalEnergy/18)) line = " AVAILABLE AMMO {:>3}% ENERGY {:>3}%".format(ammoPct, energyPct) self.writeCreditsStringBig(address, line, top=True) @@ -650,7 +664,7 @@ class RomPatcher: return s isRace = self.race is not None - startCreditAddress = 0x2f5240 + startCreditAddress = snes_to_pc(0xded240) address = startCreditAddress if isRace: addr = address - 0x40 @@ -794,6 +808,12 @@ class RomPatcher: else: self.race.writeWordMagic(w) + def writeDoorTransition(self, roomPtr): + if self.race is None: + self.romFile.writeWord(roomPtr) + else: + self.race.writeDoorTransition(roomPtr) + # write area randomizer transitions to ROM # doorConnections : a list of connections. each connection is a dictionary describing # - where to write in the ROM : @@ -805,10 +825,10 @@ class RomPatcher: # property shall point to this custom ASM. # * if not, just write doorAsmPtr as the door property directly. def writeDoorConnections(self, doorConnections): - asmAddress = 0x7F800 + asmAddress = Addresses.getOne('customDoorsAsm') for conn in doorConnections: # write door ASM for transition doors (code and pointers) -# print('Writing door connection ' + conn['ID']) +# print('Writing door connection ' + conn['ID'] + ". doorPtr="+hex(doorPtr)) doorPtr = conn['DoorPtr'] roomPtr = conn['RoomPtr'] if doorPtr in self.doorConnectionSpecific: @@ -818,7 +838,7 @@ class RomPatcher: self.romFile.seek(0x10000 + doorPtr) # write room ptr - self.romFile.writeWord(roomPtr & 0xFFFF) + self.writeDoorTransition(roomPtr & 0xFFFF) # write bitflag (if area switch we have to set bit 0x40, and remove it if same area) self.romFile.writeByte(conn['bitFlag']) @@ -891,7 +911,7 @@ class RomPatcher: # change BG table to avoid scrolling sky bug when transitioning to west ocean def patchWestOcean(self, doorPtr): - self.romFile.writeWord(doorPtr, 0x7B7BB) + self.romFile.writeWord(doorPtr, snes_to_pc(0x8fb7bb)) # forces CRE graphics refresh when exiting kraid's or croc room def forceRoomCRE(self, roomPtr, creFlag=0x2): @@ -934,7 +954,7 @@ class RomPatcher: self.romFile.writeByte(RomPatcher.buttons[button][1]) def writePlandoAddresses(self, locations): - self.romFile.seek(0x2F6000) + self.romFile.seek(Addresses.getOne('plandoAddresses')) for loc in locations: self.romFile.writeWord(loc.Address & 0xFFFF) @@ -944,7 +964,7 @@ class RomPatcher: self.romFile.writeWord(0xFFFF) def writePlandoTransitions(self, transitions, doorsPtrs, maxTransitions): - self.romFile.seek(0x2F6100) + self.romFile.seek(Addresses.getOne('plandoTransitions')) for (src, dest) in transitions: self.romFile.writeWord(doorsPtrs[src]) @@ -957,7 +977,14 @@ class RomPatcher: def enableMoonWalk(self): # replace STZ with STA since A is non-zero at this point - self.romFile.writeByte(0x8D, 0xB35D) + self.romFile.writeByte(0x8D, Addresses.getOne('moonwalk')) + + def writeAdditionalETanks(self, additionalETanks): + self.romFile.writeByte(additionalETanks, Addresses.getOne("additionalETanks")) + + def writeHellrunRate(self, hellrunRatePct): + hellrunRateVal = min(int(0x40*float(hellrunRatePct)/100.0), 0xff) + self.romFile.writeByte(hellrunRateVal, Addresses.getOne("hellrunRate")) def setOamTile(self, nth, middle, newTile, y=0xFC): # an oam entry is made of five bytes: (s000000 xxxxxxxxx) (yyyyyyyy) (YXpp000t tttttttt) @@ -976,8 +1003,8 @@ class RomPatcher: # max 32 chars # new oamlist address in free space at the end of bank 8C - self.romFile.writeWord(0xF3E9, 0x5a0e3) - self.romFile.writeWord(0xF3E9, 0x5a0e9) + self.romFile.writeWord(0xF3E9, snes_to_pc(0x8ba0e3)) + self.romFile.writeWord(0xF3E9, snes_to_pc(0x8ba0e9)) # string length versionLength = len(version) @@ -986,7 +1013,7 @@ class RomPatcher: length = versionLength + rotationLength else: length = versionLength - self.romFile.writeWord(length, 0x0673e9) + self.romFile.writeWord(length, snes_to_pc(0x8cf3e9)) versionMiddle = int(versionLength / 2) + versionLength % 2 # oams @@ -998,8 +1025,64 @@ class RomPatcher: for (i, char) in enumerate('rotation'): self.setOamTile(i, rotationMiddle, char2tile[char], y=0x8e) - def writeDoorsColor(self, doors, player): - DoorsManager.writeDoorsColor(self.romFile, doors, player) + def writeDoorsColor(self, doorsStart, player): + if self.race is None: + DoorsManager.writeDoorsColor(self.romFile, doorsStart, player, self.romFile.writeWord) + else: + DoorsManager.writeDoorsColor(self.romFile, doorsStart, player, self.writePlmWord) + + def writeDoorIndicators(self, plms, area, door): + indicatorFlags = IndicatorFlag.Standard | (IndicatorFlag.AreaRando if area else 0) | (IndicatorFlag.DoorRando if door else 0) + patchDict = self.patchAccess.getDictPatches() + additionalPLMs = self.patchAccess.getAdditionalPLMs() + def updateIndicatorPLM(door, doorType): + nonlocal additionalPLMs, patchDict + plmName = 'Indicator[%s]' % door + addPlm = False + if plmName in patchDict: + for addr,bytez in patchDict[plmName].items(): + plmBytes = bytez + break + else: + plmBytes = additionalPLMs[plmName]['plm_bytes_list'][0] + addPlm = True + w = getWord(doorType) + plmBytes[0] = w[0] + plmBytes[1] = w[1] + return plmName, addPlm + indicatorPLMs = DoorsManager.getIndicatorPLMs(self.player, indicatorFlags) + for doorName,plmType in indicatorPLMs.items(): + plmName,addPlm = updateIndicatorPLM(doorName, plmType) + if addPlm: + plms.append(plmName) + else: + self.applyIPSPatch(plmName) + + def writeObjectives(self, itemLocs, tourian): + objectives = Objectives.objDict[self.player] + objectives.writeGoals(self.romFile) + objectives.writeIntroObjectives(self.romFile, tourian) + self.writeItemsMasks(itemLocs) + # hack bomb_torizo.ips to wake BT in all cases if necessary, ie chozo bots objective is on, and nothing at bombs + if objectives.isGoalActive("activate chozo robots") and RomPatches.has(RomPatches.BombTorizoWake): + bomb = next((il for il in itemLocs if il.Location.Name == "Bomb"), None) + if bomb is not None and bomb.Item.Category == "Nothing": + for addrName in ["BTtweaksHack1", "BTtweaksHack2"]: + self.romFile.seek(Addresses.getOne(addrName)) + for b in [0xA9,0x00,0x00]: # LDA #$0000 ; set zero flag to wake BT + self.romFile.writeByte(b) + + def writeItemsMasks(self, itemLocs): + # write items/beams masks for "collect all major" objective + itemsMask = 0 + beamsMask = 0 + for il in itemLocs: + if not il.Location.restricted: + item = il.Item + itemsMask |= item.ItemBits + beamsMask |= item.BeamBits + self.romFile.writeWord(itemsMask, Addresses.getOne('itemsMask')) + self.romFile.writeWord(beamsMask, Addresses.getOne('beamsMask')) # tile number in tileset char2tile = { @@ -1025,7 +1108,7 @@ class MessageBox(object): # add 0x0c/0x06 to offsets as there's 12/6 bytes before the strings, string length is either 0x13/0x1a self.offsets = { - 'ETank': (0x2877f+0x0c, 0x13), + 'ETank': (snes_to_pc(0x85877f)+0x0c, 0x13), 'Missile': (0x287bf+0x06, 0x1a), 'Super': (0x288bf+0x06, 0x1a), 'PowerBomb': (0x289bf+0x06, 0x1a), @@ -1088,3 +1171,266 @@ class MessageBox(object): def updateAttr(self, byte, address): self.rom.writeByte(byte, address) + +class RomTypeForMusic(IntFlag): + VariaSeed = 1 + AreaSeed = 2 + BossSeed = 4 + +class MusicPatcher(object): + # rom: ROM object to patch + # romType: 0 if not varia seed, or bitwise or of RomTypeForMusic enum + # baseDir: directory containing all music data/descriptors/constraints + # constraintsFile: file to constraints JSON descriptor, relative to baseDir/constraints. + # if None, will be determined automatically from romType + def __init__(self, rom, romType, + baseDir=os.path.join(appDir, 'varia_custom_sprites', 'music'), + constraintsFile=None): + self.rom = rom + self.baseDir = baseDir + variaSeed = bool(romType & RomTypeForMusic.VariaSeed) + self.area = variaSeed and bool(romType & RomTypeForMusic.AreaSeed) + self.boss = variaSeed and bool(romType & RomTypeForMusic.BossSeed) + metaDir = os.path.join(baseDir, "_metadata") + constraintsDir = os.path.join(baseDir, "_constraints") + if constraintsFile is None: + constraintsFile = 'varia.json' if variaSeed else 'vanilla.json' + with open(os.path.join(constraintsDir, constraintsFile), 'r') as f: + self.constraints = json.load(f) + nspcInfoPath = os.path.join(baseDir, "nspc_metadata.json") + with open(nspcInfoPath, "r") as f: + nspcInfo = json.load(f) + self.nspcInfo = {} + for nspc,info in nspcInfo.items(): + self.nspcInfo[self._nspc_path(nspc)] = info + self.allTracks = {} + self.vanillaTracks = None + for metaFile in os.listdir(metaDir): + metaPath = os.path.join(metaDir, metaFile) + if not metaPath.endswith(".json"): + continue + with open(metaPath, 'r') as f: + meta = json.load(f) + # will silently overwrite entries with same name, so avoid + # conflicting descriptor files ... + self.allTracks.update(meta) + if metaFile == "vanilla.json": + self.vanillaTracks = meta + assert self.vanillaTracks is not None, "MusicPatcher: missing vanilla JSON descriptor" + self.replaceableTracks = [track for track in self.vanillaTracks if track not in self.constraints['preserve'] and track not in self.constraints['discard']] + self.musicDataTableAddress = snes_to_pc(0x8FE7E4) + self.musicDataTableMaxSize = 45 # to avoid overwriting useful data in bank 8F + + # tracks: dict with track name to replace as key, and replacing track name as value + # updateReferences: change room state headers and special tracks. may be False if you're patching a rom hack or something + # output: if not None, dump a JSON file with what was done + # replaced tracks must be in + # replaceableTracks, and new tracks must be in allTracks + # tracks not in the dict will be kept vanilla + # raise RuntimeError if not possible + def replace(self, tracks, updateReferences=True, output=None): + for track in tracks: + if track not in self.replaceableTracks: + raise RuntimeError("Cannot replace track %s" % track) + trackList = self._getTrackList(tracks) + replacedVanilla = [t for t in self.replaceableTracks if t in trackList and t not in tracks] + for van in replacedVanilla: + tracks[van] = van +# print("trackList="+str(trackList)) + musicData = self._getMusicData(trackList) +# print("musicData="+str(musicData)) + if len(musicData) > self.musicDataTableMaxSize: + raise RuntimeError("Music data table too long. %d entries, max is %d" % (len(musicData, self.musicDataTableMaxSize))) + musicDataAddresses = self._getMusicDataAddresses(musicData) + self._writeMusicData(musicDataAddresses) + self._writeMusicDataTable(musicData, musicDataAddresses) + if updateReferences == True: + self._updateReferences(trackList, musicData, tracks) + if output is not None: + self._dump(output, trackList, musicData, musicDataAddresses) + + # compose a track list from vanilla tracks, replaced tracks, and constraints + def _getTrackList(self, replacedTracks): + trackList = set() + for track in self.vanillaTracks: + if track in replacedTracks: + trackList.add(replacedTracks[track]) + elif track not in self.constraints['discard']: + trackList.add(track) + return list(trackList) + + def _nspc_path(self, nspc_path): + return os.path.join(self.baseDir, nspc_path) + + # get list of music data files to include in the ROM + # can contain empty entries, marked with a None, to account + # for fixed place data ('preserve' constraint) + def _getMusicData(self, trackList): + # first, make musicData the minimum size wrt preserved tracks + preservedTracks = {trackName:self.vanillaTracks[trackName] for trackName in self.constraints['preserve']} + preservedDataIndexes = [track['data_index'] for trackName,track in preservedTracks.items()] + musicData = [None]*(max(preservedDataIndexes)+1) + # fill preserved spots + for track in self.constraints['preserve']: + idx = self.vanillaTracks[track]['data_index'] + nspc = self._nspc_path(self.vanillaTracks[track]['nspc_path']) + if nspc not in musicData: + musicData[idx] = nspc +# print("stored " + nspc + " at "+ str(idx)) + # then fill data in remaining spots + idx = 0 + for track in trackList: + previdx = idx + if track not in self.constraints['preserve']: + nspc = self._nspc_path(self.allTracks[track]['nspc_path']) + if nspc not in musicData: + for i in range(idx, len(musicData)): +# print("at " + str(i) + ": "+str(musicData[i])) + if musicData[i] is None: + musicData[i] = nspc + idx = i+1 + break + if idx == previdx: + idx += 1 + musicData.append(nspc) +# print("stored " + nspc + " at "+ str(idx)) + return musicData + + # get addresses to store each data file to. raise RuntimeError if not possible + # pretty dumb algorithm for now, just store data wherever possible, + # prioritizing first areas in usableSpace + # store data from end of usable space to make room for other data (for hacks for instance) + def _getMusicDataAddresses(self, musicData): + usableSpace = self.constraints['usable_space_ranges_pc'] + musicDataAddresses = {} + for dataFile in musicData: + if dataFile is None: + continue + sz = os.path.getsize(dataFile) + blocks = self.nspcInfo[dataFile]['block_headers_offsets'] + for r in usableSpace: + # find a suitable address so header words are not split across banks (header is 2 words) + addr = r['end'] - sz + def isCrossBank(off): + nonlocal addr + endBankOffset = pc_to_snes(addr+off+4) & 0x7fff + return endBankOffset == 1 or endBankOffset == 3 + while addr >= r['start'] and any(isCrossBank(off) for off in blocks): + addr -= 1 + if addr >= r['start']: + musicDataAddresses[dataFile] = addr + r['end'] = addr + break + if dataFile not in musicDataAddresses: + raise RuntimeError("Cannot find enough space to store music data file "+dataFile) + return musicDataAddresses + + def _writeMusicData(self, musicDataAddresses): + for dataFile, addr in musicDataAddresses.items(): + self.rom.seek(addr) + with open(dataFile, 'rb') as f: + self.rom.write(f.read()) + + def _writeMusicDataTable(self, musicData, musicDataAddresses): + self.rom.seek(self.musicDataTableAddress) + for dataFile in musicData: + addr = pc_to_snes(musicDataAddresses[dataFile]) if dataFile in musicDataAddresses else 0 + self.rom.writeLong(addr) + + def _getDataId(self, musicData, track): + return (musicData.index(self._nspc_path(self.allTracks[track]['nspc_path']))+1)*3 + + def _getTrackId(self, track): + return self.allTracks[track]['track_index'] + 5 + + def _updateReferences(self, trackList, musicData, replacedTracks): + trackAddresses = {} + def addAddresses(track, vanillaTrackData, prio=False): + nonlocal trackAddresses + addrs = [] + prioAddrs = [] + if 'pc_addresses' in vanillaTrackData: + addrs += vanillaTrackData['pc_addresses'] + if self.area and 'pc_addresses_area' in vanillaTrackData: + prioAddrs += vanillaTrackData['pc_addresses_area'] + if self.boss and 'pc_addresses_boss' in vanillaTrackData: + prioAddrs += vanillaTrackData['pc_addresses_boss'] + if track not in trackAddresses: + trackAddresses[track] = [] + # if prioAddrs are somewhere else, remove if necessary + prioSet = set(prioAddrs) + for t,tAddrs in trackAddresses.items(): + trackAddresses[t] = list(set(tAddrs) - prioSet) + # if some of addrs are somewhere else, remove them from here + for t,tAddrs in trackAddresses.items(): + addrs = list(set(addrs) - set(tAddrs)) + trackAddresses[track] += prioAddrs + addrs + for track in trackList: + if track in replacedTracks.values(): + for van,rep in replacedTracks.items(): + if rep == track: + addAddresses(track, self.vanillaTracks[van]) + else: + addAddresses(track, self.vanillaTracks[track]) + for track in trackList: + dataId = self._getDataId(musicData, track) + trackId = self._getTrackId(track) + for addr in trackAddresses[track]: + self.rom.seek(addr) + self.rom.writeByte(dataId) + self.rom.writeByte(trackId) + self._writeSpecialReferences(replacedTracks, musicData) + + # write special (boss) data + def _writeSpecialReferences(self, replacedTracks, musicData, static=True, dynamic=True): + for track,replacement in replacedTracks.items(): + # static patches are needed only when replacing tracks + if track != replacement: + staticPatches = self.vanillaTracks[track].get("static_patches", None) + else: + staticPatches = None + # dynamic patches are similar to pc_addresses*, and must be written also + # when track is vanilla, as music data table is changed + dynamicPatches = self.vanillaTracks[track].get("dynamic_patches", None) + if static and staticPatches: + for addr,bytez in staticPatches.items(): + self.rom.seek(int(addr)) + for b in bytez: + self.rom.writeByte(b) + if dynamic and dynamicPatches: + dataId = self._getDataId(musicData, replacement) + trackId = self._getTrackId(replacement) + dataIdAddrs = dynamicPatches.get("data_id", []) + trackIdAddrs = dynamicPatches.get("track_id", []) + for addr in dataIdAddrs: + self.rom.writeByte(dataId, addr) + for addr in trackIdAddrs: + self.rom.writeByte(trackId, addr) + + def _dump(self, output, trackList, musicData, musicDataAddresses): + music={} + no=0 + for md in musicData: + if md is None: + music["NoData_%d" % no] = None + no += 1 + else: + tracks = [] + h,t=os.path.split(md) + md=os.path.join(os.path.split(h)[1], t) + for track,trackData in self.allTracks.items(): + if trackData['nspc_path'] == md: + tracks.append(track) + music[md] = tracks + musicSnesAddresses = {} + for nspc, addr in musicDataAddresses.items(): + h,t=os.path.split(nspc) + nspc=os.path.join(os.path.split(h)[1], t) + musicSnesAddresses[nspc] = "$%06x" % pc_to_snes(addr) + dump = { + "track_list": sorted(trackList), + "music_data": music, + "music_data_addresses": musicSnesAddresses + } + with open(output, 'w') as f: + json.dump(dump, f, indent=4) \ No newline at end of file diff --git a/worlds/sm/variaRandomizer/solver.py b/worlds/sm/variaRandomizer/solver.py deleted file mode 100644 index 8873814679..0000000000 --- a/worlds/sm/variaRandomizer/solver.py +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/python3 -import sys, argparse - -from solver.interactiveSolver import InteractiveSolver -from solver.standardSolver import StandardSolver -from solver.conf import Conf -import utils.log - -def interactiveSolver(args): - # to init, requires interactive/romFileName/presetFileName/output parameters in standard/plando mode - # to init, requires interactive/presetFileName/output parameters in seedless mode - # to iterate, requires interactive/state/[loc]/[item]/action/output parameters in item scope - # to iterate, requires interactive/state/[startPoint]/[endPoint]/action/output parameters in area scope - if args.action == 'init': - # init - if args.mode != 'seedless' and args.romFileName == None: - print("Missing romFileName parameter for {} mode".format(args.mode)) - sys.exit(1) - - if args.presetFileName == None or args.output == None: - print("Missing preset or output parameter") - sys.exit(1) - - solver = InteractiveSolver(args.output, args.logic) - solver.initialize(args.mode, args.romFileName, args.presetFileName, magic=args.raceMagic, fill=args.fill, startLocation=args.startLocation) - else: - # iterate - params = {} - if args.scope == 'common': - if args.action == "save": - params["lock"] = args.lock - params["escapeTimer"] = args.escapeTimer - elif args.action == "randomize": - params["minorQty"] = args.minorQty - params["energyQty"] = args.energyQty - params["forbiddenItems"] = args.forbiddenItems.split(',') if args.forbiddenItems is not None else [] - elif args.scope == 'item': - if args.state == None or args.action == None or args.output == None: - print("Missing state/action/output parameter") - sys.exit(1) - if args.action in ["add", "replace"]: - if args.mode not in ['seedless', 'race', 'debug'] and args.loc == None: - print("Missing loc parameter when using action add for item") - sys.exit(1) - if args.mode == 'plando': - if args.item == None: - print("Missing item parameter when using action add in plando/suitless mode") - sys.exit(1) - params = {'loc': args.loc, 'item': args.item, 'hide': args.hide} - elif args.action == "remove": - if args.loc != None: - params = {'loc': args.loc} - elif args.item != None: - params = {'item': args.item} - else: - params = {'count': args.count} - elif args.action == "toggle": - params = {'item': args.item} - elif args.scope == 'area': - if args.state == None or args.action == None or args.output == None: - print("Missing state/action/output parameter") - sys.exit(1) - if args.action == "add": - if args.startPoint == None or args.endPoint == None: - print("Missing start or end point parameter when using action add for item") - sys.exit(1) - params = {'startPoint': args.startPoint, 'endPoint': args.endPoint} - if args.action == "remove" and args.startPoint != None: - params = {'startPoint': args.startPoint} - elif args.scope == 'door': - if args.state == None or args.action == None or args.output == None: - print("Missing state/action/output parameter") - sys.exit(1) - if args.action == "replace": - if args.doorName is None or args.newColor is None: - print("Missing doorName or newColor parameter when using action replace for door") - sys.exit(1) - params = {'doorName': args.doorName, 'newColor': args.newColor} - elif args.action == "toggle": - if args.doorName is None: - print("Missing doorName parameter when using action toggle for door") - sys.exit(1) - params = {'doorName': args.doorName} - elif args.scope == 'dump': - if args.action == "import": - if args.dump is None: - print("Missing dump parameter when import a dump") - params = {'dump': args.dump} - params["debug"] = args.mode == 'debug' - - solver = InteractiveSolver(args.output, args.logic) - solver.iterate(args.state, args.scope, args.action, params) - -def standardSolver(args): - if args.romFileName is None: - print("Parameter --romFileName mandatory when not in interactive mode") - sys.exit(1) - - if args.difficultyTarget is None: - difficultyTarget = Conf.difficultyTarget - else: - difficultyTarget = args.difficultyTarget - - if args.pickupStrategy is None: - pickupStrategy = Conf.itemsPickup - else: - pickupStrategy = args.pickupStrategy - - # itemsForbidden is like that: [['Varia'], ['Reserve'], ['Gravity']], fix it - args.itemsForbidden = [item[0] for item in args.itemsForbidden] - - solver = StandardSolver(args.romFileName, args.presetFileName, difficultyTarget, - pickupStrategy, args.itemsForbidden, type=args.type, - firstItemsLog=args.firstItemsLog, extStatsFilename=args.extStatsFilename, - extStatsStep=args.extStatsStep, - displayGeneratedPath=args.displayGeneratedPath, - outputFileName=args.output, magic=args.raceMagic, - checkDuplicateMajor=args.checkDuplicateMajor, vcr=args.vcr, - runtimeLimit_s=args.runtimeLimit_s) - - solver.solveRom() - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Random Metroid Solver") - parser.add_argument('--romFileName', '-r', help="the input rom", nargs='?', - default=None, dest="romFileName") - parser.add_argument('--preset', '-p', help="the preset file", nargs='?', - default=None, dest='presetFileName') - parser.add_argument('--difficultyTarget', '-t', - help="the difficulty target that the solver will aim for", - dest='difficultyTarget', nargs='?', default=None, type=int) - parser.add_argument('--pickupStrategy', '-s', help="Pickup strategy for the Solver", - dest='pickupStrategy', nargs='?', default=None, - choices=['all', 'any']) - parser.add_argument('--itemsForbidden', '-f', help="Item not picked up during solving", - dest='itemsForbidden', nargs='+', default=[], action='append') - - parser.add_argument('--type', '-y', help="web or console", dest='type', nargs='?', - default='console', choices=['web', 'console']) - parser.add_argument('--checkDuplicateMajor', dest="checkDuplicateMajor", action='store_true', - help="print a warning if the same major is collected more than once") - parser.add_argument('--debug', '-d', help="activate debug logging", dest='debug', action='store_true') - parser.add_argument('--firstItemsLog', '-1', - help="path to file where for each item type the first time it was found and where will be written (spoilers!)", - nargs='?', default=None, type=str, dest='firstItemsLog') - parser.add_argument('--ext_stats', help="Generate extended stats", - nargs='?', default=None, dest='extStatsFilename') - parser.add_argument('--ext_stats_step', help="what extended stats to generate", - nargs='?', default=None, dest='extStatsStep', type=int) - parser.add_argument('--displayGeneratedPath', '-g', help="display the generated path (spoilers!)", - dest='displayGeneratedPath', action='store_true') - parser.add_argument('--race', help="Race mode magic number", dest='raceMagic', type=int) - parser.add_argument('--vcr', help="Generate VCR output file", dest='vcr', action='store_true') - # standard/interactive, web site - parser.add_argument('--output', '-o', help="When called from the website, contains the result of the solver", - dest='output', nargs='?', default=None) - # interactive, web site - parser.add_argument('--interactive', '-i', help="Activate interactive mode for the solver", - dest='interactive', action='store_true') - parser.add_argument('--state', help="JSON file of the Solver state (used in interactive mode)", - dest="state", nargs='?', default=None) - parser.add_argument('--loc', help="Name of the location to action on (used in interactive mode)", - dest="loc", nargs='?', default=None) - parser.add_argument('--action', help="Pickup item at location, remove last pickedup location, clear all (used in interactive mode)", - dest="action", nargs="?", default=None, choices=['init', 'add', 'remove', 'clear', 'get', 'save', 'replace', 'randomize', 'toggle', 'import']) - parser.add_argument('--item', help="Name of the item to place in plando mode (used in interactive mode)", - dest="item", nargs='?', default=None) - parser.add_argument('--hide', help="Hide the item to place in plando mode (used in interactive mode)", - dest="hide", action='store_true') - parser.add_argument('--startPoint', help="The start AP to connect (used in interactive mode)", - dest="startPoint", nargs='?', default=None) - parser.add_argument('--endPoint', help="The destination AP to connect (used in interactive mode)", - dest="endPoint", nargs='?', default=None) - - parser.add_argument('--mode', help="Solver mode: standard/seedless/plando (used in interactive mode)", - dest="mode", nargs="?", default=None, choices=['standard', 'seedless', 'plando', 'race', 'debug']) - parser.add_argument('--scope', help="Scope for the action: common/area/item (used in interactive mode)", - dest="scope", nargs="?", default=None, choices=['common', 'area', 'item', 'door', 'dump']) - parser.add_argument('--count', help="Number of item rollback (used in interactive mode)", - dest="count", type=int) - parser.add_argument('--lock', help="lock the plando seed (used in interactive mode)", - dest="lock", action='store_true') - parser.add_argument('--escapeTimer', help="escape timer like 03:00", dest="escapeTimer", default=None) - parser.add_argument('--fill', help="in plando load all the source seed locations/transitions as a base (used in interactive mode)", - dest="fill", action='store_true') - parser.add_argument('--startLocation', help="in plando/seedless: the start location", dest="startLocation", default="Landing Site") - parser.add_argument('--minorQty', help="rando plando (used in interactive mode)", - dest="minorQty", nargs="?", default=None, choices=[str(i) for i in range(0,101)]) - parser.add_argument('--energyQty', help="rando plando (used in interactive mode)", - dest="energyQty", nargs="?", default=None, choices=["sparse", "medium", "vanilla"]) - parser.add_argument('--forbiddenItems', help="rando plando (used in interactive mode)", - dest="forbiddenItems", nargs="?", default=None) - parser.add_argument('--doorName', help="door to replace (used in interactive mode)", - dest="doorName", nargs="?", default=None) - parser.add_argument('--newColor', help="new color for door (used in interactive mode)", - dest="newColor", nargs="?", default=None) - parser.add_argument('--logic', help='logic to use (used in interactive mode)', dest='logic', nargs='?', default="vanilla", choices=["vanilla", "rotation"]) - parser.add_argument('--runtime', - help="Maximum runtime limit in seconds. If 0 or negative, no runtime limit.", - dest='runtimeLimit_s', nargs='?', default=0, type=int) - parser.add_argument('--dump', help="dump file with autotracker state (used in interactive mode)", - dest="dump", nargs="?", default=None) - - args = parser.parse_args() - - if args.presetFileName is None: - args.presetFileName = 'worlds/sm/variaRandomizer/standard_presets/regular.json' - - if args.raceMagic != None: - if args.raceMagic <= 0 or args.raceMagic >= 0x10000: - print("Invalid magic") - sys.exit(-1) - - if args.count != None: - if args.count < 1 or args.count > 0x80: - print("Invalid count") - sys.exit(-1) - - utils.log.init(args.debug) - - if args.interactive == True: - interactiveSolver(args) - else: - standardSolver(args) diff --git a/worlds/sm/variaRandomizer/standard_presets/Torneio_SGPT3.json b/worlds/sm/variaRandomizer/standard_presets/Torneio_SGPT3.json new file mode 100644 index 0000000000..7e14143969 --- /dev/null +++ b/worlds/sm/variaRandomizer/standard_presets/Torneio_SGPT3.json @@ -0,0 +1 @@ +{"Knows": {"WallJump": [true, 1], "ShineSpark": [true, 1], "MidAirMorph": [true, 1], "CrouchJump": [true, 1], "UnequipItem": [true, 1], "Mockball": [true, 1], "SimpleShortCharge": [true, 1], "InfiniteBombJump": [true, 1], "GreenGateGlitch": [true, 1], "ShortCharge": [false, 0], "GravityJump": [true, 1], "SpringBallJump": [true, 10], "SpringBallJumpFromWall": [true, 10], "GetAroundWallJump": [true, 1], "DraygonGrappleKill": [true, 1], "DraygonSparkKill": [false, 0], "MicrowaveDraygon": [true, 1], "MicrowavePhantoon": [true, 1], "LowAmmoCroc": [false, 0], "LowStuffBotwoon": [false, 0], "LowStuffGT": [false, 0], "IceZebSkip": [false, 0], "SpeedZebSkip": [false, 0], "HiJumpMamaTurtle": [false, 0], "MaridiaWallJumps": [false, 0], "MtEverestGravJump": [false, 0], "GravLessLevel1": [true, 10], "GravLessLevel2": [false, 0], "GravLessLevel3": [false, 0], "CeilingDBoost": [true, 1], "BillyMays": [true, 1], "AlcatrazEscape": [true, 1], "ReverseGateGlitch": [true, 1], "ReverseGateGlitchHiJumpLess": [true, 1], "EarlyKraid": [true, 1], "XrayDboost": [false, 0], "XrayIce": [false, 0], "RedTowerClimb": [true, 1], "RonPopeilScrew": [false, 0], "OldMBWithSpeed": [false, 0], "Moondance": [false, 0], "HiJumpLessGauntletAccess": [true, 1], "HiJumpGauntletAccess": [true, 1], "LowGauntlet": [false, 0], "IceEscape": [false, 0], "WallJumpCathedralExit": [true, 1], "BubbleMountainWallJump": [true, 1], "DoubleChamberWallJump": [false, 0], "NovaBoost": [false, 0], "NorfairReserveDBoost": [false, 0], "CrocPBsDBoost": [false, 0], "CrocPBsIce": [false, 0], "IceMissileFromCroc": [false, 0], "FrogSpeedwayWithoutSpeed": [false, 0], "LavaDive": [true, 1], "LavaDiveNoHiJump": [false, 0], "WorstRoomIceCharge": [false, 0], "WorstRoomWallJump": [false, 0], "ScrewAttackExit": [false, 0], "ScrewAttackExitWithoutScrew": [false, 0], "FirefleasWalljump": [false, 0], "DodgeLowerNorfairEnemies": [false, 0], "ContinuousWallJump": [true, 10], "DiagonalBombJump": [false, 0], "MockballWs": [true, 10], "SpongeBathBombJump": [false, 0], "SpongeBathHiJump": [true, 1], "SpongeBathSpeed": [true, 1], "TediousMountEverest": [false, 0], "DoubleSpringBallJump": [false, 0], "BotwoonToDraygonWithIce": [false, 0], "WestSandHoleSuitlessWallJumps": [false, 0], "DraygonRoomGrappleExit": [false, 0], "DraygonRoomCrystalFlash": [false, 0], "PreciousRoomXRayExit": [false, 0], "PreciousRoomGravJumpExit": [false, 0], "MochtroidClip": [true, 1], "PuyoClip": [false, 0], "PuyoClipXRay": [false, 0], "SnailClip": [false, 0], "SuitlessPuyoClip": [false, 0], "CrystalFlashClip": [false, 0], "SuitlessCrystalFlashClip": [false, 0], "KillPlasmaPiratesWithSpark": [false, 0], "KillPlasmaPiratesWithCharge": [true, 10], "AccessSpringBallWithHiJump": [true, 1], "AccessSpringBallWithSpringBallBombJumps": [false, 0], "AccessSpringBallWithBombJumps": [false, 0], "AccessSpringBallWithSpringBallJump": [false, 0], "AccessSpringBallWithXRayClimb": [false, 0], "AccessSpringBallWithGravJump": [false, 0], "AccessSpringBallWithFlatley": [false, 0]}, "Settings": {"Ice": "Gimme energy", "MainUpperNorfair": "Gimme energy", "LowerNorfair": "Default", "Kraid": "Default", "Phantoon": "Default", "Draygon": "Default", "Ridley": "Default", "MotherBrain": "Default", "X-Ray": "I don't like spikes", "Gauntlet": "Default"}, "Controller": {"A": "Jump", "B": "Dash", "X": "Shoot", "Y": "Item Cancel", "L": "Angle Down", "R": "Angle Up", "Select": "Item Select", "Moonwalk": true}, "password": "3d7e88af5a2f11e552eb2f7dc747cb1cbc810649086020eef598913c38a0a08b", "score": 221} \ No newline at end of file diff --git a/worlds/sm/variaRandomizer/utils/doorsmanager.py b/worlds/sm/variaRandomizer/utils/doorsmanager.py index 0b2a414993..6a8ecda1cb 100644 --- a/worlds/sm/variaRandomizer/utils/doorsmanager.py +++ b/worlds/sm/variaRandomizer/utils/doorsmanager.py @@ -1,4 +1,5 @@ import random +from enum import IntEnum,IntFlag import copy from ..logic.smbool import SMBool from ..rom.rom_patches import RomPatches @@ -11,7 +12,7 @@ colorsList = ['red', 'green', 'yellow', 'wave', 'spazer', 'plasma', 'ice'] # 1/15 chance to have the door set to grey colorsListGrey = colorsList * 2 + ['grey'] -class Facing: +class Facing(IntEnum): Left = 0 Right = 1 Top = 2 @@ -38,9 +39,48 @@ colors2plm = { 'ice': plmIce } +# door color indicators PLMs (flashing on the other side of colored doors) +indicatorsDirection = { + Facing.Left: Facing.Right, + Facing.Right: Facing.Left, + Facing.Top: Facing.Bottom, + Facing.Bottom: Facing.Top +} + +# door facing left - right - top - bottom +plmRedIndicator = [0xFBB0, 0xFBB6, 0xFBBC, 0xFBC2] +plmGreenIndicator = [0xFBC8, 0xFBCE, 0xFBD4, 0xFBDA] +plmYellowIndicator = [0xFBE0, 0xFBE6, 0xFBEC, 0xFBF2] +plmGreyIndicator = [0xFBF8, 0xFBFE, 0xFC04, 0xFC0A] +plmWaveIndicator = [0xF60B, 0xF611, 0xF617, 0xF61D] +plmSpazerIndicator = [0xF63B, 0xF641, 0xF647, 0xF64D] +plmPlasmaIndicator = [0xF623, 0xF629, 0xF62F, 0xF635] +plmIceIndicator = [0xF653, 0xF659, 0xF65F, 0xF665] + +colors2plmIndicator = { + 'red': plmRedIndicator, + 'green': plmGreenIndicator, + 'yellow': plmYellowIndicator, + 'grey': plmGreyIndicator, + 'wave': plmWaveIndicator, + 'spazer': plmSpazerIndicator, + 'plasma': plmPlasmaIndicator, + 'ice': plmIceIndicator +} + +class IndicatorFlag(IntFlag): + Standard = 1 + AreaRando = 2 + DoorRando = 4 + +# indicator always there +IndicatorAll = IndicatorFlag.Standard | IndicatorFlag.AreaRando | IndicatorFlag.DoorRando +# indicator there when not in area rando +IndicatorDoor = IndicatorFlag.Standard | IndicatorFlag.DoorRando + class Door(object): - __slots__ = ('name', 'address', 'vanillaColor', 'color', 'forced', 'facing', 'hidden', 'id', 'canGrey', 'forbiddenColors') - def __init__(self, name, address, vanillaColor, facing, id=None, canGrey=False, forbiddenColors=None): + __slots__ = ('name', 'address', 'vanillaColor', 'color', 'forced', 'facing', 'hidden', 'id', 'canGrey', 'forbiddenColors','indicator') + def __init__(self, name, address, vanillaColor, facing, id=None, canGrey=False, forbiddenColors=None,indicator=0): self.name = name self.address = address self.vanillaColor = vanillaColor @@ -52,6 +92,7 @@ class Door(object): self.id = id # list of forbidden colors self.forbiddenColors = forbiddenColors + self.indicator = indicator def forceBlue(self): # custom start location, area, patches can force doors to blue @@ -115,21 +156,21 @@ class Door(object): def isRefillSave(self): return self.address is None - def writeColor(self, rom): + def writeColor(self, rom, writeWordFunc): if self.isBlue() or self.isRefillSave(): return - rom.writeWord(colors2plm[self.color][self.facing], self.address) + writeWordFunc(colors2plm[self.color][self.facing], self.address) # also set plm args high byte to never opened, even during escape if self.color == 'grey': rom.writeByte(0x90, self.address+5) - def readColor(self, rom): + def readColor(self, rom, readWordFunc): if self.forced or self.isRefillSave(): return - plm = rom.readWord(self.address) + plm = readWordFunc(self.address) if plm in plmRed: self.setColor('red') elif plm in plmGreen: @@ -147,7 +188,15 @@ class Door(object): elif plm in plmIce: self.setColor('ice') else: - raise Exception("Unknown color {} for {}".format(hex(plm), self.name)) + # we can't read the color, handle as grey door (can happen in race protected seeds) + self.setColor('grey') + + # gives the PLM ID for matching indicator door + def getIndicatorPLM(self, indicatorFlags): + ret = None + if (indicatorFlags & self.indicator) != 0 and self.color in colors2plmIndicator: + ret = colors2plmIndicator[self.color][indicatorsDirection[self.facing]] + return ret # for tracker def canHide(self): @@ -179,10 +228,10 @@ class DoorsManager(): doorsDict = {} doors = { # crateria - 'LandingSiteRight': Door('LandingSiteRight', 0x78018, 'green', Facing.Left, canGrey=True), + 'LandingSiteRight': Door('LandingSiteRight', 0x78018, 'green', Facing.Left, canGrey=True, indicator=IndicatorAll), 'LandingSiteTopRight': Door('LandingSiteTopRight', 0x07801e, 'yellow', Facing.Left), - 'KihunterBottom': Door('KihunterBottom', 0x78228, 'yellow', Facing.Top, canGrey=True), - 'KihunterRight': Door('KihunterRight', 0x78222, 'yellow', Facing.Left, canGrey=True), + 'KihunterBottom': Door('KihunterBottom', 0x78228, 'yellow', Facing.Top, canGrey=True, indicator=IndicatorDoor), + 'KihunterRight': Door('KihunterRight', 0x78222, 'yellow', Facing.Left, canGrey=True, indicator=IndicatorAll), 'FlywayRight': Door('FlywayRight', 0x78420, 'red', Facing.Left), 'GreenPiratesShaftBottomRight': Door('GreenPiratesShaftBottomRight', 0x78470, 'red', Facing.Left, canGrey=True), 'RedBrinstarElevatorTop': Door('RedBrinstarElevatorTop', 0x78256, 'yellow', Facing.Bottom), @@ -190,34 +239,34 @@ class DoorsManager(): # blue brinstar 'ConstructionZoneRight': Door('ConstructionZoneRight', 0x78784, 'red', Facing.Left), # green brinstar - 'GreenHillZoneTopRight': Door('GreenHillZoneTopRight', 0x78670, 'yellow', Facing.Left, canGrey=True), - 'NoobBridgeRight': Door('NoobBridgeRight', 0x787a6, 'green', Facing.Left, canGrey=True), + 'GreenHillZoneTopRight': Door('GreenHillZoneTopRight', 0x78670, 'yellow', Facing.Left, canGrey=True, indicator=IndicatorFlag.DoorRando), + 'NoobBridgeRight': Door('NoobBridgeRight', 0x787a6, 'green', Facing.Left, canGrey=True, indicator=IndicatorDoor), 'MainShaftRight': Door('MainShaftRight', 0x784be, 'red', Facing.Left), - 'MainShaftBottomRight': Door('MainShaftBottomRight', 0x784c4, 'red', Facing.Left, canGrey=True), + 'MainShaftBottomRight': Door('MainShaftBottomRight', 0x784c4, 'red', Facing.Left, canGrey=True, indicator=IndicatorAll), 'EarlySupersRight': Door('EarlySupersRight', 0x78512, 'red', Facing.Left), 'EtecoonEnergyTankLeft': Door('EtecoonEnergyTankLeft', 0x787c8, 'green', Facing.Right), # pink brinstar 'BigPinkTopRight': Door('BigPinkTopRight', 0x78626, 'red', Facing.Left), 'BigPinkRight': Door('BigPinkRight', 0x7861a, 'yellow', Facing.Left), - 'BigPinkBottomRight': Door('BigPinkBottomRight', 0x78620, 'green', Facing.Left, canGrey=True), + 'BigPinkBottomRight': Door('BigPinkBottomRight', 0x78620, 'green', Facing.Left, canGrey=True, indicator=IndicatorAll), 'BigPinkBottomLeft': Door('BigPinkBottomLeft', 0x7862c, 'red', Facing.Right), # red brinstar 'RedTowerLeft': Door('RedTowerLeft', 0x78866, 'yellow', Facing.Right), 'RedBrinstarFirefleaLeft': Door('RedBrinstarFirefleaLeft', 0x7886e, 'red', Facing.Right), 'RedTowerElevatorTopLeft': Door('RedTowerElevatorTopLeft', 0x788aa, 'green', Facing.Right), - 'RedTowerElevatorLeft': Door('RedTowerElevatorLeft', 0x788b0, 'yellow', Facing.Right), + 'RedTowerElevatorLeft': Door('RedTowerElevatorLeft', 0x788b0, 'yellow', Facing.Right, indicator=IndicatorAll), 'RedTowerElevatorBottomLeft': Door('RedTowerElevatorBottomLeft', 0x788b6, 'green', Facing.Right), 'BelowSpazerTopRight': Door('BelowSpazerTopRight', 0x78966, 'green', Facing.Left), # Wrecked ship - 'WestOceanRight': Door('WestOceanRight', 0x781e2, 'green', Facing.Left, canGrey=True), - 'LeCoudeBottom': Door('LeCoudeBottom', 0x7823e, 'yellow', Facing.Top, canGrey=True), - 'WreckedShipMainShaftBottom': Door('WreckedShipMainShaftBottom', 0x7c277, 'green', Facing.Top), + 'WestOceanRight': Door('WestOceanRight', 0x781e2, 'green', Facing.Left, canGrey=True, indicator=IndicatorAll), + 'LeCoudeBottom': Door('LeCoudeBottom', 0x7823e, 'yellow', Facing.Top, canGrey=True, indicator=IndicatorDoor), + 'WreckedShipMainShaftBottom': Door('WreckedShipMainShaftBottom', 0x7c277, 'green', Facing.Top, indicator=IndicatorFlag.AreaRando), 'ElectricDeathRoomTopLeft': Door('ElectricDeathRoomTopLeft', 0x7c32f, 'red', Facing.Right), # Upper Norfair 'BusinessCenterTopLeft': Door('BusinessCenterTopLeft', 0x78b00, 'green', Facing.Right), 'BusinessCenterBottomLeft': Door('BusinessCenterBottomLeft', 0x78b0c, 'red', Facing.Right), - 'CathedralEntranceRight': Door('CathedralEntranceRight', 0x78af2, 'red', Facing.Left, canGrey=True), - 'CathedralRight': Door('CathedralRight', 0x78aea, 'green', Facing.Left), + 'CathedralEntranceRight': Door('CathedralEntranceRight', 0x78af2, 'red', Facing.Left, canGrey=True, indicator=IndicatorAll), + 'CathedralRight': Door('CathedralRight', 0x78aea, 'green', Facing.Left, indicator=IndicatorAll), 'BubbleMountainTopRight': Door('BubbleMountainTopRight', 0x78c60, 'green', Facing.Left), 'BubbleMountainTopLeft': Door('BubbleMountainTopLeft', 0x78c5a, 'green', Facing.Right), 'SpeedBoosterHallRight': Door('SpeedBoosterHallRight', 0x78c7a, 'red', Facing.Left), @@ -229,13 +278,13 @@ class DoorsManager(): 'PostCrocomireUpperLeft': Door('PostCrocomireUpperLeft', 0x78bf4, 'red', Facing.Right), 'PostCrocomireShaftRight': Door('PostCrocomireShaftRight', 0x78c0c, 'red', Facing.Left), # Lower Norfair - 'RedKihunterShaftBottom': Door('RedKihunterShaftBottom', 0x7902e, 'yellow', Facing.Top), - 'WastelandLeft': Door('WastelandLeft', 0x790ba, 'green', Facing.Right, forbiddenColors=['yellow']), + 'RedKihunterShaftBottom': Door('RedKihunterShaftBottom', 0x7902e, 'yellow', Facing.Top, indicator=IndicatorFlag.AreaRando), + 'WastelandLeft': Door('WastelandLeft', 0x790ba, 'green', Facing.Right, forbiddenColors=['yellow'], indicator=IndicatorFlag.AreaRando), # Maridia - 'MainStreetBottomRight': Door('MainStreetBottomRight', 0x7c431, 'red', Facing.Left), + 'MainStreetBottomRight': Door('MainStreetBottomRight', 0x7c431, 'red', Facing.Left, indicator=IndicatorAll), 'FishTankRight': Door('FishTankRight', 0x7c475, 'red', Facing.Left), - 'CrabShaftRight': Door('CrabShaftRight', 0x7c4fb, 'green', Facing.Left), - 'ColosseumBottomRight': Door('ColosseumBottomRight', 0x7c6fb, 'green', Facing.Left), + 'CrabShaftRight': Door('CrabShaftRight', 0x7c4fb, 'green', Facing.Left, indicator=IndicatorDoor), + 'ColosseumBottomRight': Door('ColosseumBottomRight', 0x7c6fb, 'green', Facing.Left, indicator=IndicatorFlag.AreaRando), 'PlasmaSparkBottom': Door('PlasmaSparkBottom', 0x7c577, 'green', Facing.Top), 'OasisTop': Door('OasisTop', 0x7c5d3, 'green', Facing.Bottom), # refill/save @@ -310,12 +359,12 @@ class DoorsManager(): # call from rom loader @staticmethod - def loadDoorsColor(rom): + def loadDoorsColor(rom, readWordFunc): # force to blue some doors depending on patches DoorsManager.setDoorsColor() # for each door store it's color for door in DoorsManager.doors.values(): - door.readColor(rom) + door.readColor(rom, readWordFunc) DoorsManager.debugDoorsColor() # tell that we have randomized doors @@ -342,13 +391,24 @@ class DoorsManager(): # call from rom patcher @staticmethod - def writeDoorsColor(rom, doors, player): + def writeDoorsColor(rom, doors, player, readWordFunc): for door in DoorsManager.doorsDict[player].values(): - door.writeColor(rom) + door.writeColor(rom, readWordFunc) # also set save/refill doors to blue if door.id is not None: doors.append(door.id) + # returns a dict {'DoorName': indicatorPlmType } + @staticmethod + def getIndicatorPLMs(player, indicatorFlags): + ret = {} + for doorName,door in DoorsManager.doorsDict[player].items(): + plm = door.getIndicatorPLM(indicatorFlags) + if plm is not None: + ret[doorName] = plm + return ret + + # call from web @staticmethod def getAddressesToRead(): diff --git a/worlds/sm/variaRandomizer/utils/objectives.py b/worlds/sm/variaRandomizer/utils/objectives.py new file mode 100644 index 0000000000..5f585b28d9 --- /dev/null +++ b/worlds/sm/variaRandomizer/utils/objectives.py @@ -0,0 +1,804 @@ +import copy +import random +from ..rom.addresses import Addresses +from ..rom.rom import pc_to_snes +from ..logic.helpers import Bosses +from ..logic.smbool import SMBool +from ..logic.logic import Logic +from ..graph.location import locationsDict +from ..utils.parameters import Knows +from ..utils import log +import logging + +LOG = log.get('Objectives') + +class Synonyms(object): + killSynonyms = [ + "defeat", + "massacre", + "slay", + "wipe out", + "erase", + "finish", + "destroy", + "wreck", + "smash", + "crush", + "end" + ] + alreadyUsed = [] + @staticmethod + def getVerb(): + verb = random.choice(Synonyms.killSynonyms) + while verb in Synonyms.alreadyUsed: + verb = random.choice(Synonyms.killSynonyms) + Synonyms.alreadyUsed.append(verb) + return verb + +class Goal(object): + def __init__(self, name, gtype, logicClearFunc, romClearFunc, + escapeAccessPoints=None, objCompletedFuncAPs=lambda ap: [ap], + exclusion=None, items=None, text=None, introText=None, + available=True, expandableList=None, category=None, area=None, + conflictFunc=None): + self.name = name + self.available = available + self.clearFunc = logicClearFunc + self.objCompletedFuncAPs = objCompletedFuncAPs + # SNES addr in bank A1, see objectives.asm + self.checkAddr = pc_to_snes(Addresses.getOne("objective[%s]" % romClearFunc)) & 0xffff + self.escapeAccessPoints = escapeAccessPoints + if self.escapeAccessPoints is None: + self.escapeAccessPoints = (1, []) + self.rank = -1 + # possible values: + # - boss + # - miniboss + # - other + self.gtype = gtype + # example for kill three g4 + # { + # "list": [list of objectives], + # "type: "boss", + # "limit": 2 + # } + self.exclusion = exclusion + if self.exclusion is None: + self.exclusion = {"list": []} + self.items = items + if self.items is None: + self.items = [] + self.text = name if text is None else text + self.introText = introText + self.useSynonym = text is not None + self.expandableList = expandableList + if self.expandableList is None: + self.expandableList = [] + self.expandable = len(self.expandableList) > 0 + self.category = category + self.area = area + self.conflictFunc = conflictFunc + # used by solver/isolver to know if a goal has been completed + self.completed = False + + def setRank(self, rank): + self.rank = rank + + def canClearGoal(self, smbm, ap=None): + # not all objectives require an ap (like limit objectives) + return self.clearFunc(smbm, ap) + + def getText(self): + out = "{}. ".format(self.rank) + if self.useSynonym: + out += self.text.format(Synonyms.getVerb()) + else: + out += self.text + assert len(out) <= 28, "Goal text '{}' is too long: {}, max 28".format(out, len(out)) + if self.introText is not None: + self.introText = "%d. %s" % (self.rank, self.introText) + else: + self.introText = out + return out + + def getIntroText(self): + assert self.introText is not None + return self.introText + + def isLimit(self): + return "type" in self.exclusion + + def __repr__(self): + return self.name + +def getBossEscapeAccessPoint(boss): + return (1, [Bosses.accessPoints[boss]]) + +def getG4EscapeAccessPoints(n): + return (n, [Bosses.accessPoints[boss] for boss in Bosses.Golden4()]) + +def getMiniBossesEscapeAccessPoints(n): + return (n, [Bosses.accessPoints[boss] for boss in Bosses.miniBosses()]) + +def getAreaEscapeAccessPoints(area): + return (1, list({list(loc.AccessFrom.keys())[0] for loc in Logic.locations if loc.GraphArea == area})) + +_goalsList = [ + Goal("kill kraid", "boss", lambda sm, ap: Bosses.bossDead(sm, 'Kraid'), "kraid_is_dead", + escapeAccessPoints=getBossEscapeAccessPoint("Kraid"), + exclusion={"list": ["kill all G4", "kill one G4"]}, + items=["Kraid"], + text="{} kraid", + category="Bosses"), + Goal("kill phantoon", "boss", lambda sm, ap: Bosses.bossDead(sm, 'Phantoon'), "phantoon_is_dead", + escapeAccessPoints=getBossEscapeAccessPoint("Phantoon"), + exclusion={"list": ["kill all G4", "kill one G4"]}, + items=["Phantoon"], + text="{} phantoon", + category="Bosses"), + Goal("kill draygon", "boss", lambda sm, ap: Bosses.bossDead(sm, 'Draygon'), "draygon_is_dead", + escapeAccessPoints=getBossEscapeAccessPoint("Draygon"), + exclusion={"list": ["kill all G4", "kill one G4"]}, + items=["Draygon"], + text="{} draygon", + category="Bosses"), + Goal("kill ridley", "boss", lambda sm, ap: Bosses.bossDead(sm, 'Ridley'), "ridley_is_dead", + escapeAccessPoints=getBossEscapeAccessPoint("Ridley"), + exclusion={"list": ["kill all G4", "kill one G4"]}, + items=["Ridley"], + text="{} ridley", + category="Bosses"), + Goal("kill one G4", "other", lambda sm, ap: Bosses.xBossesDead(sm, 1), "boss_1_killed", + escapeAccessPoints=getG4EscapeAccessPoints(1), + exclusion={"list": ["kill kraid", "kill phantoon", "kill draygon", "kill ridley", + "kill all G4", "kill two G4", "kill three G4"], + "type": "boss", + "limit": 0}, + text="{} one golden4", + category="Bosses"), + Goal("kill two G4", "other", lambda sm, ap: Bosses.xBossesDead(sm, 2), "boss_2_killed", + escapeAccessPoints=getG4EscapeAccessPoints(2), + exclusion={"list": ["kill all G4", "kill one G4", "kill three G4"], + "type": "boss", + "limit": 1}, + text="{} two golden4", + category="Bosses"), + Goal("kill three G4", "other", lambda sm, ap: Bosses.xBossesDead(sm, 3), "boss_3_killed", + escapeAccessPoints=getG4EscapeAccessPoints(3), + exclusion={"list": ["kill all G4", "kill one G4", "kill two G4"], + "type": "boss", + "limit": 2}, + text="{} three golden4", + category="Bosses"), + Goal("kill all G4", "other", lambda sm, ap: Bosses.allBossesDead(sm), "all_g4_dead", + escapeAccessPoints=getG4EscapeAccessPoints(4), + exclusion={"list": ["kill kraid", "kill phantoon", "kill draygon", "kill ridley", "kill one G4", "kill two G4", "kill three G4"]}, + items=["Kraid", "Phantoon", "Draygon", "Ridley"], + text="{} all golden4", + expandableList=["kill kraid", "kill phantoon", "kill draygon", "kill ridley"], + category="Bosses"), + Goal("kill spore spawn", "miniboss", lambda sm, ap: Bosses.bossDead(sm, 'SporeSpawn'), "spore_spawn_is_dead", + escapeAccessPoints=getBossEscapeAccessPoint("SporeSpawn"), + exclusion={"list": ["kill all mini bosses", "kill one miniboss"]}, + items=["SporeSpawn"], + text="{} spore spawn", + category="Minibosses"), + Goal("kill botwoon", "miniboss", lambda sm, ap: Bosses.bossDead(sm, 'Botwoon'), "botwoon_is_dead", + escapeAccessPoints=getBossEscapeAccessPoint("Botwoon"), + exclusion={"list": ["kill all mini bosses", "kill one miniboss"]}, + items=["Botwoon"], + text="{} botwoon", + category="Minibosses"), + Goal("kill crocomire", "miniboss", lambda sm, ap: Bosses.bossDead(sm, 'Crocomire'), "crocomire_is_dead", + escapeAccessPoints=getBossEscapeAccessPoint("Crocomire"), + exclusion={"list": ["kill all mini bosses", "kill one miniboss"]}, + items=["Crocomire"], + text="{} crocomire", + category="Minibosses"), + Goal("kill golden torizo", "miniboss", lambda sm, ap: Bosses.bossDead(sm, 'GoldenTorizo'), "golden_torizo_is_dead", + escapeAccessPoints=getBossEscapeAccessPoint("GoldenTorizo"), + exclusion={"list": ["kill all mini bosses", "kill one miniboss"]}, + items=["GoldenTorizo"], + text="{} golden torizo", + category="Minibosses", + conflictFunc=lambda settings, player: settings.qty['energy'] == 'ultra sparse' and (not Knows.knowsDict[player].LowStuffGT or (Knows.knowsDict[player].LowStuffGT.difficulty > settings.maxDiff))), + Goal("kill one miniboss", "other", lambda sm, ap: Bosses.xMiniBossesDead(sm, 1), "miniboss_1_killed", + escapeAccessPoints=getMiniBossesEscapeAccessPoints(1), + exclusion={"list": ["kill spore spawn", "kill botwoon", "kill crocomire", "kill golden torizo", + "kill all mini bosses", "kill two minibosses", "kill three minibosses"], + "type": "miniboss", + "limit": 0}, + text="{} one miniboss", + category="Minibosses"), + Goal("kill two minibosses", "other", lambda sm, ap: Bosses.xMiniBossesDead(sm, 2), "miniboss_2_killed", + escapeAccessPoints=getMiniBossesEscapeAccessPoints(2), + exclusion={"list": ["kill all mini bosses", "kill one miniboss", "kill three minibosses"], + "type": "miniboss", + "limit": 1}, + text="{} two minibosses", + category="Minibosses"), + Goal("kill three minibosses", "other", lambda sm, ap: Bosses.xMiniBossesDead(sm, 3), "miniboss_3_killed", + escapeAccessPoints=getMiniBossesEscapeAccessPoints(3), + exclusion={"list": ["kill all mini bosses", "kill one miniboss", "kill two minibosses"], + "type": "miniboss", + "limit": 2}, + text="{} three minibosses", + category="Minibosses"), + Goal("kill all mini bosses", "other", lambda sm, ap: Bosses.allMiniBossesDead(sm), "all_mini_bosses_dead", + escapeAccessPoints=getMiniBossesEscapeAccessPoints(4), + exclusion={"list": ["kill spore spawn", "kill botwoon", "kill crocomire", "kill golden torizo", + "kill one miniboss", "kill two minibosses", "kill three minibosses"]}, + items=["SporeSpawn", "Botwoon", "Crocomire", "GoldenTorizo"], + text="{} all mini bosses", + expandableList=["kill spore spawn", "kill botwoon", "kill crocomire", "kill golden torizo"], + category="Minibosses", + conflictFunc=lambda settings, player: settings.qty['energy'] == 'ultra sparse' and (not Knows.knowsDict[player].LowStuffGT or (Knows.knowsDict[player].LowStuffGT.difficulty > settings.maxDiff))), + # not available in AP + #Goal("finish scavenger hunt", "other", lambda sm, ap: SMBool(True), "scavenger_hunt_completed", + # exclusion={"list": []}, # will be auto-completed + # available=False), + Goal("nothing", "other", lambda sm, ap: Objectives.objDict[sm.player].canAccess(sm, ap, "Landing Site"), "nothing_objective", + escapeAccessPoints=(1, ["Landing Site"])), # with no objectives at all, escape auto triggers only in crateria + Goal("collect 25% items", "items", lambda sm, ap: SMBool(True), "collect_25_items", + exclusion={"list": ["collect 50% items", "collect 75% items", "collect 100% items"]}, + category="Items", + introText="collect 25 percent of items"), + Goal("collect 50% items", "items", lambda sm, ap: SMBool(True), "collect_50_items", + exclusion={"list": ["collect 25% items", "collect 75% items", "collect 100% items"]}, + category="Items", + introText="collect 50 percent of items"), + Goal("collect 75% items", "items", lambda sm, ap: SMBool(True), "collect_75_items", + exclusion={"list": ["collect 25% items", "collect 50% items", "collect 100% items"]}, + category="Items", + introText="collect 75 percent of items"), + Goal("collect 100% items", "items", lambda sm, ap: SMBool(True), "collect_100_items", + exclusion={"list": ["collect 25% items", "collect 50% items", "collect 75% items", "collect all upgrades"]}, + category="Items", + introText="collect all items"), + Goal("collect all upgrades", "items", lambda sm, ap: SMBool(True), "all_major_items", + category="Items"), + Goal("clear crateria", "items", lambda sm, ap: SMBool(True), "crateria_cleared", + category="Items", + area="Crateria"), + Goal("clear green brinstar", "items", lambda sm, ap: SMBool(True), "green_brin_cleared", + category="Items", + area="GreenPinkBrinstar"), + Goal("clear red brinstar", "items", lambda sm, ap: SMBool(True), "red_brin_cleared", + category="Items", + area="RedBrinstar"), + Goal("clear wrecked ship", "items", lambda sm, ap: SMBool(True), "ws_cleared", + category="Items", + area="WreckedShip"), + Goal("clear kraid's lair", "items", lambda sm, ap: SMBool(True), "kraid_cleared", + category="Items", + area="Kraid"), + Goal("clear upper norfair", "items", lambda sm, ap: SMBool(True), "upper_norfair_cleared", + category="Items", + area="Norfair"), + Goal("clear croc's lair", "items", lambda sm, ap: SMBool(True), "croc_cleared", + category="Items", + area="Crocomire"), + Goal("clear lower norfair", "items", lambda sm, ap: SMBool(True), "lower_norfair_cleared", + category="Items", + area="LowerNorfair"), + Goal("clear west maridia", "items", lambda sm, ap: SMBool(True), "west_maridia_cleared", + category="Items", + area="WestMaridia"), + Goal("clear east maridia", "items", lambda sm, ap: SMBool(True), "east_maridia_cleared", + category="Items", + area="EastMaridia"), + Goal("tickle the red fish", "other", + lambda sm, ap: sm.wand(sm.haveItem('Grapple'), Objectives.objDict[sm.player].canAccess(sm, ap, "Red Fish Room Bottom")), + "fish_tickled", + escapeAccessPoints=(1, ["Red Fish Room Bottom"]), + objCompletedFuncAPs=lambda ap: ["Red Fish Room Bottom"], + category="Memes"), + Goal("kill the orange geemer", "other", + lambda sm, ap: sm.wand(Objectives.objDict[sm.player].canAccess(sm, ap, "Bowling"), # XXX this unnecessarily adds canPassBowling as requirement + sm.wor(sm.haveItem('Wave'), sm.canUsePowerBombs())), + "orange_geemer", + escapeAccessPoints=(1, ["Bowling"]), + objCompletedFuncAPs=lambda ap: ["Bowling"], + text="{} orange geemer", + category="Memes"), + Goal("kill shaktool", "other", + lambda sm, ap: sm.wand(Objectives.objDict[sm.player].canAccess(sm, ap, "Oasis Bottom"), + sm.canTraverseSandPits(), + sm.canAccessShaktoolFromPantsRoom()), + "shak_dead", + escapeAccessPoints=(1, ["Oasis Bottom"]), + objCompletedFuncAPs=lambda ap: ["Oasis Bottom"], + text="{} shaktool", + category="Memes"), + Goal("activate chozo robots", "other", lambda sm, ap: sm.wand(Objectives.objDict[sm.player].canAccessLocation(sm, ap, "Bomb"), + Objectives.objDict[sm.player].canAccessLocation(sm, ap, "Gravity Suit"), + sm.haveItem("GoldenTorizo"), + sm.canPassLowerNorfairChozo()), # graph access implied by GT loc + "all_chozo_robots", + category="Memes", + escapeAccessPoints=(3, ["Landing Site", "Screw Attack Bottom", "Bowling"]), + objCompletedFuncAPs=lambda ap: ["Landing Site", "Screw Attack Bottom", "Bowling"], + exclusion={"list": ["kill golden torizo"]}, + conflictFunc=lambda settings, player: settings.qty['energy'] == 'ultra sparse' and (not Knows.knowsDict[player].LowStuffGT or (Knows.knowsDict[player].LowStuffGT.difficulty > settings.maxDiff))), + Goal("visit the animals", "other", lambda sm, ap: sm.wand(Objectives.objDict[sm.player].canAccess(sm, ap, "Big Pink"), sm.haveItem("SpeedBooster"), # dachora + Objectives.objDict[sm.player].canAccess(sm, ap, "Etecoons Bottom")), # Etecoons + "visited_animals", + category="Memes", + escapeAccessPoints=(2, ["Big Pink", "Etecoons Bottom"]), + objCompletedFuncAPs=lambda ap: ["Big Pink", "Etecoons Bottom"]), + Goal("kill king cacatac", "other", + lambda sm, ap: Objectives.objDict[sm.player].canAccess(sm, ap, 'Bubble Mountain Top'), + "king_cac_dead", + category="Memes", + escapeAccessPoints=(1, ['Bubble Mountain Top']), + objCompletedFuncAPs=lambda ap: ['Bubble Mountain Top']) +] + + +_goals = {goal.name:goal for goal in _goalsList} + +def completeGoalData(): + # "nothing" is incompatible with everything + _goals["nothing"].exclusion["list"] = [goal.name for goal in _goalsList] + areaGoals = [goal.name for goal in _goalsList if goal.area is not None] + # if we need 100% items, don't require "clear area", as it covers those + _goals["collect 100% items"].exclusion["list"] += areaGoals[:] + # if we have scav hunt, don't require "clear area" (HUD behaviour incompatibility) + # not available in AP + #_goals["finish scavenger hunt"].exclusion["list"] += areaGoals[:] + # remove clear area goals if disabled tourian, as escape can trigger as soon as an area is cleared, + # even if ship is not currently reachable + for goal in areaGoals: + _goals[goal].exclusion['tourian'] = "Disabled" + +completeGoalData() + +class Objectives(object): + maxActiveGoals = 5 + vanillaGoals = ["kill kraid", "kill phantoon", "kill draygon", "kill ridley"] + scavHuntGoal = ["finish scavenger hunt"] + objDict = {} + + def __init__(self, player=0, tourianRequired=True, randoSettings=None): + self.player = player + self.activeGoals = [] + self.nbActiveGoals = 0 + self.totalItemsCount = 100 + self.goals = copy.deepcopy(_goals) + self.graph = None + self._tourianRequired = tourianRequired + self.randoSettings = randoSettings + Objectives.objDict[player] = self + + @property + def tourianRequired(self): + assert self._tourianRequired is not None + return self._tourianRequired + + def resetGoals(self): + self.activeGoals = [] + self.nbActiveGoals = 0 + + def conflict(self, newGoal): + if newGoal.exclusion.get('tourian') == "Disabled" and self.tourianRequired == False: + LOG.debug("new goal %s conflicts with disabled Tourian" % newGoal.name) + return True + LOG.debug("check if new goal {} conflicts with existing active goals".format(newGoal.name)) + count = 0 + for goal in self.activeGoals: + if newGoal.name in goal.exclusion["list"]: + LOG.debug("new goal {} in exclusion list of active goal {}".format(newGoal.name, goal.name)) + return True + if goal.name in newGoal.exclusion["list"]: + LOG.debug("active goal {} in exclusion list of new goal {}".format(goal.name, newGoal.name)) + return True + # count bosses/minibosses already active if new goal has a limit + if newGoal.exclusion.get("type") == goal.gtype: + count += 1 + LOG.debug("new goal limit type: {} same as active goal {}. count: {}".format(newGoal.exclusion["type"], goal.name, count)) + if count > newGoal.exclusion.get("limit", 0): + LOG.debug("new goal {} limit {} is lower than active goals of type: {}".format(newGoal.name, newGoal.exclusion["limit"], newGoal.exclusion["type"])) + return True + LOG.debug("no direct conflict detected for new goal {}".format(newGoal.name)) + + # if at least one active goal has a limit and new goal has the same type of one of the existing limit + # check that new goal doesn't exceed the limit + for goal in self.activeGoals: + goalExclusionType = goal.exclusion.get("type") + if goalExclusionType is not None and goalExclusionType == newGoal.gtype: + count = 0 + for lgoal in self.activeGoals: + if lgoal.gtype == newGoal.gtype: + count += 1 + # add new goal to the count + if count >= goal.exclusion["limit"]: + LOG.debug("new Goal {} would excess limit {} of active goal {}".format(newGoal.name, goal.exclusion["limit"], goal.name)) + return True + + LOG.debug("no backward conflict detected for new goal {}".format(newGoal.name)) + + if self.randoSettings is not None and newGoal.conflictFunc is not None: + if newGoal.conflictFunc(self.randoSettings, self.player): + LOG.debug("new Goal {} is conflicting with rando settings".format(newGoal.name)) + return True + LOG.debug("no conflict with rando settings detected for new goal {}".format(newGoal.name)) + + return False + + def addGoal(self, goalName, completed=False): + LOG.debug("addGoal: {}".format(goalName)) + goal = self.goals[goalName] + if self.conflict(goal): + return + self.nbActiveGoals += 1 + assert self.nbActiveGoals <= self.maxActiveGoals, "Too many active goals" + goal.setRank(self.nbActiveGoals) + goal.completed = completed + self.activeGoals.append(goal) + + def removeGoal(self, goal): + self.nbActiveGoals -= 1 + self.activeGoals.remove(goal) + + def clearGoals(self): + self.nbActiveGoals = 0 + self.activeGoals.clear() + + def isGoalActive(self, goalName): + return self.goals[goalName] in self.activeGoals + + # having graph as a global sucks but Objectives instances are all over the place, + # goals must access it, and it doesn't change often + def setGraph(self, graph, maxDiff): + self.graph = graph + self.maxDiff = maxDiff + for goalName, goal in self.goals.items(): + if goal.area is not None: + goal.escapeAccessPoints = getAreaEscapeAccessPoints(goal.area) + + def canAccess(self, sm, src, dst): + return SMBool(self.graph.canAccess(sm, src, dst, self.maxDiff)) + + def canAccessLocation(self, sm, ap, locName): + loc = locationsDict[locName] + availLocs = self.graph.getAvailableLocations([loc], sm, self.maxDiff, ap) + return SMBool(loc in availLocs) + + def setVanilla(self): + for goal in self.vanillaGoals: + self.addGoal(goal) + + def isVanilla(self): + # kill G4 and/or scav hunt + if len(self.activeGoals) == 1: + for goal in self.activeGoals: + if goal.name not in self.scavHuntGoal: + return False + return True + elif len(self.activeGoals) == 4: + for goal in self.activeGoals: + if goal.name not in self.vanillaGoals: + return False + return True + elif len(self.activeGoals) == 5: + for goal in self.activeGoals: + if goal.name not in self.vanillaGoals + self.scavHuntGoal: + return False + return True + else: + return False + + def setScavengerHunt(self): + self.addGoal("finish scavenger hunt") + + def updateScavengerEscapeAccess(self, ap): + assert self.isGoalActive("finish scavenger hunt") + (_, apList) = self.goals['finish scavenger hunt'].escapeAccessPoints + apList.append(ap) + + def _replaceEscapeAccessPoints(self, goal, aps): + (_, apList) = self.goals[goal].escapeAccessPoints + apList.clear() + apList += aps + + def updateItemPercentEscapeAccess(self, collectedLocsAccessPoints): + for pct in [25,50,75,100]: + goal = 'collect %d%% items' % pct + self._replaceEscapeAccessPoints(goal, collectedLocsAccessPoints) + # not exactly accurate, but player has all upgrades to escape + self._replaceEscapeAccessPoints("collect all upgrades", collectedLocsAccessPoints) + + def setScavengerHuntFunc(self, scavClearFunc): + self.goals["finish scavenger hunt"].clearFunc = scavClearFunc + + def setItemPercentFuncs(self, totalItemsCount=None, allUpgradeTypes=None): + def getPctFunc(pct, totalItemsCount): + def f(sm, ap): + nonlocal pct, totalItemsCount + return sm.hasItemsPercent(pct, totalItemsCount) + return f + + for pct in [25,50,75,100]: + goal = 'collect %d%% items' % pct + self.goals[goal].clearFunc = getPctFunc(pct, totalItemsCount) + if allUpgradeTypes is not None: + self.goals["collect all upgrades"].clearFunc = lambda sm, ap: sm.haveItems(allUpgradeTypes) + + def setAreaFuncs(self, funcsByArea): + goalsByArea = {goal.area:goal for goalName, goal in self.goals.items()} + for area, func in funcsByArea.items(): + if area in goalsByArea: + goalsByArea[area].clearFunc = func + + def setSolverMode(self, solver): + self.setScavengerHuntFunc(solver.scavengerHuntComplete) + # in rando we know the number of items after randomizing, so set the functions only for the solver + self.setItemPercentFuncs(allUpgradeTypes=solver.majorUpgrades) + + def getObjAreaFunc(area): + def f(sm, ap): + nonlocal solver, area + visitedLocs = set([loc.Name for loc in solver.visitedLocations]) + return SMBool(all(locName in visitedLocs for locName in solver.splitLocsByArea[area])) + return f + self.setAreaFuncs({area:getObjAreaFunc(area) for area in solver.splitLocsByArea}) + + def expandGoals(self): + LOG.debug("Active goals:"+str(self.activeGoals)) + # try to replace 'kill all G4' with the four associated objectives. + # we need at least 3 empty objectives out of the max (-1 +4) + if self.maxActiveGoals - self.nbActiveGoals < 3: + return + + expandable = None + for goal in self.activeGoals: + if goal.expandable: + expandable = goal + break + + if expandable is None: + return + + LOG.debug("replace {} with {}".format(expandable.name, expandable.expandableList)) + self.removeGoal(expandable) + for name in expandable.expandableList: + self.addGoal(name) + + # rebuild ranks + for i, goal in enumerate(self.activeGoals, 1): + goal.rank = i + + # call from logic + def canClearGoals(self, smbm, ap): + result = SMBool(True) + for goal in self.activeGoals: + result = smbm.wand(result, goal.canClearGoal(smbm, ap)) + return result + + # call from solver + def checkGoals(self, smbm, ap): + ret = {} + + for goal in self.activeGoals: + if goal.completed is True: + continue + # check if goal can be completed + ret[goal.name] = goal.canClearGoal(smbm, ap) + + return ret + + def setGoalCompleted(self, goalName, completed): + for goal in self.activeGoals: + if goal.name == goalName: + goal.completed = completed + return + assert False, "Can't set goal {} completion to {}, goal not active".format(goalName, completed) + + def allGoalsCompleted(self): + for goal in self.activeGoals: + if goal.completed is False: + return False + return True + + def getGoalFromCheckFunction(self, checkFunction): + for name, goal in self.goals.items(): + if goal.checkAddr == checkFunction: + return goal + assert True, "Goal with check function {} not found".format(hex(checkFunction)) + + def getTotalItemsCount(self): + return self.totalItemsCount + + # call from web + def getAddressesToRead(self): + terminator = 1 + objectiveSize = 2 + bytesToRead = (self.maxActiveGoals + terminator) * objectiveSize + return [Addresses.getOne('objectivesList')+i for i in range(0, bytesToRead+1)] + Addresses.getWeb('totalItems') + Addresses.getWeb("itemsMask") + Addresses.getWeb("beamsMask") + + def getExclusions(self): + # to compute exclusions in the front end + return {goalName: goal.exclusion for goalName, goal in self.goals.items()} + + def getObjectivesTypes(self): + # to compute exclusions in the front end + types = {'boss': [], 'miniboss': []} + for goalName, goal in self.goals.items(): + if goal.gtype in types: + types[goal.gtype].append(goalName) + return types + + def getObjectivesSort(self): + return list(self.goals.keys()) + + def getObjectivesCategories(self): + return {goal.name: goal.category for goal in self.goals.values() if goal.category is not None} + + # call from rando check pool and solver + + def getMandatoryBosses(self): + r = [goal.items for goal in self.activeGoals] + return [item for items in r for item in items] + + def checkLimitObjectives(self, beatableBosses): + # check that there's enough bosses/minibosses for limit objectives + from ..logic.smboolmanager import SMBoolManager + smbm = SMBoolManager(self.player) + smbm.addItems(beatableBosses) + for goal in self.activeGoals: + if not goal.isLimit(): + continue + if not goal.canClearGoal(smbm): + return False + return True + + # call from solver + def getGoalsList(self): + return [goal.name for goal in self.activeGoals] + + # call from interactivesolver + def getState(self): + return {goal.name: goal.completed for goal in self.activeGoals} + + def setState(self, state): + for goalName, completed in state.items(): + self.addGoal(goalName, completed) + + def resetGoals(self): + for goal in self.activeGoals: + goal.completed = False + + # call from rando + @staticmethod + def getAllGoals(removeNothing=False): + return [goal.name for goal in _goals.values() if goal.available and (not removeNothing or goal.name != "nothing")] + + # call from rando + def setRandom(self, nbGoals, availableGoals): + while self.nbActiveGoals < nbGoals and availableGoals: + goalName = random.choice(availableGoals) + self.addGoal(goalName) + availableGoals.remove(goalName) + + # call from solver + def readGoals(self, romReader): + self.resetGoals() + romReader.romFile.seek(Addresses.getOne('objectivesList')) + checkFunction = romReader.romFile.readWord() + while checkFunction != 0x0000: + goal = self.getGoalFromCheckFunction(checkFunction) + self.activeGoals.append(goal) + checkFunction = romReader.romFile.readWord() + + # read number of available items for items % objectives + self.totalItemsCount = romReader.romFile.readByte(Addresses.getOne('totalItems')) + + for goal in self.activeGoals: + LOG.debug("active goal: {}".format(goal.name)) + + self._tourianRequired = not romReader.patchPresent('Escape_Trigger') + LOG.debug("tourianRequired: {}".format(self.tourianRequired)) + + # call from rando + def writeGoals(self, romFile): + # write check functions + romFile.seek(Addresses.getOne('objectivesList')) + for goal in self.activeGoals: + romFile.writeWord(goal.checkAddr) + # list terminator + romFile.writeWord(0x0000) + + # compute chars + char2tile = { + '.': 0x4A, + '?': 0x4B, + '!': 0x4C, + ' ': 0x00, + '%': 0x02, + '*': 0x03, + '0': 0x04, + 'a': 0x30, + } + for i in range(1, ord('z')-ord('a')+1): + char2tile[chr(ord('a')+i)] = char2tile['a']+i + for i in range(1, ord('9')-ord('0')+1): + char2tile[chr(ord('0')+i)] = char2tile['0']+i + + # write text + tileSize = 2 + lineLength = 32 * tileSize + firstChar = 3 * tileSize + # start at 8th line + baseAddr = Addresses.getOne('objectivesText') + lineLength * 8 + firstChar + # space between two lines of text + space = 3 if self.nbActiveGoals == 5 else 4 + for i, goal in enumerate(self.activeGoals): + addr = baseAddr + i * lineLength * space + text = goal.getText() + romFile.seek(addr) + for c in text: + if c not in char2tile: + continue + romFile.writeWord(0x3800 + char2tile[c]) + + # write goal completed positions y in sprites OAM + baseY = 0x40 + addr = Addresses.getOne('objectivesSpritesOAM') + spritemapSize = 5 + 2 + for i, goal in enumerate(self.activeGoals): + y = baseY + i * space * 8 + # sprite center is at 128 + y = (y - 128) & 0xFF + romFile.writeByte(y, addr+4 + i*spritemapSize) + + def writeIntroObjectives(self, rom, tourian): + if self.isVanilla() and tourian == "Vanilla": + return + # objectives or tourian are not vanilla, prepare intro text + # two \n for an actual newline + text = "MISSION OBJECTIVES\n" + for goal in self.activeGoals: + text += "\n\n%s" % goal.getIntroText() + text += "\n\n\nTOURIAN IS %s\n\n\n" % tourian + text += "CHECK OBJECTIVES STATUS IN\n\n" + text += "THE PAUSE SCREEN" + # actually write text in ROM + self._writeIntroText(rom, text.upper()) + + def _writeIntroText(self, rom, text, startX=1, startY=2): + # for character translation + charCodes = { + ' ': 0xD67D, + '.': 0xD75D, + '!': 0xD77B, + "'": 0xD76F, + '0': 0xD721, + 'A': 0xD685 + } + def addCharRange(start, end, base): # inclusive range + for c in range(ord(start), ord(end)+1): + offset = c - ord(base) + charCodes[chr(c)] = charCodes[base]+offset*6 + addCharRange('B', 'Z', 'A') + addCharRange('1', '9', '0') + # actually write chars + x, y = startX, startY + def writeChar(c, frameDelay=2): + nonlocal rom, x, y + assert x <= 0x1F and y <= 0x18, "Intro text formatting error (x=0x%x, y=0x%x):\n%s" % (x, y, text) + if c == '\n': + x = startX + y += 1 + else: + assert c in charCodes, "Invalid intro char "+c + rom.writeWord(frameDelay) + rom.writeByte(x) + rom.writeByte(y) + rom.writeWord(charCodes[c]) + x += 1 + rom.seek(Addresses.getOne('introText')) + for c in text: + writeChar(c) + # write trailer, see intro_text.asm + rom.writeWord(0xAE5B) + rom.writeWord(0x9698) diff --git a/worlds/sm/variaRandomizer/utils/parameters.py b/worlds/sm/variaRandomizer/utils/parameters.py index 2d9313b580..4d9079a619 100644 --- a/worlds/sm/variaRandomizer/utils/parameters.py +++ b/worlds/sm/variaRandomizer/utils/parameters.py @@ -46,19 +46,19 @@ text2diff = { def diff4solver(difficulty): if difficulty == -1: - return "break" + return ("break", "break") elif difficulty < medium: - return "easy" + return ("easy", "easy") elif difficulty < hard: - return "medium" + return ("medium", "medium") elif difficulty < harder: - return "hard" + return ("hard", "hard") elif difficulty < hardcore: - return "harder" + return ("harder", "very hard") elif difficulty < mania: - return "hardcore" + return ("hardcore", "hardcore") else: - return "mania" + return ("mania", "mania") # allow multiple local repo appDir = str(Path(__file__).parents[4]) @@ -120,7 +120,7 @@ class Knows: Mockball = SMBool(True, easy, ['Mockball']) desc['Mockball'] = {'display': 'Mockball', - 'title': 'Morph from runing without loosing momentum to get Early Super and Ice Beam', + 'title': 'Morph from running without loosing momentum to get Early Super and Ice Beam', 'href': 'https://wiki.supermetroid.run/index.php?title=Mockball', 'rooms': ['Early Supers Room', 'Ice Beam Gate Room']} @@ -161,7 +161,7 @@ class Knows: SpringBallJump = SMBool(True, hard, ['SpringBallJump']) desc['SpringBallJump'] = {'display': 'SpringBall-Jump', 'title': 'Do a SpringBall Jump from a jump to Access to Wrecked Ship Etank without anything else, Suitless Maridia navigation', - 'href': 'https://www.youtube.com/watch?v=8ldQUIgBavw&t=49s', + 'href': 'https://www.twitch.tv/videos/147442861', 'rooms': ['Sponge Bath', 'East Ocean', 'Main Street', 'Crab Shaft', 'Pseudo Plasma Spark Room', 'Mama Turtle Room', 'The Precious Room', 'Spring Ball Room', 'East Sand Hole', diff --git a/worlds/sm/variaRandomizer/utils/utils.py b/worlds/sm/variaRandomizer/utils/utils.py index 46c14a16f1..08a8e0379b 100644 --- a/worlds/sm/variaRandomizer/utils/utils.py +++ b/worlds/sm/variaRandomizer/utils/utils.py @@ -56,7 +56,7 @@ def exists(resource: str): return os.path.exists(resource) def isStdPreset(preset): - return preset in ['newbie', 'casual', 'regular', 'veteran', 'expert', 'master', 'samus', 'solution', 'Season_Races', 'SMRAT2021'] + return preset in ['newbie', 'casual', 'regular', 'veteran', 'expert', 'master', 'samus', 'solution', 'Season_Races', 'SMRAT2021', 'Torneio_SGPT3'] def getPresetDir(preset) -> str: if isStdPreset(preset): @@ -316,6 +316,7 @@ class PresetLoaderDict(PresetLoader): def getDefaultMultiValues(): from ..graph.graph_utils import GraphUtils + from ..utils.objectives import Objectives defaultMultiValues = { 'startLocation': GraphUtils.getStartAccessPointNames(), 'majorsSplit': ['Full', 'FullWithHUD', 'Major', 'Chozo', 'Scavenger'], @@ -323,7 +324,10 @@ def getDefaultMultiValues(): 'progressionDifficulty': ['easier', 'normal', 'harder'], 'morphPlacement': ['early', 'normal'], #['early', 'late', 'normal'], 'energyQty': ['ultra sparse', 'sparse', 'medium', 'vanilla'], - 'gravityBehaviour': ['Vanilla', 'Balanced', 'Progressive'] + 'gravityBehaviour': ['Vanilla', 'Balanced', 'Progressive'], + 'areaRandomization': ['off', 'full', 'light'], + 'objective': Objectives.getAllGoals(removeNothing=True), + 'tourian': ['Vanilla', 'Fast', 'Disabled'] } return defaultMultiValues @@ -363,17 +367,16 @@ def loadRandoPreset(world, player, args): args.noVariaTweaks = not world.varia_tweaks[player].value args.maxDifficulty = diffs[world.max_difficulty[player].value] #args.suitsRestriction = world.suits_restriction[player].value - #args.hideItems = world.hide_items[player].value + args.hideItems = world.hide_items[player].value args.strictMinors = world.strict_minors[player].value args.noLayout = not world.layout_patches[player].value args.gravityBehaviour = defaultMultiValues["gravityBehaviour"][world.gravity_behaviour[player].value] args.nerfedCharge = world.nerfed_charge[player].value - args.area = world.area_randomization[player].value != 0 - if args.area: + args.area = world.area_randomization[player].current_key + if args.area != "off": args.areaLayoutBase = not world.area_layout[player].value - args.lightArea = world.area_randomization[player].value == 1 - #args.escapeRando - #args.noRemoveEscapeEnemies + args.escapeRando = world.escape_rando[player].value + args.noRemoveEscapeEnemies = not world.remove_escape_enemies[player].value args.doorsColorsRando = world.doors_colors_rando[player].value args.allowGreyDoors = world.allow_grey_doors[player].value args.bosses = world.boss_randomization[player].value @@ -384,7 +387,12 @@ def loadRandoPreset(world, player, args): if world.fun_suits[player].value: args.superFun.append("Suits") - ipsPatches = {"spin_jump_restart":"spinjumprestart", "rando_speed":"rando_speed", "elevators_doors_speed":"elevators_doors_speed", "refill_before_save":"refill_before_save"} + ipsPatches = { "spin_jump_restart":"spinjumprestart", + "rando_speed":"rando_speed", + "elevators_speed":"elevators_speed", + "fast_doors":"fast_doors", + "refill_before_save":"refill_before_save", + "relaxed_round_robin_cf":"relaxed_round_robin_cf"} for settingName, patchName in ipsPatches.items(): if hasattr(world, settingName) and getattr(world, settingName)[player].value: args.patches.append(patchName + '.ips') @@ -399,7 +407,6 @@ def loadRandoPreset(world, player, args): #args.majorsSplit #args.scavNumLocs #args.scavRandomized - #args.scavEscape args.startLocation = defaultMultiValues["startLocation"][world.start_location[player].value] #args.progressionDifficulty #args.progressionSpeed @@ -408,6 +415,8 @@ def loadRandoPreset(world, player, args): args.powerBombQty = world.power_bomb_qty[player].value / float(10) args.minorQty = world.minor_qty[player].value args.energyQty = defaultMultiValues["energyQty"][world.energy_qty[player].value] + args.objective = world.objective[player].value + args.tourian = defaultMultiValues["tourian"][world.tourian[player].value] #args.minimizerN #args.minimizerTourian @@ -425,7 +434,6 @@ def getRandomizerDefaultParameters(): defaultParams['majorsSplitMultiSelect'] = defaultMultiValues['majorsSplit'] defaultParams['scavNumLocs'] = "10" defaultParams['scavRandomized'] = "off" - defaultParams['scavEscape'] = "off" defaultParams['startLocation'] = "Landing Site" defaultParams['startLocationMultiSelect'] = defaultMultiValues['startLocation'] defaultParams['maxDifficulty'] = 'hardcore' @@ -444,9 +452,13 @@ def getRandomizerDefaultParameters(): defaultParams['minorQty'] = "100" defaultParams['energyQty'] = "vanilla" defaultParams['energyQtyMultiSelect'] = defaultMultiValues['energyQty'] + defaultParams['objectiveRandom'] = "off" + defaultParams['nbObjective'] = "4" + defaultParams['objective'] = ["kill all G4"] + defaultParams['objectiveMultiSelect'] = defaultMultiValues['objective'] + defaultParams['tourian'] = "Vanilla" defaultParams['areaRandomization'] = "off" defaultParams['areaLayout'] = "off" - defaultParams['lightAreaRandomization'] = "off" defaultParams['doorsColorsRando'] = "off" defaultParams['allowGreyDoors'] = "off" defaultParams['escapeRando'] = "off" @@ -454,7 +466,6 @@ def getRandomizerDefaultParameters(): defaultParams['bossRandomization'] = "off" defaultParams['minimizer'] = "off" defaultParams['minimizerQty'] = "45" - defaultParams['minimizerTourian'] = "off" defaultParams['funCombat'] = "off" defaultParams['funMovement'] = "off" defaultParams['funSuits'] = "off" @@ -463,8 +474,10 @@ def getRandomizerDefaultParameters(): defaultParams['gravityBehaviour'] = "Balanced" defaultParams['gravityBehaviourMultiSelect'] = defaultMultiValues['gravityBehaviour'] defaultParams['nerfedCharge'] = "off" + defaultParams['relaxed_round_robin_cf'] = "off" defaultParams['itemsounds'] = "on" - defaultParams['elevators_doors_speed'] = "on" + defaultParams['elevators_speed'] = "on" + defaultParams['fast_doors'] = "on" defaultParams['spinjumprestart'] = "off" defaultParams['rando_speed'] = "off" defaultParams['Infinite_Space_Jump'] = "off" @@ -496,4 +509,22 @@ def fixEnergy(items): items.append('{}-ETank'.format(maxETank)) if maxReserve > 0: items.append('{}-Reserve'.format(maxReserve)) + + + # keep biggest crystal flash + cfs = [i for i in items if i.find('CrystalFlash') != -1] + if len(cfs) > 1: + maxCf = 0 + for cf in cfs: + nCf = int(cf[0:cf.find('-CrystalFlash')]) + if nCf > maxCf: + maxCf = nCf + items.remove(cf) + items.append('{}-CrystalFlash'.format(maxCf)) return items + +def dumpErrorMsg(outFileName, msg): + print("DIAG: " + msg) + if outFileName is not None: + with open(outFileName, 'w') as jsonFile: + json.dump({"errorMsg": msg}, jsonFile) \ No newline at end of file diff --git a/worlds/sm/variaRandomizer/utils/version.py b/worlds/sm/variaRandomizer/utils/version.py index 686ade2816..1c736c268a 100644 --- a/worlds/sm/variaRandomizer/utils/version.py +++ b/worlds/sm/variaRandomizer/utils/version.py @@ -1,3 +1,5 @@ +import Utils + # version displayed on the title screen, must be a max 32 chars [a-z0-9.-] string # either 'beta' or 'r.yyyy.mm.dd' -displayedVersion = 'beta' +displayedVersion = 'r.2022.11.01-ap.' + Utils.__version__ From 81411a191cf165353c06006328bf87544a2a9c88 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Sun, 9 Apr 2023 22:12:23 -0400 Subject: [PATCH 018/489] KH2: init cleanup and random visit locking fix and docs update. (#1652) Random Visit Locking wasn't copying correctly init cleanup and moved itempool population to create_items Updated docs due to a lot of people having issues setting it up. --- worlds/kh2/Options.py | 4 +- worlds/kh2/__init__.py | 448 +++++++++++++------------ worlds/kh2/docs/en_Kingdom Hearts 2.md | 33 -- worlds/kh2/docs/setup_en.md | 104 ++++-- worlds/kh2/mod_template/mod.yml | 12 - worlds/kh2/test/TestGoal.py | 5 +- 6 files changed, 320 insertions(+), 286 deletions(-) diff --git a/worlds/kh2/Options.py b/worlds/kh2/Options.py index 31708218e2..7a6f106aa9 100644 --- a/worlds/kh2/Options.py +++ b/worlds/kh2/Options.py @@ -141,8 +141,8 @@ class Cups(Choice): class LevelDepth(Choice): """Determines How many locations you want on levels - Level 50:23 checks spread through 50 levels. - Level 99:23 checks spread through 99 levels. + Level 50: 23 checks spread through 50 levels. + Level 99: 23 checks spread through 99 levels. Level 50 sanity: 49 checks spread through 50 levels. Level 99 sanity: 98 checks spread through 99 levels. diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index e36c81e806..06036c6803 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -1,4 +1,3 @@ - from BaseClasses import Tutorial, ItemClassification import logging @@ -41,6 +40,10 @@ class KH2World(World): def __init__(self, multiworld: "MultiWorld", player: int): super().__init__(multiworld, player) + self.visitlocking_dict = None + self.plando_locations = None + self.luckyemblemamount = None + self.luckyemblemrequired = None self.BountiesRequired = None self.BountiesAmount = None self.hitlist = None @@ -57,15 +60,14 @@ class KH2World(World): self.growth_list = list() for x in range(4): self.growth_list.extend(Movement_Table.keys()) - self.visitlocking_dict = Progression_Dicts["AllVisitLocking"] def fill_slot_data(self) -> dict: - return {"hitlist": self.hitlist, - "LocalItems": self.LocalItems, - "Goal": self.multiworld.Goal[self.player].value, - "FinalXemnas": self.multiworld.FinalXemnas[self.player].value, - "LuckyEmblemsRequired": self.multiworld.LuckyEmblemsRequired[self.player].value, - "BountyRequired": self.multiworld.BountyRequired[self.player].value} + return {"hitlist": self.hitlist, + "LocalItems": self.LocalItems, + "Goal": self.multiworld.Goal[self.player].value, + "FinalXemnas": self.multiworld.FinalXemnas[self.player].value, + "LuckyEmblemsRequired": self.multiworld.LuckyEmblemsRequired[self.player].value, + "BountyRequired": self.multiworld.BountyRequired[self.player].value} def create_item(self, name: str, ) -> Item: data = item_dictionary_table[name] @@ -78,185 +80,10 @@ class KH2World(World): return created_item - def generate_early(self) -> None: - # Item Quantity dict because Abilities can be a problem for KH2's Software. - for item, data in item_dictionary_table.items(): - self.item_quantity_dict[item] = data.quantity - - if self.multiworld.KeybladeAbilities[self.player] == "support": - self.sora_keyblade_ability_pool.extend(SupportAbility_Table.keys()) - elif self.multiworld.KeybladeAbilities[self.player] == "action": - self.sora_keyblade_ability_pool.extend(ActionAbility_Table.keys()) - else: - # both action and support on keyblades. - # TODO: make option to just exclude scom - self.sora_keyblade_ability_pool.extend(ActionAbility_Table.keys()) - self.sora_keyblade_ability_pool.extend(SupportAbility_Table.keys()) - - for item, value in self.multiworld.start_inventory[self.player].value.items(): - if item in ActionAbility_Table.keys() or item in SupportAbility_Table.keys() or exclusionItem_table["StatUps"]: - # cannot have more than the quantity for abilties - if value > item_dictionary_table[item].quantity: - logging.info(f"{self.multiworld.get_file_safe_player_name(self.player)} cannot have more than {item_dictionary_table[item].quantity} of {item}" - f"Changing the amount to the max amount") - value = item_dictionary_table[item].quantity - self.item_quantity_dict[item] -= value - - # Option to turn off Promise Charm Item - if not self.multiworld.Promise_Charm[self.player]: - self.item_quantity_dict[ItemName.PromiseCharm] = 0 - - for ability in self.multiworld.BlacklistKeyblade[self.player].value: - if ability in self.sora_keyblade_ability_pool: - self.sora_keyblade_ability_pool.remove(ability) - - # Option to turn off all superbosses. Can do this individually but its like 20+ checks - if not self.multiworld.SuperBosses[self.player] and not self.multiworld.Goal[self.player] == "hitlist": - for superboss in exclusion_table["Datas"]: - self.multiworld.exclude_locations[self.player].value.add(superboss) - for superboss in exclusion_table["SuperBosses"]: - self.multiworld.exclude_locations[self.player].value.add(superboss) - - # Option to turn off Olympus Colosseum Cups. - if self.multiworld.Cups[self.player] == "no_cups": - for cup in exclusion_table["Cups"]: - self.multiworld.exclude_locations[self.player].value.add(cup) - # exclude only hades paradox. If cups and hades paradox then nothing is excluded - elif self.multiworld.Cups[self.player] == "cups": - self.multiworld.exclude_locations[self.player].value.add(LocationName.HadesCupTrophyParadoxCups) - - if self.multiworld.Goal[self.player] == "lucky_emblem_hunt": - luckyemblemamount = self.multiworld.LuckyEmblemsAmount[self.player].value - luckyemblemrequired = self.multiworld.LuckyEmblemsRequired[self.player].value - if luckyemblemamount < luckyemblemrequired: - logging.info(f"Lucky Emblem Amount {self.multiworld.LuckyEmblemsAmount[self.player].value} is less than required " - f"{self.multiworld.LuckyEmblemsRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}." - f" Setting amount to {self.multiworld.LuckyEmblemsRequired[self.player].value}") - luckyemblemamount = max(luckyemblemamount, luckyemblemrequired) - self.multiworld.LuckyEmblemsAmount[self.player].value = luckyemblemamount - - self.item_quantity_dict[ItemName.LuckyEmblem] = item_dictionary_table[ItemName.LuckyEmblem].quantity + luckyemblemamount - # give this proof to unlock the final door once the player has the amount of lucky emblem required - self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 - - # hitlist - elif self.multiworld.Goal[self.player] == "hitlist": - self.RandomSuperBoss.extend(exclusion_table["Hitlist"]) - self.BountiesAmount = self.multiworld.BountyAmount[self.player].value - self.BountiesRequired = self.multiworld.BountyRequired[self.player].value - - for location in self.multiworld.exclude_locations[self.player].value: - if location in self.RandomSuperBoss: - self.RandomSuperBoss.remove(location) - # Testing if the player has the right amount of Bounties for Completion. - if len(self.RandomSuperBoss) < self.BountiesAmount: - logging.info(f"{self.multiworld.get_file_safe_player_name(self.player)} has too many bounties than bosses." - f" Setting total bounties to {len(self.RandomSuperBoss)}") - self.BountiesAmount = len(self.RandomSuperBoss) - self.multiworld.BountyAmount[self.player].value = self.BountiesAmount - - if len(self.RandomSuperBoss) < self.BountiesRequired: - logging.info(f"{self.multiworld.get_file_safe_player_name(self.player)} has too many required bounties." - f" Setting required bounties to {len(self.RandomSuperBoss)}") - self.BountiesRequired = len(self.RandomSuperBoss) - self.multiworld.BountyRequired[self.player].value = self.BountiesRequired - - if self.BountiesAmount < self.BountiesRequired: - logging.info(f"Bounties Amount {self.multiworld.BountyAmount[self.player].value} is less than required " - f"{self.multiworld.BountyRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}." - f" Setting amount to {self.multiworld.BountyRequired[self.player].value}") - self.BountiesAmount = max(self.BountiesAmount, self.BountiesRequired) - self.multiworld.BountyAmount[self.player].value = self.BountiesAmount - - self.multiworld.start_hints[self.player].value.add(ItemName.Bounty) - self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 - - while len(self.sora_keyblade_ability_pool) < len(self.keyblade_slot_copy): - self.sora_keyblade_ability_pool.append( - self.multiworld.per_slot_randoms[self.player].choice(list(SupportAbility_Table.keys()))) - - for item in DonaldAbility_Table.keys(): - data = self.item_quantity_dict[item] - for _ in range(data): - self.donald_ability_pool.append(item) - self.item_quantity_dict[item] = 0 - # 32 is the amount of donald abilities - while len(self.donald_ability_pool) < 32: - self.donald_ability_pool.append(self.multiworld.per_slot_randoms[self.player].choice(self.donald_ability_pool)) - - for item in GoofyAbility_Table.keys(): - data = self.item_quantity_dict[item] - for _ in range(data): - self.goofy_ability_pool.append(item) - self.item_quantity_dict[item] = 0 - # 32 is the amount of goofy abilities - while len(self.goofy_ability_pool) < 33: - self.goofy_ability_pool.append(self.multiworld.per_slot_randoms[self.player].choice(self.goofy_ability_pool)) - - def generate_basic(self): - itempool: typing.List[KH2Item] = [] - - self.hitlist = list() - self.filler_items.extend(item_groups["Filler"]) - - if self.multiworld.FinalXemnas[self.player]: - self.multiworld.get_location(LocationName.FinalXemnas, self.player).place_locked_item( - self.create_item(ItemName.Victory)) - else: - self.multiworld.get_location(LocationName.FinalXemnas, self.player).place_locked_item( - self.create_item(self.multiworld.per_slot_randoms[self.player].choice(self.filler_items))) - self.totalLocations -= 1 - - if self.multiworld.Goal[self.player] == "hitlist": - for bounty in range(self.BountiesAmount): - randomBoss = self.multiworld.per_slot_randoms[self.player].choice(self.RandomSuperBoss) - self.multiworld.get_location(randomBoss, self.player).place_locked_item( - self.create_item(ItemName.Bounty)) - self.hitlist.append(self.location_name_to_id[randomBoss]) - self.RandomSuperBoss.remove(randomBoss) - self.totalLocations -= 1 - - # Kingdom Key cannot have No Experience so plandoed here instead of checking 26 times if its kingdom key - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) - while random_ability == ItemName.NoExperience: - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) - self.multiworld.get_location(LocationName.KingdomKeySlot, self.player).place_locked_item(self.create_item(random_ability)) - self.item_quantity_dict[random_ability] -= 1 - self.sora_keyblade_ability_pool.remove(random_ability) - self.totalLocations -= 1 - - # plando keyblades because they can only have abilities - for keyblade in self.keyblade_slot_copy: - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) - self.multiworld.get_location(keyblade, self.player).place_locked_item(self.create_item(random_ability)) - self.item_quantity_dict[random_ability] -= 1 - self.sora_keyblade_ability_pool.remove(random_ability) - self.totalLocations -= 1 - - # Placing Donald Abilities on donald locations - for donaldLocation in Locations.Donald_Checks.keys(): - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.donald_ability_pool) - self.multiworld.get_location(donaldLocation, self.player).place_locked_item( - self.create_item(random_ability)) - self.totalLocations -= 1 - self.donald_ability_pool.remove(random_ability) - - # Placing Goofy Abilities on goofy locations - for goofyLocation in Locations.Goofy_Checks.keys(): - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.goofy_ability_pool) - self.multiworld.get_location(goofyLocation, self.player).place_locked_item(self.create_item(random_ability)) - self.totalLocations -= 1 - self.goofy_ability_pool.remove(random_ability) - - # same item placed because you can only get one of these 2 locations - # they are both under the same flag so the player gets both locations just one of the two items - random_stt_item = self.multiworld.per_slot_randoms[self.player].choice(self.filler_items) - self.multiworld.get_location(LocationName.JunkChampionBelt, self.player).place_locked_item( - self.create_item(random_stt_item)) - self.multiworld.get_location(LocationName.JunkMedal, self.player).place_locked_item( - self.create_item(random_stt_item)) - self.totalLocations -= 2 + def create_items(self) -> None: + itempool: typing.List[Item] = [] + self.visitlocking_dict = Progression_Dicts["AllVisitLocking"].copy() if self.multiworld.Schmovement[self.player] != "level_0": for _ in range(self.multiworld.Schmovement[self.player].value): for name in {ItemName.HighJump, ItemName.QuickRun, ItemName.DodgeRoll, ItemName.AerialDodge, @@ -273,17 +100,15 @@ class KH2World(World): self.growth_list.remove(random_growth) self.multiworld.push_precollected(self.create_item(random_growth)) - # no visit locking if self.multiworld.Visitlocking[self.player] == "no_visit_locking": for item, amount in Progression_Dicts["AllVisitLocking"].items(): for _ in range(amount): self.multiworld.push_precollected(self.create_item(item)) self.item_quantity_dict[item] -= 1 + self.visitlocking_dict[item] -= 1 if self.visitlocking_dict[item] == 0: self.visitlocking_dict.pop(item) - self.visitlocking_dict[item] -= 1 - # first and second visit locking elif self.multiworld.Visitlocking[self.player] == "second_visit_locking": for item in Progression_Dicts["2VisitLocking"]: self.item_quantity_dict[item] -= 1 @@ -303,7 +128,227 @@ class KH2World(World): self.visitlocking_dict.pop(item) self.multiworld.push_precollected(self.create_item(item)) - # there are levels but level 1 is there to keep code clean + for item in item_dictionary_table: + data = self.item_quantity_dict[item] + itempool += [self.create_item(item) for _ in range(data)] + + # Creating filler for unfilled locations + itempool += [self.create_filler() + for _ in range(self.totalLocations-len(itempool))] + self.multiworld.itempool += itempool + + def generate_early(self) -> None: + # Item Quantity dict because Abilities can be a problem for KH2's Software. + for item, data in item_dictionary_table.items(): + self.item_quantity_dict[item] = data.quantity + # Dictionary to mark locations with their plandoed item + # Example. Final Xemnas: Victory + self.plando_locations = dict() + self.hitlist = [] + self.starting_invo_verify() + + # Option to turn off Promise Charm Item + if not self.multiworld.Promise_Charm[self.player]: + self.item_quantity_dict[ItemName.PromiseCharm] = 0 + + self.set_excluded_locations() + + if self.multiworld.Goal[self.player] == "lucky_emblem_hunt": + self.luckyemblemamount = self.multiworld.LuckyEmblemsAmount[self.player].value + self.luckyemblemrequired = self.multiworld.LuckyEmblemsRequired[self.player].value + self.emblem_verify() + + # hitlist + elif self.multiworld.Goal[self.player] == "hitlist": + self.RandomSuperBoss.extend(exclusion_table["Hitlist"]) + self.BountiesAmount = self.multiworld.BountyAmount[self.player].value + self.BountiesRequired = self.multiworld.BountyRequired[self.player].value + + self.hitlist_verify() + + for bounty in range(self.BountiesAmount): + randomBoss = self.multiworld.per_slot_randoms[self.player].choice(self.RandomSuperBoss) + self.plando_locations[randomBoss] = ItemName.Bounty + self.hitlist.append(self.location_name_to_id[randomBoss]) + self.RandomSuperBoss.remove(randomBoss) + self.totalLocations -= 1 + + self.donald_fill() + self.goofy_fill() + self.keyblade_fill() + + if self.multiworld.FinalXemnas[self.player]: + self.plando_locations[LocationName.FinalXemnas] = ItemName.Victory + else: + self.plando_locations[LocationName.FinalXemnas] = self.create_filler().name + + # same item placed because you can only get one of these 2 locations + # they are both under the same flag so the player gets both locations just one of the two items + random_stt_item = self.create_filler().name + for location in {LocationName.JunkMedal, LocationName.JunkMedal}: + self.plando_locations[location] = random_stt_item + self.level_subtraction() + # subtraction from final xemnas and stt + self.totalLocations -= 3 + + def pre_fill(self): + for location, item in self.plando_locations.items(): + self.multiworld.get_location(location, self.player).place_locked_item( + self.create_item(item)) + + def create_regions(self): + location_table = setup_locations() + create_regions(self.multiworld, self.player, location_table) + connect_regions(self.multiworld, self.player) + + def set_rules(self): + set_rules(self.multiworld, self.player) + + def generate_output(self, output_directory: str): + patch_kh2(self, output_directory) + + def donald_fill(self): + for item in DonaldAbility_Table: + data = self.item_quantity_dict[item] + for _ in range(data): + self.donald_ability_pool.append(item) + self.item_quantity_dict[item] = 0 + # 32 is the amount of donald abilities + while len(self.donald_ability_pool) < 32: + self.donald_ability_pool.append( + self.multiworld.per_slot_randoms[self.player].choice(self.donald_ability_pool)) + # Placing Donald Abilities on donald locations + for donaldLocation in Locations.Donald_Checks.keys(): + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.donald_ability_pool) + self.plando_locations[donaldLocation] = random_ability + self.totalLocations -= 1 + self.donald_ability_pool.remove(random_ability) + + def goofy_fill(self): + for item in GoofyAbility_Table.keys(): + data = self.item_quantity_dict[item] + for _ in range(data): + self.goofy_ability_pool.append(item) + self.item_quantity_dict[item] = 0 + # 32 is the amount of goofy abilities + while len(self.goofy_ability_pool) < 33: + self.goofy_ability_pool.append( + self.multiworld.per_slot_randoms[self.player].choice(self.goofy_ability_pool)) + # Placing Goofy Abilities on goofy locations + for goofyLocation in Locations.Goofy_Checks.keys(): + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.goofy_ability_pool) + self.plando_locations[goofyLocation] = random_ability + self.totalLocations -= 1 + self.goofy_ability_pool.remove(random_ability) + + def keyblade_fill(self): + if self.multiworld.KeybladeAbilities[self.player] == "support": + self.sora_keyblade_ability_pool.extend(SupportAbility_Table.keys()) + elif self.multiworld.KeybladeAbilities[self.player] == "action": + self.sora_keyblade_ability_pool.extend(ActionAbility_Table.keys()) + else: + # both action and support on keyblades. + # TODO: make option to just exclude scom + self.sora_keyblade_ability_pool.extend(ActionAbility_Table.keys()) + self.sora_keyblade_ability_pool.extend(SupportAbility_Table.keys()) + + for ability in self.multiworld.BlacklistKeyblade[self.player].value: + if ability in self.sora_keyblade_ability_pool: + self.sora_keyblade_ability_pool.remove(ability) + + while len(self.sora_keyblade_ability_pool) < len(self.keyblade_slot_copy): + self.sora_keyblade_ability_pool.append( + self.multiworld.per_slot_randoms[self.player].choice(list(SupportAbility_Table.keys()))) + + # Kingdom Key cannot have No Experience so plandoed here instead of checking 26 times if its kingdom key + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) + while random_ability == ItemName.NoExperience: + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) + self.plando_locations[LocationName.KingdomKeySlot] = random_ability + self.item_quantity_dict[random_ability] -= 1 + self.sora_keyblade_ability_pool.remove(random_ability) + + # plando keyblades because they can only have abilities + for keyblade in self.keyblade_slot_copy: + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) + self.plando_locations[keyblade] = random_ability + self.item_quantity_dict[random_ability] -= 1 + self.sora_keyblade_ability_pool.remove(random_ability) + self.totalLocations -= 1 + + def starting_invo_verify(self): + for item, value in self.multiworld.start_inventory[self.player].value.items(): + if item in ActionAbility_Table \ + or item in SupportAbility_Table or exclusionItem_table["StatUps"]: + # cannot have more than the quantity for abilties + if value > item_dictionary_table[item].quantity: + logging.info( + f"{self.multiworld.get_file_safe_player_name(self.player)} cannot have more than {item_dictionary_table[item].quantity} of {item}" + f"Changing the amount to the max amount") + value = item_dictionary_table[item].quantity + self.item_quantity_dict[item] -= value + + def emblem_verify(self): + if self.luckyemblemamount < self.luckyemblemrequired: + logging.info( + f"Lucky Emblem Amount {self.multiworld.LuckyEmblemsAmount[self.player].value} is less than required " + f"{self.multiworld.LuckyEmblemsRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}." + f" Setting amount to {self.multiworld.LuckyEmblemsRequired[self.player].value}") + luckyemblemamount = max(self.luckyemblemamount, self.luckyemblemrequired) + self.multiworld.LuckyEmblemsAmount[self.player].value = luckyemblemamount + + self.item_quantity_dict[ItemName.LuckyEmblem] = item_dictionary_table[ + ItemName.LuckyEmblem].quantity + self.luckyemblemamount + # give this proof to unlock the final door once the player has the amount of lucky emblem required + self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 + + def hitlist_verify(self): + for location in self.multiworld.exclude_locations[self.player].value: + if location in self.RandomSuperBoss: + self.RandomSuperBoss.remove(location) + + # Testing if the player has the right amount of Bounties for Completion. + if len(self.RandomSuperBoss) < self.BountiesAmount: + logging.info( + f"{self.multiworld.get_file_safe_player_name(self.player)} has more bounties than bosses." + f" Setting total bounties to {len(self.RandomSuperBoss)}") + self.BountiesAmount = len(self.RandomSuperBoss) + self.multiworld.BountyAmount[self.player].value = self.BountiesAmount + + if len(self.RandomSuperBoss) < self.BountiesRequired: + logging.info(f"{self.multiworld.get_file_safe_player_name(self.player)} has too many required bounties." + f" Setting required bounties to {len(self.RandomSuperBoss)}") + self.BountiesRequired = len(self.RandomSuperBoss) + self.multiworld.BountyRequired[self.player].value = self.BountiesRequired + + if self.BountiesAmount < self.BountiesRequired: + logging.info(f"Bounties Amount {self.multiworld.BountyAmount[self.player].value} is less than required " + f"{self.multiworld.BountyRequired[self.player].value} for player {self.multiworld.get_file_safe_player_name(self.player)}." + f" Setting amount to {self.multiworld.BountyRequired[self.player].value}") + self.BountiesAmount = max(self.BountiesAmount, self.BountiesRequired) + self.multiworld.BountyAmount[self.player].value = self.BountiesAmount + + self.multiworld.start_hints[self.player].value.add(ItemName.Bounty) + self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 + + def set_excluded_locations(self): + # Option to turn off all superbosses. Can do this individually but its like 20+ checks + if not self.multiworld.SuperBosses[self.player] and not self.multiworld.Goal[self.player] == "hitlist": + for superboss in exclusion_table["Datas"]: + self.multiworld.exclude_locations[self.player].value.add(superboss) + for superboss in exclusion_table["SuperBosses"]: + self.multiworld.exclude_locations[self.player].value.add(superboss) + + # Option to turn off Olympus Colosseum Cups. + if self.multiworld.Cups[self.player] == "no_cups": + for cup in exclusion_table["Cups"]: + self.multiworld.exclude_locations[self.player].value.add(cup) + # exclude only hades paradox. If cups and hades paradox then nothing is excluded + elif self.multiworld.Cups[self.player] == "cups": + self.multiworld.exclude_locations[self.player].value.add(LocationName.HadesCupTrophyParadoxCups) + + def level_subtraction(self): + # there are levels but level 1 is there for the yamls if self.multiworld.LevelDepth[self.player] == "level_99_sanity": # level 99 sanity self.totalLocations -= 1 @@ -317,24 +362,5 @@ class KH2World(World): # level 50/99 since they contain the same amount of levels self.totalLocations -= 76 - for item in item_dictionary_table: - data = self.item_quantity_dict[item] - for _ in range(data): - itempool.append(self.create_item(item)) - - # Creating filler for unfilled locations - while len(itempool) < self.totalLocations: - item = self.multiworld.per_slot_randoms[self.player].choice(self.filler_items) - itempool += [self.create_item(item)] - self.multiworld.itempool += itempool - - def create_regions(self): - location_table = setup_locations() - create_regions(self.multiworld, self.player, location_table) - connect_regions(self.multiworld, self.player) - - def set_rules(self): - set_rules(self.multiworld, self.player) - - def generate_output(self, output_directory: str): - patch_kh2(self, output_directory) + def get_filler_item_name(self) -> str: + return self.multiworld.random.choice([ItemName.PowerBoost, ItemName.MagicBoost, ItemName.DefenseBoost, ItemName.APBoost]) diff --git a/worlds/kh2/docs/en_Kingdom Hearts 2.md b/worlds/kh2/docs/en_Kingdom Hearts 2.md index bb14731699..d132b29ca4 100644 --- a/worlds/kh2/docs/en_Kingdom Hearts 2.md +++ b/worlds/kh2/docs/en_Kingdom Hearts 2.md @@ -72,36 +72,3 @@ With the help of Shananas, Num, and ZakTheRobot we have many QoL features such a - Removal of Absent Silhouette and go straight into the Data Fights. - And much more can be found at [Kingdom Hearts 2 GoA Overview](https://tommadness.github.io/KH2Randomizer/overview/) -

Recommendation

- -- Recommended making a save at the start of the GoA before opening anything. This will be the recommended file to load if/when your game crashes. - - If you don't want to have a save in the GoA. Disconnect the client, load the auto save, and then reconnect the client after it loads the auto save. -- Recommended to set fps limit to 60fps. -- Recommended to run the game in windows/borderless windowed mode. Fullscreen is stable but the game can crash if you alt-tab out. -- Recommend viewing [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1Embae0t7pIrbzvX-NRywk7bTHHEvuFzzQBUUpSUL7Ak/edit?usp=sharing) - -

F.A.Q.

- -- Why am I not getting magic? - - If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. -- Why am I missing worlds/portals in the GoA? - - You are missing the required visit locking item to access the world/portal. -- What versions of Kingdom Hearts 2 are supported? - - Currently `only` the most up to date version on the Epic Game Store is supported `1.0.0.8_WW`. Emulator may be added in the future. -- Why did I crash? - - The port of Kingdom Hearts 2 can and will randomly crash, this is the fault of the game not the randomizer or the archipelago client. - - If you have a continuous/constant crash (in the same area/event every time) you will want to reverify your installed files. This can be done by doing the following: Open Epic Game Store --> Library --> Click Triple Dots --> Manage --> Verify -- Why am I getting dummy items or letters? - - You will need to get the `JaredWeakStrike/APCompanion` (you can find how to get this in the setup guide) -- Why is my HP/MP continuously increasing without stopping? - - You do not have `JaredWeakStrike/APCompanion` setup correctly. Make Sure it is above the GOA in the mod manager. -- Why am I not sending or receiving items? - - Make sure you are connected to the KH2 client and the correct room (for more information reference the setup guide) -- Why did I not load in to the correct visit - - You need to trigger a cutscene or visit The World That Never Was for it to update you have recevied the item. -- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save`? - - Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. -- How do I load an auto save? - - To load an auto-save, hold down the Select or your equivalent on your prefered controller while choosing a file. Make sure to hold the button down the whole time. -- How do I do a soft reset? - - Hold L1+L2+R1+R2+Start or your equivalent on your prefered controller at the same time to immediately reset the game to the start screen. diff --git a/worlds/kh2/docs/setup_en.md b/worlds/kh2/docs/setup_en.md index e54d5c40b3..17235042e1 100644 --- a/worlds/kh2/docs/setup_en.md +++ b/worlds/kh2/docs/setup_en.md @@ -1,48 +1,98 @@ # Kingdom Hearts 2 Archipelago Setup Guide

Quick Links

-- [Main Page](../../../../games/Kingdom%20Hearts%202/info/en) -- [Settings Page](../../../../games/Kingdom%20Hearts%202/player-settings) +- [Game Info Page](../../../../games/Kingdom%20Hearts%202/info/en) +- [Player Settings Page](../../../../games/Kingdom%20Hearts%202/player-settings) -

Setting up the Mod Manager

+

Required Software:

+ `Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) +- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
+ 1. `3.0.0 OpenKH Mod Manager with Panacea`
+ 2. `Install mod from KH2FM-Mods-Num/GoA-ROM-Edition`
+ 3. `Setup Lua Backend From the 3.0.0 KH2Randomizer.exe per the setup guide linked above`
-Follow this Guide [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) +- Needed for Archipelago + 1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases)
+ 2. `Install mod from JaredWeakStrike/APCompanion`
+ 3. `Install mod from KH2FM-Mods-equations19/auto-save`
+ 4. `AP Randomizer Seed` +

Required: Archipelago Companion Mod

-

Loading A Seed

+Load this mod just like the GoA ROM you did during the KH2 Rando setup. `JaredWeakStrike/APCompanion`
+Have this mod second-highest priority below the .zip seed.
+This mod is based upon Num's Garden of Assemblege Mod and requires it to work. Without Num this could not be possible. -When you generate a game you will see a download link for a KH2 .zip seed on the room page. Download the seed then open OpenKH Mod Manager and click the green plus and `Select and install Mod Archive`. Make sure the seed is on the top of the list (Highest Priority) +

Required: Auto Save Mod

+Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` Location doesn't matter, required in case of crashes. -

Archipelago Compainion Mod and recommended mods

+

Installing A Seed

-Load this mod just like the GoA ROM you did during the KH2 Rando setup. `JaredWeakStrike/APCompanion` Have this mod second highest priority below the .zip seed -Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/auto-save` Location doesn't matter, recommended in case of crashes. -Load this mod just like the GoA ROM you did during the KH2 Rando setup. `KH2FM-Mods-equations19/soft-reset` Location doesn't matter, recommneded in case of soft locks. +When you generate a game you will see a download link for a KH2 .zip seed on the room page. Download the seed then open OpenKH Mod Manager and click the green plus and `Select and install Mod Archive`.
+Make sure the seed is on the top of the list (Highest Priority)
+After Installing the seed click `Mod Loader -> Build/Build and Run`. Every slot is a unique mod to install and will be needed be repatched for different slots/rooms. + +

What the Mod Manager Should Look Like.

+![image](https://i.imgur.com/QgRfjP1.png)

Using the KH2 Client

-Once you have started the game through OpenKH Mod Manager and are on the title screen run the ArchipelagoKH2Client.exe. When you successfully connect to the server the client will automatically hook into the game to send/receive checks. If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect. Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you. Most checks will be sent to you anywhere outside of a load or cutscene but if you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. +Once you have started the game through OpenKH Mod Manager and are on the title screen run the [ArchipelagoKH2Client.exe](https://github.com/ArchipelagoMW/Archipelago/releases).
+When you successfully connect to the server the client will automatically hook into the game to send/receive checks.
+If the client ever loses connection to the game, it will also disconnect from the server and you will need to reconnect.
+`Make sure the game is open whenever you try to connect the client to the server otherwise it will immediately disconnect you.`
+Most checks will be sent to you anywhere outside a load or cutscene.
+`If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable.` +
+

KH2 Client should look like this:

+![image](https://i.imgur.com/qP6CmV8.png) +
+Enter `The room's port number` into the top box where the x's are and press "Connect". Follow the prompts there and you should be connected -

Generating a game

-

What is a YAML?

+

Common Pitfalls

+- Having an old GOA Lua Script in your `C:\Users\*YourName*\Documents\KINGDOM HEARTS HD 1.5+2.5 ReMIX\scripts\kh2` folder. + - Pressing F2 while in game should look like this. ![image](https://i.imgur.com/ABSdtPC.png) +
+- Not having Lua Backend Configured Correctly. + - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Lua Backend Configuration Step. +
+- Loading into Simulated Twilight Town Instead of the GOA. + - To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps. -YAML is the file format which Archipelago uses in order to configure a player's world. It allows you to dictate which -game you will be playing as well as the settings you would like for that game. -YAML is a format very similar to JSON however it is made to be more human-readable. If you are ever unsure of the -validity of your YAML file you may check the file by uploading it to the check page on the Archipelago website. Check -page: [YAML Validation Page](/mysterycheck) +

Best Practices

-

Creating a YAML

+- Make a save at the start of the GoA before opening anything. This will be the file to select when loading an autosave if/when your game crashes. + - If you don't want to have a save in the GoA. Disconnect the client, load the auto save, and then reconnect the client after it loads the auto save. +- Set fps limit to 60fps. +- Run the game in windows/borderless windowed mode. Fullscreen is stable but the game can crash if you alt-tab out. -YAML files may be generated on the Archipelago website by visiting the games page and clicking the "Settings Page" link -under any game. Clicking "Export Settings" in a game's settings page will download the YAML to your system. Games -page: [Archipelago Games List](/games) +

Requirement/logic sheet

+Have any questions on what's in logic? This spreadsheet has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1Embae0t7pIrbzvX-NRywk7bTHHEvuFzzQBUUpSUL7Ak/edit?usp=sharing) +

F.A.Q.

-In a multiworld there must be one YAML per world. Any number of players can play on each world using either the game's -native coop system or using Archipelago's coop support. Each world will hold one slot in the multiworld and will have a -slot name and, if the relevant game requires it, files to associate it with that multiworld. +- Why am I getting wallpapered while going into a world for the first time? + - Your `Lua Backend` was not configured correctly. Look over the step in the [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/) guide. +- Why am I not getting magic? + - If you obtain magic, you will need to pause your game to have it show up in your inventory, then enter a new room for it to become properly usable. +- Why am I missing worlds/portals in the GoA? + - You are missing the required visit locking item to access the world/portal. +- What versions of Kingdom Hearts 2 are supported? + - Currently `only` the most up to date version on the Epic Game Store is supported `1.0.0.8_WW`. Emulator may be added in the future. +- Why did I crash? + - The port of Kingdom Hearts 2 can and will randomly crash, this is the fault of the game not the randomizer or the archipelago client. + - If you have a continuous/constant crash (in the same area/event every time) you will want to reverify your installed files. This can be done by doing the following: Open Epic Game Store --> Library --> Click Triple Dots --> Manage --> Verify +- Why am I getting dummy items or letters? + - You will need to get the `JaredWeakStrike/APCompanion` (you can find how to get this if you scroll up) +- Why is my HP/MP continuously increasing without stopping? + - You do not have `JaredWeakStrike/APCompanion` setup correctly. Make Sure it is above the GOA in the mod manager. +- Why am I not sending or receiving items? + - Make sure you are connected to the KH2 client and the correct room (for more information scroll up) +- Why did I not load in to the correct visit + - You need to trigger a cutscene or visit The World That Never Was for it to update you have recevied the item. +- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save`? + - Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. +- How do I load an auto save? + - To load an auto-save, hold down the Select or your equivalent on your prefered controller while choosing a file. Make sure to hold the button down the whole time. -If multiple people plan to play in one world cooperatively then they will only need one YAML for their coop world. If -each player is planning on playing their own game then they will each need a YAML. diff --git a/worlds/kh2/mod_template/mod.yml b/worlds/kh2/mod_template/mod.yml index 0ceda5181a..4246132c26 100644 --- a/worlds/kh2/mod_template/mod.yml +++ b/worlds/kh2/mod_template/mod.yml @@ -1,16 +1,4 @@ assets: -- method: binarc - multi: - - name: msg/us/jm.bar - - name: msg/uk/jm.bar - name: msg/jp/jm.bar - source: - - method: kh2msg - name: jm - source: - - language: en - name: jm.yml - type: list - method: binarc name: 00battle.bin source: diff --git a/worlds/kh2/test/TestGoal.py b/worlds/kh2/test/TestGoal.py index 6cc63da334..97874da2d0 100644 --- a/worlds/kh2/test/TestGoal.py +++ b/worlds/kh2/test/TestGoal.py @@ -1,5 +1,6 @@ from . import KH2TestBase -from ..Names import ItemName,LocationName,RegionName +from ..Names import ItemName + class TestDefault(KH2TestBase): options = {} @@ -18,10 +19,12 @@ class TestLuckyEmblem(KH2TestBase): self.collect_all_but([ItemName.LuckyEmblem]) self.assertBeatable(True) + class TestHitList(KH2TestBase): options = { "Goal": 2, } + def testEverything(self): self.collect_all_but([ItemName.Bounty]) self.assertBeatable(True) From 9d73988030faaad6f5c7ce789c36b9946a0db7a5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 10 Apr 2023 04:33:47 +0200 Subject: [PATCH 019/489] Tests: check that options have a docstring (#823) --- test/general/TestOptions.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 test/general/TestOptions.py diff --git a/test/general/TestOptions.py b/test/general/TestOptions.py new file mode 100644 index 0000000000..b7058183e0 --- /dev/null +++ b/test/general/TestOptions.py @@ -0,0 +1,11 @@ +import unittest +from worlds.AutoWorld import AutoWorldRegister + + +class TestOptions(unittest.TestCase): + def testOptionsHaveDocString(self): + for gamename, world_type in AutoWorldRegister.world_types.items(): + if not world_type.hidden: + for option_key, option in world_type.option_definitions.items(): + with self.subTest(game=gamename, option=option_key): + self.assertTrue(option.__doc__) From 94a02510c058226e1ad01f454c47dbdf528af9fe Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 10 Apr 2023 14:07:37 -0500 Subject: [PATCH 020/489] core: Region management helpers (#761) --- BaseClasses.py | 23 ++++++++++++ test/general/TestHelpers.py | 71 +++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 test/general/TestHelpers.py diff --git a/BaseClasses.py b/BaseClasses.py index 53fe407ecd..9c2c6d6a2a 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -836,6 +836,29 @@ class Region: for entrance in self.entrances: # BFS might be better here, trying DFS for now. return entrance.parent_region.get_connecting_entrance(is_main_entrance) + def add_locations(self, locations: Dict[str, Optional[int]], location_type: Optional[typing.Type[Location]] = None) -> None: + """Adds locations to the Region object, where location_type is your Location class and locations is a dict of + location names to address.""" + if location_type is None: + location_type = Location + for location, address in locations.items(): + self.locations.append(location_type(self.player, location, address, self)) + + def add_exits(self, exits: Dict[str, Optional[str]], rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None: + """ + Connects current region to regions in exit dictionary. Passed region names must exist first. + + :param exits: exits from the region. format is {"connecting_region", "exit_name"} + :param rules: rules for the exits from this region. format is {"connecting_region", rule} + """ + for exiting_region, name in exits.items(): + ret = Entrance(self.player, name, self) if name \ + else Entrance(self.player, f"{self.name} -> {exiting_region}", self) + if rules and exiting_region in rules: + ret.access_rule = rules[exiting_region] + self.exits.append(ret) + ret.connect(self.multiworld.get_region(exiting_region, self.player)) + def __repr__(self): return self.__str__() diff --git a/test/general/TestHelpers.py b/test/general/TestHelpers.py new file mode 100644 index 0000000000..e27d22632f --- /dev/null +++ b/test/general/TestHelpers.py @@ -0,0 +1,71 @@ +from random import seed +from typing import Dict, Optional, Callable, Tuple, List + +from BaseClasses import MultiWorld, CollectionState, Region +import unittest + + +class TestHelpers(unittest.TestCase): + multiworld: MultiWorld + player: int = 1 + + def setUp(self) -> None: + self.multiworld = MultiWorld(self.player) + self.multiworld.game[self.player] = "helper_test_game" + self.multiworld.player_name = {1: "Tester"} + self.multiworld.set_seed(seed) + self.multiworld.set_default_common_options() + + def testRegionHelpers(self) -> None: + regions: Dict[str, str] = { + "TestRegion1": "I'm an apple", + "TestRegion2": "I'm a banana", + } + + locations: Dict[str, Dict[str, Optional[int]]] = { + "TestRegion1": { + "loc_1": 123, + "loc_2": 456, + "event_loc": None, + }, + "TestRegion2": { + "loc_1": 321, + "loc_2": 654, + } + } + + reg_exits: Dict[str, Dict[str, Optional[str]]] = { + "TestRegion1": {"TestRegion2": "connection"}, + "TestRegion2": {"TestRegion1": None}, + } + + exit_rules: Dict[str, Callable[[CollectionState], bool]] = { + "TestRegion1": lambda state: state.has("test_item", self.player) + } + + self.multiworld.regions += [Region(region, self.player, self.multiworld, regions[region]) for region in regions] + + with self.subTest("Test Location Creation Helper"): + for region, loc_pair in locations.items(): + self.multiworld.get_region(region, self.player).add_locations(loc_pair) + + created_location_names = [loc.name for loc in self.multiworld.get_locations()] + for loc_pair in locations.values(): + for loc_name in loc_pair: + self.assertTrue(loc_name in created_location_names) + + with self.subTest("Test Exit Creation Helper"): + for region, exit_dict in reg_exits.items(): + self.multiworld.get_region(region, self.player).add_exits(exit_dict, exit_rules) + + created_exit_names = [exit.name for region in self.multiworld.get_regions() for exit in region.exits] + for parent, exit_pair in reg_exits.items(): + for exit_reg, exit_name in exit_pair.items(): + if exit_name: + self.assertTrue(exit_name in created_exit_names) + else: + self.assertTrue(f"{parent} -> {exit_reg}" in created_exit_names) + if exit_reg in exit_rules: + entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}" + self.assertEqual(exit_rules[exit_reg], + self.multiworld.get_entrance(entrance_name, self.player).access_rule) From e49ffc64f2e9f72d9a03e718d95460e407097c91 Mon Sep 17 00:00:00 2001 From: Ziktofel Date: Mon, 10 Apr 2023 21:08:49 +0200 Subject: [PATCH 021/489] SC2: Rebalance item classes (#1512) It's been a month, if Condor disapproves this can be reverted. I don't really know what to do about the SC2WOL situation going forward. --- worlds/sc2wol/Items.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py index 9776e4fed1..e17a7cbc3b 100644 --- a/worlds/sc2wol/Items.py +++ b/worlds/sc2wol/Items.py @@ -57,11 +57,11 @@ item_table = { "Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3, parent_item="Missile Turret"), "Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4, parent_item="SCV"), "Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5, parent_item="SCV"), - "Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6, classification=ItemClassification.filler), + "Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6), "Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7), "Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, parent_item="Marine"), "Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression, parent_item="Marine"), - "Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.progression, parent_item="Medic"), + "Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.filler, parent_item="Medic"), "Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression, parent_item="Medic"), "Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler, parent_item="Firebat"), "Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13, parent_item="Firebat"), @@ -126,8 +126,8 @@ item_table = { "Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13, classification=ItemClassification.progression), "Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.filler), "Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, classification=ItemClassification.progression), - "Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16, classification=ItemClassification.filler), - "Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17, classification=ItemClassification.filler), + "Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16), + "Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17), "Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18, ItemClassification.progression), "Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.progression), From 8d559daa353892e0af11e5f97d8410f36fae7d3c Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 10 Apr 2023 14:12:06 -0500 Subject: [PATCH 022/489] LTTP: use server for counting locations instead of having 249/216 (#1648) --- worlds/alttp/Client.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index 3484355db5..6d0f9f4d10 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -331,7 +331,9 @@ async def track_locations(ctx, roomid, roomdata) -> bool: ctx.locations_checked.add(location_id) location = ctx.location_names[location_id] snes_logger.info( - f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') + f'New Check: {location} ' + + f'({len(ctx.checked_locations + 1 if ctx.checked_locations else ctx.locations_checked)}/' + + f'{len(ctx.missing_locations) + len(ctx.checked_locations)})') try: shop_data = await snes_read(ctx, SHOP_ADDR, SHOP_LEN) From c7284f90d9a3243ec9c841bea09343ba6682bdc5 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 10 Apr 2023 21:13:33 +0200 Subject: [PATCH 023/489] Core: implement start_inventory_from_pool (#1170) * Core: implement start_inventory_from_pool * Factorio/LttP/Subnautica: add start_inventory_from_pool Option --- BaseClasses.py | 2 +- Main.py | 29 +++++++++++++++++++++++++++++ Options.py | 7 +++++++ worlds/alttp/Options.py | 5 +++-- worlds/alttp/Rom.py | 4 ++-- worlds/factorio/Options.py | 4 +++- worlds/subnautica/Options.py | 3 ++- 7 files changed, 47 insertions(+), 7 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 9c2c6d6a2a..11f9160c1f 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -113,7 +113,6 @@ class MultiWorld(): self.dark_world_light_cone = False self.rupoor_cost = 10 self.aga_randomness = True - self.lock_aga_door_in_escape = False self.save_and_quit_from_boss = True self.custom = False self.customitemarray = [] @@ -122,6 +121,7 @@ class MultiWorld(): self.early_items = {player: {} for player in self.player_ids} self.local_early_items = {player: {} for player in self.player_ids} self.indirect_connections = {} + self.start_inventory_from_pool = {player: Options.StartInventoryPool({}) for player in range(1, players + 1)} self.fix_trock_doors = self.AttributeProxy( lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted') self.fix_skullwoods_exit = self.AttributeProxy( diff --git a/Main.py b/Main.py index 03b2e1b155..6eda78f5b4 100644 --- a/Main.py +++ b/Main.py @@ -115,6 +115,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for item_name, count in world.start_inventory[player].value.items(): for _ in range(count): world.push_precollected(world.create_item(item_name, player)) + for item_name, count in world.start_inventory_from_pool[player].value.items(): + for _ in range(count): + world.push_precollected(world.create_item(item_name, player)) logger.info('Creating World.') AutoWorld.call_all(world, "create_regions") @@ -149,6 +152,32 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No AutoWorld.call_all(world, "generate_basic") + # remove starting inventory from pool items. + # Because some worlds don't actually create items during create_items this has to be as late as possible. + if any(world.start_inventory_from_pool[player].value for player in world.player_ids): + new_items: List[Item] = [] + depletion_pool: Dict[int, Dict[str, int]] = { + player: world.start_inventory_from_pool[player].value.copy() for player in world.player_ids} + for player, items in depletion_pool.items(): + player_world: AutoWorld.World = world.worlds[player] + for count in items.values(): + new_items.append(player_world.create_filler()) + target: int = sum(sum(items.values()) for items in depletion_pool.values()) + for item in world.itempool: + if depletion_pool[item.player].get(item.name, 0): + target -= 1 + depletion_pool[item.player][item.name] -= 1 + # quick abort if we have found all items + if not target: + break + else: + new_items.append(item) + for player, remaining_items in depletion_pool.items(): + if remaining_items: + raise Exception(f"{world.get_player_name(player)}" + f" is trying to remove items from their pool that don't exist: {remaining_items}") + world.itempool[:] = new_items + # temporary home for item links, should be moved out of Main for group_id, group in world.groups.items(): def find_common_pool(players: Set[int], shared_pool: Set[str]) -> Tuple[ diff --git a/Options.py b/Options.py index 48d802b5b1..50c16a893d 100644 --- a/Options.py +++ b/Options.py @@ -897,6 +897,13 @@ class StartInventory(ItemDict): display_name = "Start Inventory" +class StartInventoryPool(StartInventory): + """Start with these items and don't place them in the world. + The game decides what the replacement items will be.""" + verify_item_name = True + display_name = "Start Inventory from Pool" + + class StartHints(ItemSet): """Start with these item's locations prefilled into the !hint command.""" display_name = "Start Hints" diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index dd007954e1..b4b0958ac2 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -1,7 +1,7 @@ import typing from BaseClasses import MultiWorld -from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice, PlandoBosses +from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool, PlandoBosses class Logic(Choice): @@ -466,5 +466,6 @@ alttp_options: typing.Dict[str, type(Option)] = { "beemizer_total_chance": BeemizerTotalChance, "beemizer_trap_chance": BeemizerTrapChance, "death_link": DeathLink, - "allow_collect": AllowCollect + "allow_collect": AllowCollect, + "start_inventory_from_pool": StartInventoryPool, } diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 6b0ad9ad64..99826157d8 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -1247,8 +1247,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool): # assorted fixes rom.write_byte(0x1800A2, 0x01 if world.fix_fake_world[ player] else 0x00) # Toggle whether to be in real/fake dark world when dying in a DW dungeon before killing aga1 - rom.write_byte(0x180169, - 0x01 if world.lock_aga_door_in_escape else 0x00) # Lock or unlock aga tower door during escape sequence. + # Lock or unlock aga tower door during escape sequence. + rom.write_byte(0x180169, 0x00) if world.mode[player] == 'inverted': rom.write_byte(0x180169, 0x02) # lock aga/ganon tower door with crystals in inverted rom.write_byte(0x180171, diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index 85394cae44..0331c2d013 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -2,7 +2,8 @@ from __future__ import annotations import typing import datetime -from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink, Toggle +from Options import Choice, OptionDict, OptionSet, ItemDict, Option, DefaultOnToggle, Range, DeathLink, Toggle, \ + StartInventoryPool from schema import Schema, Optional, And, Or # schema helpers @@ -454,6 +455,7 @@ factorio_options: typing.Dict[str, type(Option)] = { "evolution_trap_increase": EvolutionTrapIncrease, "death_link": DeathLink, "energy_link": EnergyLink, + "start_inventory_from_pool": StartInventoryPool, } # spoilers below. If you spoil it for yourself, please at least don't spoil it for anyone else. diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index 91c4866142..fa66026d7d 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,6 +1,6 @@ import typing -from Options import Choice, Range, DeathLink, DefaultOnToggle +from Options import Choice, Range, DeathLink, DefaultOnToggle, StartInventoryPool from .Creatures import all_creatures, Definitions @@ -104,4 +104,5 @@ options = { "creature_scans": CreatureScans, "creature_scan_logic": AggressiveScanLogic, "death_link": SubnauticaDeathLink, + "start_inventory_from_pool": StartInventoryPool, } From 77fbd0eb2b7ad47eae56ca91b0bc395fa486dd68 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 10 Apr 2023 14:44:20 -0500 Subject: [PATCH 024/489] MultiServer: Notify clients of hint points (#1548) * notify clients of their amount of hint points on initial connection and when hinting * send in connect packet instead of sending a RoomUpdate on connect * send hint_points update in `on_new_hint` * add to connected packet docs * hint_points isn't a new variable on RoomUpdate now * note roomupdate can contain connected members * add the hint point stuff to commonclient * only show hint points when relevant and default to 0 * Revert "note roomupdate can contain connected members" * remove hint_points from roomupdate args list and condense explanation of possible packet args * updates from phar's review * Small tweak to wording in RoomUpdate --------- Co-authored-by: Fabian Dill Co-authored-by: Phar --- CommonClient.py | 2 ++ MultiServer.py | 7 ++++++- docs/network protocol.md | 20 ++++++++++---------- kvui.py | 6 +++--- 4 files changed, 21 insertions(+), 14 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 4892f69f06..2e10f6d5c0 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -166,6 +166,7 @@ class CommonContext: server_address: typing.Optional[str] password: typing.Optional[str] hint_cost: typing.Optional[int] + hint_points: typing.Optional[int] player_names: typing.Dict[int, str] finished_game: bool @@ -711,6 +712,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict): ctx.slot = args["slot"] # int keys get lost in JSON transfer ctx.slot_info = {int(pid): data for pid, data in args["slot_info"].items()} + ctx.hint_points = args.get("hint_points", 0) ctx.consume_players_package(args["players"]) msgs = [] if ctx.locations_checked: diff --git a/MultiServer.py b/MultiServer.py index 974886a9ca..57f93d6713 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -700,6 +700,10 @@ class Context: targets: typing.Set[Client] = set(self.stored_data_notification_clients[key]) if targets: self.broadcast(targets, [{"cmd": "SetReply", "key": key, "value": self.hints[team, slot]}]) + self.broadcast(self.clients[team][slot], [{ + "cmd": "RoomUpdate", + "hint_points": get_slot_points(self, team, slot) + }]) def update_aliases(ctx: Context, team: int): @@ -1639,7 +1643,8 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict): "players": ctx.get_players_package(), "missing_locations": get_missing_checks(ctx, team, slot), "checked_locations": get_checked_checks(ctx, team, slot), - "slot_info": ctx.slot_info + "slot_info": ctx.slot_info, + "hint_points": get_slot_points(ctx, team, slot), } reply = [connected_packet] start_inventory = get_start_inventory(ctx, slot, client.remote_start_inventory) diff --git a/docs/network protocol.md b/docs/network protocol.md index f4e261dcee..052d62a531 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -128,7 +128,8 @@ Sent to clients when the connection handshake is successfully completed. | missing_locations | list\[int\] | Contains ids of remaining locations that need to be checked. Useful for trackers, among other things. | | checked_locations | list\[int\] | Contains ids of all locations that have been checked. Useful for trackers, among other things. Location ids are in the range of ± 253-1. | | slot_data | dict\[str, any\] | Contains a json object for slot related data, differs per game. Empty if not required. Not present if slot_data in [Connect](#Connect) is false. | -| slot_info | dict\[int, [NetworkSlot](#NetworkSlot)\] | maps each slot to a [NetworkSlot](#NetworkSlot) information | +| slot_info | dict\[int, [NetworkSlot](#NetworkSlot)\] | maps each slot to a [NetworkSlot](#NetworkSlot) information. | +| hint_points | int | Number of hint points that the current player has. | ### ReceivedItems Sent to clients when they receive an item. @@ -146,17 +147,16 @@ Sent to clients to acknowledge a received [LocationScouts](#LocationScouts) pack | locations | list\[[NetworkItem](#NetworkItem)\] | Contains list of item(s) in the location(s) scouted. | ### RoomUpdate -Sent when there is a need to update information about the present game session. Generally useful for async games. -Once authenticated (received Connected), this may also contain data from Connected. +Sent when there is a need to update information about the present game session. #### Arguments -The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring: +RoomUpdate may contain the same arguments from [RoomInfo](#RoomInfo) and, once authenticated, arguments from +[Connected](#Connected) with the following exceptions: -| Name | Type | Notes | -| ---- | ---- | ----- | -| hint_points | int | New argument. The client's current hint points. | -| players | list\[[NetworkPlayer](#NetworkPlayer)\] | Send in the event of an alias rename. Always sends all players, whether connected or not. | -| checked_locations | list\[int\] | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. | -| missing_locations | list\[int\] | Should never be sent as an update, if needed is the inverse of checked_locations. | +| Name | Type | Notes | +|-------------------|-----------------------------------------|-----------------------------------------------------------------------------------------------------------------------| +| players | list\[[NetworkPlayer](#NetworkPlayer)\] | Sent in the event of an alias rename. Always sends all players, whether connected or not. | +| checked_locations | list\[int\] | May be a partial update, containing new locations that were checked, especially from a coop partner in the same slot. | +| missing_locations | - | Never sent in this packet. If needed, it is the inverse of `checked_locations`. | All arguments for this packet are optional, only changes are sent. diff --git a/kvui.py b/kvui.py index 66da8c16a3..262f3684d9 100644 --- a/kvui.py +++ b/kvui.py @@ -151,11 +151,11 @@ class ServerLabel(HovererableLabel): min_cost = int(ctx.server_version >= (0, 3, 9)) text += f"\nA new !hint costs {ctx.hint_cost}% of checks made. " \ f"For you this means every " \ - f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))}" \ - f" location checks." + f"{max(min_cost, int(ctx.hint_cost * 0.01 * ctx.total_locations))} " \ + "location checks." \ + f"\nYou currently have {ctx.hint_points} points." elif ctx.hint_cost == 0: text += "\n!hint is free to use." - else: text += f"\nYou are not authenticated yet." From cdf7ca1dcc0271fec283b4fb1089bd8950e01c0a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 10 Apr 2023 23:54:56 +0200 Subject: [PATCH 025/489] Core: allow ordered valid keys (#1690) Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> --- Options.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/Options.py b/Options.py index 50c16a893d..6fe70ad95f 100644 --- a/Options.py +++ b/Options.py @@ -715,8 +715,16 @@ class SpecialRange(Range): f"random-range-high--, or random-range--.") -class VerifyKeys: - valid_keys = frozenset() +class FreezeValidKeys(AssembleOptions): + def __new__(mcs, name, bases, attrs): + if "valid_keys" in attrs: + attrs["_valid_keys"] = frozenset(attrs["valid_keys"]) + return super(FreezeValidKeys, mcs).__new__(mcs, name, bases, attrs) + + +class VerifyKeys(metaclass=FreezeValidKeys): + valid_keys: typing.Iterable = [] + _valid_keys: frozenset # gets created by AssembleOptions from valid_keys valid_keys_casefold: bool = False convert_name_groups: bool = False verify_item_name: bool = False @@ -728,10 +736,10 @@ class VerifyKeys: if cls.valid_keys: data = set(data) dataset = set(word.casefold() for word in data) if cls.valid_keys_casefold else set(data) - extra = dataset - cls.valid_keys + extra = dataset - cls._valid_keys if extra: raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. " - f"Allowed keys: {cls.valid_keys}.") + f"Allowed keys: {cls._valid_keys}.") def verify(self, world: typing.Type[World], player_name: str, plando_options: "PlandoOptions") -> None: if self.convert_name_groups and self.verify_item_name: @@ -792,6 +800,10 @@ class ItemDict(OptionDict): class OptionList(Option[typing.List[typing.Any]], VerifyKeys): + # Supports duplicate entries and ordering. + # If only unique entries are needed and input order of elements does not matter, OptionSet should be used instead. + # Not a docstring so it doesn't get grabbed by the options system. + default: typing.List[typing.Any] = [] supports_weighting = False From 0c1e3097c318842cf2ea31972b1e5022e090eec5 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Mon, 10 Apr 2023 17:01:54 -0500 Subject: [PATCH 026/489] WebHost: Set defaults for lists/sets on Weighted Settings page (#1692) --- WebHostLib/options.py | 3 +++ WebHostLib/static/assets/weighted-settings.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index a4d7ccc17c..9b6fc90402 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -131,6 +131,7 @@ def create(): "type": "items-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, "description": get_html_doc(option), + "defaultValue": list(option.default) } elif issubclass(option, Options.LocationSet): @@ -138,6 +139,7 @@ def create(): "type": "locations-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, "description": get_html_doc(option), + "defaultValue": list(option.default) } elif issubclass(option, Options.VerifyKeys): @@ -147,6 +149,7 @@ def create(): "displayName": option.display_name if hasattr(option, "display_name") else option_name, "description": get_html_doc(option), "options": list(option.valid_keys), + "defaultValue": list(option.default) if hasattr(option, "default") else [] } else: diff --git a/WebHostLib/static/assets/weighted-settings.js b/WebHostLib/static/assets/weighted-settings.js index e471e0837a..11854d3ce0 100644 --- a/WebHostLib/static/assets/weighted-settings.js +++ b/WebHostLib/static/assets/weighted-settings.js @@ -91,7 +91,7 @@ const createDefaultSettings = (settingData) => { case 'items-list': case 'locations-list': case 'custom-list': - newSettings[game][gameSetting] = []; + newSettings[game][gameSetting] = setting.defaultValue; break; default: From 5eadbc984062b0b52d929bef6afe2a713edb2d92 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Mon, 10 Apr 2023 19:44:59 -0400 Subject: [PATCH 027/489] Major Game Update: Stardew Valley v3.x.x - The BK Update (#1686) This is a major update for Stardew Valley, for version 3.x.x. Changes include a large number of new features, including Seasons Randomizer, SeedShuffle, Museumsanity, Friendsanity, Complete Collection Goal, Full House Goal, friendship multiplier Co-authored-by: Jouramie --- worlds/stardew_valley/__init__.py | 169 ++- worlds/stardew_valley/bundles.py | 2 +- worlds/stardew_valley/data/__init__.py | 2 + .../stardew_valley/{ => data}/bundle_data.py | 40 +- worlds/stardew_valley/data/common_data.py | 9 + worlds/stardew_valley/data/crops.csv | 37 + worlds/stardew_valley/data/crops_data.py | 45 + worlds/stardew_valley/data/entrance_data.py | 2 + worlds/stardew_valley/data/fish_data.py | 124 ++ worlds/stardew_valley/data/game_item.py | 13 + worlds/stardew_valley/data/items.csv | 181 ++- worlds/stardew_valley/data/locations.csv | 531 +++++++++ worlds/stardew_valley/data/monster_data.py | 8 + worlds/stardew_valley/data/museum_data.py | 301 +++++ worlds/stardew_valley/data/region_data.py | 99 ++ worlds/stardew_valley/data/season_data.py | 10 + worlds/stardew_valley/data/villagers_data.py | 250 ++++ .../stardew_valley/docs/en_Stardew Valley.md | 18 +- worlds/stardew_valley/docs/setup_en.md | 11 +- worlds/stardew_valley/fish_data.py | 127 -- worlds/stardew_valley/game_item.py | 26 - worlds/stardew_valley/items.py | 153 ++- worlds/stardew_valley/locations.py | 56 +- worlds/stardew_valley/logic.py | 1045 +++++++---------- worlds/stardew_valley/options.py | 277 +++-- worlds/stardew_valley/regions.py | 433 +++---- worlds/stardew_valley/rules.py | 208 +++- worlds/stardew_valley/stardew_rule.py | 360 ++++++ worlds/stardew_valley/test/TestAllLogic.py | 53 - worlds/stardew_valley/test/TestBundles.py | 2 +- worlds/stardew_valley/test/TestGeneration.py | 145 ++- worlds/stardew_valley/test/TestItems.py | 76 +- worlds/stardew_valley/test/TestLogic.py | 314 +---- .../test/TestLogicSimplification.py | 99 +- worlds/stardew_valley/test/TestOptions.py | 163 ++- .../stardew_valley/test/TestResourcePack.py | 76 -- worlds/stardew_valley/test/TestRules.py | 309 +++++ worlds/stardew_valley/test/__init__.py | 40 +- 38 files changed, 4055 insertions(+), 1759 deletions(-) rename worlds/stardew_valley/{ => data}/bundle_data.py (92%) create mode 100644 worlds/stardew_valley/data/common_data.py create mode 100644 worlds/stardew_valley/data/crops.csv create mode 100644 worlds/stardew_valley/data/crops_data.py create mode 100644 worlds/stardew_valley/data/entrance_data.py create mode 100644 worlds/stardew_valley/data/fish_data.py create mode 100644 worlds/stardew_valley/data/game_item.py create mode 100644 worlds/stardew_valley/data/monster_data.py create mode 100644 worlds/stardew_valley/data/museum_data.py create mode 100644 worlds/stardew_valley/data/region_data.py create mode 100644 worlds/stardew_valley/data/season_data.py create mode 100644 worlds/stardew_valley/data/villagers_data.py delete mode 100644 worlds/stardew_valley/fish_data.py delete mode 100644 worlds/stardew_valley/game_item.py create mode 100644 worlds/stardew_valley/stardew_rule.py delete mode 100644 worlds/stardew_valley/test/TestAllLogic.py delete mode 100644 worlds/stardew_valley/test/TestResourcePack.py create mode 100644 worlds/stardew_valley/test/TestRules.py diff --git a/worlds/stardew_valley/__init__.py b/worlds/stardew_valley/__init__.py index 79c057fcbe..5d32f77500 100644 --- a/worlds/stardew_valley/__init__.py +++ b/worlds/stardew_valley/__init__.py @@ -1,15 +1,16 @@ from typing import Dict, Any, Iterable, Optional, Union -from BaseClasses import Region, Entrance, Location, Item, Tutorial +from BaseClasses import Region, Entrance, Location, Item, Tutorial, CollectionState from worlds.AutoWorld import World, WebWorld from . import rules, logic, options from .bundles import get_all_bundles, Bundle -from .items import item_table, create_items, ItemData, Group +from .items import item_table, create_items, ItemData, Group, items_by_group from .locations import location_table, create_locations, LocationData -from .logic import StardewLogic, StardewRule, _True, _And +from .logic import StardewLogic, StardewRule, True_ from .options import stardew_valley_options, StardewOptions, fetch_options from .regions import create_regions from .rules import set_rules +from ..generic.Rules import set_rule client_version = 0 @@ -52,8 +53,8 @@ class StardewValleyWorld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {name: data.code for name, data in location_table.items()} - data_version = 1 - required_client_version = (0, 3, 9) + data_version = 2 + required_client_version = (0, 4, 0) options: StardewOptions logic: StardewLogic @@ -88,38 +89,66 @@ class StardewValleyWorld(World): create_locations(add_location, self.options, self.multiworld.random) def create_items(self): - locations_count = len([location - for location in self.multiworld.get_locations(self.player) - if not location.event]) + self.precollect_starting_season() items_to_exclude = [excluded_items for excluded_items in self.multiworld.precollected_items[self.player] if not item_table[excluded_items.name].has_any_group(Group.RESOURCE_PACK, Group.FRIENDSHIP_PACK)] - created_items = create_items(self.create_item, locations_count + len(items_to_exclude), self.options, + + if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled: + items_to_exclude = [item for item in items_to_exclude + if item_table[item.name] not in items_by_group[Group.SEASON]] + + locations_count = len([location + for location in self.multiworld.get_locations(self.player) + if not location.event]) + + created_items = create_items(self.create_item, locations_count, items_to_exclude, self.options, self.multiworld.random) + self.multiworld.itempool += created_items - for item in items_to_exclude: - self.multiworld.itempool.remove(item) - - self.setup_season_events() + self.setup_early_items() + self.setup_month_events() self.setup_victory() - def set_rules(self): - set_rules(self.multiworld, self.player, self.options, self.logic, self.modified_bundles) + def precollect_starting_season(self) -> Optional[StardewItem]: + if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive: + return - def create_item(self, item: Union[str, ItemData]) -> StardewItem: - if isinstance(item, str): - item = item_table[item] + season_pool = items_by_group[Group.SEASON] - return StardewItem(item.name, item.classification, item.code, self.player) + if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled: + for season in season_pool: + self.multiworld.push_precollected(self.create_item(season)) + return - def setup_season_events(self): - self.multiworld.push_precollected(self.create_item("Spring")) - self.create_event_location(location_table["Summer"], self.logic.received("Spring"), "Summer") - self.create_event_location(location_table["Fall"], self.logic.received("Summer"), "Fall") - self.create_event_location(location_table["Winter"], self.logic.received("Fall"), "Winter") - self.create_event_location(location_table["Year Two"], self.logic.received("Winter"), "Year Two") + if [item for item in self.multiworld.precollected_items[self.player] + if item.name in {season.name for season in items_by_group[Group.SEASON]}]: + return + + if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_randomized_not_winter: + season_pool = [season for season in season_pool if season.name != "Winter"] + + starting_season = self.create_item(self.multiworld.random.choice(season_pool)) + self.multiworld.push_precollected(starting_season) + + def setup_early_items(self): + if (self.options[options.BuildingProgression] == + options.BuildingProgression.option_progressive_early_shipping_bin): + self.multiworld.early_items[self.player]["Shipping Bin"] = 1 + + if self.options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive: + self.multiworld.early_items[self.player]["Progressive Backpack"] = 1 + + def setup_month_events(self): + for i in range(0, 8): + month_end = LocationData(None, "Stardew Valley", f"Month End {i + 1}") + if i == 0: + self.create_event_location(month_end, True_(), "Month End") + continue + + self.create_event_location(month_end, self.logic.received("Month End", i).simplify(), "Month End") def setup_victory(self): if self.options[options.Goal] == options.Goal.option_community_center: @@ -142,16 +171,70 @@ class StardewValleyWorld(World): self.create_event_location(location_table["Catch Every Fish"], self.logic.can_catch_every_fish().simplify(), "Victory") + elif self.options[options.Goal] == options.Goal.option_complete_collection: + self.create_event_location(location_table["Complete the Museum Collection"], + self.logic.can_complete_museum().simplify(), + "Victory") + elif self.options[options.Goal] == options.Goal.option_full_house: + self.create_event_location(location_table["Full House"], + self.logic.can_have_two_children().simplify(), + "Victory") self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player) - def create_event_location(self, location_data: LocationData, rule: StardewRule, item: str): + def create_item(self, item: Union[str, ItemData]) -> StardewItem: + if isinstance(item, str): + item = item_table[item] + + return StardewItem(item.name, item.classification, item.code, self.player) + + def create_event_location(self, location_data: LocationData, rule: StardewRule, item: Optional[str] = None): + if item is None: + item = location_data.name + region = self.multiworld.get_region(location_data.region, self.player) location = StardewLocation(self.player, location_data.name, None, region) location.access_rule = rule region.locations.append(location) location.place_locked_item(self.create_item(item)) + def set_rules(self): + set_rules(self.multiworld, self.player, self.options, self.logic, self.modified_bundles) + self.force_first_month_once_all_early_items_are_found() + + def force_first_month_once_all_early_items_are_found(self): + """ + The Fill algorithm sweeps all event when calculating the early location. This causes an issue where + location only locked behind event are considered early, which they are not really... + + This patches the issue, by adding a dependency to the first month end on all early items, so all the locations + that depends on it will not be considered early. This requires at least one early item to be progression, or + it just won't work... + """ + + early_items = [] + for player, item_count in self.multiworld.early_items.items(): + for item, count in item_count.items(): + if self.multiworld.worlds[player].create_item(item).advancement: + early_items.append((player, item, count)) + + for item, count in self.multiworld.local_early_items[self.player].items(): + if self.create_item(item).advancement: + early_items.append((self.player, item, count)) + + def first_month_require_all_early_items(state: CollectionState) -> bool: + for player, item, count in early_items: + if not state.has(item, player, count): + return False + + return True + + first_month_end = self.multiworld.get_location("Month End 1", self.player) + set_rule(first_month_end, first_month_require_all_early_items) + + def generate_basic(self): + pass + def get_filler_item_name(self) -> str: return "Joja Cola" @@ -162,28 +245,16 @@ class StardewValleyWorld(World): key, value = self.modified_bundles[bundle_key].to_pair() modified_bundles[key] = value - return { - "starting_money": self.options[options.StartingMoney], - "entrance_randomization": self.options[options.EntranceRandomization], - "backpack_progression": self.options[options.BackpackProgression], - "tool_progression": self.options[options.ToolProgression], - "elevator_progression": self.options[options.TheMinesElevatorsProgression], - "skill_progression": self.options[options.SkillProgression], - "building_progression": self.options[options.BuildingProgression], - "arcade_machine_progression": self.options[options.ArcadeMachineLocations], - "help_wanted_locations": self.options[options.HelpWantedLocations], - "fishsanity": self.options[options.Fishsanity], - "death_link": self.options["death_link"], - "goal": self.options[options.Goal], + excluded_options = [options.ResourcePackMultiplier, options.BundleRandomization, options.BundlePrice, + options.NumberOfPlayerBuffs] + slot_data = dict(self.options.options) + for option in excluded_options: + slot_data.pop(option.internal_name) + slot_data.update({ "seed": self.multiworld.per_slot_randoms[self.player].randrange(1000000000), # Seed should be max 9 digits - "multiple_day_sleep_enabled": self.options[options.MultipleDaySleepEnabled], - "multiple_day_sleep_cost": self.options[options.MultipleDaySleepCost], - "experience_multiplier": self.options[options.ExperienceMultiplier], - "debris_multiplier": self.options[options.DebrisMultiplier], - "quick_start": self.options[options.QuickStart], - "gifting": self.options[options.Gifting], - "gift_tax": self.options[options.GiftTax], - "modified_bundles": modified_bundles, "randomized_entrances": self.randomized_entrances, - "client_version": "2.2.2", - } + "modified_bundles": modified_bundles, + "client_version": "3.0.0", + }) + + return slot_data diff --git a/worlds/stardew_valley/bundles.py b/worlds/stardew_valley/bundles.py index f87e3d6730..7cbb139237 100644 --- a/worlds/stardew_valley/bundles.py +++ b/worlds/stardew_valley/bundles.py @@ -1,7 +1,7 @@ from random import Random from typing import List, Dict, Union -from .bundle_data import * +from .data.bundle_data import * from .logic import StardewLogic from .options import BundleRandomization, BundlePrice diff --git a/worlds/stardew_valley/data/__init__.py b/worlds/stardew_valley/data/__init__.py index e69de29bb2..d14d9cfb8e 100644 --- a/worlds/stardew_valley/data/__init__.py +++ b/worlds/stardew_valley/data/__init__.py @@ -0,0 +1,2 @@ +from .crops_data import CropItem, SeedItem, all_crops, all_purchasable_seeds +from .fish_data import FishItem, all_fish diff --git a/worlds/stardew_valley/bundle_data.py b/worlds/stardew_valley/data/bundle_data.py similarity index 92% rename from worlds/stardew_valley/bundle_data.py rename to worlds/stardew_valley/data/bundle_data.py index cfc5d482ad..d82a632af5 100644 --- a/worlds/stardew_valley/bundle_data.py +++ b/worlds/stardew_valley/data/bundle_data.py @@ -1,14 +1,9 @@ from dataclasses import dataclass from . import fish_data +from .common_data import quality_dict from .game_item import GameItem - -quality_dict = { - 0: "", - 1: "Silver", - 2: "Gold", - 3: "Iridium" -} +from .museum_data import Mineral @dataclass(frozen=True) @@ -218,16 +213,16 @@ iridium_bar = BundleItem.item_bundle("Iridium Bar", 337, 1, 0) refined_quartz = BundleItem.item_bundle("Refined Quartz", 338, 2, 0) coal = BundleItem.item_bundle("Coal", 382, 5, 0) -quartz = BundleItem.item_bundle("Quartz", 80, 1, 0) -fire_quartz = BundleItem.item_bundle("Fire Quartz", 82, 1, 0) -frozen_tear = BundleItem.item_bundle("Frozen Tear", 84, 1, 0) -earth_crystal = BundleItem.item_bundle("Earth Crystal", 86, 1, 0) -emerald = BundleItem.item_bundle("Emerald", 60, 1, 0) -aquamarine = BundleItem.item_bundle("Aquamarine", 62, 1, 0) -ruby = BundleItem.item_bundle("Ruby", 64, 1, 0) -amethyst = BundleItem.item_bundle("Amethyst", 66, 1, 0) -topaz = BundleItem.item_bundle("Topaz", 68, 1, 0) -jade = BundleItem.item_bundle("Jade", 70, 1, 0) +quartz = BundleItem(Mineral.quartz, 1, 0) +fire_quartz = BundleItem(Mineral.fire_quartz, 1, 0) +frozen_tear = BundleItem(Mineral.frozen_tear, 1, 0) +earth_crystal = BundleItem(Mineral.earth_crystal, 1, 0) +emerald = BundleItem(Mineral.emerald, 1, 0) +aquamarine = BundleItem(Mineral.aquamarine, 1, 0) +ruby = BundleItem(Mineral.ruby, 1, 0) +amethyst = BundleItem(Mineral.amethyst, 1, 0) +topaz = BundleItem(Mineral.topaz, 1, 0) +jade = BundleItem(Mineral.jade, 1, 0) slime = BundleItem.item_bundle("Slime", 766, 99, 0) bug_meat = BundleItem.item_bundle("Bug Meat", 684, 10, 0) @@ -325,13 +320,12 @@ elvish_jewelry = BundleItem.item_bundle("Elvish Jewelry", 104, 1, 0) ancient_drum = BundleItem.item_bundle("Ancient Drum", 123, 1, 0) dried_starfish = BundleItem.item_bundle("Dried Starfish", 116, 1, 0) -# TODO Dye Bundle -dye_red_items = [cranberries, dwarf_scroll_1, hot_pepper, radish, rhubarb, spaghetti, strawberry, tomato, tulip] +dye_red_items = [cranberries, hot_pepper, radish, rhubarb, spaghetti, strawberry, tomato, tulip] dye_orange_items = [poppy, pumpkin, apricot, orange, spice_berry, winter_root] -dye_yellow_items = [dried_starfish, dwarf_scroll_4, elvish_jewelry, corn, parsnip, summer_spangle, sunflower] -dye_green_items = [dwarf_scroll_2, fiddlehead_fern, kale, artichoke, bok_choy, green_bean] -dye_blue_items = [blueberry, dwarf_scroll_3, blue_jazz, blackberry, crystal_fruit] -dye_purple_items = [ancient_drum, beet, crocus, eggplant, red_cabbage, sweet_pea] +dye_yellow_items = [corn, parsnip, summer_spangle, sunflower] +dye_green_items = [fiddlehead_fern, kale, artichoke, bok_choy, green_bean] +dye_blue_items = [blueberry, blue_jazz, blackberry, crystal_fruit] +dye_purple_items = [beet, crocus, eggplant, red_cabbage, sweet_pea] dye_items = [dye_red_items, dye_orange_items, dye_yellow_items, dye_green_items, dye_blue_items, dye_purple_items] field_research_items = [purple_mushroom, nautilus_shell, chub, geode, frozen_geode, magma_geode, omni_geode, rainbow_shell, amethyst, bream, carp] diff --git a/worlds/stardew_valley/data/common_data.py b/worlds/stardew_valley/data/common_data.py new file mode 100644 index 0000000000..8a2d0f5eec --- /dev/null +++ b/worlds/stardew_valley/data/common_data.py @@ -0,0 +1,9 @@ +fishing_chest = "Fishing Chest" +secret_note = "Secret Note" + +quality_dict = { + 0: "", + 1: "Silver", + 2: "Gold", + 3: "Iridium" +} diff --git a/worlds/stardew_valley/data/crops.csv b/worlds/stardew_valley/data/crops.csv new file mode 100644 index 0000000000..749a9c74b5 --- /dev/null +++ b/worlds/stardew_valley/data/crops.csv @@ -0,0 +1,37 @@ +crop,farm_growth_seasons,seed,seed_seasons,seed_regions +Amaranth,Fall,Amaranth Seeds,Fall,"Pierre's General Store,JojaMart" +Artichoke,Fall,Artichoke Seeds,Fall,"Pierre's General Store,JojaMart" +Beet,Fall,Beet Seeds,Fall,The Desert +Blue Jazz,Spring,Jazz Seeds,Spring,"Pierre's General Store,JojaMart" +Blueberry,Summer,Blueberry Seeds,Summer,"Pierre's General Store,JojaMart" +Bok Choy,Fall,Bok Choy Seeds,Fall,"Pierre's General Store,JojaMart" +Cactus Fruit,,Cactus Seeds,,The Desert +Cauliflower,Spring,Cauliflower Seeds,Spring,"Pierre's General Store,JojaMart" +Corn,"Summer,Fall",Corn Seeds,"Summer,Fall","Pierre's General Store,JojaMart" +Cranberries,Fall,Cranberry Seeds,Fall,"Pierre's General Store,JojaMart" +Eggplant,Fall,Eggplant Seeds,Fall,"Pierre's General Store,JojaMart" +Fairy Rose,Fall,Fairy Seeds,Fall,"Pierre's General Store,JojaMart" +Garlic,Spring,Garlic Seeds,Spring,"Pierre's General Store,JojaMart" +Grape,Fall,Grape Starter,Fall,"Pierre's General Store,JojaMart" +Green Bean,Spring,Bean Starter,Spring,"Pierre's General Store,JojaMart" +Hops,Summer,Hops Starter,Summer,"Pierre's General Store,JojaMart" +Hot Pepper,Summer,Pepper Seeds,Summer,"Pierre's General Store,JojaMart" +Kale,Spring,Kale Seeds,Spring,"Pierre's General Store,JojaMart" +Melon,Summer,Melon Seeds,Summer,"Pierre's General Store,JojaMart" +Parsnip,Spring,Parsnip Seeds,Spring,"Pierre's General Store,JojaMart" +Poppy,Summer,Poppy Seeds,Summer,"Pierre's General Store,JojaMart" +Potato,Spring,Potato Seeds,Spring,"Pierre's General Store,JojaMart" +Pumpkin,Fall,Pumpkin Seeds,Fall,"Pierre's General Store,JojaMart" +Radish,Summer,Radish Seeds,Summer,"Pierre's General Store,JojaMart" +Red Cabbage,Summer,Red Cabbage Seeds,Summer,"Pierre's General Store,JojaMart" +Rhubarb,Spring,Rhubarb Seeds,Spring,The Desert +Starfruit,Summer,Starfruit Seeds,Summer,The Desert +Strawberry,Spring,Strawberry Seeds,Spring,"Pierre's General Store,JojaMart" +Summer Spangle,Summer,Spangle Seeds,Summer,"Pierre's General Store,JojaMart" +Sunflower,"Summer,Fall",Sunflower Seeds,"Summer,Fall","Pierre's General Store,JojaMart" +Sweet Gem Berry,Fall,Rare Seed,"Spring,Summer",Traveling Cart +Tomato,Summer,Tomato Seeds,Summer,"Pierre's General Store,JojaMart" +Tulip,Spring,Tulip Bulb,Spring,"Pierre's General Store,JojaMart" +Unmilled Rice,Spring,Rice Shoot,Spring,"Pierre's General Store,JojaMart" +Wheat,"Summer,Fall",Wheat Seeds,"Summer,Fall","Pierre's General Store,JojaMart" +Yam,Fall,Yam Seeds,Fall,"Pierre's General Store,JojaMart" diff --git a/worlds/stardew_valley/data/crops_data.py b/worlds/stardew_valley/data/crops_data.py new file mode 100644 index 0000000000..f5e652b8d4 --- /dev/null +++ b/worlds/stardew_valley/data/crops_data.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass +from typing import List + + +@dataclass(frozen=True) +class SeedItem: + name: str + seasons: List[str] + regions: List[str] + + +@dataclass(frozen=True) +class CropItem: + name: str + farm_growth_seasons: List[str] + seed: SeedItem + + +def load_crop_csv(): + import csv + try: + from importlib.resources import files + except ImportError: + from importlib_resources import files # noqa + + with files(__package__).joinpath("crops.csv").open() as file: + reader = csv.DictReader(file) + crops = [] + seeds = [] + + for item in reader: + seeds.append(SeedItem(item["seed"], + [season for season in item["seed_seasons"].split(",")] + if item["seed_seasons"] else [], + [region for region in item["seed_regions"].split(",")] + if item["seed_regions"] else [])) + crops.append(CropItem(item["crop"], + [season for season in item["farm_growth_seasons"].split(",")] + if item["farm_growth_seasons"] else [], + seeds[-1])) + return crops, seeds + + +# TODO Those two should probably be split to we can include rest of seeds +all_crops, all_purchasable_seeds = load_crop_csv() diff --git a/worlds/stardew_valley/data/entrance_data.py b/worlds/stardew_valley/data/entrance_data.py new file mode 100644 index 0000000000..585668cd4f --- /dev/null +++ b/worlds/stardew_valley/data/entrance_data.py @@ -0,0 +1,2 @@ +class Entrance: + to_stardew_valley = "To Stardew Valley" diff --git a/worlds/stardew_valley/data/fish_data.py b/worlds/stardew_valley/data/fish_data.py new file mode 100644 index 0000000000..0ab967b102 --- /dev/null +++ b/worlds/stardew_valley/data/fish_data.py @@ -0,0 +1,124 @@ +from dataclasses import dataclass +from typing import List, Tuple, Union + +from . import season_data as season +from .game_item import GameItem +from .region_data import SVRegion + + +@dataclass(frozen=True) +class FishItem(GameItem): + locations: Tuple[str] + seasons: Tuple[str] + difficulty: int + + def __repr__(self): + return f"{self.name} [{self.item_id}] (Locations: {self.locations} |" \ + f" Seasons: {self.seasons} |" \ + f" Difficulty: {self.difficulty}) " + + +fresh_water = (SVRegion.farm, SVRegion.forest, SVRegion.town, SVRegion.mountain) +ocean = (SVRegion.beach,) +town_river = (SVRegion.town,) +mountain_lake = (SVRegion.mountain,) +forest_pond = (SVRegion.forest,) +forest_river = (SVRegion.forest,) +secret_woods = (SVRegion.secret_woods,) +mines_floor_20 = (SVRegion.mines_floor_20,) +mines_floor_60 = (SVRegion.mines_floor_60,) +mines_floor_100 = (SVRegion.mines_floor_100,) +sewers = (SVRegion.sewers,) +desert = (SVRegion.desert,) +mutant_bug_lair = (SVRegion.mutant_bug_lair,) +witch_swamp = (SVRegion.witch_swamp,) +night_market = (SVRegion.beach,) +ginger_island_ocean = (SVRegion.ginger_island,) +ginger_island_river = (SVRegion.ginger_island,) +pirate_cove = (SVRegion.pirate_cove,) + +all_fish: List[FishItem] = [] + + +def create_fish(name: str, item_id: int, locations: Tuple[str, ...], seasons: Union[str, Tuple[str, ...]], + difficulty: int) -> FishItem: + if isinstance(seasons, str): + seasons = (seasons,) + + fish_item = FishItem(name, item_id, locations, seasons, difficulty) + all_fish.append(fish_item) + return fish_item + + +albacore = create_fish("Albacore", 705, ocean, (season.fall, season.winter), 60) +anchovy = create_fish("Anchovy", 129, ocean, (season.spring, season.fall), 30) +blue_discus = create_fish("Blue Discus", 838, ginger_island_river, season.all_seasons, 60) +bream = create_fish("Bream", 132, town_river + forest_river, season.all_seasons, 35) +bullhead = create_fish("Bullhead", 700, mountain_lake, season.all_seasons, 46) +carp = create_fish("Carp", 142, mountain_lake + secret_woods + sewers + mutant_bug_lair, season.not_winter, 15) +catfish = create_fish("Catfish", 143, town_river + forest_river + secret_woods, (season.spring, season.fall), 75) +chub = create_fish("Chub", 702, forest_river + mountain_lake, season.all_seasons, 35) +dorado = create_fish("Dorado", 704, forest_river, season.summer, 78) +eel = create_fish("Eel", 148, ocean, (season.spring, season.fall), 70) +flounder = create_fish("Flounder", 267, ocean, (season.spring, season.summer), 50) +ghostfish = create_fish("Ghostfish", 156, mines_floor_20 + mines_floor_60, season.all_seasons, 50) +halibut = create_fish("Halibut", 708, ocean, season.not_fall, 50) +herring = create_fish("Herring", 147, ocean, (season.spring, season.winter), 25) +ice_pip = create_fish("Ice Pip", 161, mines_floor_60, season.all_seasons, 85) +largemouth_bass = create_fish("Largemouth Bass", 136, mountain_lake, season.all_seasons, 50) +lava_eel = create_fish("Lava Eel", 162, mines_floor_100, season.all_seasons, 90) +lingcod = create_fish("Lingcod", 707, town_river + forest_river + mountain_lake, season.winter, 85) +lionfish = create_fish("Lionfish", 837, ginger_island_ocean, season.all_seasons, 50) +midnight_carp = create_fish("Midnight Carp", 269, mountain_lake + forest_pond + ginger_island_river, + (season.fall, season.winter), 55) +octopus = create_fish("Octopus", 149, ocean, season.summer, 95) +perch = create_fish("Perch", 141, town_river + forest_river + forest_pond + mountain_lake, season.winter, 35) +pike = create_fish("Pike", 144, town_river + forest_river + forest_pond, (season.summer, season.winter), 60) +pufferfish = create_fish("Pufferfish", 128, ocean + ginger_island_ocean, season.summer, 80) +rainbow_trout = create_fish("Rainbow Trout", 138, town_river + forest_river + mountain_lake, season.summer, 45) +red_mullet = create_fish("Red Mullet", 146, ocean, (season.summer, season.winter), 55) +red_snapper = create_fish("Red Snapper", 150, ocean, (season.summer, season.fall), 40) +salmon = create_fish("Salmon", 139, town_river + forest_river, season.fall, 50) +sandfish = create_fish("Sandfish", 164, desert, season.all_seasons, 65) +sardine = create_fish("Sardine", 131, ocean, (season.spring, season.fall, season.winter), 30) +scorpion_carp = create_fish("Scorpion Carp", 165, desert, season.all_seasons, 90) +sea_cucumber = create_fish("Sea Cucumber", 154, ocean, (season.fall, season.winter), 40) +shad = create_fish("Shad", 706, town_river + forest_river, season.not_winter, 45) +slimejack = create_fish("Slimejack", 796, mutant_bug_lair, season.all_seasons, 55) +smallmouth_bass = create_fish("Smallmouth Bass", 137, town_river + forest_river, (season.spring, season.fall), 28) +squid = create_fish("Squid", 151, ocean, season.winter, 75) +stingray = create_fish("Stingray", 836, pirate_cove, season.all_seasons, 80) +stonefish = create_fish("Stonefish", 158, mines_floor_20, season.all_seasons, 65) +sturgeon = create_fish("Sturgeon", 698, mountain_lake, (season.summer, season.winter), 78) +sunfish = create_fish("Sunfish", 145, town_river + forest_river, (season.spring, season.summer), 30) +super_cucumber = create_fish("Super Cucumber", 155, ocean + ginger_island_ocean, (season.summer, season.fall), 80) +tiger_trout = create_fish("Tiger Trout", 699, town_river + forest_river, (season.fall, season.winter), 60) +tilapia = create_fish("Tilapia", 701, ocean + ginger_island_ocean, (season.summer, season.fall), 50) +tuna = create_fish("Tuna", 130, ocean + ginger_island_ocean, (season.summer, season.winter), 70) +void_salmon = create_fish("Void Salmon", 795, witch_swamp, season.all_seasons, 80) +walleye = create_fish("Walleye", 140, town_river + forest_river + forest_pond + mountain_lake, season.fall, 45) +woodskip = create_fish("Woodskip", 734, secret_woods, season.all_seasons, 50) + +blob_fish = create_fish("Blobfish", 800, night_market, season.winter, 75) +midnight_squid = create_fish("Midnight Squid", 798, night_market, season.winter, 55) +spook_fish = create_fish("Spook Fish", 799, night_market, season.winter, 60) + +angler = create_fish("Angler", 160, town_river, season.fall, 85) +crimsonfish = create_fish("Crimsonfish", 159, ocean, season.summer, 95) +glacierfish = create_fish("Glacierfish", 775, forest_river, season.winter, 100) +legend = create_fish("Legend", 163, mountain_lake, season.spring, 110) +mutant_carp = create_fish("Mutant Carp", 682, sewers, season.all_seasons, 80) + +clam = create_fish("Clam", 372, ocean, season.all_seasons, -1) +cockle = create_fish("Cockle", 718, ocean, season.all_seasons, -1) +crab = create_fish("Crab", 717, ocean, season.all_seasons, -1) +crayfish = create_fish("Crayfish", 716, fresh_water, season.all_seasons, -1) +lobster = create_fish("Lobster", 715, ocean, season.all_seasons, -1) +mussel = create_fish("Mussel", 719, ocean, season.all_seasons, -1) +oyster = create_fish("Oyster", 723, ocean, season.all_seasons, -1) +periwinkle = create_fish("Periwinkle", 722, fresh_water, season.all_seasons, -1) +shrimp = create_fish("Shrimp", 720, ocean, season.all_seasons, -1) +snail = create_fish("Snail", 721, fresh_water, season.all_seasons, -1) + +legendary_fish = [crimsonfish, angler, legend, glacierfish, mutant_carp] +special_fish = [*legendary_fish, blob_fish, lava_eel, octopus, scorpion_carp, ice_pip, super_cucumber, dorado] diff --git a/worlds/stardew_valley/data/game_item.py b/worlds/stardew_valley/data/game_item.py new file mode 100644 index 0000000000..cac86d527d --- /dev/null +++ b/worlds/stardew_valley/data/game_item.py @@ -0,0 +1,13 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class GameItem: + name: str + item_id: int + + def __repr__(self): + return f"{self.name} [{self.item_id}]" + + def __lt__(self, other): + return self.name < other.name diff --git a/worlds/stardew_valley/data/items.csv b/worlds/stardew_valley/data/items.csv index 425186ed4f..f2a4503455 100644 --- a/worlds/stardew_valley/data/items.csv +++ b/worlds/stardew_valley/data/items.csv @@ -1,7 +1,7 @@ id,name,classification,groups 0,Joja Cola,filler,TRASH -15,Rusty Key,progression, -16,Dwarvish Translation Guide,progression, +15,Rusty Key,progression,MUSEUM +16,Dwarvish Translation Guide,progression,MUSEUM 17,Bridge Repair,progression,COMMUNITY_REWARD 18,Greenhouse,progression,COMMUNITY_REWARD 19,Glittering Boulder Removed,progression,COMMUNITY_REWARD @@ -98,20 +98,99 @@ id,name,classification,groups 110,Luck Bonus,useful, 111,Lava Katana,progression,"MINES_FLOOR_110,WEAPON" 112,Progressive House,progression, -113,Traveling Merchant: Sunday,progression, -114,Traveling Merchant: Monday,progression, -115,Traveling Merchant: Tuesday,progression, -116,Traveling Merchant: Wednesday,progression, -117,Traveling Merchant: Thursday,progression, -118,Traveling Merchant: Friday,progression, -119,Traveling Merchant: Saturday,progression, +113,Traveling Merchant: Sunday,progression,TRAVELING_MERCHANT_DAY +114,Traveling Merchant: Monday,progression,TRAVELING_MERCHANT_DAY +115,Traveling Merchant: Tuesday,progression,TRAVELING_MERCHANT_DAY +116,Traveling Merchant: Wednesday,progression,TRAVELING_MERCHANT_DAY +117,Traveling Merchant: Thursday,progression,TRAVELING_MERCHANT_DAY +118,Traveling Merchant: Friday,progression,TRAVELING_MERCHANT_DAY +119,Traveling Merchant: Saturday,progression,TRAVELING_MERCHANT_DAY 120,Traveling Merchant Stock Size,progression, 121,Traveling Merchant Discount,progression, 122,Return Scepter,useful, -5000,Resource Pack: 500 Money,useful,"BASE_RESOURCE,RESOURCE_PACK" -5001,Resource Pack: 1000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK" -5002,Resource Pack: 1500 Money,useful,"BASE_RESOURCE,RESOURCE_PACK" -5003,Resource Pack: 2000 Money,useful,"BASE_RESOURCE,RESOURCE_PACK" +123,Progressive Season,progression, +124,Spring,progression,SEASON +125,Summer,progression,SEASON +126,Fall,progression,SEASON +127,Winter,progression,SEASON +128,Amaranth Seeds,progression,SEED_SHUFFLE +129,Artichoke Seeds,progression,SEED_SHUFFLE +130,Beet Seeds,progression,SEED_SHUFFLE +131,Jazz Seeds,progression,SEED_SHUFFLE +132,Blueberry Seeds,progression,SEED_SHUFFLE +133,Bok Choy Seeds,progression,SEED_SHUFFLE +134,Cauliflower Seeds,progression,SEED_SHUFFLE +135,Corn Seeds,progression,SEED_SHUFFLE +136,Cranberry Seeds,progression,SEED_SHUFFLE +137,Eggplant Seeds,progression,SEED_SHUFFLE +138,Fairy Seeds,progression,SEED_SHUFFLE +139,Garlic Seeds,progression,SEED_SHUFFLE +140,Grape Starter,progression,SEED_SHUFFLE +141,Bean Starter,progression,SEED_SHUFFLE +142,Hops Starter,progression,SEED_SHUFFLE +143,Pepper Seeds,progression,SEED_SHUFFLE +144,Kale Seeds,progression,SEED_SHUFFLE +145,Melon Seeds,progression,SEED_SHUFFLE +146,Parsnip Seeds,progression,SEED_SHUFFLE +147,Poppy Seeds,progression,SEED_SHUFFLE +148,Potato Seeds,progression,SEED_SHUFFLE +149,Pumpkin Seeds,progression,SEED_SHUFFLE +150,Radish Seeds,progression,SEED_SHUFFLE +151,Red Cabbage Seeds,progression,SEED_SHUFFLE +152,Rhubarb Seeds,progression,SEED_SHUFFLE +153,Starfruit Seeds,progression,SEED_SHUFFLE +154,Strawberry Seeds,progression,SEED_SHUFFLE +155,Spangle Seeds,progression,SEED_SHUFFLE +156,Sunflower Seeds,progression,SEED_SHUFFLE +157,Tomato Seeds,progression,SEED_SHUFFLE +158,Tulip Bulb,progression,SEED_SHUFFLE +159,Rice Shoot,progression,SEED_SHUFFLE +160,Wheat Seeds,progression,SEED_SHUFFLE +161,Yam Seeds,progression,SEED_SHUFFLE +162,Cactus Seeds,progression,SEED_SHUFFLE +163,Magic Rock Candy,useful,MUSEUM +164,Ancient Seeds Recipe,progression,MUSEUM +165,Ancient Seeds,useful,MUSEUM +166,Traveling Merchant Metal Detector,progression,MUSEUM +167,Alex: 1 <3,progression,FRIENDSANITY +168,Elliott: 1 <3,progression,FRIENDSANITY +169,Harvey: 1 <3,progression,FRIENDSANITY +170,Sam: 1 <3,progression,FRIENDSANITY +171,Sebastian: 1 <3,progression,FRIENDSANITY +172,Shane: 1 <3,progression,FRIENDSANITY +173,Abigail: 1 <3,progression,FRIENDSANITY +174,Emily: 1 <3,progression,FRIENDSANITY +175,Haley: 1 <3,progression,FRIENDSANITY +176,Leah: 1 <3,progression,FRIENDSANITY +177,Maru: 1 <3,progression,FRIENDSANITY +178,Penny: 1 <3,progression,FRIENDSANITY +179,Caroline: 1 <3,progression,FRIENDSANITY +180,Clint: 1 <3,progression,FRIENDSANITY +181,Demetrius: 1 <3,progression,FRIENDSANITY +182,Dwarf: 1 <3,progression,FRIENDSANITY +183,Evelyn: 1 <3,progression,FRIENDSANITY +184,George: 1 <3,progression,FRIENDSANITY +185,Gus: 1 <3,progression,FRIENDSANITY +186,Jas: 1 <3,progression,FRIENDSANITY +187,Jodi: 1 <3,progression,FRIENDSANITY +188,Kent: 1 <3,progression,FRIENDSANITY +189,Krobus: 1 <3,progression,FRIENDSANITY +190,Leo: 1 <3,progression,FRIENDSANITY +191,Lewis: 1 <3,progression,FRIENDSANITY +192,Linus: 1 <3,progression,FRIENDSANITY +193,Marnie: 1 <3,progression,FRIENDSANITY +194,Pam: 1 <3,progression,FRIENDSANITY +195,Pierre: 1 <3,progression,FRIENDSANITY +196,Robin: 1 <3,progression,FRIENDSANITY +197,Sandy: 1 <3,progression,FRIENDSANITY +198,Vincent: 1 <3,progression,FRIENDSANITY +199,Willy: 1 <3,progression,FRIENDSANITY +200,Wizard: 1 <3,progression,FRIENDSANITY +201,Pet: 1 <3,progression,FRIENDSANITY +5000,Resource Pack: 500 Money,filler,"BASE_RESOURCE,RESOURCE_PACK" +5001,Resource Pack: 1000 Money,filler,"BASE_RESOURCE,RESOURCE_PACK" +5002,Resource Pack: 1500 Money,filler,"BASE_RESOURCE,RESOURCE_PACK" +5003,Resource Pack: 2000 Money,filler,"BASE_RESOURCE,RESOURCE_PACK" 5004,Resource Pack: 25 Stone,filler,"BASE_RESOURCE,RESOURCE_PACK" 5005,Resource Pack: 50 Stone,filler,"BASE_RESOURCE,RESOURCE_PACK" 5006,Resource Pack: 75 Stone,filler,"BASE_RESOURCE,RESOURCE_PACK" @@ -120,10 +199,10 @@ id,name,classification,groups 5009,Resource Pack: 50 Wood,filler,"BASE_RESOURCE,RESOURCE_PACK" 5010,Resource Pack: 75 Wood,filler,"BASE_RESOURCE,RESOURCE_PACK" 5011,Resource Pack: 100 Wood,filler,"BASE_RESOURCE,RESOURCE_PACK" -5012,Resource Pack: 5 Hardwood,useful,"BASE_RESOURCE,RESOURCE_PACK" -5013,Resource Pack: 10 Hardwood,useful,"BASE_RESOURCE,RESOURCE_PACK" -5014,Resource Pack: 15 Hardwood,useful,"BASE_RESOURCE,RESOURCE_PACK" -5015,Resource Pack: 20 Hardwood,useful,"BASE_RESOURCE,RESOURCE_PACK" +5012,Resource Pack: 5 Hardwood,filler,"BASE_RESOURCE,RESOURCE_PACK" +5013,Resource Pack: 10 Hardwood,filler,"BASE_RESOURCE,RESOURCE_PACK" +5014,Resource Pack: 15 Hardwood,filler,"BASE_RESOURCE,RESOURCE_PACK" +5015,Resource Pack: 20 Hardwood,filler,"BASE_RESOURCE,RESOURCE_PACK" 5016,Resource Pack: 15 Fiber,filler,"BASE_RESOURCE,RESOURCE_PACK" 5017,Resource Pack: 30 Fiber,filler,"BASE_RESOURCE,RESOURCE_PACK" 5018,Resource Pack: 45 Fiber,filler,"BASE_RESOURCE,RESOURCE_PACK" @@ -178,10 +257,10 @@ id,name,classification,groups 5067,Resource Pack: 6 Magma Geode,filler,"GEODE,RESOURCE_PACK" 5068,Resource Pack: 9 Magma Geode,filler,"GEODE,RESOURCE_PACK" 5069,Resource Pack: 12 Magma Geode,filler,"GEODE,RESOURCE_PACK" -5070,Resource Pack: 2 Omni Geode,useful,"GEODE,RESOURCE_PACK" -5071,Resource Pack: 4 Omni Geode,useful,"GEODE,RESOURCE_PACK" -5072,Resource Pack: 6 Omni Geode,useful,"GEODE,RESOURCE_PACK" -5073,Resource Pack: 8 Omni Geode,useful,"GEODE,RESOURCE_PACK" +5070,Resource Pack: 2 Omni Geode,filler,"GEODE,RESOURCE_PACK" +5071,Resource Pack: 4 Omni Geode,filler,"GEODE,RESOURCE_PACK" +5072,Resource Pack: 6 Omni Geode,filler,"GEODE,RESOURCE_PACK" +5073,Resource Pack: 8 Omni Geode,filler,"GEODE,RESOURCE_PACK" 5074,Resource Pack: 25 Copper Ore,filler,"ORE,RESOURCE_PACK" 5075,Resource Pack: 50 Copper Ore,filler,"ORE,RESOURCE_PACK" 5076,Resource Pack: 75 Copper Ore,filler,"ORE,RESOURCE_PACK" @@ -192,14 +271,14 @@ id,name,classification,groups 5081,Resource Pack: 50 Iron Ore,filler,"ORE,RESOURCE_PACK" 5082,Resource Pack: 75 Iron Ore,filler,"ORE,RESOURCE_PACK" 5083,Resource Pack: 100 Iron Ore,filler,"ORE,RESOURCE_PACK" -5084,Resource Pack: 12 Gold Ore,useful,"ORE,RESOURCE_PACK" -5085,Resource Pack: 25 Gold Ore,useful,"ORE,RESOURCE_PACK" -5086,Resource Pack: 38 Gold Ore,useful,"ORE,RESOURCE_PACK" -5087,Resource Pack: 50 Gold Ore,useful,"ORE,RESOURCE_PACK" -5088,Resource Pack: 5 Iridium Ore,useful,"ORE,RESOURCE_PACK" -5089,Resource Pack: 10 Iridium Ore,useful,"ORE,RESOURCE_PACK" -5090,Resource Pack: 15 Iridium Ore,useful,"ORE,RESOURCE_PACK" -5091,Resource Pack: 20 Iridium Ore,useful,"ORE,RESOURCE_PACK" +5084,Resource Pack: 12 Gold Ore,filler,"ORE,RESOURCE_PACK" +5085,Resource Pack: 25 Gold Ore,filler,"ORE,RESOURCE_PACK" +5086,Resource Pack: 38 Gold Ore,filler,"ORE,RESOURCE_PACK" +5087,Resource Pack: 50 Gold Ore,filler,"ORE,RESOURCE_PACK" +5088,Resource Pack: 5 Iridium Ore,filler,"ORE,RESOURCE_PACK" +5089,Resource Pack: 10 Iridium Ore,filler,"ORE,RESOURCE_PACK" +5090,Resource Pack: 15 Iridium Ore,filler,"ORE,RESOURCE_PACK" +5091,Resource Pack: 20 Iridium Ore,filler,"ORE,RESOURCE_PACK" 5092,Resource Pack: 5 Quartz,filler,"ORE,RESOURCE_PACK" 5093,Resource Pack: 10 Quartz,filler,"ORE,RESOURCE_PACK" 5094,Resource Pack: 15 Quartz,filler,"ORE,RESOURCE_PACK" @@ -240,24 +319,24 @@ id,name,classification,groups 5129,Resource Pack: 28 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" 5130,Resource Pack: 36 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" 5131,Resource Pack: 40 Deluxe Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" -5132,Resource Pack: 2 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" -5133,Resource Pack: 6 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" -5134,Resource Pack: 10 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" -5135,Resource Pack: 14 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" -5136,Resource Pack: 18 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" -5137,Resource Pack: 20 Deluxe Fertilizer,useful,"FERTILIZER,RESOURCE_PACK" -5138,Resource Pack: 2 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" -5139,Resource Pack: 6 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" -5140,Resource Pack: 10 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" -5141,Resource Pack: 14 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" -5142,Resource Pack: 18 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" -5143,Resource Pack: 20 Deluxe Retaining Soil,useful,"FERTILIZER,RESOURCE_PACK" -5144,Resource Pack: 2 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" -5145,Resource Pack: 6 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" -5146,Resource Pack: 10 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" -5147,Resource Pack: 14 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" -5148,Resource Pack: 18 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" -5149,Resource Pack: 20 Hyper Speed-Gro,useful,"FERTILIZER,RESOURCE_PACK" +5132,Resource Pack: 2 Deluxe Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5133,Resource Pack: 6 Deluxe Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5134,Resource Pack: 10 Deluxe Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5135,Resource Pack: 14 Deluxe Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5136,Resource Pack: 18 Deluxe Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5137,Resource Pack: 20 Deluxe Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" +5138,Resource Pack: 2 Deluxe Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5139,Resource Pack: 6 Deluxe Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5140,Resource Pack: 10 Deluxe Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5141,Resource Pack: 14 Deluxe Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5142,Resource Pack: 18 Deluxe Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5143,Resource Pack: 20 Deluxe Retaining Soil,filler,"FERTILIZER,RESOURCE_PACK" +5144,Resource Pack: 2 Hyper Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5145,Resource Pack: 6 Hyper Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5146,Resource Pack: 10 Hyper Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5147,Resource Pack: 14 Hyper Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5148,Resource Pack: 18 Hyper Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" +5149,Resource Pack: 20 Hyper Speed-Gro,filler,"FERTILIZER,RESOURCE_PACK" 5150,Resource Pack: 2 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" 5151,Resource Pack: 6 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" 5152,Resource Pack: 10 Tree Fertilizer,filler,"FERTILIZER,RESOURCE_PACK" @@ -306,7 +385,7 @@ id,name,classification,groups 5195,Resource Pack: 4 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" 5196,Resource Pack: 5 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" 5197,Resource Pack: 6 Crab Pot,filler,"FISHING_RESOURCE,RESOURCE_PACK" -5198,Friendship Bonus (1 <3),useful,FRIENDSHIP_PACK -5199,Friendship Bonus (2 <3),useful,FRIENDSHIP_PACK -5200,Friendship Bonus (3 <3),useful,FRIENDSHIP_PACK -5201,Friendship Bonus (4 <3),useful,FRIENDSHIP_PACK +5198,Friendship Bonus (1 <3),filler,FRIENDSHIP_PACK +5199,Friendship Bonus (2 <3),filler,FRIENDSHIP_PACK +5200,Friendship Bonus (3 <3),filler,FRIENDSHIP_PACK +5201,Friendship Bonus (4 <3),filler,FRIENDSHIP_PACK diff --git a/worlds/stardew_valley/data/locations.csv b/worlds/stardew_valley/data/locations.csv index abad3c042d..b36096e6f6 100644 --- a/worlds/stardew_valley/data/locations.csv +++ b/worlds/stardew_valley/data/locations.csv @@ -377,3 +377,534 @@ id,region,name,tags 1063,Beach,Fishsanity: Mussel,FISHSANITY 1064,Beach,Fishsanity: Shrimp,FISHSANITY 1065,Beach,Fishsanity: Oyster,FISHSANITY +1100,Stardew Valley,Museumsanity: 5 Donations,MUSEUM_MILESTONES +1101,Stardew Valley,Museumsanity: 10 Donations,MUSEUM_MILESTONES +1102,Stardew Valley,Museumsanity: 15 Donations,MUSEUM_MILESTONES +1103,Stardew Valley,Museumsanity: 20 Donations,MUSEUM_MILESTONES +1104,Stardew Valley,Museumsanity: 25 Donations,MUSEUM_MILESTONES +1105,Stardew Valley,Museumsanity: 30 Donations,MUSEUM_MILESTONES +1106,Stardew Valley,Museumsanity: 35 Donations,MUSEUM_MILESTONES +1107,Stardew Valley,Museumsanity: 40 Donations,MUSEUM_MILESTONES +1108,Stardew Valley,Museumsanity: 50 Donations,MUSEUM_MILESTONES +1109,Stardew Valley,Museumsanity: 60 Donations,MUSEUM_MILESTONES +1110,Stardew Valley,Museumsanity: 70 Donations,MUSEUM_MILESTONES +1111,Stardew Valley,Museumsanity: 80 Donations,MUSEUM_MILESTONES +1112,Stardew Valley,Museumsanity: 90 Donations,MUSEUM_MILESTONES +1113,Stardew Valley,Museumsanity: 95 Donations,MUSEUM_MILESTONES +1114,Stardew Valley,Museumsanity: 11 Minerals,MUSEUM_MILESTONES +1115,Stardew Valley,Museumsanity: 21 Minerals,MUSEUM_MILESTONES +1116,Stardew Valley,Museumsanity: 31 Minerals,MUSEUM_MILESTONES +1117,Stardew Valley,Museumsanity: 41 Minerals,MUSEUM_MILESTONES +1118,Stardew Valley,Museumsanity: 50 Minerals,MUSEUM_MILESTONES +1119,Stardew Valley,Museumsanity: 3 Artifacts,MUSEUM_MILESTONES +1120,Stardew Valley,Museumsanity: 6 Artifacts,MUSEUM_MILESTONES +1121,Stardew Valley,Museumsanity: 9 Artifacts,MUSEUM_MILESTONES +1122,Stardew Valley,Museumsanity: 11 Artifacts,MUSEUM_MILESTONES +1123,Stardew Valley,Museumsanity: 15 Artifacts,MUSEUM_MILESTONES +1124,Stardew Valley,Museumsanity: 20 Artifacts,MUSEUM_MILESTONES +1125,Stardew Valley,Museumsanity: Dwarf Scrolls,MUSEUM_MILESTONES +1126,Stardew Valley,Museumsanity: Skeleton Front,MUSEUM_MILESTONES +1127,Stardew Valley,Museumsanity: Skeleton Middle,MUSEUM_MILESTONES +1128,Stardew Valley,Museumsanity: Skeleton Back,MUSEUM_MILESTONES +1201,The Mines - Floor 20,Museumsanity: Dwarf Scroll I,MUSEUM_DONATIONS +1202,The Mines - Floor 20,Museumsanity: Dwarf Scroll II,MUSEUM_DONATIONS +1203,The Mines - Floor 60,Museumsanity: Dwarf Scroll III,MUSEUM_DONATIONS +1204,The Mines - Floor 100,Museumsanity: Dwarf Scroll IV,MUSEUM_DONATIONS +1205,Town,Museumsanity: Chipped Amphora,MUSEUM_DONATIONS +1206,Forest,Museumsanity: Arrowhead,MUSEUM_DONATIONS +1207,Forest,Museumsanity: Ancient Doll,MUSEUM_DONATIONS +1208,Forest,Museumsanity: Elvish Jewelry,MUSEUM_DONATIONS +1209,Forest,Museumsanity: Chewing Stick,MUSEUM_DONATIONS +1210,Forest,Museumsanity: Ornamental Fan,MUSEUM_DONATIONS +1211,Mountain,Museumsanity: Dinosaur Egg,MUSEUM_DONATIONS +1212,Stardew Valley,Museumsanity: Rare Disc,MUSEUM_DONATIONS +1213,Forest,Museumsanity: Ancient Sword,MUSEUM_DONATIONS +1214,Town,Museumsanity: Rusty Spoon,MUSEUM_DONATIONS +1215,Farm,Museumsanity: Rusty Spur,MUSEUM_DONATIONS +1216,Mountain,Museumsanity: Rusty Cog,MUSEUM_DONATIONS +1217,Farm,Museumsanity: Chicken Statue,MUSEUM_DONATIONS +1218,Forest,Museumsanity: Ancient Seed,"MUSEUM_DONATIONS,MUSEUM_MILESTONES" +1219,Forest,Museumsanity: Prehistoric Tool,MUSEUM_DONATIONS +1220,Beach,Museumsanity: Dried Starfish,MUSEUM_DONATIONS +1221,Beach,Museumsanity: Anchor,MUSEUM_DONATIONS +1222,Beach,Museumsanity: Glass Shards,MUSEUM_DONATIONS +1223,Forest,Museumsanity: Bone Flute,MUSEUM_DONATIONS +1224,Forest,Museumsanity: Prehistoric Handaxe,MUSEUM_DONATIONS +1225,The Mines - Floor 20,Museumsanity: Dwarvish Helm,MUSEUM_DONATIONS +1226,The Mines - Floor 60,Museumsanity: Dwarf Gadget,MUSEUM_DONATIONS +1227,Forest,Museumsanity: Ancient Drum,MUSEUM_DONATIONS +1228,The Desert,Museumsanity: Golden Mask,MUSEUM_DONATIONS +1229,The Desert,Museumsanity: Golden Relic,MUSEUM_DONATIONS +1230,Town,Museumsanity: Strange Doll (Green),MUSEUM_DONATIONS +1231,The Desert,Museumsanity: Strange Doll,MUSEUM_DONATIONS +1232,Forest,Museumsanity: Prehistoric Scapula,MUSEUM_DONATIONS +1233,Forest,Museumsanity: Prehistoric Tibia,MUSEUM_DONATIONS +1234,Ginger Island,Museumsanity: Prehistoric Skull,MUSEUM_DONATIONS +1235,Ginger Island,Museumsanity: Skeletal Hand,MUSEUM_DONATIONS +1236,Ginger Island,Museumsanity: Prehistoric Rib,MUSEUM_DONATIONS +1237,Ginger Island,Museumsanity: Prehistoric Vertebra,MUSEUM_DONATIONS +1238,Ginger Island,Museumsanity: Skeletal Tail,MUSEUM_DONATIONS +1239,Ginger Island,Museumsanity: Nautilus Fossil,MUSEUM_DONATIONS +1240,Forest,Museumsanity: Amphibian Fossil,MUSEUM_DONATIONS +1241,Forest,Museumsanity: Palm Fossil,MUSEUM_DONATIONS +1242,Forest,Museumsanity: Trilobite,MUSEUM_DONATIONS +1243,The Mines - Floor 20,Museumsanity: Quartz,MUSEUM_DONATIONS +1244,The Mines - Floor 100,Museumsanity: Fire Quartz,MUSEUM_DONATIONS +1245,The Mines - Floor 60,Museumsanity: Frozen Tear,MUSEUM_DONATIONS +1246,The Mines - Floor 20,Museumsanity: Earth Crystal,MUSEUM_DONATIONS +1247,The Mines - Floor 100,Museumsanity: Emerald,MUSEUM_DONATIONS +1248,The Mines - Floor 60,Museumsanity: Aquamarine,MUSEUM_DONATIONS +1249,The Mines - Floor 100,Museumsanity: Ruby,MUSEUM_DONATIONS +1250,The Mines - Floor 20,Museumsanity: Amethyst,MUSEUM_DONATIONS +1251,The Mines - Floor 20,Museumsanity: Topaz,MUSEUM_DONATIONS +1252,The Mines - Floor 60,Museumsanity: Jade,MUSEUM_DONATIONS +1253,The Mines - Floor 60,Museumsanity: Diamond,MUSEUM_DONATIONS +1254,Skull Cavern Floor 100,Museumsanity: Prismatic Shard,MUSEUM_DONATIONS +1255,Town,Museumsanity: Alamite,MUSEUM_DONATIONS +1256,Town,Museumsanity: Bixite,MUSEUM_DONATIONS +1257,Town,Museumsanity: Baryte,MUSEUM_DONATIONS +1258,Town,Museumsanity: Aerinite,MUSEUM_DONATIONS +1259,Town,Museumsanity: Calcite,MUSEUM_DONATIONS +1260,Town,Museumsanity: Dolomite,MUSEUM_DONATIONS +1261,Town,Museumsanity: Esperite,MUSEUM_DONATIONS +1262,Town,Museumsanity: Fluorapatite,MUSEUM_DONATIONS +1263,Town,Museumsanity: Geminite,MUSEUM_DONATIONS +1264,Town,Museumsanity: Helvite,MUSEUM_DONATIONS +1265,Town,Museumsanity: Jamborite,MUSEUM_DONATIONS +1266,Town,Museumsanity: Jagoite,MUSEUM_DONATIONS +1267,Town,Museumsanity: Kyanite,MUSEUM_DONATIONS +1268,Town,Museumsanity: Lunarite,MUSEUM_DONATIONS +1269,Town,Museumsanity: Malachite,MUSEUM_DONATIONS +1270,Town,Museumsanity: Neptunite,MUSEUM_DONATIONS +1271,Town,Museumsanity: Lemon Stone,MUSEUM_DONATIONS +1272,Town,Museumsanity: Nekoite,MUSEUM_DONATIONS +1273,Town,Museumsanity: Orpiment,MUSEUM_DONATIONS +1274,Town,Museumsanity: Petrified Slime,MUSEUM_DONATIONS +1275,Town,Museumsanity: Thunder Egg,MUSEUM_DONATIONS +1276,Town,Museumsanity: Pyrite,MUSEUM_DONATIONS +1277,Town,Museumsanity: Ocean Stone,MUSEUM_DONATIONS +1278,Town,Museumsanity: Ghost Crystal,MUSEUM_DONATIONS +1279,Town,Museumsanity: Tigerseye,MUSEUM_DONATIONS +1280,Town,Museumsanity: Jasper,MUSEUM_DONATIONS +1281,Town,Museumsanity: Opal,MUSEUM_DONATIONS +1282,Town,Museumsanity: Fire Opal,MUSEUM_DONATIONS +1283,Town,Museumsanity: Celestine,MUSEUM_DONATIONS +1284,Town,Museumsanity: Marble,MUSEUM_DONATIONS +1285,Town,Museumsanity: Sandstone,MUSEUM_DONATIONS +1286,Town,Museumsanity: Granite,MUSEUM_DONATIONS +1287,Town,Museumsanity: Basalt,MUSEUM_DONATIONS +1288,Town,Museumsanity: Limestone,MUSEUM_DONATIONS +1289,Town,Museumsanity: Soapstone,MUSEUM_DONATIONS +1290,Town,Museumsanity: Hematite,MUSEUM_DONATIONS +1291,Town,Museumsanity: Mudstone,MUSEUM_DONATIONS +1292,Town,Museumsanity: Obsidian,MUSEUM_DONATIONS +1293,Town,Museumsanity: Slate,MUSEUM_DONATIONS +1294,Town,Museumsanity: Fairy Stone,MUSEUM_DONATIONS +1295,Town,Museumsanity: Star Shards,MUSEUM_DONATIONS +1301,Town,Friendsanity: Alex 1 <3,FRIENDSANITY +1302,Town,Friendsanity: Alex 2 <3,FRIENDSANITY +1303,Town,Friendsanity: Alex 3 <3,FRIENDSANITY +1304,Town,Friendsanity: Alex 4 <3,FRIENDSANITY +1305,Town,Friendsanity: Alex 5 <3,FRIENDSANITY +1306,Town,Friendsanity: Alex 6 <3,FRIENDSANITY +1307,Town,Friendsanity: Alex 7 <3,FRIENDSANITY +1308,Town,Friendsanity: Alex 8 <3,FRIENDSANITY +1309,Town,Friendsanity: Alex 9 <3,FRIENDSANITY +1310,Town,Friendsanity: Alex 10 <3,FRIENDSANITY +1311,Town,Friendsanity: Alex 11 <3,FRIENDSANITY +1312,Town,Friendsanity: Alex 12 <3,FRIENDSANITY +1313,Town,Friendsanity: Alex 13 <3,FRIENDSANITY +1314,Town,Friendsanity: Alex 14 <3,FRIENDSANITY +1315,Beach,Friendsanity: Elliott 1 <3,FRIENDSANITY +1316,Beach,Friendsanity: Elliott 2 <3,FRIENDSANITY +1317,Beach,Friendsanity: Elliott 3 <3,FRIENDSANITY +1318,Beach,Friendsanity: Elliott 4 <3,FRIENDSANITY +1319,Beach,Friendsanity: Elliott 5 <3,FRIENDSANITY +1320,Beach,Friendsanity: Elliott 6 <3,FRIENDSANITY +1321,Beach,Friendsanity: Elliott 7 <3,FRIENDSANITY +1322,Beach,Friendsanity: Elliott 8 <3,FRIENDSANITY +1323,Beach,Friendsanity: Elliott 9 <3,FRIENDSANITY +1324,Beach,Friendsanity: Elliott 10 <3,FRIENDSANITY +1325,Beach,Friendsanity: Elliott 11 <3,FRIENDSANITY +1326,Beach,Friendsanity: Elliott 12 <3,FRIENDSANITY +1327,Beach,Friendsanity: Elliott 13 <3,FRIENDSANITY +1328,Beach,Friendsanity: Elliott 14 <3,FRIENDSANITY +1329,Town,Friendsanity: Harvey 1 <3,FRIENDSANITY +1330,Town,Friendsanity: Harvey 2 <3,FRIENDSANITY +1331,Town,Friendsanity: Harvey 3 <3,FRIENDSANITY +1332,Town,Friendsanity: Harvey 4 <3,FRIENDSANITY +1333,Town,Friendsanity: Harvey 5 <3,FRIENDSANITY +1334,Town,Friendsanity: Harvey 6 <3,FRIENDSANITY +1335,Town,Friendsanity: Harvey 7 <3,FRIENDSANITY +1336,Town,Friendsanity: Harvey 8 <3,FRIENDSANITY +1337,Town,Friendsanity: Harvey 9 <3,FRIENDSANITY +1338,Town,Friendsanity: Harvey 10 <3,FRIENDSANITY +1339,Town,Friendsanity: Harvey 11 <3,FRIENDSANITY +1340,Town,Friendsanity: Harvey 12 <3,FRIENDSANITY +1341,Town,Friendsanity: Harvey 13 <3,FRIENDSANITY +1342,Town,Friendsanity: Harvey 14 <3,FRIENDSANITY +1343,Town,Friendsanity: Sam 1 <3,FRIENDSANITY +1344,Town,Friendsanity: Sam 2 <3,FRIENDSANITY +1345,Town,Friendsanity: Sam 3 <3,FRIENDSANITY +1346,Town,Friendsanity: Sam 4 <3,FRIENDSANITY +1347,Town,Friendsanity: Sam 5 <3,FRIENDSANITY +1348,Town,Friendsanity: Sam 6 <3,FRIENDSANITY +1349,Town,Friendsanity: Sam 7 <3,FRIENDSANITY +1350,Town,Friendsanity: Sam 8 <3,FRIENDSANITY +1351,Town,Friendsanity: Sam 9 <3,FRIENDSANITY +1352,Town,Friendsanity: Sam 10 <3,FRIENDSANITY +1353,Town,Friendsanity: Sam 11 <3,FRIENDSANITY +1354,Town,Friendsanity: Sam 12 <3,FRIENDSANITY +1355,Town,Friendsanity: Sam 13 <3,FRIENDSANITY +1356,Town,Friendsanity: Sam 14 <3,FRIENDSANITY +1357,Carpenter Shop,Friendsanity: Sebastian 1 <3,FRIENDSANITY +1358,Carpenter Shop,Friendsanity: Sebastian 2 <3,FRIENDSANITY +1359,Carpenter Shop,Friendsanity: Sebastian 3 <3,FRIENDSANITY +1360,Carpenter Shop,Friendsanity: Sebastian 4 <3,FRIENDSANITY +1361,Carpenter Shop,Friendsanity: Sebastian 5 <3,FRIENDSANITY +1362,Carpenter Shop,Friendsanity: Sebastian 6 <3,FRIENDSANITY +1363,Carpenter Shop,Friendsanity: Sebastian 7 <3,FRIENDSANITY +1364,Carpenter Shop,Friendsanity: Sebastian 8 <3,FRIENDSANITY +1365,Carpenter Shop,Friendsanity: Sebastian 9 <3,FRIENDSANITY +1366,Carpenter Shop,Friendsanity: Sebastian 10 <3,FRIENDSANITY +1367,Carpenter Shop,Friendsanity: Sebastian 11 <3,FRIENDSANITY +1368,Carpenter Shop,Friendsanity: Sebastian 12 <3,FRIENDSANITY +1369,Carpenter Shop,Friendsanity: Sebastian 13 <3,FRIENDSANITY +1370,Carpenter Shop,Friendsanity: Sebastian 14 <3,FRIENDSANITY +1371,Marnie's Ranch,Friendsanity: Shane 1 <3,FRIENDSANITY +1372,Marnie's Ranch,Friendsanity: Shane 2 <3,FRIENDSANITY +1373,Marnie's Ranch,Friendsanity: Shane 3 <3,FRIENDSANITY +1374,Marnie's Ranch,Friendsanity: Shane 4 <3,FRIENDSANITY +1375,Marnie's Ranch,Friendsanity: Shane 5 <3,FRIENDSANITY +1376,Marnie's Ranch,Friendsanity: Shane 6 <3,FRIENDSANITY +1377,Marnie's Ranch,Friendsanity: Shane 7 <3,FRIENDSANITY +1378,Marnie's Ranch,Friendsanity: Shane 8 <3,FRIENDSANITY +1379,Marnie's Ranch,Friendsanity: Shane 9 <3,FRIENDSANITY +1380,Marnie's Ranch,Friendsanity: Shane 10 <3,FRIENDSANITY +1381,Marnie's Ranch,Friendsanity: Shane 11 <3,FRIENDSANITY +1382,Marnie's Ranch,Friendsanity: Shane 12 <3,FRIENDSANITY +1383,Marnie's Ranch,Friendsanity: Shane 13 <3,FRIENDSANITY +1384,Marnie's Ranch,Friendsanity: Shane 14 <3,FRIENDSANITY +1385,Town,Friendsanity: Abigail 1 <3,FRIENDSANITY +1386,Town,Friendsanity: Abigail 2 <3,FRIENDSANITY +1387,Town,Friendsanity: Abigail 3 <3,FRIENDSANITY +1388,Town,Friendsanity: Abigail 4 <3,FRIENDSANITY +1389,Town,Friendsanity: Abigail 5 <3,FRIENDSANITY +1390,Town,Friendsanity: Abigail 6 <3,FRIENDSANITY +1391,Town,Friendsanity: Abigail 7 <3,FRIENDSANITY +1392,Town,Friendsanity: Abigail 8 <3,FRIENDSANITY +1393,Town,Friendsanity: Abigail 9 <3,FRIENDSANITY +1394,Town,Friendsanity: Abigail 10 <3,FRIENDSANITY +1395,Town,Friendsanity: Abigail 11 <3,FRIENDSANITY +1396,Town,Friendsanity: Abigail 12 <3,FRIENDSANITY +1397,Town,Friendsanity: Abigail 13 <3,FRIENDSANITY +1398,Town,Friendsanity: Abigail 14 <3,FRIENDSANITY +1399,Town,Friendsanity: Emily 1 <3,FRIENDSANITY +1400,Town,Friendsanity: Emily 2 <3,FRIENDSANITY +1401,Town,Friendsanity: Emily 3 <3,FRIENDSANITY +1402,Town,Friendsanity: Emily 4 <3,FRIENDSANITY +1403,Town,Friendsanity: Emily 5 <3,FRIENDSANITY +1404,Town,Friendsanity: Emily 6 <3,FRIENDSANITY +1405,Town,Friendsanity: Emily 7 <3,FRIENDSANITY +1406,Town,Friendsanity: Emily 8 <3,FRIENDSANITY +1407,Town,Friendsanity: Emily 9 <3,FRIENDSANITY +1408,Town,Friendsanity: Emily 10 <3,FRIENDSANITY +1409,Town,Friendsanity: Emily 11 <3,FRIENDSANITY +1410,Town,Friendsanity: Emily 12 <3,FRIENDSANITY +1411,Town,Friendsanity: Emily 13 <3,FRIENDSANITY +1412,Town,Friendsanity: Emily 14 <3,FRIENDSANITY +1413,Town,Friendsanity: Haley 1 <3,FRIENDSANITY +1414,Town,Friendsanity: Haley 2 <3,FRIENDSANITY +1415,Town,Friendsanity: Haley 3 <3,FRIENDSANITY +1416,Town,Friendsanity: Haley 4 <3,FRIENDSANITY +1417,Town,Friendsanity: Haley 5 <3,FRIENDSANITY +1418,Town,Friendsanity: Haley 6 <3,FRIENDSANITY +1419,Town,Friendsanity: Haley 7 <3,FRIENDSANITY +1420,Town,Friendsanity: Haley 8 <3,FRIENDSANITY +1421,Town,Friendsanity: Haley 9 <3,FRIENDSANITY +1422,Town,Friendsanity: Haley 10 <3,FRIENDSANITY +1423,Town,Friendsanity: Haley 11 <3,FRIENDSANITY +1424,Town,Friendsanity: Haley 12 <3,FRIENDSANITY +1425,Town,Friendsanity: Haley 13 <3,FRIENDSANITY +1426,Town,Friendsanity: Haley 14 <3,FRIENDSANITY +1427,Forest,Friendsanity: Leah 1 <3,FRIENDSANITY +1428,Forest,Friendsanity: Leah 2 <3,FRIENDSANITY +1429,Forest,Friendsanity: Leah 3 <3,FRIENDSANITY +1430,Forest,Friendsanity: Leah 4 <3,FRIENDSANITY +1431,Forest,Friendsanity: Leah 5 <3,FRIENDSANITY +1432,Forest,Friendsanity: Leah 6 <3,FRIENDSANITY +1433,Forest,Friendsanity: Leah 7 <3,FRIENDSANITY +1434,Forest,Friendsanity: Leah 8 <3,FRIENDSANITY +1435,Forest,Friendsanity: Leah 9 <3,FRIENDSANITY +1436,Forest,Friendsanity: Leah 10 <3,FRIENDSANITY +1437,Forest,Friendsanity: Leah 11 <3,FRIENDSANITY +1438,Forest,Friendsanity: Leah 12 <3,FRIENDSANITY +1439,Forest,Friendsanity: Leah 13 <3,FRIENDSANITY +1440,Forest,Friendsanity: Leah 14 <3,FRIENDSANITY +1441,Carpenter Shop,Friendsanity: Maru 1 <3,FRIENDSANITY +1442,Carpenter Shop,Friendsanity: Maru 2 <3,FRIENDSANITY +1443,Carpenter Shop,Friendsanity: Maru 3 <3,FRIENDSANITY +1444,Carpenter Shop,Friendsanity: Maru 4 <3,FRIENDSANITY +1445,Carpenter Shop,Friendsanity: Maru 5 <3,FRIENDSANITY +1446,Carpenter Shop,Friendsanity: Maru 6 <3,FRIENDSANITY +1447,Carpenter Shop,Friendsanity: Maru 7 <3,FRIENDSANITY +1448,Carpenter Shop,Friendsanity: Maru 8 <3,FRIENDSANITY +1449,Carpenter Shop,Friendsanity: Maru 9 <3,FRIENDSANITY +1450,Carpenter Shop,Friendsanity: Maru 10 <3,FRIENDSANITY +1451,Carpenter Shop,Friendsanity: Maru 11 <3,FRIENDSANITY +1452,Carpenter Shop,Friendsanity: Maru 12 <3,FRIENDSANITY +1453,Carpenter Shop,Friendsanity: Maru 13 <3,FRIENDSANITY +1454,Carpenter Shop,Friendsanity: Maru 14 <3,FRIENDSANITY +1455,Town,Friendsanity: Penny 1 <3,FRIENDSANITY +1456,Town,Friendsanity: Penny 2 <3,FRIENDSANITY +1457,Town,Friendsanity: Penny 3 <3,FRIENDSANITY +1458,Town,Friendsanity: Penny 4 <3,FRIENDSANITY +1459,Town,Friendsanity: Penny 5 <3,FRIENDSANITY +1460,Town,Friendsanity: Penny 6 <3,FRIENDSANITY +1461,Town,Friendsanity: Penny 7 <3,FRIENDSANITY +1462,Town,Friendsanity: Penny 8 <3,FRIENDSANITY +1463,Town,Friendsanity: Penny 9 <3,FRIENDSANITY +1464,Town,Friendsanity: Penny 10 <3,FRIENDSANITY +1465,Town,Friendsanity: Penny 11 <3,FRIENDSANITY +1466,Town,Friendsanity: Penny 12 <3,FRIENDSANITY +1467,Town,Friendsanity: Penny 13 <3,FRIENDSANITY +1468,Town,Friendsanity: Penny 14 <3,FRIENDSANITY +1469,Town,Friendsanity: Caroline 1 <3,FRIENDSANITY +1470,Town,Friendsanity: Caroline 2 <3,FRIENDSANITY +1471,Town,Friendsanity: Caroline 3 <3,FRIENDSANITY +1472,Town,Friendsanity: Caroline 4 <3,FRIENDSANITY +1473,Town,Friendsanity: Caroline 5 <3,FRIENDSANITY +1474,Town,Friendsanity: Caroline 6 <3,FRIENDSANITY +1475,Town,Friendsanity: Caroline 7 <3,FRIENDSANITY +1476,Town,Friendsanity: Caroline 8 <3,FRIENDSANITY +1477,Town,Friendsanity: Caroline 9 <3,FRIENDSANITY +1478,Town,Friendsanity: Caroline 10 <3,FRIENDSANITY +1480,Town,Friendsanity: Clint 1 <3,FRIENDSANITY +1481,Town,Friendsanity: Clint 2 <3,FRIENDSANITY +1482,Town,Friendsanity: Clint 3 <3,FRIENDSANITY +1483,Town,Friendsanity: Clint 4 <3,FRIENDSANITY +1484,Town,Friendsanity: Clint 5 <3,FRIENDSANITY +1485,Town,Friendsanity: Clint 6 <3,FRIENDSANITY +1486,Town,Friendsanity: Clint 7 <3,FRIENDSANITY +1487,Town,Friendsanity: Clint 8 <3,FRIENDSANITY +1488,Town,Friendsanity: Clint 9 <3,FRIENDSANITY +1489,Town,Friendsanity: Clint 10 <3,FRIENDSANITY +1491,Carpenter Shop,Friendsanity: Demetrius 1 <3,FRIENDSANITY +1492,Carpenter Shop,Friendsanity: Demetrius 2 <3,FRIENDSANITY +1493,Carpenter Shop,Friendsanity: Demetrius 3 <3,FRIENDSANITY +1494,Carpenter Shop,Friendsanity: Demetrius 4 <3,FRIENDSANITY +1495,Carpenter Shop,Friendsanity: Demetrius 5 <3,FRIENDSANITY +1496,Carpenter Shop,Friendsanity: Demetrius 6 <3,FRIENDSANITY +1497,Carpenter Shop,Friendsanity: Demetrius 7 <3,FRIENDSANITY +1498,Carpenter Shop,Friendsanity: Demetrius 8 <3,FRIENDSANITY +1499,Carpenter Shop,Friendsanity: Demetrius 9 <3,FRIENDSANITY +1500,Carpenter Shop,Friendsanity: Demetrius 10 <3,FRIENDSANITY +1502,The Mines,Friendsanity: Dwarf 1 <3,FRIENDSANITY +1503,The Mines,Friendsanity: Dwarf 2 <3,FRIENDSANITY +1504,The Mines,Friendsanity: Dwarf 3 <3,FRIENDSANITY +1505,The Mines,Friendsanity: Dwarf 4 <3,FRIENDSANITY +1506,The Mines,Friendsanity: Dwarf 5 <3,FRIENDSANITY +1507,The Mines,Friendsanity: Dwarf 6 <3,FRIENDSANITY +1508,The Mines,Friendsanity: Dwarf 7 <3,FRIENDSANITY +1509,The Mines,Friendsanity: Dwarf 8 <3,FRIENDSANITY +1510,The Mines,Friendsanity: Dwarf 9 <3,FRIENDSANITY +1511,The Mines,Friendsanity: Dwarf 10 <3,FRIENDSANITY +1513,Town,Friendsanity: Evelyn 1 <3,FRIENDSANITY +1514,Town,Friendsanity: Evelyn 2 <3,FRIENDSANITY +1515,Town,Friendsanity: Evelyn 3 <3,FRIENDSANITY +1516,Town,Friendsanity: Evelyn 4 <3,FRIENDSANITY +1517,Town,Friendsanity: Evelyn 5 <3,FRIENDSANITY +1518,Town,Friendsanity: Evelyn 6 <3,FRIENDSANITY +1519,Town,Friendsanity: Evelyn 7 <3,FRIENDSANITY +1520,Town,Friendsanity: Evelyn 8 <3,FRIENDSANITY +1521,Town,Friendsanity: Evelyn 9 <3,FRIENDSANITY +1522,Town,Friendsanity: Evelyn 10 <3,FRIENDSANITY +1524,Town,Friendsanity: George 1 <3,FRIENDSANITY +1525,Town,Friendsanity: George 2 <3,FRIENDSANITY +1526,Town,Friendsanity: George 3 <3,FRIENDSANITY +1527,Town,Friendsanity: George 4 <3,FRIENDSANITY +1528,Town,Friendsanity: George 5 <3,FRIENDSANITY +1529,Town,Friendsanity: George 6 <3,FRIENDSANITY +1530,Town,Friendsanity: George 7 <3,FRIENDSANITY +1531,Town,Friendsanity: George 8 <3,FRIENDSANITY +1532,Town,Friendsanity: George 9 <3,FRIENDSANITY +1533,Town,Friendsanity: George 10 <3,FRIENDSANITY +1535,Town,Friendsanity: Gus 1 <3,FRIENDSANITY +1536,Town,Friendsanity: Gus 2 <3,FRIENDSANITY +1537,Town,Friendsanity: Gus 3 <3,FRIENDSANITY +1538,Town,Friendsanity: Gus 4 <3,FRIENDSANITY +1539,Town,Friendsanity: Gus 5 <3,FRIENDSANITY +1540,Town,Friendsanity: Gus 6 <3,FRIENDSANITY +1541,Town,Friendsanity: Gus 7 <3,FRIENDSANITY +1542,Town,Friendsanity: Gus 8 <3,FRIENDSANITY +1543,Town,Friendsanity: Gus 9 <3,FRIENDSANITY +1544,Town,Friendsanity: Gus 10 <3,FRIENDSANITY +1546,Marnie's Ranch,Friendsanity: Jas 1 <3,FRIENDSANITY +1547,Marnie's Ranch,Friendsanity: Jas 2 <3,FRIENDSANITY +1548,Marnie's Ranch,Friendsanity: Jas 3 <3,FRIENDSANITY +1549,Marnie's Ranch,Friendsanity: Jas 4 <3,FRIENDSANITY +1550,Marnie's Ranch,Friendsanity: Jas 5 <3,FRIENDSANITY +1551,Marnie's Ranch,Friendsanity: Jas 6 <3,FRIENDSANITY +1552,Marnie's Ranch,Friendsanity: Jas 7 <3,FRIENDSANITY +1553,Marnie's Ranch,Friendsanity: Jas 8 <3,FRIENDSANITY +1554,Marnie's Ranch,Friendsanity: Jas 9 <3,FRIENDSANITY +1555,Marnie's Ranch,Friendsanity: Jas 10 <3,FRIENDSANITY +1557,Town,Friendsanity: Jodi 1 <3,FRIENDSANITY +1558,Town,Friendsanity: Jodi 2 <3,FRIENDSANITY +1559,Town,Friendsanity: Jodi 3 <3,FRIENDSANITY +1560,Town,Friendsanity: Jodi 4 <3,FRIENDSANITY +1561,Town,Friendsanity: Jodi 5 <3,FRIENDSANITY +1562,Town,Friendsanity: Jodi 6 <3,FRIENDSANITY +1563,Town,Friendsanity: Jodi 7 <3,FRIENDSANITY +1564,Town,Friendsanity: Jodi 8 <3,FRIENDSANITY +1565,Town,Friendsanity: Jodi 9 <3,FRIENDSANITY +1566,Town,Friendsanity: Jodi 10 <3,FRIENDSANITY +1568,Town,Friendsanity: Kent 1 <3,FRIENDSANITY +1569,Town,Friendsanity: Kent 2 <3,FRIENDSANITY +1570,Town,Friendsanity: Kent 3 <3,FRIENDSANITY +1571,Town,Friendsanity: Kent 4 <3,FRIENDSANITY +1572,Town,Friendsanity: Kent 5 <3,FRIENDSANITY +1573,Town,Friendsanity: Kent 6 <3,FRIENDSANITY +1574,Town,Friendsanity: Kent 7 <3,FRIENDSANITY +1575,Town,Friendsanity: Kent 8 <3,FRIENDSANITY +1576,Town,Friendsanity: Kent 9 <3,FRIENDSANITY +1577,Town,Friendsanity: Kent 10 <3,FRIENDSANITY +1579,Sewers,Friendsanity: Krobus 1 <3,FRIENDSANITY +1580,Sewers,Friendsanity: Krobus 2 <3,FRIENDSANITY +1581,Sewers,Friendsanity: Krobus 3 <3,FRIENDSANITY +1582,Sewers,Friendsanity: Krobus 4 <3,FRIENDSANITY +1583,Sewers,Friendsanity: Krobus 5 <3,FRIENDSANITY +1584,Sewers,Friendsanity: Krobus 6 <3,FRIENDSANITY +1585,Sewers,Friendsanity: Krobus 7 <3,FRIENDSANITY +1586,Sewers,Friendsanity: Krobus 8 <3,FRIENDSANITY +1587,Sewers,Friendsanity: Krobus 9 <3,FRIENDSANITY +1588,Sewers,Friendsanity: Krobus 10 <3,FRIENDSANITY +1590,Ginger Island,Friendsanity: Leo 1 <3,FRIENDSANITY +1591,Ginger Island,Friendsanity: Leo 2 <3,FRIENDSANITY +1592,Ginger Island,Friendsanity: Leo 3 <3,FRIENDSANITY +1593,Ginger Island,Friendsanity: Leo 4 <3,FRIENDSANITY +1594,Ginger Island,Friendsanity: Leo 5 <3,FRIENDSANITY +1595,Ginger Island,Friendsanity: Leo 6 <3,FRIENDSANITY +1596,Ginger Island,Friendsanity: Leo 7 <3,FRIENDSANITY +1597,Ginger Island,Friendsanity: Leo 8 <3,FRIENDSANITY +1598,Ginger Island,Friendsanity: Leo 9 <3,FRIENDSANITY +1599,Ginger Island,Friendsanity: Leo 10 <3,FRIENDSANITY +1601,Town,Friendsanity: Lewis 1 <3,FRIENDSANITY +1602,Town,Friendsanity: Lewis 2 <3,FRIENDSANITY +1603,Town,Friendsanity: Lewis 3 <3,FRIENDSANITY +1604,Town,Friendsanity: Lewis 4 <3,FRIENDSANITY +1605,Town,Friendsanity: Lewis 5 <3,FRIENDSANITY +1606,Town,Friendsanity: Lewis 6 <3,FRIENDSANITY +1607,Town,Friendsanity: Lewis 7 <3,FRIENDSANITY +1608,Town,Friendsanity: Lewis 8 <3,FRIENDSANITY +1609,Town,Friendsanity: Lewis 9 <3,FRIENDSANITY +1610,Town,Friendsanity: Lewis 10 <3,FRIENDSANITY +1612,Mountain,Friendsanity: Linus 1 <3,FRIENDSANITY +1613,Mountain,Friendsanity: Linus 2 <3,FRIENDSANITY +1614,Mountain,Friendsanity: Linus 3 <3,FRIENDSANITY +1615,Mountain,Friendsanity: Linus 4 <3,FRIENDSANITY +1616,Mountain,Friendsanity: Linus 5 <3,FRIENDSANITY +1617,Mountain,Friendsanity: Linus 6 <3,FRIENDSANITY +1618,Mountain,Friendsanity: Linus 7 <3,FRIENDSANITY +1619,Mountain,Friendsanity: Linus 8 <3,FRIENDSANITY +1620,Mountain,Friendsanity: Linus 9 <3,FRIENDSANITY +1621,Mountain,Friendsanity: Linus 10 <3,FRIENDSANITY +1623,Marnie's Ranch,Friendsanity: Marnie 1 <3,FRIENDSANITY +1624,Marnie's Ranch,Friendsanity: Marnie 2 <3,FRIENDSANITY +1625,Marnie's Ranch,Friendsanity: Marnie 3 <3,FRIENDSANITY +1626,Marnie's Ranch,Friendsanity: Marnie 4 <3,FRIENDSANITY +1627,Marnie's Ranch,Friendsanity: Marnie 5 <3,FRIENDSANITY +1628,Marnie's Ranch,Friendsanity: Marnie 6 <3,FRIENDSANITY +1629,Marnie's Ranch,Friendsanity: Marnie 7 <3,FRIENDSANITY +1630,Marnie's Ranch,Friendsanity: Marnie 8 <3,FRIENDSANITY +1631,Marnie's Ranch,Friendsanity: Marnie 9 <3,FRIENDSANITY +1632,Marnie's Ranch,Friendsanity: Marnie 10 <3,FRIENDSANITY +1634,Town,Friendsanity: Pam 1 <3,FRIENDSANITY +1635,Town,Friendsanity: Pam 2 <3,FRIENDSANITY +1636,Town,Friendsanity: Pam 3 <3,FRIENDSANITY +1637,Town,Friendsanity: Pam 4 <3,FRIENDSANITY +1638,Town,Friendsanity: Pam 5 <3,FRIENDSANITY +1639,Town,Friendsanity: Pam 6 <3,FRIENDSANITY +1640,Town,Friendsanity: Pam 7 <3,FRIENDSANITY +1641,Town,Friendsanity: Pam 8 <3,FRIENDSANITY +1642,Town,Friendsanity: Pam 9 <3,FRIENDSANITY +1643,Town,Friendsanity: Pam 10 <3,FRIENDSANITY +1645,Town,Friendsanity: Pierre 1 <3,FRIENDSANITY +1646,Town,Friendsanity: Pierre 2 <3,FRIENDSANITY +1647,Town,Friendsanity: Pierre 3 <3,FRIENDSANITY +1648,Town,Friendsanity: Pierre 4 <3,FRIENDSANITY +1649,Town,Friendsanity: Pierre 5 <3,FRIENDSANITY +1650,Town,Friendsanity: Pierre 6 <3,FRIENDSANITY +1651,Town,Friendsanity: Pierre 7 <3,FRIENDSANITY +1652,Town,Friendsanity: Pierre 8 <3,FRIENDSANITY +1653,Town,Friendsanity: Pierre 9 <3,FRIENDSANITY +1654,Town,Friendsanity: Pierre 10 <3,FRIENDSANITY +1656,Carpenter Shop,Friendsanity: Robin 1 <3,FRIENDSANITY +1657,Carpenter Shop,Friendsanity: Robin 2 <3,FRIENDSANITY +1658,Carpenter Shop,Friendsanity: Robin 3 <3,FRIENDSANITY +1659,Carpenter Shop,Friendsanity: Robin 4 <3,FRIENDSANITY +1660,Carpenter Shop,Friendsanity: Robin 5 <3,FRIENDSANITY +1661,Carpenter Shop,Friendsanity: Robin 6 <3,FRIENDSANITY +1662,Carpenter Shop,Friendsanity: Robin 7 <3,FRIENDSANITY +1663,Carpenter Shop,Friendsanity: Robin 8 <3,FRIENDSANITY +1664,Carpenter Shop,Friendsanity: Robin 9 <3,FRIENDSANITY +1665,Carpenter Shop,Friendsanity: Robin 10 <3,FRIENDSANITY +1667,The Desert,Friendsanity: Sandy 1 <3,FRIENDSANITY +1668,The Desert,Friendsanity: Sandy 2 <3,FRIENDSANITY +1669,The Desert,Friendsanity: Sandy 3 <3,FRIENDSANITY +1670,The Desert,Friendsanity: Sandy 4 <3,FRIENDSANITY +1671,The Desert,Friendsanity: Sandy 5 <3,FRIENDSANITY +1672,The Desert,Friendsanity: Sandy 6 <3,FRIENDSANITY +1673,The Desert,Friendsanity: Sandy 7 <3,FRIENDSANITY +1674,The Desert,Friendsanity: Sandy 8 <3,FRIENDSANITY +1675,The Desert,Friendsanity: Sandy 9 <3,FRIENDSANITY +1676,The Desert,Friendsanity: Sandy 10 <3,FRIENDSANITY +1678,Town,Friendsanity: Vincent 1 <3,FRIENDSANITY +1679,Town,Friendsanity: Vincent 2 <3,FRIENDSANITY +1680,Town,Friendsanity: Vincent 3 <3,FRIENDSANITY +1681,Town,Friendsanity: Vincent 4 <3,FRIENDSANITY +1682,Town,Friendsanity: Vincent 5 <3,FRIENDSANITY +1683,Town,Friendsanity: Vincent 6 <3,FRIENDSANITY +1684,Town,Friendsanity: Vincent 7 <3,FRIENDSANITY +1685,Town,Friendsanity: Vincent 8 <3,FRIENDSANITY +1686,Town,Friendsanity: Vincent 9 <3,FRIENDSANITY +1687,Town,Friendsanity: Vincent 10 <3,FRIENDSANITY +1689,Beach,Friendsanity: Willy 1 <3,FRIENDSANITY +1690,Beach,Friendsanity: Willy 2 <3,FRIENDSANITY +1691,Beach,Friendsanity: Willy 3 <3,FRIENDSANITY +1692,Beach,Friendsanity: Willy 4 <3,FRIENDSANITY +1693,Beach,Friendsanity: Willy 5 <3,FRIENDSANITY +1694,Beach,Friendsanity: Willy 6 <3,FRIENDSANITY +1695,Beach,Friendsanity: Willy 7 <3,FRIENDSANITY +1696,Beach,Friendsanity: Willy 8 <3,FRIENDSANITY +1697,Beach,Friendsanity: Willy 9 <3,FRIENDSANITY +1698,Beach,Friendsanity: Willy 10 <3,FRIENDSANITY +1700,Forest,Friendsanity: Wizard 1 <3,FRIENDSANITY +1701,Forest,Friendsanity: Wizard 2 <3,FRIENDSANITY +1702,Forest,Friendsanity: Wizard 3 <3,FRIENDSANITY +1703,Forest,Friendsanity: Wizard 4 <3,FRIENDSANITY +1704,Forest,Friendsanity: Wizard 5 <3,FRIENDSANITY +1705,Forest,Friendsanity: Wizard 6 <3,FRIENDSANITY +1706,Forest,Friendsanity: Wizard 7 <3,FRIENDSANITY +1707,Forest,Friendsanity: Wizard 8 <3,FRIENDSANITY +1708,Forest,Friendsanity: Wizard 9 <3,FRIENDSANITY +1709,Forest,Friendsanity: Wizard 10 <3,FRIENDSANITY +1710,Farm,Friendsanity: Pet 1 <3,FRIENDSANITY +1711,Farm,Friendsanity: Pet 2 <3,FRIENDSANITY +1712,Farm,Friendsanity: Pet 3 <3,FRIENDSANITY +1713,Farm,Friendsanity: Pet 4 <3,FRIENDSANITY +1714,Farm,Friendsanity: Pet 5 <3,FRIENDSANITY +1715,Farm,Friendsanity: Friend 1 <3,FRIENDSANITY +1716,Farm,Friendsanity: Friend 2 <3,FRIENDSANITY +1717,Farm,Friendsanity: Friend 3 <3,FRIENDSANITY +1718,Farm,Friendsanity: Friend 4 <3,FRIENDSANITY +1719,Farm,Friendsanity: Friend 5 <3,FRIENDSANITY +1720,Farm,Friendsanity: Friend 6 <3,FRIENDSANITY +1721,Farm,Friendsanity: Friend 7 <3,FRIENDSANITY +1722,Farm,Friendsanity: Friend 8 <3,FRIENDSANITY +1723,Farm,Friendsanity: Suitor 9 <3,FRIENDSANITY +1724,Farm,Friendsanity: Suitor 10 <3,FRIENDSANITY +1725,Farm,Friendsanity: Spouse 11 <3,FRIENDSANITY +1726,Farm,Friendsanity: Spouse 12 <3,FRIENDSANITY +1727,Farm,Friendsanity: Spouse 13 <3,FRIENDSANITY +1728,Farm,Friendsanity: Spouse 14 <3,FRIENDSANITY diff --git a/worlds/stardew_valley/data/monster_data.py b/worlds/stardew_valley/data/monster_data.py new file mode 100644 index 0000000000..6030571f89 --- /dev/null +++ b/worlds/stardew_valley/data/monster_data.py @@ -0,0 +1,8 @@ +class Monster: + duggy = "Duggy" + blue_slime = "Blue Slime" + pepper_rex = "Pepper Rex" + stone_golem = "Stone Golem" + + +frozen_monsters = (Monster.blue_slime,) diff --git a/worlds/stardew_valley/data/museum_data.py b/worlds/stardew_valley/data/museum_data.py new file mode 100644 index 0000000000..bc3197d10f --- /dev/null +++ b/worlds/stardew_valley/data/museum_data.py @@ -0,0 +1,301 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import List, Tuple, Union, Optional + +from . import common_data as common +from .game_item import GameItem +from .monster_data import Monster +from .region_data import SVRegion + + +@dataclass(frozen=True) +class MuseumItem(GameItem): + locations: Tuple[str, ...] + geodes: Tuple[str, ...] + monsters: Tuple[str, ...] + difficulty: float + + @staticmethod + def of(name: str, + item_id: int, + difficulty: float, + locations: Union[str, Tuple[str, ...]], + geodes: Union[str, Tuple[str, ...]], + monsters: Union[str, Tuple[str, ...]]) -> MuseumItem: + if isinstance(locations, str): + locations = (locations,) + + if isinstance(geodes, str): + geodes = (geodes,) + + if isinstance(monsters, str): + monsters = (monsters,) + + return MuseumItem(name, item_id, locations, geodes, monsters, difficulty) + + def __repr__(self): + return f"{self.name} [{self.item_id}] (Locations: {self.locations} |" \ + f" Geodes: {self.geodes} |" \ + f" Monsters: {self.monsters}) " + + +class Geode: + geode = "Geode" + frozen_geode = "Frozen Geode" + magma_geode = "Magma Geode" + omni_geode = "Omni Geode" + artifact_trove = "Artifact Trove" + + +unlikely = () + +all_artifact_items: List[MuseumItem] = [] +all_mineral_items: List[MuseumItem] = [] + +all_museum_items: List[MuseumItem] = [] + + +def create_artifact(name: str, + item_id: int, + difficulty: float, + locations: Union[str, Tuple[str, ...]] = (), + geodes: Union[str, Tuple[str, ...]] = (), + monsters: Union[str, Tuple[str, ...]] = ()) -> MuseumItem: + artifact_item = MuseumItem.of(name, item_id, difficulty, locations, geodes, monsters) + all_artifact_items.append(artifact_item) + all_museum_items.append(artifact_item) + return artifact_item + + +def create_mineral(name: str, + item_id: int, + locations: Union[str, Tuple[str, ...]], + geodes: Union[str, Tuple[str, ...]] = (), + monsters: Union[str, Tuple[str, ...]] = (), + difficulty: Optional[float] = None) -> MuseumItem: + if difficulty is None: + difficulty = 0 + if "Geode" in geodes: + difficulty += 1.0 / 32.0 * 100 + if "Frozen Geode" in geodes: + difficulty += 1.0 / 30.0 * 100 + if "Magma Geode" in geodes: + difficulty += 1.0 / 26.0 * 100 + if "Omni Geode" in geodes: + difficulty += 31.0 / 2750.0 * 100 + + mineral_item = MuseumItem.of(name, item_id, difficulty, locations, geodes, monsters) + all_mineral_items.append(mineral_item) + all_museum_items.append(mineral_item) + return mineral_item + + +class Artifact: + dwarf_scroll_i = create_artifact("Dwarf Scroll I", 96, 5.6, SVRegion.mines_floor_20, + monsters=unlikely) + dwarf_scroll_ii = create_artifact("Dwarf Scroll II", 97, 3, SVRegion.mines_floor_20, + monsters=unlikely) + dwarf_scroll_iii = create_artifact("Dwarf Scroll III", 98, 7.5, SVRegion.mines_floor_60, + monsters=Monster.blue_slime) + dwarf_scroll_iv = create_artifact("Dwarf Scroll IV", 99, 4, SVRegion.mines_floor_100) + chipped_amphora = create_artifact("Chipped Amphora", 100, 6.7, SVRegion.town, + geodes=Geode.artifact_trove) + arrowhead = create_artifact("Arrowhead", 101, 8.5, (SVRegion.mountain, SVRegion.forest, SVRegion.bus_stop), + geodes=Geode.artifact_trove) + ancient_doll = create_artifact("Ancient Doll", 103, 13.1, (SVRegion.mountain, SVRegion.forest, SVRegion.bus_stop), + geodes=(Geode.artifact_trove, common.fishing_chest)) + elvish_jewelry = create_artifact("Elvish Jewelry", 104, 5.3, SVRegion.forest, + geodes=(Geode.artifact_trove, common.fishing_chest)) + chewing_stick = create_artifact("Chewing Stick", 105, 10.3, (SVRegion.mountain, SVRegion.forest, SVRegion.town), + geodes=(Geode.artifact_trove, common.fishing_chest)) + ornamental_fan = create_artifact("Ornamental Fan", 106, 7.4, (SVRegion.beach, SVRegion.forest, SVRegion.town), + geodes=(Geode.artifact_trove, common.fishing_chest)) + dinosaur_egg = create_artifact("Dinosaur Egg", 107, 11.4, (SVRegion.mountain, SVRegion.skull_cavern), + geodes=common.fishing_chest, + monsters=Monster.pepper_rex) + rare_disc = create_artifact("Rare Disc", 108, 5.6, SVRegion.stardew_valley, + geodes=(Geode.artifact_trove, common.fishing_chest), + monsters=unlikely) + ancient_sword = create_artifact("Ancient Sword", 109, 5.8, (SVRegion.forest, SVRegion.mountain), + geodes=(Geode.artifact_trove, common.fishing_chest)) + rusty_spoon = create_artifact("Rusty Spoon", 110, 9.6, SVRegion.town, + geodes=(Geode.artifact_trove, common.fishing_chest)) + rusty_spur = create_artifact("Rusty Spur", 111, 15.6, SVRegion.farm, + geodes=(Geode.artifact_trove, common.fishing_chest)) + rusty_cog = create_artifact("Rusty Cog", 112, 9.6, SVRegion.mountain, + geodes=(Geode.artifact_trove, common.fishing_chest)) + chicken_statue = create_artifact("Chicken Statue", 113, 13.5, SVRegion.farm, + geodes=(Geode.artifact_trove, common.fishing_chest)) + ancient_seed = create_artifact("Ancient Seed", 114, 8.4, (SVRegion.forest, SVRegion.mountain), + geodes=(Geode.artifact_trove, common.fishing_chest), + monsters=unlikely) + prehistoric_tool = create_artifact("Prehistoric Tool", 115, 11.1, (SVRegion.mountain, SVRegion.forest, SVRegion.bus_stop), + geodes=(Geode.artifact_trove, common.fishing_chest)) + dried_starfish = create_artifact("Dried Starfish", 116, 12.5, SVRegion.beach, + geodes=(Geode.artifact_trove, common.fishing_chest)) + anchor = create_artifact("Anchor", 117, 8.5, SVRegion.beach, geodes=(Geode.artifact_trove, common.fishing_chest)) + glass_shards = create_artifact("Glass Shards", 118, 11.5, SVRegion.beach, + geodes=(Geode.artifact_trove, common.fishing_chest)) + bone_flute = create_artifact("Bone Flute", 119, 6.3, (SVRegion.mountain, SVRegion.forest, SVRegion.town), + geodes=(Geode.artifact_trove, common.fishing_chest)) + prehistoric_handaxe = create_artifact("Prehistoric Handaxe", 120, 13.7, + (SVRegion.mountain, SVRegion.forest, SVRegion.bus_stop), + geodes=Geode.artifact_trove) + dwarvish_helm = create_artifact("Dwarvish Helm", 121, 8.7, SVRegion.mines_floor_20, + geodes=(Geode.geode, Geode.omni_geode, Geode.artifact_trove)) + dwarf_gadget = create_artifact("Dwarf Gadget", 122, 9.7, SVRegion.mines_floor_60, + geodes=(Geode.magma_geode, Geode.omni_geode, Geode.artifact_trove)) + ancient_drum = create_artifact("Ancient Drum", 123, 9.5, (SVRegion.bus_stop, SVRegion.forest, SVRegion.town), + geodes=(Geode.frozen_geode, Geode.omni_geode, Geode.artifact_trove)) + golden_mask = create_artifact("Golden Mask", 124, 6.7, SVRegion.desert, + geodes=Geode.artifact_trove) + golden_relic = create_artifact("Golden Relic", 125, 9.7, SVRegion.desert, + geodes=Geode.artifact_trove) + strange_doll_green = create_artifact("Strange Doll (Green)", 126, 10, SVRegion.town, + geodes=common.secret_note) + strange_doll = create_artifact("Strange Doll", 127, 10, SVRegion.desert, + geodes=common.secret_note) + prehistoric_scapula = create_artifact("Prehistoric Scapula", 579, 6.2, + (SVRegion.dig_site, SVRegion.forest, SVRegion.town)) + prehistoric_tibia = create_artifact("Prehistoric Tibia", 580, 16.6, + (SVRegion.dig_site, SVRegion.forest, SVRegion.railroad)) + prehistoric_skull = create_artifact("Prehistoric Skull", 581, 3.9, (SVRegion.dig_site, SVRegion.mountain)) + skeletal_hand = create_artifact("Skeletal Hand", 582, 7.9, (SVRegion.dig_site, SVRegion.backwoods, SVRegion.beach)) + prehistoric_rib = create_artifact("Prehistoric Rib", 583, 15, (SVRegion.dig_site, SVRegion.farm, SVRegion.town), + monsters=Monster.pepper_rex) + prehistoric_vertebra = create_artifact("Prehistoric Vertebra", 584, 12.7, (SVRegion.dig_site, SVRegion.bus_stop), + monsters=Monster.pepper_rex) + skeletal_tail = create_artifact("Skeletal Tail", 585, 5.1, (SVRegion.dig_site, SVRegion.mines_floor_20), + geodes=common.fishing_chest) + nautilus_fossil = create_artifact("Nautilus Fossil", 586, 6.9, (SVRegion.dig_site, SVRegion.beach), + geodes=common.fishing_chest) + amphibian_fossil = create_artifact("Amphibian Fossil", 587, 6.3, (SVRegion.dig_site, SVRegion.forest, SVRegion.mountain), + geodes=common.fishing_chest) + palm_fossil = create_artifact("Palm Fossil", 588, 10.2, + (SVRegion.dig_site, SVRegion.desert, SVRegion.forest, SVRegion.beach)) + trilobite = create_artifact("Trilobite", 589, 7.4, (SVRegion.dig_site, SVRegion.desert, SVRegion.forest, SVRegion.beach)) + + +class Mineral: + quartz = create_mineral("Quartz", 80, SVRegion.mines_floor_20, + monsters=Monster.stone_golem) + fire_quartz = create_mineral("Fire Quartz", 82, SVRegion.mines_floor_100, + geodes=(Geode.magma_geode, Geode.omni_geode, common.fishing_chest), + difficulty=1.0 / 12.0) + frozen_tear = create_mineral("Frozen Tear", 84, SVRegion.mines_floor_60, + geodes=(Geode.frozen_geode, Geode.omni_geode, common.fishing_chest), + monsters=unlikely, + difficulty=1.0 / 12.0) + earth_crystal = create_mineral("Earth Crystal", 86, SVRegion.mines_floor_20, + geodes=(Geode.geode, Geode.omni_geode, common.fishing_chest), + monsters=Monster.duggy, + difficulty=1.0 / 12.0) + emerald = create_mineral("Emerald", 60, SVRegion.mines_floor_100, + geodes=common.fishing_chest) + aquamarine = create_mineral("Aquamarine", 62, SVRegion.mines_floor_60, + geodes=common.fishing_chest) + ruby = create_mineral("Ruby", 64, SVRegion.mines_floor_100, + geodes=common.fishing_chest) + amethyst = create_mineral("Amethyst", 66, SVRegion.mines_floor_20, + geodes=common.fishing_chest) + topaz = create_mineral("Topaz", 68, SVRegion.mines_floor_20, + geodes=common.fishing_chest) + jade = create_mineral("Jade", 70, SVRegion.mines_floor_60, + geodes=common.fishing_chest) + diamond = create_mineral("Diamond", 72, SVRegion.mines_floor_60, + geodes=common.fishing_chest) + prismatic_shard = create_mineral("Prismatic Shard", 74, SVRegion.perfect_skull_cavern, + geodes=unlikely, + monsters=unlikely) + alamite = create_mineral("Alamite", 538, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + bixite = create_mineral("Bixite", 539, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode), + monsters=unlikely) + baryte = create_mineral("Baryte", 540, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + aerinite = create_mineral("Aerinite", 541, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + calcite = create_mineral("Calcite", 542, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + dolomite = create_mineral("Dolomite", 543, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + esperite = create_mineral("Esperite", 544, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + fluorapatite = create_mineral("Fluorapatite", 545, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + geminite = create_mineral("Geminite", 546, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + helvite = create_mineral("Helvite", 547, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + jamborite = create_mineral("Jamborite", 548, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + jagoite = create_mineral("Jagoite", 549, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + kyanite = create_mineral("Kyanite", 550, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + lunarite = create_mineral("Lunarite", 551, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + malachite = create_mineral("Malachite", 552, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + neptunite = create_mineral("Neptunite", 553, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + lemon_stone = create_mineral("Lemon Stone", 554, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + nekoite = create_mineral("Nekoite", 555, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + orpiment = create_mineral("Orpiment", 556, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + petrified_slime = create_mineral("Petrified Slime", 557, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + thunder_egg = create_mineral("Thunder Egg", 558, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + pyrite = create_mineral("Pyrite", 559, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + ocean_stone = create_mineral("Ocean Stone", 560, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + ghost_crystal = create_mineral("Ghost Crystal", 561, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + tigerseye = create_mineral("Tigerseye", 562, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + jasper = create_mineral("Jasper", 563, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + opal = create_mineral("Opal", 564, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + fire_opal = create_mineral("Fire Opal", 565, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + celestine = create_mineral("Celestine", 566, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + marble = create_mineral("Marble", 567, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + sandstone = create_mineral("Sandstone", 568, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + granite = create_mineral("Granite", 569, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + basalt = create_mineral("Basalt", 570, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + limestone = create_mineral("Limestone", 571, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + soapstone = create_mineral("Soapstone", 572, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + hematite = create_mineral("Hematite", 573, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + mudstone = create_mineral("Mudstone", 574, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + obsidian = create_mineral("Obsidian", 575, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + slate = create_mineral("Slate", 576, SVRegion.town, + geodes=(Geode.geode, Geode.omni_geode)) + fairy_stone = create_mineral("Fairy Stone", 577, SVRegion.town, + geodes=(Geode.frozen_geode, Geode.omni_geode)) + star_shards = create_mineral("Star Shards", 578, SVRegion.town, + geodes=(Geode.magma_geode, Geode.omni_geode)) + + +dwarf_scrolls = (Artifact.dwarf_scroll_i, Artifact.dwarf_scroll_ii, Artifact.dwarf_scroll_iii, Artifact.dwarf_scroll_iv) +skeleton_front = (Artifact.prehistoric_skull, Artifact.skeletal_hand, Artifact.prehistoric_scapula) +skeleton_middle = (Artifact.prehistoric_rib, Artifact.prehistoric_vertebra) +skeleton_back = (Artifact.prehistoric_tibia, Artifact.skeletal_tail) + +all_museum_items_by_name = {item.name: item for item in all_museum_items} diff --git a/worlds/stardew_valley/data/region_data.py b/worlds/stardew_valley/data/region_data.py new file mode 100644 index 0000000000..8f1eb1ab08 --- /dev/null +++ b/worlds/stardew_valley/data/region_data.py @@ -0,0 +1,99 @@ +class SVRegion: + menu = "Menu" + stardew_valley = "Stardew Valley" + farm_house = "Farmhouse" + cellar = "Cellar" + farm = "Farm" + town = "Town" + beach = "Beach" + mountain = "Mountain" + forest = "Forest" + bus_stop = "Bus Stop" + backwoods = "Backwoods" + railroad = "Railroad" + secret_woods = "Secret Woods" + community_center = "Community Center" + pantry = "Pantry" + crafts_room = "Crafts Room" + fish_tank = "Fish Tank" + boiler_room = "Boiler Room" + vault = "Vault" + bulletin_board = "Bulletin Board" + desert = "The Desert" + mines = "The Mines" + skull_cavern_entrance = "Skull Cavern Entrance" + skull_cavern = "Skull Cavern" + sewers = "Sewers" + mutant_bug_lair = "Mutant Bug Lair" + witch_swamp = "Witch's Swamp" + ginger_island = "Ginger Island" + pirate_cove = ginger_island + dig_site = ginger_island + perfect_skull_cavern = "Skull Cavern Floor 100" + hospital = "Hospital" + carpenter = "Carpenter Shop" + alex_house = "Alex's House" + elliott_house = "Elliott's House" + ranch = "Marnie's Ranch" + traveling_cart = "Traveling Cart" + farm_cave = "Farmcave" + greenhouse = "Greenhouse" + tunnel = "Tunnel" + tunnel_entrance = "Tunnel Entrance" + leah_house = "Leah's Cottage" + wizard_tower = "Wizard Tower" + wizard_basement = "Wizard Basement" + tent = "Tent" + sebastian_room = "Sebastian's Room" + adventurer_guild = "Adventurer's Guild" + quarry = "Quarry" + quarry_mine_entrance = "Quarry Mine Entrance" + quarry_mine = "Quarry Mine" + witch_warp_cave = "Witch Warp Cave" + harvey_room = "Harvey's Room" + pierre_store = "Pierre's General Store" + sunroom = "Sunroom" + saloon = "Saloon" + blacksmith = "Clint's Blacksmith" + trailer = "Trailer" + museum = "Museum" + mayor_house = "Mayor's Manor" + haley_house = "Haley's House" + sam_house = "Sam's House" + jojamart = "JojaMart" + fish_shop = "Willy's Fish Shop" + tide_pools = "Tide Pools" + bathhouse_entrance = "Bathhouse Entrance" + locker_room = "Locker Room" + public_bath = "Public Bath" + jotpk_world_1 = "JotPK World 1" + jotpk_world_2 = "JotPK World 2" + jotpk_world_3 = "JotPK World 3" + junimo_kart_1 = "Junimo Kart 1" + junimo_kart_2 = "Junimo Kart 2" + junimo_kart_3 = "Junimo Kart 3" + mines_floor_5 = "The Mines - Floor 5" + mines_floor_10 = "The Mines - Floor 10" + mines_floor_15 = "The Mines - Floor 15" + mines_floor_20 = "The Mines - Floor 20" + mines_floor_25 = "The Mines - Floor 25" + mines_floor_30 = "The Mines - Floor 30" + mines_floor_35 = "The Mines - Floor 35" + mines_floor_40 = "The Mines - Floor 40" + mines_floor_45 = "The Mines - Floor 45" + mines_floor_50 = "The Mines - Floor 50" + mines_floor_55 = "The Mines - Floor 55" + mines_floor_60 = "The Mines - Floor 60" + mines_floor_65 = "The Mines - Floor 65" + mines_floor_70 = "The Mines - Floor 70" + mines_floor_75 = "The Mines - Floor 75" + mines_floor_80 = "The Mines - Floor 80" + mines_floor_85 = "The Mines - Floor 85" + mines_floor_90 = "The Mines - Floor 90" + mines_floor_95 = "The Mines - Floor 95" + mines_floor_100 = "The Mines - Floor 100" + mines_floor_105 = "The Mines - Floor 105" + mines_floor_110 = "The Mines - Floor 110" + mines_floor_115 = "The Mines - Floor 115" + mines_floor_120 = "The Mines - Floor 120" + diff --git a/worlds/stardew_valley/data/season_data.py b/worlds/stardew_valley/data/season_data.py new file mode 100644 index 0000000000..1ebef04955 --- /dev/null +++ b/worlds/stardew_valley/data/season_data.py @@ -0,0 +1,10 @@ +spring = "Spring" +summer = "Summer" +fall = "Fall" +winter = "Winter" + +not_spring = (summer, fall, winter) +not_summer = (spring, fall, winter) +not_fall = (spring, summer, winter) +not_winter = (spring, summer, fall) +all_seasons = (spring, summer, fall, winter) diff --git a/worlds/stardew_valley/data/villagers_data.py b/worlds/stardew_valley/data/villagers_data.py new file mode 100644 index 0000000000..14582d2567 --- /dev/null +++ b/worlds/stardew_valley/data/villagers_data.py @@ -0,0 +1,250 @@ +from dataclasses import dataclass +from typing import Set, List, FrozenSet, Tuple +from .region_data import SVRegion + + +@dataclass(frozen=True) +class Villager: + name: str + bachelor: bool + locations: Tuple[str] + birthday: str + gifts: Tuple[str] + available: bool + + def __repr__(self): + return f"{self.name} [Bachelor: {self.bachelor}] [Available from start: {self.available}]" \ + f"(Locations: {self.locations} |" \ + f" Birthday: {self.birthday} |" \ + f" Gifts: {self.gifts}) " + + +town = (SVRegion.town,) +beach = (SVRegion.beach,) +forest = (SVRegion.forest,) +mountain = (SVRegion.mountain,) +hospital = (SVRegion.hospital,) +carpenter = (SVRegion.carpenter,) +alex_house = (SVRegion.alex_house,) +elliott_house = (SVRegion.elliott_house,) +ranch = (SVRegion.ranch,) +mines = (SVRegion.mines,) +desert = (SVRegion.desert,) +oasis = (SVRegion.desert,) +sewers = (SVRegion.sewers,) +island = (SVRegion.ginger_island,) + +golden_pumpkin = ("Golden Pumpkin",) +# magic_rock_candy = ("Magic Rock Candy",) +pearl = ("Pearl",) +prismatic_shard = ("Prismatic Shard",) +rabbit_foot = ("Rabbit's Foot",) +universal_loves = golden_pumpkin + pearl + prismatic_shard + rabbit_foot # , *magic_rock_candy} +universal_loves_no_prismatic_shard = golden_pumpkin + pearl + rabbit_foot # , *magic_rock_candy} +universal_loves_no_rabbit_foot = golden_pumpkin + pearl + prismatic_shard # , *magic_rock_candy} +complete_breakfast = ("Complete Breakfast",) +salmon_dinner = ("Salmon Dinner",) +crab_cakes = ("Crab Cakes",) +duck_feather = ("Duck Feather",) +lobster = ("Lobster",) +pomegranate = ("Pomegranate",) +squid_ink = ("Squid Ink",) +# tom_kha_soup = ("Tom Kha Soup",) +elliott_loves = duck_feather + lobster + pomegranate + squid_ink + crab_cakes # | tom_kha_soup +coffee = ("Coffee",) +pickles = ("Pickles",) +# super_meal = ("Super Meal",) +truffle_oil = ("Truffle Oil",) +wine = ("Wine",) +harvey_loves = coffee + pickles + truffle_oil + wine # | super_meal +cactus_fruit = ("Cactus Fruit",) +maple_bar = ("Maple Bar",) +pizza = ("Pizza",) +tigerseye = ("Tigerseye",) +sam_loves = cactus_fruit + maple_bar + pizza + tigerseye +frozen_tear = ("Frozen Tear",) +obsidian = ("Obsidian",) +# pumpkin_soup = ("Pumpkin Soup",) +# sashimi = ("Sashimi",) +void_egg = ("Void Egg",) +sebastian_loves = frozen_tear + obsidian + void_egg # | pumpkin_soup + sashimi +beer = ("Beer",) +hot_pepper = ("Hot Pepper",) +# pepper_poppers = ("Pepper Poppers",) +shane_loves = beer + hot_pepper + pizza # | pepper_poppers +amethyst = ("Amethyst",) +# banana_pudding = ("Banana Pudding",) +blackberry_cobbler = ("Blackberry Cobbler",) +chocolate_cake = ("Chocolate Cake",) +pufferfish = ("Pufferfish",) +pumpkin = ("Pumpkin",) +# spicy_eel = ("Spicy Eel",) +abigail_loves = amethyst + blackberry_cobbler + chocolate_cake + pufferfish + pumpkin # | banana_pudding + spicy_eel +aquamarine = ("Aquamarine",) +cloth = ("Cloth",) +emerald = ("Emerald",) +jade = ("Jade",) +ruby = ("Ruby",) +survival_burger = ("Survival Burger",) +topaz = ("Topaz",) +wool = ("Wool",) +emily_loves = amethyst + aquamarine + cloth + emerald + jade + ruby + survival_burger + topaz + wool +coconut = ("Coconut",) +fruit_salad = ("Fruit Salad",) +pink_cake = ("Pink Cake",) +sunflower = ("Sunflower",) +haley_loves = coconut + fruit_salad + pink_cake + sunflower +goat_cheese = ("Goat Cheese",) +poppyseed_muffin = ("Poppyseed Muffin",) +salad = ("Salad",) +stir_fry = ("Stir Fry",) +truffle = ("Truffle",) +# vegetable_medley = ("Vegetable Medley",) +leah_loves = goat_cheese + poppyseed_muffin + salad + stir_fry + truffle + wine # | vegetable_medley +battery_pack = ("Battery Pack",) +cauliflower = ("Cauliflower",) +cheese_cauliflower = ("Cheese Cauliflower",) +diamond = ("Diamond",) +gold_bar = ("Gold Bar",) +iridium_bar = ("Iridium Bar",) +miners_treat = ("Miner's Treat",) +pepper_poppers = ("Pepper Poppers",) +radioactive_bar = ("Radioactive Bar",) +rhubarb_pie = ("Rhubarb Pie",) +strawberry = ("Strawberry",) +maru_loves = battery_pack + cauliflower + diamond + gold_bar + iridium_bar + miners_treat + radioactive_bar + strawberry # | cheese_cauliflower + pepper_poppers + rhubarb_pie +melon = ("Melon",) +poppy = ("Poppy",) +# red_plate = ("Red Plate",) +roots_platter = ("Roots Platter",) +sandfish = ("Sandfish",) +penny_loves = diamond + emerald + melon + poppy + poppyseed_muffin + roots_platter + sandfish # | tom_kha_soup + red_plate +# fish_taco = ("Fish Taco",) +green_tea = ("Green Tea",) +summer_spangle = ("Summer Spangle",) +tropical_curry = ("Tropical Curry",) +caroline_loves = summer_spangle + tropical_curry # | fish_taco + green_tea +artichoke_dip = ("Artichoke Dip",) +fiddlehead_risotto = ("Fiddlehead Risotto",) +omni_geode = ("Omni Geode",) +clint_loves = amethyst + aquamarine + artichoke_dip + emerald + fiddlehead_risotto + gold_bar + iridium_bar + jade + \ + omni_geode + ruby + topaz +# bean_hotpot = ("Bean Hotpot",) +ice_cream = ("Ice Cream",) +# rice_pudding = ("Rice Pudding",) +demetrius_loves = ice_cream + strawberry # | bean_hotpot + rice_pudding +lemon_stone = ("Lemon Stone",) +dwarf_loves = amethyst + aquamarine + emerald + jade + lemon_stone + omni_geode + ruby + topaz +beet = ("Beet",) +fairy_rose = ("Fairy Rose",) +# stuffing = ("Stuffing",) +tulip = ("Tulip",) +evelyn_loves = beet + chocolate_cake + diamond + fairy_rose + tulip # | stuffing +# fried_mushroom = ("Fried Mushroom",) +leek = ("Leek",) +george_loves = leek # | fried_mushroom +# escargot = ("Escargot",) +orange = ("Orange",) +gus_loves = diamond + orange + tropical_curry # | escargot + fish_taco +plum_pudding = ("Plum Pudding",) +jas_loves = fairy_rose + pink_cake + plum_pudding +# crispy_bass = ("Crispy Bass",) +# eggplant_parmesan = ("Eggplant Parmesan",) +# fried_eel = ("Fried Eel",) +pancakes = ("Pancakes",) +jodi_loves = chocolate_cake + diamond + pancakes + rhubarb_pie # | vegetable_medley + crispy_bass + eggplant_parmesan + fried_eel +roasted_hazelnuts = ("Roasted Hazelnuts",) +kent_loves = fiddlehead_risotto + roasted_hazelnuts +void_mayonnaise = ("Void Mayonnaise",) +wild_horseradish = ("Wild Horseradish",) +krobus_loves = diamond + iridium_bar + pumpkin + void_egg + void_mayonnaise + wild_horseradish +mango = ("Mango",) +ostrich_egg = ("Ostrich Egg",) +# poi = ("Poi",) +leo_loves = duck_feather + mango + ostrich_egg # | poi +# autumns_bounty = ("Autumn's Bounty",) +glazed_yams = ("Glazed Yams",) +lewis_loves = glazed_yams + green_tea + hot_pepper # | autumns_bounty + vegetable_medley +# blueberry_tart = ("Blueberry Tart",) +dish_o_the_sea = ("Dish O' The Sea",) +yam = ("Yam",) +linus_loves = cactus_fruit + coconut + dish_o_the_sea + yam # | blueberry_tart +farmers_lunch = ("Farmer's Lunch",) +pumpkin_pie = ("Pumpkin Pie",) +marnie_loves = diamond + farmers_lunch + pink_cake + pumpkin_pie +mead = ("Mead",) +pale_ale = ("Pale Ale",) +parsnip = ("Parsnip",) +# parsnip_soup = ("Parsnip Soup",) +pina_colada = ("Piña Colada",) +pam_loves = beer + cactus_fruit + glazed_yams + mead + pale_ale + parsnip + pina_colada # | parsnip_soup +# fried_calamari = ("Fried Calamari",) +pierre_loves = () # fried_calamari +peach = ("Peach",) +spaghetti = ("Spaghetti",) +robin_loves = goat_cheese + peach + spaghetti +crocus = ("Crocus",) +daffodil = ("Daffodil",) +# mango_stocky_rice = ("Mango Sticky Rice",) +sweet_pea = ("Sweet Pea",) +sandy_loves = crocus + daffodil + sweet_pea # | mango_stocky_rice +cranberry_candy = ("Cranberry Candy",) +ginger_ale = ("Ginger Ale",) +grape = ("Grape",) +snail = ("Snail",) +vincent_loves = cranberry_candy + ginger_ale + grape + pink_cake + snail +catfish = ("Catfish",) +octopus = ("Octopus",) +willy_loves = catfish + diamond + iridium_bar + mead + octopus + pumpkin +purple_mushroom = ("Purple Mushroom",) +solar_essence = ("Solar Essence",) +super_cucumber = ("Super Cucumber",) +void_essence = ("Void Essence",) +wizard_loves = purple_mushroom + solar_essence + super_cucumber + void_essence + +all_villagers: List[Villager] = [] + + +def villager(name: str, bachelor: bool, locations: Tuple[str, ...], birthday: str, gifts: Tuple[str, ...], + available: bool) -> Villager: + npc = Villager(name, bachelor, locations, birthday, gifts, available) + all_villagers.append(npc) + return npc + + +josh = villager("Alex", True, town + alex_house, "Summer", universal_loves + complete_breakfast + salmon_dinner, True) +elliott = villager("Elliott", True, town + beach + elliott_house, "Fall", universal_loves + elliott_loves, True) +harvey = villager("Harvey", True, town + hospital, "Winter", universal_loves + harvey_loves, True) +sam = villager("Sam", True, town, "Summer", universal_loves + sam_loves, True) +sebastian = villager("Sebastian", True, carpenter, "Winter", universal_loves + sebastian_loves, True) +shane = villager("Shane", True, ranch, "Spring", universal_loves + shane_loves, True) +best_girl = villager("Abigail", True, town, "Fall", universal_loves + abigail_loves, True) +emily = villager("Emily", True, town, "Spring", universal_loves + emily_loves, True) +hoe = villager("Haley", True, town, "Spring", universal_loves_no_prismatic_shard + haley_loves, True) +leah = villager("Leah", True, forest, "Winter", universal_loves + leah_loves, True) +nerd = villager("Maru", True, carpenter, "Summer", universal_loves + maru_loves, True) +penny = villager("Penny", True, town, "Fall", universal_loves_no_rabbit_foot + penny_loves, True) +caroline = villager("Caroline", False, town, "Winter", universal_loves + caroline_loves, True) +clint = villager("Clint", False, town, "Winter", universal_loves + clint_loves, True) +demetrius = villager("Demetrius", False, carpenter, "Summer", universal_loves + demetrius_loves, True) +dwarf = villager("Dwarf", False, mines, "Summer", universal_loves + dwarf_loves, False) +gilf = villager("Evelyn", False, town, "Winter", universal_loves + evelyn_loves, True) +boomer = villager("George", False, town, "Fall", universal_loves + george_loves, True) +gus = villager("Gus", False, town, "Summer", universal_loves + gus_loves, True) +jas = villager("Jas", False, ranch, "Summer", universal_loves + jas_loves, True) +jodi = villager("Jodi", False, town, "Fall", universal_loves + jodi_loves, True) +kent = villager("Kent", False, town, "Spring", universal_loves + kent_loves, False) +krobus = villager("Krobus", False, sewers, "Winter", universal_loves + krobus_loves, False) +leo = villager("Leo", False, island, "Summer", universal_loves + leo_loves, False) +lewis = villager("Lewis", False, town, "Spring", universal_loves + lewis_loves, True) +linus = villager("Linus", False, mountain, "Winter", universal_loves + linus_loves, True) +marnie = villager("Marnie", False, ranch, "Fall", universal_loves + marnie_loves, True) +pam = villager("Pam", False, town, "Spring", universal_loves + pam_loves, True) +pierre = villager("Pierre", False, town, "Spring", universal_loves + pierre_loves, True) +milf = villager("Robin", False, carpenter, "Fall", universal_loves + robin_loves, True) +sandy = villager("Sandy", False, oasis, "Fall", universal_loves + sandy_loves, False) +vincent = villager("Vincent", False, town, "Spring", universal_loves + vincent_loves, True) +willy = villager("Willy", False, beach, "Summer", universal_loves + willy_loves, True) +wizard = villager("Wizard", False, forest, "Winter", universal_loves + wizard_loves, True) + +all_villagers_by_name = {item.name: item for item in all_villagers} diff --git a/worlds/stardew_valley/docs/en_Stardew Valley.md b/worlds/stardew_valley/docs/en_Stardew Valley.md index aef280864b..a8aa098bf8 100644 --- a/worlds/stardew_valley/docs/en_Stardew Valley.md +++ b/worlds/stardew_valley/docs/en_Stardew Valley.md @@ -19,6 +19,8 @@ The player can choose from a number of goals, using their YAML settings. - Reach the bottom of the Pelican Town Mineshaft - Complete the "Cryptic Note" quest, by meeting Mr Qi on floor 100 of the Skull Cavern - Get the achievement "Master Angler", which requires catching every fish in the game +- Get the achievement "A Complete Collection", which requires donating all the artifacts and minerals to the museum +- Get the achievement "Full House", which requires getting married and having two kids. ## What are location check in Stardew Valley? @@ -38,12 +40,26 @@ There also are a number of location checks that are optional, and individual pla - Arcade Machines - Help Wanted quests - Fishsanity: Catching individual fish +- Museumsanity: Donating individual items to the museum, or reaching the museum milestones for donations +- Friendsanity: Reaching specific friendship levels with NPCs ## Which items can be in another player's world? Every normal reward from the above locations can be in another player's world. For the locations which do not include a normal reward, Resource Packs are instead added to the pool. These can contain ores, seeds, fertilizers, warp totems, etc. -There are a few extra items, which are added to the pool but do not have a matching location. These include + +A player can enable some settings that will add some items to the pool that are relevant to progression +- Seasons Randomizer: + - All 4 seasons will be items, and one of them will be selected randomly and be added to the player's start inventory + - At the end of each month, the player can choose the next season, instead of following the vanilla season order. On Seasons Randomizer, they can only choose from the seasons they have received. +- Seed Shuffle: + - Every single seed in the game starts off locked and cannot be purchased from any merchant. Their unlocks are received as multiworld items. + - The way merchants sell seeds is considerably changed. Pierre sells fewer seeds at a high price, while Joja sells unlimited seeds but in huge discount packs, not individually. +- Museumsanity: + - The items that are normally obtained from museum donation milestones are added to the item pool. Some items, like the magic rock candy, are duplicated for convenience. + - The Traveling Merchant now sells artifacts and minerals, with a bias towards undonated ones, to mitigate randomness. She will sell these items as the player receives "Traveling Merchant Metal Detector" items. + +There are a few extra vanilla items, which are added to the pool for convenience, but do not have a matching location. These include - Wizard Buildings - Return Scepter diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index 1b2ed15643..37290d69d7 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -5,7 +5,7 @@ - Stardew Valley on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/)) - SMAPI ([Mod loader for Stardew Valley](https://smapi.io/)) - [StardewArchipelago Mod Release 2.x.x](https://github.com/agilbert1412/StardewArchipelago/releases) - - It is important to use a mod release of version 2.x.x to play seeds that have been generated here. Later releases can only be used with later releases of the world generator, that are not hosted on archipelago.gg yet. + - It is important to use a mod release of version 3.x.x to play seeds that have been generated here. Later releases can only be used with later releases of the world generator, that are not hosted on archipelago.gg yet. ## Optional Software - Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases) @@ -56,7 +56,14 @@ The password is optional. Your game will connect automatically to Archipelago, and reconnect automatically when loading the save, later. -You will never need to enter this information again for this character. +You will never need to enter this information again for this character, unless your room changes its ip or port. +If the room's ip or port **does** change, you can follow these instructions to modify the connection information of your save file +- Launch modded Stardew Valley +- While **on the main menu** of the game, enter the follow command **in the SMAPI console**: +- `connect_override ip:port slot password` +- Example: `connect_override archipelago.gg:38281 StardewPlayer` +- Load your save game. The new connection information will be used, instead of the saved one +- Play a full day, sleep, and save the game. This connection information will overwrite the previous one and become permanent. ### Interacting with the MultiWorld from in-game diff --git a/worlds/stardew_valley/fish_data.py b/worlds/stardew_valley/fish_data.py deleted file mode 100644 index 270accb478..0000000000 --- a/worlds/stardew_valley/fish_data.py +++ /dev/null @@ -1,127 +0,0 @@ -from typing import List, Tuple - -from .game_item import FishItem - -spring = ("Spring",) -summer = ("Summer",) -fall = ("Fall",) -winter = ("Winter",) -spring_summer = (*spring, *summer) -spring_fall = (*spring, *fall) -spring_winter = (*spring, *winter) -summer_fall = (*summer, *fall) -summer_winter = (*summer, *winter) -fall_winter = (*fall, *winter) -spring_summer_fall = (*spring, *summer, *fall) -spring_summer_winter = (*spring, *summer, *winter) -spring_fall_winter = (*spring, *fall, *winter) -all_seasons = (*spring, *summer, *fall, *winter) - -town = ("Town",) -beach = ("Beach",) -mountain = ("Mountain",) -forest = ("Forest",) -secret_woods = ("Secret Woods",) -desert = ("The Desert",) -mines_20 = ("The Mines - Floor 20",) -mines_60 = ("The Mines - Floor 60",) -mines_100 = ("The Mines - Floor 100",) -sewers = ("Sewers",) -mutant_bug_lair = ("Mutant Bug Lair",) -witch_swamp = ("Witch's Swamp",) -ginger_island = ("Ginger Island",) -ginger_island_ocean = ginger_island -ginger_island_river = ginger_island -pirate_cove = ginger_island -night_market = beach -lakes = (*mountain, *secret_woods, *sewers) -ocean = beach -rivers = (*town, *forest) -rivers_secret_woods = (*rivers, *secret_woods) -forest_mountain = (*forest, *mountain) -rivers_mountain_lake = (*town, *forest, *mountain) -mines_20_60 = (*mines_20, *mines_60) - -all_fish_items: List[FishItem] = [] - - -def fish(name: str, item_id: int, locations: Tuple[str, ...], seasons: Tuple[str, ...], difficulty: int) -> FishItem: - fish_item = FishItem(name, item_id, locations, seasons, difficulty) - all_fish_items.append(fish_item) - return fish_item - - -carp = fish("Carp", 142, lakes, all_seasons, 15) -herring = fish("Herring", 147, ocean, spring_winter, 25) -smallmouth_bass = fish("Smallmouth Bass", 137, rivers, spring_fall, 28) -anchovy = fish("Anchovy", 129, ocean, spring_fall, 30) -sardine = fish("Sardine", 131, ocean, spring_fall_winter, 30) -sunfish = fish("Sunfish", 145, rivers, spring_summer, 30) -perch = fish("Perch", 141, rivers_mountain_lake, winter, 35) -chub = fish("Chub", 702, forest_mountain, all_seasons, 35) -bream = fish("Bream", 132, rivers, all_seasons, 35) -red_snapper = fish("Red Snapper", 150, ocean, summer_fall, 40) -sea_cucumber = fish("Sea Cucumber", 154, ocean, fall_winter, 40) -rainbow_trout = fish("Rainbow Trout", 138, rivers_mountain_lake, summer, 45) -walleye = fish("Walleye", 140, rivers_mountain_lake, fall, 45) -shad = fish("Shad", 706, rivers, spring_summer_fall, 45) -bullhead = fish("Bullhead", 700, mountain, all_seasons, 46) -largemouth_bass = fish("Largemouth Bass", 136, mountain, all_seasons, 50) -salmon = fish("Salmon", 139, rivers, fall, 50) -ghostfish = fish("Ghostfish", 156, mines_20_60, all_seasons, 50) -tilapia = fish("Tilapia", 701, ocean, summer_fall, 50) -woodskip = fish("Woodskip", 734, secret_woods, all_seasons, 50) -flounder = fish("Flounder", 267, ocean, spring_summer, 50) -halibut = fish("Halibut", 708, ocean, spring_summer_winter, 50) -lionfish = fish("Lionfish", 837, ginger_island_ocean, all_seasons, 50) -slimejack = fish("Slimejack", 796, mutant_bug_lair, all_seasons, 55) -midnight_carp = fish("Midnight Carp", 269, forest_mountain, fall_winter, 55) -red_mullet = fish("Red Mullet", 146, ocean, summer_winter, 55) -pike = fish("Pike", 144, rivers, summer_winter, 60) -tiger_trout = fish("Tiger Trout", 699, rivers, fall_winter, 60) -blue_discus = fish("Blue Discus", 838, ginger_island_river, all_seasons, 60) -albacore = fish("Albacore", 705, ocean, fall_winter, 60) -sandfish = fish("Sandfish", 164, desert, all_seasons, 65) -stonefish = fish("Stonefish", 158, mines_20, all_seasons, 65) -tuna = fish("Tuna", 130, ocean, summer_winter, 70) -eel = fish("Eel", 148, ocean, spring_fall, 70) -catfish = fish("Catfish", 143, rivers_secret_woods, spring_fall, 75) -squid = fish("Squid", 151, ocean, winter, 75) -sturgeon = fish("Sturgeon", 698, mountain, summer_winter, 78) -dorado = fish("Dorado", 704, forest, summer, 78) -pufferfish = fish("Pufferfish", 128, ocean, summer, 80) -void_salmon = fish("Void Salmon", 795, witch_swamp, all_seasons, 80) -super_cucumber = fish("Super Cucumber", 155, ocean, summer_fall, 80) -stingray = fish("Stingray", 836, pirate_cove, all_seasons, 80) -ice_pip = fish("Ice Pip", 161, mines_60, all_seasons, 85) -lingcod = fish("Lingcod", 707, rivers_mountain_lake, winter, 85) -scorpion_carp = fish("Scorpion Carp", 165, desert, all_seasons, 90) -lava_eel = fish("Lava Eel", 162, mines_100, all_seasons, 90) -octopus = fish("Octopus", 149, ocean, summer, 95) - -midnight_squid = fish("Midnight Squid", 798, night_market, winter, 55) -spook_fish = fish("Spook Fish", 799, night_market, winter, 60) -blob_fish = fish("Blobfish", 800, night_market, winter, 75) - -crimsonfish = fish("Crimsonfish", 159, ocean, summer, 95) -angler = fish("Angler", 160, town, fall, 85) -legend = fish("Legend", 163, mountain, spring, 110) -glacierfish = fish("Glacierfish", 775, forest, winter, 100) -mutant_carp = fish("Mutant Carp", 682, sewers, all_seasons, 80) - -crayfish = fish("Crayfish", 716, rivers, all_seasons, -1) -snail = fish("Snail", 721, rivers, all_seasons, -1) -periwinkle = fish("Periwinkle", 722, rivers, all_seasons, -1) -lobster = fish("Lobster", 715, ocean, all_seasons, -1) -clam = fish("Clam", 372, ocean, all_seasons, -1) -crab = fish("Crab", 717, ocean, all_seasons, -1) -cockle = fish("Cockle", 718, ocean, all_seasons, -1) -mussel = fish("Mussel", 719, ocean, all_seasons, -1) -shrimp = fish("Shrimp", 720, ocean, all_seasons, -1) -oyster = fish("Oyster", 723, ocean, all_seasons, -1) - -legendary_fish = [crimsonfish, angler, legend, glacierfish, mutant_carp] -special_fish = [*legendary_fish, blob_fish, lava_eel, octopus, scorpion_carp, ice_pip, super_cucumber, dorado] - -all_fish_items_by_name = {fish.name: fish for fish in all_fish_items} -all_fish_items_by_id = {fish.item_id: fish for fish in all_fish_items} diff --git a/worlds/stardew_valley/game_item.py b/worlds/stardew_valley/game_item.py deleted file mode 100644 index 6b8eb6c6aa..0000000000 --- a/worlds/stardew_valley/game_item.py +++ /dev/null @@ -1,26 +0,0 @@ -from dataclasses import dataclass -from typing import Tuple - - -@dataclass(frozen=True) -class GameItem: - name: str - item_id: int - - def __repr__(self): - return f"{self.name} [{self.item_id}]" - - def __lt__(self, other): - return self.name < other.name - - -@dataclass(frozen=True) -class FishItem(GameItem): - locations: Tuple[str] - seasons: Tuple[str] - difficulty: int - - def __repr__(self): - return f"{self.name} [{self.item_id}] (Locations: {self.locations} |" \ - f" Seasons: {self.seasons} |" \ - f" Difficulty: {self.difficulty}) " diff --git a/worlds/stardew_valley/items.py b/worlds/stardew_valley/items.py index 03419a1610..3f7c53b55c 100644 --- a/worlds/stardew_valley/items.py +++ b/worlds/stardew_valley/items.py @@ -14,6 +14,8 @@ from typing import Dict, List, Protocol, Union, Set, Optional, FrozenSet from BaseClasses import Item, ItemClassification from . import options, data +from .data.villagers_data import all_villagers +from .options import StardewOptions ITEM_CODE_OFFSET = 717000 @@ -47,7 +49,12 @@ class Group(enum.Enum): ORE = enum.auto() FERTILIZER = enum.auto() SEED = enum.auto() + SEED_SHUFFLE = enum.auto() FISHING_RESOURCE = enum.auto() + SEASON = enum.auto() + TRAVELING_MERCHANT_DAY = enum.auto() + MUSEUM = enum.auto() + FRIENDSANITY = enum.auto() @dataclass(frozen=True) @@ -132,7 +139,7 @@ def load_item_csv(): try: from importlib.resources import files except ImportError: - from importlib_resources import files + from importlib_resources import files # noqa items = [] with files(data).joinpath("items.csv").open() as file: @@ -149,7 +156,7 @@ def load_resource_pack_csv() -> List[ResourcePackData]: try: from importlib.resources import files except ImportError: - from importlib_resources import files + from importlib_resources import files # noqa resource_packs = [] with files(data).joinpath("resource_packs.csv").open() as file: @@ -166,11 +173,7 @@ def load_resource_pack_csv() -> List[ResourcePackData]: events = [ ItemData(None, "Victory", ItemClassification.progression), - ItemData(None, "Spring", ItemClassification.progression), - ItemData(None, "Summer", ItemClassification.progression), - ItemData(None, "Fall", ItemClassification.progression), - ItemData(None, "Winter", ItemClassification.progression), - ItemData(None, "Year Two", ItemClassification.progression), + ItemData(None, "Month End", ItemClassification.progression), ] all_items: List[ItemData] = load_item_csv() + events @@ -197,10 +200,14 @@ initialize_item_table() initialize_groups() -def create_items(item_factory: StardewItemFactory, locations_count: int, world_options: options.StardewOptions, - random: Random) \ - -> List[Item]: +def create_items(item_factory: StardewItemFactory, locations_count: int, items_to_exclude: List[Item], world_options: StardewOptions, + random: Random) -> List[Item]: items = create_unique_items(item_factory, world_options, random) + + for item in items_to_exclude: + if item in items: + items.remove(item) + assert len(items) <= locations_count, \ "There should be at least as many locations as there are mandatory items" logger.debug(f"Created {len(items)} unique items") @@ -212,7 +219,37 @@ def create_items(item_factory: StardewItemFactory, locations_count: int, world_o return items -def create_backpack_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): +def create_unique_items(item_factory: StardewItemFactory, world_options: StardewOptions, random: Random) -> List[Item]: + items = [] + + items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD]) + + create_backpack_items(item_factory, world_options, items) + create_mine_rewards(item_factory, items, random) + create_mine_elevators(item_factory, world_options, items) + create_tools(item_factory, world_options, items) + create_skills(item_factory, world_options, items) + create_wizard_buildings(item_factory, items) + create_carpenter_buildings(item_factory, world_options, items) + items.append(item_factory("Beach Bridge")) + create_special_quest_rewards(item_factory, items) + create_stardrops(item_factory, items) + create_museum_items(item_factory, world_options, items) + create_arcade_machine_items(item_factory, world_options, items) + items.append(item_factory(random.choice(items_by_group[Group.GALAXY_WEAPONS]))) + items.append( + item_factory(friendship_pack.create_name_from_multiplier(world_options[options.ResourcePackMultiplier]))) + create_player_buffs(item_factory, world_options, items) + items.extend(create_traveling_merchant_items(item_factory)) + items.append(item_factory("Return Scepter")) + items.extend(create_seasons(item_factory, world_options)) + items.extend(create_seeds(item_factory, world_options)) + create_friendsanity_items(item_factory, world_options, items) + + return items + + +def create_backpack_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): if (world_options[options.BackpackProgression] == options.BackpackProgression.option_progressive or world_options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive): items.extend(item_factory(item) for item in ["Progressive Backpack"] * 2) @@ -232,7 +269,7 @@ def create_mine_rewards(item_factory: StardewItemFactory, items: List[Item], ran items.append(item_factory("Skull Key")) -def create_mine_elevators(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): +def create_mine_elevators(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): if (world_options[options.TheMinesElevatorsProgression] == options.TheMinesElevatorsProgression.option_progressive or world_options[options.TheMinesElevatorsProgression] == @@ -240,13 +277,13 @@ def create_mine_elevators(item_factory: StardewItemFactory, world_options: optio items.extend([item_factory(item) for item in ["Progressive Mine Elevator"] * 24]) -def create_tools(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): +def create_tools(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): if world_options[options.ToolProgression] == options.ToolProgression.option_progressive: items.extend(item_factory(item) for item in items_by_group[Group.PROGRESSIVE_TOOLS] * 4) items.append(item_factory("Golden Scythe")) -def create_skills(item_factory: StardewItemFactory, world_options: options.StardewOptions, items: List[Item]): +def create_skills(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): if world_options[options.SkillProgression] == options.SkillProgression.option_progressive: items.extend([item_factory(item) for item in items_by_group[Group.SKILL_LEVEL_UP] * 10]) @@ -260,7 +297,7 @@ def create_wizard_buildings(item_factory: StardewItemFactory, items: List[Item]) items.append(item_factory("Gold Clock")) -def create_carpenter_buildings(item_factory: StardewItemFactory, world_options: options.StardewOptions, +def create_carpenter_buildings(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): if world_options[options.BuildingProgression] in {options.BuildingProgression.option_progressive, options.BuildingProgression.option_progressive_early_shipping_bin}: @@ -297,7 +334,41 @@ def create_stardrops(item_factory: StardewItemFactory, items: List[Item]): items.append(item_factory("Stardrop")) # Old Master Cannoli -def create_arcade_machine_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, +def create_museum_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): + if world_options[options.Museumsanity] == options.Museumsanity.option_none: + return + items.extend(item_factory(item) for item in ["Magic Rock Candy"] * 5) + items.extend(item_factory(item) for item in ["Ancient Seeds"] * 5) + items.extend(item_factory(item) for item in ["Traveling Merchant Metal Detector"] * 4) + items.append(item_factory("Ancient Seeds Recipe")) + items.append(item_factory("Stardrop")) + items.append(item_factory("Rusty Key")) + items.append(item_factory("Dwarvish Translation Guide")) + + +def create_friendsanity_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): + if world_options[options.Friendsanity] == options.Friendsanity.option_none: + return + exclude_non_bachelors = world_options[options.Friendsanity] == options.Friendsanity.option_bachelors + exclude_locked_villagers = world_options[options.Friendsanity] == options.Friendsanity.option_starting_npcs or \ + world_options[options.Friendsanity] == options.Friendsanity.option_bachelors + exclude_post_marriage_hearts = world_options[options.Friendsanity] != options.Friendsanity.option_all_with_marriage + for villager in all_villagers: + if not villager.available and exclude_locked_villagers: + continue + if not villager.bachelor and exclude_non_bachelors: + continue + for heart in range(1, 15): + if villager.bachelor and exclude_post_marriage_hearts and heart > 8: + continue + if villager.bachelor or heart < 11: + items.append(item_factory(f"{villager.name}: 1 <3")) + if not exclude_non_bachelors: + for heart in range(1, 6): + items.append(item_factory(f"Pet: 1 <3")) + + +def create_arcade_machine_items(item_factory: StardewItemFactory, world_options: StardewOptions, items: List[Item]): if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling: items.append(item_factory("JotPK: Progressive Boots")) @@ -321,43 +392,29 @@ def create_player_buffs(item_factory: StardewItemFactory, world_options: options items.extend(item_factory(item) for item in ["Luck Bonus"] * number_of_buffs) -def create_traveling_merchant_items(item_factory: StardewItemFactory, items: List[Item]): - items.append(item_factory("Traveling Merchant: Sunday")) - items.append(item_factory("Traveling Merchant: Monday")) - items.append(item_factory("Traveling Merchant: Tuesday")) - items.append(item_factory("Traveling Merchant: Wednesday")) - items.append(item_factory("Traveling Merchant: Thursday")) - items.append(item_factory("Traveling Merchant: Friday")) - items.append(item_factory("Traveling Merchant: Saturday")) - items.extend(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6) - items.extend(item_factory(item) for item in ["Traveling Merchant Discount"] * 8) +def create_traveling_merchant_items(item_factory: StardewItemFactory) -> List[Item]: + return [ + *(item_factory(item) for item in items_by_group[Group.TRAVELING_MERCHANT_DAY]), + *(item_factory(item) for item in ["Traveling Merchant Stock Size"] * 6), + *(item_factory(item) for item in ["Traveling Merchant Discount"] * 8), + ] -def create_unique_items(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random) -> \ - List[Item]: - items = [] +def create_seasons(item_factory: StardewItemFactory, world_options: StardewOptions) -> List[Item]: + if world_options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled: + return [] - items.extend(item_factory(item) for item in items_by_group[Group.COMMUNITY_REWARD]) + if world_options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive: + return [item_factory(item) for item in ["Progressive Season"] * 3] - create_backpack_items(item_factory, world_options, items) - create_mine_rewards(item_factory, items, random) - create_mine_elevators(item_factory, world_options, items) - create_tools(item_factory, world_options, items) - create_skills(item_factory, world_options, items) - create_wizard_buildings(item_factory, items) - create_carpenter_buildings(item_factory, world_options, items) - items.append(item_factory("Beach Bridge")) - create_special_quest_rewards(item_factory, items) - create_stardrops(item_factory, items) - create_arcade_machine_items(item_factory, world_options, items) - items.append(item_factory(random.choice(items_by_group[Group.GALAXY_WEAPONS]))) - items.append( - item_factory(friendship_pack.create_name_from_multiplier(world_options[options.ResourcePackMultiplier]))) - create_player_buffs(item_factory, world_options, items) - create_traveling_merchant_items(item_factory, items) - items.append(item_factory("Return Scepter")) + return [item_factory(item) for item in items_by_group[Group.SEASON]] - return items + +def create_seeds(item_factory: StardewItemFactory, world_options: StardewOptions) -> List[Item]: + if world_options[options.SeedShuffle] == options.SeedShuffle.option_disabled: + return [] + + return [item_factory(item) for item in items_by_group[Group.SEED_SHUFFLE]] def fill_with_resource_packs(item_factory: StardewItemFactory, world_options: options.StardewOptions, random: Random, diff --git a/worlds/stardew_valley/locations.py b/worlds/stardew_valley/locations.py index a7cb70c570..13af203bff 100644 --- a/worlds/stardew_valley/locations.py +++ b/worlds/stardew_valley/locations.py @@ -5,7 +5,9 @@ from random import Random from typing import Optional, Dict, Protocol, List, FrozenSet from . import options, data -from .fish_data import legendary_fish, special_fish, all_fish_items +from .data.fish_data import legendary_fish, special_fish, all_fish +from .data.museum_data import all_museum_items +from .data.villagers_data import all_villagers LOCATION_CODE_OFFSET = 717000 @@ -46,6 +48,9 @@ class LocationTags(enum.Enum): HELP_WANTED = enum.auto() TRAVELING_MERCHANT = enum.auto() FISHSANITY = enum.auto() + MUSEUM_MILESTONES = enum.auto() + MUSEUM_DONATIONS = enum.auto() + FRIENDSANITY = enum.auto() @dataclass(frozen=True) @@ -88,10 +93,8 @@ events_locations = [ LocationData(None, "The Mines - Floor 120", "Reach the Bottom of The Mines"), LocationData(None, "Skull Cavern", "Complete Quest Cryptic Note"), LocationData(None, "Stardew Valley", "Catch Every Fish"), - LocationData(None, "Stardew Valley", "Summer"), - LocationData(None, "Stardew Valley", "Fall"), - LocationData(None, "Stardew Valley", "Winter"), - LocationData(None, "Stardew Valley", "Year Two"), + LocationData(None, "Stardew Valley", "Complete the Museum Collection"), + LocationData(None, "Stardew Valley", "Full House"), ] all_locations = load_location_csv() + events_locations @@ -133,11 +136,46 @@ def extend_fishsanity_locations(randomized_locations: List[LocationData], fishsa randomized_locations.extend(location_table[f"{prefix}{legendary.name}"] for legendary in legendary_fish) elif fishsanity == options.Fishsanity.option_special: randomized_locations.extend(location_table[f"{prefix}{special.name}"] for special in special_fish) - elif fishsanity == options.Fishsanity.option_random_selection: + elif fishsanity == options.Fishsanity.option_randomized: randomized_locations.extend(location_table[f"{prefix}{fish.name}"] - for fish in all_fish_items if random.random() < 0.4) + for fish in all_fish if random.random() < 0.4) elif fishsanity == options.Fishsanity.option_all: - randomized_locations.extend(location_table[f"{prefix}{fish.name}"] for fish in all_fish_items) + randomized_locations.extend(location_table[f"{prefix}{fish.name}"] for fish in all_fish) + + +def extend_museumsanity_locations(randomized_locations: List[LocationData], museumsanity: int, random: Random): + prefix = "Museumsanity: " + if museumsanity == options.Museumsanity.option_none: + return + elif museumsanity == options.Museumsanity.option_milestones: + randomized_locations.extend(locations_by_tag[LocationTags.MUSEUM_MILESTONES]) + elif museumsanity == options.Museumsanity.option_randomized: + randomized_locations.extend(location_table[f"{prefix}{museum_item.name}"] + for museum_item in all_museum_items if random.random() < 0.4) + elif museumsanity == options.Museumsanity.option_all: + randomized_locations.extend(location_table[f"{prefix}{museum_item.name}"] for museum_item in all_museum_items) + + +def extend_friendsanity_locations(randomized_locations: List[LocationData], friendsanity: int): + if friendsanity == options.Friendsanity.option_none: + return + exclude_non_bachelors = friendsanity == options.Friendsanity.option_bachelors + exclude_locked_villagers = friendsanity == options.Friendsanity.option_starting_npcs or \ + friendsanity == options.Friendsanity.option_bachelors + exclude_post_marriage_hearts = friendsanity != options.Friendsanity.option_all_with_marriage + for villager in all_villagers: + if not villager.available and exclude_locked_villagers: + continue + if not villager.bachelor and exclude_non_bachelors: + continue + for heart in range(1, 15): + if villager.bachelor and exclude_post_marriage_hearts and heart > 8: + continue + if villager.bachelor or heart < 11: + randomized_locations.append(location_table[f"Friendsanity: {villager.name} {heart} <3"]) + if not exclude_non_bachelors: + for heart in range(1, 6): + randomized_locations.append(location_table[f"Friendsanity: Pet {heart} <3"]) def create_locations(location_collector: StardewLocationCollector, @@ -170,6 +208,8 @@ def create_locations(location_collector: StardewLocationCollector, extend_help_wanted_quests(randomized_locations, world_options[options.HelpWantedLocations]) extend_fishsanity_locations(randomized_locations, world_options[options.Fishsanity], random) + extend_museumsanity_locations(randomized_locations, world_options[options.Museumsanity], random) + extend_friendsanity_locations(randomized_locations, world_options[options.Friendsanity]) for location_data in randomized_locations: location_collector(location_data.name, location_data.code, location_data.region) diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py index 85a5bb08d4..c4d157b827 100644 --- a/worlds/stardew_valley/logic.py +++ b/worlds/stardew_valley/logic.py @@ -1,16 +1,19 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import Dict, Union, Optional, Iterable, Sized, Tuple, List, FrozenSet +from typing import Dict, Union, Optional, Iterable, Sized, Tuple, List -from BaseClasses import CollectionState, ItemClassification from . import options -from .bundle_data import BundleItem -from .fish_data import all_fish_items -from .game_item import FishItem -from .items import all_items, Group, item_table +from .data import all_fish, FishItem, all_purchasable_seeds, SeedItem, all_crops, CropItem +from .data.bundle_data import BundleItem +from .data.museum_data import all_museum_items, MuseumItem +from .data.region_data import SVRegion +from .data.villagers_data import all_villagers_by_name +from .items import all_items, Group from .options import StardewOptions +from .stardew_rule import False_, Reach, Or, True_, Received, Count, And, Has, TotalReceived, StardewRule +MONEY_PER_MONTH = 15000 MISSING_ITEM = "THIS ITEM IS MISSING" tool_materials = { @@ -27,36 +30,36 @@ tool_prices = { "Iridium": 25000 } -skill_level_per_season = { - "Spring": { +skill_level_per_month_end = { + 0: { "Farming": 2, "Fishing": 2, "Foraging": 2, "Mining": 2, "Combat": 2, }, - "Summer": { + 1: { "Farming": 4, "Fishing": 4, "Foraging": 4, "Mining": 4, "Combat": 3, }, - "Fall": { + 2: { "Farming": 7, "Fishing": 5, "Foraging": 5, "Mining": 5, "Combat": 4, }, - "Winter": { + 3: { "Farming": 7, "Fishing": 7, "Foraging": 6, "Mining": 7, "Combat": 5, }, - "Year Two": { + 4: { "Farming": 10, "Fishing": 10, "Foraging": 10, @@ -64,8 +67,8 @@ skill_level_per_season = { "Combat": 10, }, } -season_per_skill_level: Dict[Tuple[str, int], str] = {} -season_per_total_level: Dict[int, str] = {} +month_end_per_skill_level: Dict[Tuple[str, int], int] = {} +month_end_per_total_level: Dict[int, int] = {} def initialize_season_per_skill_level(): @@ -76,509 +79,188 @@ def initialize_season_per_skill_level(): "Mining": 0, "Combat": 0, } - for season, skills in skill_level_per_season.items(): + for month_end, skills in skill_level_per_month_end.items(): for skill, expected_level in skills.items(): for level_up in range(current_level[skill] + 1, expected_level + 1): skill_level = (skill, level_up) - if skill_level not in season_per_skill_level: - season_per_skill_level[skill_level] = season + if skill_level not in month_end_per_skill_level: + month_end_per_skill_level[skill_level] = month_end level_up = 0 for level_up in range(level_up + 1, sum(skills.values()) + 1): - if level_up not in season_per_total_level: - season_per_total_level[level_up] = season + if level_up not in month_end_per_total_level: + month_end_per_total_level[level_up] = month_end initialize_season_per_skill_level() week_days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] -class StardewRule: - def __call__(self, state: CollectionState) -> bool: - raise NotImplementedError - - def __or__(self, other) -> StardewRule: - if isinstance(other, _Or): - return _Or(self, *other.rules) - - return _Or(self, other) - - def __and__(self, other) -> StardewRule: - if isinstance(other, _And): - return _And(other.rules.union({self})) - - return _And(self, other) - - def get_difficulty(self): - raise NotImplementedError - - def simplify(self) -> StardewRule: - return self - - -class _True(StardewRule): - - def __new__(cls, _cache=[]): # noqa - if not _cache: - _cache.append(super(_True, cls).__new__(cls)) - return _cache[0] - - def __call__(self, state: CollectionState) -> bool: - return True - - def __or__(self, other) -> StardewRule: - return self - - def __and__(self, other) -> StardewRule: - return other - - def __repr__(self): - return "True" - - def get_difficulty(self): - return 0 - - -class _False(StardewRule): - - def __new__(cls, _cache=[]): # noqa - if not _cache: - _cache.append(super(_False, cls).__new__(cls)) - return _cache[0] - - def __call__(self, state: CollectionState) -> bool: - return False - - def __or__(self, other) -> StardewRule: - return other - - def __and__(self, other) -> StardewRule: - return self - - def __repr__(self): - return "False" - - def get_difficulty(self): - return 999999999 - - -class _Or(StardewRule): - rules: FrozenSet[StardewRule] - - def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): - rules_list = set() - if isinstance(rule, Iterable): - rules_list.update(rule) - else: - rules_list.add(rule) - - if rules is not None: - rules_list.update(rules) - - assert rules_list, "Can't create a Or conditions without rules" - - new_rules = set() - for rule in rules_list: - if isinstance(rule, _Or): - new_rules.update(rule.rules) - else: - new_rules.add(rule) - rules_list = new_rules - - self.rules = frozenset(rules_list) - - def __call__(self, state: CollectionState) -> bool: - return any(rule(state) for rule in self.rules) - - def __repr__(self): - return f"({' | '.join(repr(rule) for rule in self.rules)})" - - def __or__(self, other): - if isinstance(other, _True): - return other - if isinstance(other, _False): - return self - if isinstance(other, _Or): - return _Or(self.rules.union(other.rules)) - - return _Or(self.rules.union({other})) - - def __eq__(self, other): - return isinstance(other, self.__class__) and other.rules == self.rules - - def __hash__(self): - return hash(self.rules) - - def get_difficulty(self): - return min(rule.get_difficulty() for rule in self.rules) - - def simplify(self) -> StardewRule: - if any(isinstance(rule, _True) for rule in self.rules): - return _True() - - simplified_rules = {rule.simplify() for rule in self.rules} - simplified_rules = {rule for rule in simplified_rules if rule is not _False()} - - if not simplified_rules: - return _False() - - if len(simplified_rules) == 1: - return next(iter(simplified_rules)) - - return _Or(simplified_rules) - - -class _And(StardewRule): - rules: frozenset[StardewRule] - - def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): - rules_list = set() - if isinstance(rule, Iterable): - rules_list.update(rule) - else: - rules_list.add(rule) - - if rules is not None: - rules_list.update(rules) - - assert rules_list, "Can't create a And conditions without rules" - - new_rules = set() - for rule in rules_list: - if isinstance(rule, _And): - new_rules.update(rule.rules) - else: - new_rules.add(rule) - rules_list = new_rules - - self.rules = frozenset(rules_list) - - def __call__(self, state: CollectionState) -> bool: - return all(rule(state) for rule in self.rules) - - def __repr__(self): - return f"({' & '.join(repr(rule) for rule in self.rules)})" - - def __and__(self, other): - if isinstance(other, _True): - return self - if isinstance(other, _False): - return other - if isinstance(other, _And): - return _And(self.rules.union(other.rules)) - - return _And(self.rules.union({other})) - - def __eq__(self, other): - return isinstance(other, self.__class__) and other.rules == self.rules - - def __hash__(self): - return hash(self.rules) - - def get_difficulty(self): - return max(rule.get_difficulty() for rule in self.rules) - - def simplify(self) -> StardewRule: - if any(isinstance(rule, _False) for rule in self.rules): - return _False() - - simplified_rules = {rule.simplify() for rule in self.rules} - simplified_rules = {rule for rule in simplified_rules if rule is not _True()} - - if not simplified_rules: - return _True() - - if len(simplified_rules) == 1: - return next(iter(simplified_rules)) - - return _And(simplified_rules) - - -class _Count(StardewRule): - count: int - rules: List[StardewRule] - - def __init__(self, count: int, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): - rules_list = [] - if isinstance(rule, Iterable): - rules_list.extend(rule) - else: - rules_list.append(rule) - - if rules is not None: - rules_list.extend(rules) - - assert rules_list, "Can't create a Count conditions without rules" - assert len(rules_list) >= count, "Count need at least as many rules at the count" - - self.rules = rules_list - self.count = count - - def __call__(self, state: CollectionState) -> bool: - c = 0 - for r in self.rules: - if r(state): - c += 1 - if c >= self.count: - return True - return False - - def __repr__(self): - return f"Received {self.count} {repr(self.rules)}" - - def get_difficulty(self): - rules_sorted_by_difficulty = sorted(self.rules, key=lambda x: x.get_difficulty()) - easiest_n_rules = rules_sorted_by_difficulty[0:self.count] - return max(rule.get_difficulty() for rule in easiest_n_rules) - - def simplify(self): - return _Count(self.count, [rule.simplify() for rule in self.rules]) - - -class _TotalReceived(StardewRule): - count: int - items: Iterable[str] - player: int - - def __init__(self, count: int, items: Union[str, Iterable[str]], player: int): - items_list = [] - if isinstance(items, Iterable): - items_list.extend(items) - else: - items_list.append(items) - - assert items_list, "Can't create a Total Received conditions without items" - for item in items_list: - assert item_table[item].classification & ItemClassification.progression, \ - "Item has to be progression to be used in logic" - - self.player = player - self.items = items_list - self.count = count - - def __call__(self, state: CollectionState) -> bool: - c = 0 - for item in self.items: - c += state.count(item, self.player) - if c >= self.count: - return True - return False - - def __repr__(self): - return f"Received {self.count} {self.items}" - - def get_difficulty(self): - return self.count - - -@dataclass(frozen=True) -class _Received(StardewRule): - item: str - player: int - count: int - - def __post_init__(self): - assert item_table[self.item].classification & ItemClassification.progression, \ - "Item has to be progression to be used in logic" - - def __call__(self, state: CollectionState) -> bool: - return state.has(self.item, self.player, self.count) - - def __repr__(self): - if self.count == 1: - return f"Received {self.item}" - return f"Received {self.count} {self.item}" - - def get_difficulty(self): - if self.item == "Spring": - return 0 - if self.item == "Summer": - return 1 - if self.item == "Fall": - return 2 - if self.item == "Winter": - return 3 - if self.item == "Year Two": - return 4 - return self.count - - -@dataclass(frozen=True) -class _Reach(StardewRule): - spot: str - resolution_hint: str - player: int - - def __call__(self, state: CollectionState) -> bool: - return state.can_reach(self.spot, self.resolution_hint, self.player) - - def __repr__(self): - return f"Reach {self.resolution_hint} {self.spot}" - - def get_difficulty(self): - return 1 - - -@dataclass(frozen=True) -class _Has(StardewRule): - item: str - # For sure there is a better way than just passing all the rules everytime - other_rules: Dict[str, StardewRule] - - def __call__(self, state: CollectionState) -> bool: - if isinstance(self.item, str): - return self.other_rules[self.item](state) - - def __repr__(self): - if not self.item in self.other_rules: - return f"Has {self.item} -> {MISSING_ITEM}" - return f"Has {self.item} -> {repr(self.other_rules[self.item])}" - - def get_difficulty(self): - return self.other_rules[self.item].get_difficulty() + 1 - - def __hash__(self): - return hash(self.item) - - def simplify(self) -> StardewRule: - return self.other_rules[self.item].simplify() - - -@dataclass(frozen=True) +@dataclass(frozen=True, repr=False) class StardewLogic: player: int options: StardewOptions item_rules: Dict[str, StardewRule] = field(default_factory=dict) + tree_fruit_rules: Dict[str, StardewRule] = field(default_factory=dict) + seed_rules: Dict[str, StardewRule] = field(default_factory=dict) + crops_rules: Dict[str, StardewRule] = field(default_factory=dict) fish_rules: Dict[str, StardewRule] = field(default_factory=dict) + museum_rules: Dict[str, StardewRule] = field(default_factory=dict) building_rules: Dict[str, StardewRule] = field(default_factory=dict) quest_rules: Dict[str, StardewRule] = field(default_factory=dict) def __post_init__(self): - self.fish_rules.update({fish.name: self.can_catch_fish(fish) for fish in all_fish_items}) + self.fish_rules.update({fish.name: self.can_catch_fish(fish) for fish in all_fish}) + self.museum_rules.update({donation.name: self.can_find_museum_item(donation) for donation in all_museum_items}) + + self.tree_fruit_rules.update({ + "Apple": self.has_lived_months(1) & (self.has_season("Fall") | self.can_reach_region(SVRegion.greenhouse)), + "Apricot": self.has_lived_months(1) & (self.has_season("Spring") | self.can_reach_region(SVRegion.greenhouse)), + "Cherry": self.has_lived_months(1) & (self.has_season("Spring") | self.can_reach_region(SVRegion.greenhouse)), + "Orange": self.has_lived_months(1) & (self.has_season("Summer") | self.can_reach_region(SVRegion.greenhouse)), + "Peach": self.has_lived_months(1) & (self.has_season("Summer") | self.can_reach_region(SVRegion.greenhouse)), + "Pomegranate": self.has_lived_months(1) & (self.has_season("Fall") | self.can_reach_region(SVRegion.greenhouse)), + "Banana Sapling": self.can_reach_region(SVRegion.ginger_island), + "Mango Sapling": self.can_reach_region(SVRegion.ginger_island), + "Banana": self.has("Banana Sapling") & (self.has_season("Summer") | self.can_reach_region(SVRegion.greenhouse)), + "Mango": self.has("Mango Sapling") & (self.has_season("Summer") | self.can_reach_region(SVRegion.greenhouse)), + }) + + self.seed_rules.update({seed.name: self.can_buy_seed(seed) for seed in all_purchasable_seeds}) + self.crops_rules.update({crop.name: self.can_grow_crop(crop) for crop in all_crops}) + self.crops_rules.update({ + "Coffee Bean": (self.has_season("Spring") | self.has_season("Summer")) & self.has_traveling_merchant(), + }) self.item_rules.update({ "Aged Roe": self.has("Preserves Jar") & self.has("Roe"), - "Algae Soup": self.can_cook() & self.has("Green Algae") & self.can_have_relationship("Clint", 3), - "Amaranth": self.received("Fall"), - "Amethyst": self.can_mine_in_the_mines_floor_1_40(), - "Ancient Drum": self.has("Frozen Geode"), + "Algae Soup": self.can_cook() & self.has("Green Algae") & self.has_relationship("Clint", 3), "Any Egg": self.has("Chicken Egg") | self.has("Duck Egg"), - "Apple": self.received("Fall"), - "Apricot": self.received("Year Two"), - "Aquamarine": self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern(), - "Artichoke": self.received("Year Two") & self.received("Fall"), + "Artichoke Dip": self.can_cook() & self.has_season("Fall") & self.has("Artichoke") & self.has("Cow Milk"), + "Artifact Trove": self.has("Omni Geode") & self.can_reach_region(SVRegion.desert), "Bait": self.has_skill_level("Fishing", 2), "Bat Wing": self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern(), "Battery Pack": self.has("Lightning Rod"), + "Bean Hotpot": self.can_cook() & self.has_relationship("Clint", 7) & self.has("Green Bean"), "Bee House": self.has_skill_level("Farming", 3) & self.has("Iron Bar") & self.has("Maple Syrup"), "Beer": (self.has("Keg") & self.has("Wheat")) | self.can_spend_money(400), - "Beet": self.received("Fall") & self.can_reach_region("The Desert"), - "Blackberry": self.received("Fall"), - "Blue Jazz": self.received("Spring"), - "Blueberry": self.received("Summer"), - "Blueberry Tart": self.has("Blueberry") & self.has("Any Egg") & self.can_have_relationship("Pierre", 3), - "Bok Choy": self.received("Fall"), - "Bouquet": self.can_have_relationship("Any", 8), + "Blackberry": self.has_season("Fall"), + "Blackberry Cobbler": self.can_cook() & self.has_season("Fall") & self.has_year_two() & + self.has("Blackberry") & self.has("Sugar") & self.has("Wheat Flour"), + "Blueberry Tart": self.has("Blueberry") & self.has("Any Egg") & self.has_relationship("Pierre", 3), + "Bouquet": self.has_relationship("Bachelor", 8), "Bread": self.can_spend_money(120) | (self.can_spend_money(100) & self.can_cook()), "Broken CD": self.can_crab_pot(), "Broken Glasses": self.can_crab_pot(), "Bug Meat": self.can_mine_in_the_mines_floor_1_40(), - "Cactus Fruit": self.can_reach_region("The Desert"), - "Cauliflower": self.received("Spring"), - "Cave Carrot": self.has_mine_elevator_to_floor(10), + "Cactus Fruit": self.can_reach_region(SVRegion.desert), + "Cave Carrot": self.can_mine_to_floor(10), "Caviar": self.has("Preserves Jar") & self.has("Sturgeon Roe"), - "Chanterelle": self.received("Fall") & self.can_reach_region("Secret Woods"), + "Chanterelle": self.has_season("Fall") & self.can_reach_region(SVRegion.secret_woods), "Cheese Press": self.has_skill_level("Farming", 6) & self.has("Hardwood") & self.has("Copper Bar"), "Cheese": (self.has("Cow Milk") & self.has("Cheese Press")) | - (self.can_reach_region("The Desert") & self.has("Emerald")), - "Cheese Cauliflower": self.has(["Cheese", "Cauliflower"]) & self.can_have_relationship("Pam", 3) & + (self.can_reach_region(SVRegion.desert) & self.has("Emerald")), + "Cheese Cauliflower": self.has(["Cheese", "Cauliflower"]) & self.has_relationship("Pam", 3) & self.can_cook(), - "Cherry": self.received("Year Two"), "Chicken": self.has_building("Coop"), "Chicken Egg": self.has(["Egg", "Egg (Brown)", "Large Egg", "Large Egg (Brown)"], 1), - "Chowder": self.can_cook() & self.can_have_relationship("Willy", 3) & self.has(["Clam", "Cow Milk"]), - "Clam": _True(), - "Clay": _True(), + "Chocolate Cake": self.can_cook() & self.has_season("Winter") & self.has("Wheat Flour") & self.has( + "Sugar") & self.has("Any Egg"), + "Chowder": self.can_cook() & self.has_relationship("Willy", 3) & self.has(["Clam", "Cow Milk"]), + "Clam": True_(), + "Clay": True_(), + "Glazed Yams": self.can_cook() & self.has_season("Fall") & self.has("Yam") & self.has("Sugar"), "Cloth": (self.has("Wool") & self.has("Loom")) | - (self.can_reach_region("The Desert") & self.has("Aquamarine")), - "Coal": _True(), - "Cockle": _True(), - "Coconut": self.can_reach_region("The Desert"), + (self.can_reach_region(SVRegion.desert) & self.has("Aquamarine")), + "Coal": True_(), + "Cockle": True_(), + "Coconut": self.can_reach_region(SVRegion.desert), "Coffee": (self.has("Keg") & self.has("Coffee Bean")) | self.has("Coffee Maker") | self.can_spend_money(300) | self.has("Hot Java Ring"), - "Coffee Bean": (self.received("Spring") | self.received("Summer")) & - (self.can_mine_in_the_mines_floor_41_80() | _True()), # Travelling merchant - "Coffee Maker": _False(), - "Common Mushroom": self.received("Fall") | - (self.received("Spring") & self.can_reach_region("Secret Woods")), + "Coffee Maker": False_(), + "Common Mushroom": self.has_season("Fall") | + (self.has_season("Spring") & self.can_reach_region(SVRegion.secret_woods)), + "Complete Breakfast": self.can_cook() & self.has_season("Spring") & self.has_lived_months(4) & + self.has("Fried Egg") & self.has("Cow Milk") & self.has("Hashbrowns") | self.has( + "Pancakes"), "Copper Bar": self.can_smelt("Copper Ore"), "Copper Ore": self.can_mine_in_the_mines_floor_1_40() | self.can_mine_in_the_skull_cavern(), - "Coral": self.can_reach_region("Tide Pools") | self.received("Summer"), - "Corn": self.received("Summer") | self.received("Fall"), + "Coral": self.can_reach_region(SVRegion.tide_pools) | self.has_season("Summer"), "Cow": self.has_building("Barn"), "Cow Milk": self.has("Milk") | self.has("Large Milk"), "Crab": self.can_crab_pot(), + "Crab Cakes": self.can_mine_in_the_skull_cavern() | + (self.can_cook() & self.has_season("Fall") & self.has_year_two() & self.has("Crab") & + self.has("Wheat Flour") & self.has("Chicken Egg") & self.has("Oil")), "Crab Pot": self.has_skill_level("Fishing", 3), - "Cranberries": self.received("Fall"), + "Cranberry Candy": self.can_cook() & self.has_season("Winter") & self.has("Cranberries") & + self.has("Apple") & self.has("Sugar"), "Crayfish": self.can_crab_pot(), - "Crocus": self.received("Winter"), - "Crystal Fruit": self.received("Winter"), - "Daffodil": self.received("Spring"), - "Dandelion": self.received("Spring"), + "Crispy Bass": self.can_cook() & self.has_relationship("Kent", 3) & self.has("Largemouth Bass") & + self.has("Wheat Flour") & self.has("Oil"), + "Crocus": self.has_season("Winter"), + "Crystal Fruit": self.has_season("Winter"), + "Daffodil": self.has_season("Spring"), + "Dandelion": self.has_season("Spring"), "Dish O' The Sea": self.can_cook() & self.has_skill_level("Fishing", 3) & self.has(["Sardine", "Hashbrowns"]), - "Dorado": self.can_fish(78) & self.received("Summer"), - "Dried Starfish": self.can_fish() & self.can_reach_region("Beach"), + "Dorado": self.can_fish(78) & self.has_season("Summer"), + "Dried Starfish": self.can_fish() & self.can_reach_region(SVRegion.beach), "Driftwood": self.can_crab_pot(), "Duck Egg": self.has("Duck"), "Duck Feather": self.has("Duck"), "Duck": self.has_building("Big Coop"), - "Dwarf Scroll I": self.can_mine_in_the_mines_floor_1_40(), - "Dwarf Scroll II": self.can_mine_in_the_mines_floor_1_40(), - "Dwarf Scroll III": self.can_mine_in_the_mines_floor_1_40(), - "Dwarf Scroll IV": self.can_mine_in_the_mines_floor_81_120(), - "Earth Crystal": self.can_mine_in_the_mines_floor_1_40(), "Egg": self.has("Chicken"), "Egg (Brown)": self.has("Chicken"), - "Eggplant": self.received("Fall"), - "Elvish Jewelry": self.can_fish() & self.can_reach_region("Forest"), - "Emerald": self.can_mine_in_the_mines_floor_81_120() | self.can_mine_in_the_skull_cavern(), - "Fairy Rose": self.received("Fall"), + "Eggplant Parmesan": self.can_cook() & self.has_relationship("Lewis", 7) & self.has("Eggplant") & self.has( + "Tomato"), + "Escargot": self.can_cook() & self.has_relationship("Willy", 5) & self.has("Snail") & self.has("Garlic"), "Farmer's Lunch": self.can_cook() & self.has_skill_level("Farming", 3) & self.has("Omelet") & self.has( "Parsnip"), - "Fiber": _True(), - "Fiddlehead Fern": self.can_reach_region("Secret Woods") & self.received("Summer"), - "Fire Quartz": self.can_mine_in_the_mines_floor_81_120(), + "Fiber": True_(), + "Fiddlehead Fern": self.can_reach_region(SVRegion.secret_woods) & self.has_season("Summer"), + "Fiddlehead Risotto": self.can_cook() & self.has_season("Fall") & self.has("Oil") & + self.has("Fiddlehead Fern") & self.has("Garlic"), + "Fishing Chest": self.can_fish_chests(), + "Fish Taco": self.can_cook() & self.has_relationship("Linus", 7) & self.has("Tuna") & self.has("Tortilla") & + self.has("Red Cabbage") & self.has("Mayonnaise"), + "Fried Calamari": self.can_cook() & self.has_relationship("Jodi", 3) & self.has("Squid") & + self.has("Wheat Flour") & self.has("Oil"), + "Fried Eel": self.can_cook() & self.has_relationship("George", 3) & self.has("Eel") & self.has("Oil"), "Fried Egg": self.can_cook() & self.has("Any Egg"), - "Fried Mushroom": self.can_cook() & self.can_have_relationship("Demetrius", 3) & self.has( + "Fried Mushroom": self.can_cook() & self.has_relationship("Demetrius", 3) & self.has( "Morel") & self.has("Common Mushroom"), "Frozen Geode": self.can_mine_in_the_mines_floor_41_80(), - "Frozen Tear": self.can_mine_in_the_mines_floor_41_80(), + "Fruit Salad": self.can_cook() & self.has_season("Fall") & self.has_year_two() & self.has("Blueberry") & + self.has("Melon") & self.has("Apricot"), "Furnace": self.has("Stone") & self.has("Copper Ore"), "Geode": self.can_mine_in_the_mines_floor_1_40(), + "Ginger": self.can_reach_region(SVRegion.ginger_island), + "Ginger Ale": self.can_cook() & self.can_reach_region(SVRegion.ginger_island) & self.has("Ginger") & self.has( + "Sugar"), "Goat Cheese": self.has("Goat Milk") & self.has("Cheese Press"), "Goat Milk": self.has("Goat"), "Goat": self.has_building("Big Barn"), "Gold Bar": self.can_smelt("Gold Ore"), "Gold Ore": self.can_mine_in_the_mines_floor_81_120() | self.can_mine_in_the_skull_cavern(), - "Grape": self.received("Summer"), + "Golden Pumpkin": self.has_season("Fall") | self.has("Artifact Trove"), "Green Algae": self.can_fish(), - "Green Bean": self.received("Spring"), + "Green Tea": self.has("Keg") & self.has("Tea Leaves"), "Hardwood": self.has_tool("Axe", "Copper"), "Hashbrowns": self.can_cook() & self.can_spend_money(50) & self.has("Potato"), - "Hazelnut": self.received("Fall"), - "Holly": self.received("Winter"), - "Honey": self.can_reach_region("The Desert") | + "Hazelnut": self.has_season("Fall"), + "Holly": self.has_season("Winter"), + "Honey": self.can_reach_region(SVRegion.desert) | (self.has("Bee House") & - (self.received("Spring") | self.received("Summer") | self.received("Fall"))), - "Hops": self.received("Summer"), - "Hot Java Ring": self.can_reach_region("Ginger Island"), - "Hot Pepper": self.received("Summer"), + (self.has_season("Spring") | self.has_season("Summer") | self.has_season("Fall"))), + "Hot Java Ring": self.can_reach_region(SVRegion.ginger_island), + "Ice Cream": (self.has_season("Summer") & self.can_reach_region(SVRegion.town)) | self.can_reach_region( + "The Desert"), + # | (self.can_cook() & self.has_relationship("Jodi", 7) & self.has("Cow Milk") & self.has("Sugar")), "Iridium Bar": self.can_smelt("Iridium Ore"), "Iridium Ore": self.can_mine_in_the_skull_cavern(), "Iron Bar": self.can_smelt("Iron Ore"), "Iron Ore": self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern(), - "Jade": self.can_mine_in_the_mines_floor_41_80(), "Jelly": self.has("Preserves Jar"), "JotPK Small Buff": self.has_jotpk_power_level(2), "JotPK Medium Buff": self.has_jotpk_power_level(4), @@ -589,134 +271,158 @@ class StardewLogic: "Junimo Kart Medium Buff": self.has_junimo_kart_power_level(4), "Junimo Kart Big Buff": self.has_junimo_kart_power_level(6), "Junimo Kart Max Buff": self.has_junimo_kart_power_level(8), - "Kale": self.received("Spring"), "Keg": self.has_skill_level("Farming", 8) & self.has("Iron Bar") & self.has("Copper Bar") & self.has( "Oak Resin"), "Large Egg": self.has("Chicken"), "Large Egg (Brown)": self.has("Chicken"), "Large Goat Milk": self.has("Goat"), "Large Milk": self.has("Cow"), - "Leek": self.received("Spring"), + "Leek": self.has_season("Spring"), "Lightning Rod": self.has_skill_level("Foraging", 6), "Lobster": self.can_crab_pot(), "Loom": self.has_skill_level("Farming", 7) & self.has("Pine Tar"), + "Magic Rock Candy": self.can_reach_region(SVRegion.desert) & self.has("Prismatic Shard"), "Magma Geode": self.can_mine_in_the_mines_floor_81_120() | (self.has("Lava Eel") & self.has_building("Fish Pond")), "Maki Roll": self.can_cook() & self.can_fish(), + "Maple Bar": self.can_cook() & self.has_season("Summer") & self.has_year_two() & self.has("Maple Syrup") & + self.has("Sugar") & self.has("Wheat Flour"), "Maple Syrup": self.has("Tapper"), + "Mayonnaise": self.has("Mayonnaise Machine") & self.has("Chicken Egg"), + "Mayonnaise Machine": self.has_skill_level("Farming", 2) & self.has("Wood") & self.has("Stone") & + self.has("Earth Crystal") & self.has("Copper Bar"), "Mead": self.has("Keg") & self.has("Honey"), - "Melon": self.received("Summer"), "Milk": self.has("Cow"), "Miner's Treat": self.can_cook() & self.has_skill_level("Mining", 3) & self.has("Cow Milk") & self.has( "Cave Carrot"), - "Morel": self.can_reach_region("Secret Woods") & self.received("Year Two"), - "Mussel": _True(), - "Nautilus Shell": self.received("Winter"), + "Morel": self.can_reach_region(SVRegion.secret_woods), + "Mussel": True_(), + "Nautilus Shell": self.has_season("Winter"), "Oak Resin": self.has("Tapper"), + "Oil": True_(), "Oil Maker": self.has_skill_level("Farming", 8) & self.has("Hardwood") & self.has("Gold Bar"), "Omelet": self.can_cook() & self.can_spend_money(100) & self.has("Any Egg") & self.has("Cow Milk"), "Omni Geode": self.can_mine_in_the_mines_floor_41_80() | - self.can_reach_region("The Desert") | + self.can_reach_region(SVRegion.desert) | self.can_do_panning() | self.received("Rusty Key") | (self.has("Octopus") & self.has_building("Fish Pond")) | - self.can_reach_region("Ginger Island"), - "Orange": self.received("Summer"), - "Ostrich": self.has_building("Barn"), - "Oyster": _True(), + self.can_reach_region(SVRegion.ginger_island), + "Ostrich": self.has_building("Barn") & self.has("Ostrich Egg"), + "Ostrich Egg": self.can_reach_region(SVRegion.ginger_island), + "Oyster": True_(), "Pale Ale": self.has("Keg") & self.has("Hops"), - "Pale Broth": self.can_cook() & self.can_have_relationship("Marnie", 3) & self.has("White Algae"), + "Pale Broth": self.can_cook() & self.has_relationship("Marnie", 3) & self.has("White Algae"), "Pancakes": self.can_cook() & self.can_spend_money(100) & self.has("Any Egg"), - "Parsnip": self.received("Spring"), - "Parsnip Soup": self.can_cook() & self.can_have_relationship("Caroline", 3) & self.has( + "Parsnip Soup": self.can_cook() & self.has_relationship("Caroline", 3) & self.has( "Parsnip") & self.has("Cow Milk"), - "Peach": self.received("Summer"), + "Pearl": (self.has("Blobfish") & self.has_building("Fish Pond")) | + (self.has_lived_months(4) & self.has("Artifact Trove")), "Pepper Poppers": self.can_cook() & self.has("Cheese") & self.has( - "Hot Pepper") & self.can_have_relationship("Shane", 3), + "Hot Pepper") & self.has_relationship("Shane", 3), "Periwinkle": self.can_crab_pot(), "Pickles": self.has("Preserves Jar"), "Pig": self.has_building("Deluxe Barn"), + "Piña Colada": False_(), # self.can_reach_region(SVRegion.ginger_island), "Pine Tar": self.has("Tapper"), + "Pink Cake": self.can_cook() & self.has_season("Summer") & self.has("Melon") & self.has( + "Wheat Flour") & self.has("Sugar") & self.has("Any Egg"), "Pizza": self.can_spend_money(600), - "Pomegranate": self.received("Fall"), - "Poppy": self.received("Summer"), - "Potato": self.received("Spring"), + "Plum Pudding": self.can_cook() & self.has_season("Winter") & self.has("Wild Plum") & + self.has("Wheat Flour") & self.has("Sugar"), + "Poppyseed Muffin": self.can_cook() & self.has_season("Winter") & self.has_year_two() & + self.has("Poppy") & self.has("Wheat Flour") & self.has("Sugar"), "Preserves Jar": self.has_skill_level("Farming", 4), - "Prismatic Shard": self.received("Year Two"), - "Pumpkin": self.received("Fall"), + "Pumpkin Pie": self.can_cook() & self.has_season("Winter") & self.has("Wheat Flour") & + self.has("Cow Milk") & self.has("Sugar"), "Purple Mushroom": self.can_mine_in_the_mines_floor_81_120() | self.can_mine_in_the_skull_cavern(), - "Quartz": self.can_mine_in_the_mines_floor_1_40(), "Rabbit": self.has_building("Deluxe Coop"), "Rabbit's Foot": self.has("Rabbit"), - "Radish": self.received("Summer"), - "Rainbow Shell": self.received("Summer"), + "Radioactive Bar": self.can_smelt("Radioactive Ore"), + "Radioactive Ore": self.can_mine_perfectly_in_the_skull_cavern() & self.can_reach_region(SVRegion.ginger_island), + "Rainbow Shell": self.has_season("Summer"), "Rain Totem": self.has_skill_level("Foraging", 9), "Recycling Machine": self.has_skill_level("Fishing", 4) & self.has("Wood") & self.has("Stone") & self.has("Iron Bar"), - "Red Cabbage": self.received("Year Two"), - "Red Mushroom": self.can_reach_region("Secret Woods") & (self.received("Summer") | self.received("Fall")), - "Refined Quartz": self.has("Quartz") | self.has("Fire Quartz") | + "Red Mushroom": self.can_reach_region(SVRegion.secret_woods) & ( + self.has_season("Summer") | self.has_season("Fall")), + "Refined Quartz": self.can_smelt("Quartz") | self.can_smelt("Fire Quartz") | (self.has("Recycling Machine") & (self.has("Broken CD") | self.has("Broken Glasses"))), - "Rhubarb": self.received("Spring") & self.can_reach_region("The Desert"), + "Rhubarb Pie": self.can_cook() & self.has_relationship("Marnie", 7) & self.has("Rhubarb") & + self.has("Wheat Flour") & self.has("Sugar"), + "Rice": True_(), + "Rice Pudding": self.can_cook() & self.has_relationship("Evelyn", 7) & self.has("Cow Milk") & + self.has("Sugar") & self.has("Rice"), "Roe": self.can_fish() & self.has_building("Fish Pond"), "Roots Platter": self.can_cook() & self.has_skill_level("Combat", 3) & self.has("Cave Carrot") & self.has("Winter Root"), - "Ruby": self.can_mine_in_the_mines_floor_81_120() | self.can_do_panning(), - "Salad": self.can_spend_money(220) | ( - self.can_cook() & self.can_have_relationship("Emily", 3) & self.has("Leek") & self.has( - "Dandelion")), - "Salmonberry": self.received("Spring"), - "Salmon Dinner": self.can_cook() & self.can_have_relationship("Gus", 3) & self.has("Salmon") & self.has( + "Roasted Hazelnuts": self.can_cook() & self.has_season("Summer") & self.has("Hazelnut"), + "Salad": self.can_spend_money(220), + # | (self.can_cook() & self.has_relationship("Emily", 3) & self.has("Leek") & self.has("Dandelion") & self.has("Vinegar")), + "Salmonberry": self.has_season("Spring"), + "Salmon Dinner": self.can_cook() & self.has_relationship("Gus", 3) & self.has("Salmon") & self.has( "Amaranth") & self.has("Kale"), - "Sashimi": self.can_fish() & self.can_cook() & self.can_have_relationship("Linus", 3), - "Sea Urchin": self.can_reach_region("Tide Pools") | self.received("Summer"), - "Seaweed": self.can_fish() | self.can_reach_region("Tide Pools"), + "Sashimi": self.can_fish() & self.can_cook() & self.has_relationship("Linus", 3), + "Sea Urchin": self.can_reach_region(SVRegion.tide_pools) | self.has_season("Summer"), + "Seaweed": self.can_fish() | self.can_reach_region(SVRegion.tide_pools), + "Secret Note": self.received("Magnifying Glass"), "Sheep": self.has_building("Deluxe Barn"), "Shrimp": self.can_crab_pot(), "Slime": self.can_mine_in_the_mines_floor_1_40(), "Snail": self.can_crab_pot(), - "Snow Yam": self.received("Winter"), + "Snow Yam": self.has_season("Winter"), "Soggy Newspaper": self.can_crab_pot(), "Solar Essence": self.can_mine_in_the_mines_floor_41_80() | self.can_mine_in_the_skull_cavern(), "Spaghetti": self.can_spend_money(240), - "Spice Berry": self.received("Summer"), - "Spring Onion": self.received("Spring"), + "Spice Berry": self.has_season("Summer"), + "Spring Onion": self.has_season("Spring"), + "Squid Ink": self.can_mine_in_the_mines_floor_81_120() | ( + self.has_building("Fish Pond") & self.has("Squid")), "Staircase": self.has_skill_level("Mining", 2), - "Starfruit": (self.received("Summer") | self.received("Greenhouse")) & self.can_reach_region("The Desert"), + "Stir Fry": self.can_cook() & self.has_season("Spring") & self.has("Cave Carrot") & + self.has("Common Mushroom") & self.has("Kale") & self.has("Oil"), "Stone": self.has_tool("Pickaxe"), - "Strawberry": self.received("Spring"), + "Stuffing": self.has_season("Winter") | + (self.can_cook() & self.has_relationship("Pam", 7) & self.has("Bread") & + self.has("Cranberries") & self.has("Hazelnut")), "Sturgeon Roe": self.has("Sturgeon") & self.has_building("Fish Pond"), - "Summer Spangle": self.received("Summer"), - "Sunflower": self.received("Summer") | self.received("Fall"), + "Sugar": True_(), "Survival Burger": self.can_cook() & self.has_skill_level("Foraging", 2) & self.has(["Bread", "Cave Carrot", "Eggplant"]), - "Sweet Gem Berry": (self.received("Fall") | self.received("Greenhouse")) & self.has_traveling_merchant(), - "Sweet Pea": self.received("Summer"), + "Sweet Pea": self.has_season("Summer"), "Tapper": self.has_skill_level("Foraging", 3), - "Tomato": self.received("Summer"), - "Topaz": self.can_mine_in_the_mines_floor_1_40(), + "Tea Bush": self.has_relationship("Caroline", 2), + "Tea Leaves": self.has_lived_months(1) & self.has("Tea Bush"), "Tortilla": self.can_cook() & self.can_spend_money(100) & self.has("Corn"), "Trash": self.can_crab_pot(), "Triple Shot Espresso": (self.has("Hot Java Ring") | (self.can_cook() & self.can_spend_money(5000) & self.has("Coffee"))), + "Tropical Curry": False_(), + # self.can_cook() & self.can_reach_region(SVRegion.ginger_island) & self.has("Coconut") & self.has("Pineapple") & self.has("Hot Pepper"), "Truffle Oil": self.has("Truffle") & self.has("Oil Maker"), - "Truffle": self.has("Pig") & self.received("Year Two"), - "Tulip": self.received("Spring"), - "Unmilled Rice": self.received("Year Two"), + "Truffle": self.has("Pig") & self.has_spring_summer_or_fall(), + "Vegetable Medley": self.can_cook() & self.has_relationship("Caroline", 7) & self.has("Tomato") & self.has( + "Beet"), + "Vinegar": True_(), + "Void Egg": self.can_meet("Krobus") | (self.has_building("Fish Pond") & self.has("Void Salmon")), "Void Essence": self.can_mine_in_the_mines_floor_81_120() | self.can_mine_in_the_skull_cavern(), - "Wheat": self.received("Summer") | self.received("Fall"), + "Void Mayonnaise": self.has("Mayonnaise Machine") & self.has("Void Egg"), + "Wheat Flour": True_(), "White Algae": self.can_fish() & self.can_mine_in_the_mines_floor_1_40(), - "Wild Horseradish": self.received("Spring"), - "Wild Plum": self.received("Fall"), + "Wild Horseradish": self.has_season("Spring"), + "Wild Plum": self.has_season("Fall"), "Wilted Bouquet": self.has("Furnace") & self.has("Bouquet") & self.has("Coal"), "Wine": self.has("Keg"), - "Winter Root": self.received("Winter"), + "Winter Root": self.has_season("Winter"), "Wood": self.has_tool("Axe"), "Wool": self.has("Rabbit") | self.has("Sheep"), - "Yam": self.received("Fall"), "Hay": self.has_building("Silo"), }) self.item_rules.update(self.fish_rules) + self.item_rules.update(self.museum_rules) + self.item_rules.update(self.tree_fruit_rules) + self.item_rules.update(self.seed_rules) + self.item_rules.update(self.crops_rules) self.building_rules.update({ "Barn": self.can_spend_money(6000) & self.has(["Wood", "Stone"]), @@ -740,117 +446,109 @@ class StardewLogic: }) self.quest_rules.update({ - "Introductions": _True(), + "Introductions": True_(), "How To Win Friends": self.can_complete_quest("Introductions"), - "Getting Started": self.received("Spring") & self.has_tool("Hoe") & self.has_tool("Watering Can"), - "To The Beach": self.received("Spring"), + "Getting Started": self.has("Parsnip") & self.has_tool("Hoe") & self.has_tool("Watering Can"), + "To The Beach": True_(), "Raising Animals": self.can_complete_quest("Getting Started") & self.has_building("Coop"), "Advancement": self.can_complete_quest("Getting Started") & self.has_skill_level("Farming", 1), "Archaeology": self.has_tool("Hoe") | self.can_mine_in_the_mines_floor_1_40() | self.can_fish(), - "Meet The Wizard": self.received("Spring") & self.can_reach_region("Community Center"), + "Meet The Wizard": True_() & self.can_reach_region(SVRegion.community_center), "Forging Ahead": self.has("Copper Ore") & self.has("Furnace"), "Smelting": self.has("Copper Bar"), "Initiation": self.can_mine_in_the_mines_floor_1_40(), - "Robin's Lost Axe": self.received("Spring"), - "Jodi's Request": self.received("Spring") & self.has("Cauliflower"), - "Mayor's \"Shorts\"": self.received("Summer") & self.can_have_relationship("Marnie", 4), - "Blackberry Basket": self.received("Fall"), - "Marnie's Request": self.can_have_relationship("Marnie", 3) & self.has("Cave Carrot"), - "Pam Is Thirsty": self.received("Summer") & self.has("Pale Ale"), - "A Dark Reagent": self.received("Winter") & self.has("Void Essence"), - "Cow's Delight": self.received("Fall") & self.has("Amaranth"), - "The Skull Key": self.received("Skull Key") & self.can_reach_region("The Desert"), - "Crop Research": self.received("Summer") & self.has("Melon"), - "Knee Therapy": self.received("Summer") & self.has("Hot Pepper"), - "Robin's Request": self.received("Winter") & self.has("Hardwood"), + "Robin's Lost Axe": self.has_season("Spring"), + "Jodi's Request": self.has_season("Spring") & self.has("Cauliflower"), + "Mayor's \"Shorts\"": self.has_season("Summer") & self.has_relationship("Marnie", 4), + "Blackberry Basket": self.has_season("Fall"), + "Marnie's Request": self.has_relationship("Marnie", 3) & self.has("Cave Carrot"), + "Pam Is Thirsty": self.has_season("Summer") & self.has("Pale Ale"), + "A Dark Reagent": self.has_season("Winter") & self.has("Void Essence"), + "Cow's Delight": self.has_season("Fall") & self.has("Amaranth"), + "The Skull Key": self.received("Skull Key") & self.can_reach_region(SVRegion.desert), + "Crop Research": self.has_season("Summer") & self.has("Melon"), + "Knee Therapy": self.has_season("Summer") & self.has("Hot Pepper"), + "Robin's Request": self.has_season("Winter") & self.has("Hardwood"), "Qi's Challenge": self.can_mine_in_the_skull_cavern(), - "The Mysterious Qi": self.has("Battery Pack") & self.can_reach_region("The Desert") & self.has( + "The Mysterious Qi": self.has("Battery Pack") & self.can_reach_region(SVRegion.desert) & self.has( "Rainbow Shell") & self.has("Beet") & self.has("Solar Essence"), - "Carving Pumpkins": self.received("Fall") & self.has("Pumpkin"), - "A Winter Mystery": self.received("Winter"), - "Strange Note": self.received("Magnifying Glass") & self.can_reach_region("Secret Woods") & self.has( + "Carving Pumpkins": self.has_season("Fall") & self.has("Pumpkin"), + "A Winter Mystery": self.has_season("Winter"), + "Strange Note": self.received("Magnifying Glass") & self.can_reach_region(SVRegion.secret_woods) & self.has( "Maple Syrup"), - "Cryptic Note": self.received("Magnifying Glass") & self.can_mine_perfectly_in_the_skull_cavern(), - "Fresh Fruit": self.received("Year Two") & self.has("Apricot"), - "Aquatic Research": self.received("Year Two") & self.has("Pufferfish"), - "A Soldier's Star": self.received("Year Two") & self.has("Starfruit"), - "Mayor's Need": self.received("Year Two") & self.has("Truffle Oil"), - "Wanted: Lobster": self.received("Year Two") & self.has("Lobster"), - "Pam Needs Juice": self.received("Year Two") & self.has("Battery Pack"), - "Fish Casserole": self.received("Year Two") & self.can_have_relationship("Jodi", 4) & self.has( - "Largemouth Bass"), - "Catch A Squid": self.received("Year Two") & self.has("Squid"), - "Fish Stew": self.received("Year Two") & self.has("Albacore"), - "Pierre's Notice": self.received("Year Two") & self.has("Sashimi"), - "Clint's Attempt": self.received("Year Two") & self.has("Amethyst"), - "A Favor For Clint": self.received("Year Two") & self.has("Iron Bar"), - "Staff Of Power": self.received("Year Two") & self.has("Iridium Bar"), - "Granny's Gift": self.received("Year Two") & self.has("Leek"), - "Exotic Spirits": self.received("Year Two") & self.has("Coconut"), - "Catch a Lingcod": self.received("Year Two") & self.has("Lingcod"), + "Cryptic Note": self.received("Magnifying Glass") & self.can_reach_region(SVRegion.perfect_skull_cavern), + "Fresh Fruit": self.has("Apricot"), + "Aquatic Research": self.has("Pufferfish"), + "A Soldier's Star": self.has_relationship("Kent") & self.has("Starfruit"), + "Mayor's Need": self.has("Truffle Oil"), + "Wanted: Lobster": self.has("Lobster"), + "Pam Needs Juice": self.has("Battery Pack"), + "Fish Casserole": self.has_relationship("Jodi", 4) & self.has("Largemouth Bass"), + "Catch A Squid": self.has("Squid"), + "Fish Stew": self.has("Albacore"), + "Pierre's Notice": self.has("Sashimi"), + "Clint's Attempt": self.has("Amethyst"), + "A Favor For Clint": self.has("Iron Bar"), + "Staff Of Power": self.has("Iridium Bar"), + "Granny's Gift": self.has("Leek"), + "Exotic Spirits": self.has("Coconut"), + "Catch a Lingcod": self.has("Lingcod"), }) def has(self, items: Union[str, (Iterable[str], Sized)], count: Optional[int] = None) -> StardewRule: if isinstance(items, str): - return _Has(items, self.item_rules) + return Has(items, self.item_rules) + + if len(items) == 0: + return True_() if count is None or count == len(items): - return _And(self.has(item) for item in items) + return And(self.has(item) for item in items) if count == 1: - return _Or(self.has(item) for item in items) + return Or(self.has(item) for item in items) - return _Count(count, (self.has(item) for item in items)) + return Count(count, (self.has(item) for item in items)) def received(self, items: Union[str, Iterable[str]], count: Optional[int] = 1) -> StardewRule: + if count <= 0 or not items: + return True_() + if isinstance(items, str): - return _Received(items, self.player, count) + return Received(items, self.player, count) if count is None: - return _And(self.received(item) for item in items) + return And(self.received(item) for item in items) if count == 1: - return _Or(self.received(item) for item in items) + return Or(self.received(item) for item in items) - return _TotalReceived(count, items, self.player) + return TotalReceived(count, items, self.player) def can_reach_region(self, spot: str) -> StardewRule: - return _Reach(spot, "Region", self.player) + return Reach(spot, "Region", self.player) def can_reach_any_region(self, spots: Iterable[str]) -> StardewRule: - return _Or(self.can_reach_region(spot) for spot in spots) + return Or(self.can_reach_region(spot) for spot in spots) + + def can_reach_all_regions(self, spots: Iterable[str]) -> StardewRule: + return And(self.can_reach_region(spot) for spot in spots) def can_reach_location(self, spot: str) -> StardewRule: - return _Reach(spot, "Location", self.player) + return Reach(spot, "Location", self.player) def can_reach_entrance(self, spot: str) -> StardewRule: - return _Reach(spot, "Entrance", self.player) + return Reach(spot, "Entrance", self.player) def can_have_earned_total_money(self, amount: int) -> StardewRule: - if amount <= 10000: - return self.received("Spring") - elif amount <= 30000: - return self.received("Summer") - elif amount <= 60000: - return self.received("Fall") - elif amount <= 70000: - return self.received("Winter") - return self.received("Year Two") + return self.has_lived_months(min(8, amount // MONEY_PER_MONTH)) def can_spend_money(self, amount: int) -> StardewRule: - if amount <= 2000: - return self.received("Spring") - elif amount <= 8000: - return self.received("Summer") - elif amount <= 15000: - return self.received("Fall") - elif amount <= 18000: - return self.received("Winter") - return self.received("Year Two") + return self.has_lived_months(min(8, amount // (MONEY_PER_MONTH // 5))) def has_tool(self, tool: str, material: str = "Basic") -> StardewRule: if material == "Basic": - return _True() + return True_() if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: return self.received(f"Progressive {tool}", count=tool_materials[material]) @@ -859,7 +557,7 @@ class StardewLogic: def has_skill_level(self, skill: str, level: int) -> StardewRule: if level == 0: - return _True() + return True_() if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: return self.received(f"{skill} Level", count=level) @@ -867,11 +565,11 @@ class StardewLogic: if skill == "Fishing" and self.options[options.ToolProgression] == options.ToolProgression.option_progressive: return self.can_get_fishing_xp() - return self.received(season_per_skill_level[(skill, level)]) + return self.has_lived_months(month_end_per_skill_level[(skill, level)]) def has_total_skill_level(self, level: int) -> StardewRule: if level == 0: - return _True() + return True_() if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: skills_items = ["Farming Level", "Mining Level", "Foraging Level", @@ -879,9 +577,9 @@ class StardewLogic: return self.received(skills_items, count=level) if level > 40 and self.options[options.ToolProgression] == options.ToolProgression.option_progressive: - return self.received(season_per_total_level[level]) & self.can_get_fishing_xp() + return self.has_lived_months(month_end_per_total_level[level]) & self.can_get_fishing_xp() - return self.received(season_per_total_level[level]) + return self.has_lived_months(month_end_per_total_level[level]) def has_building(self, building: str) -> StardewRule: if not self.options[options.BuildingProgression] == options.BuildingProgression.option_vanilla: @@ -896,29 +594,29 @@ class StardewLogic: building = " ".join(["Progressive", *building.split(" ")[1:]]) return self.received(f"{building}", count) - return _Has(building, self.building_rules) + return Has(building, self.building_rules) def has_house(self, upgrade_level: int) -> StardewRule: if upgrade_level < 1: - return _True() + return True_() if upgrade_level > 3: - return _False() + return False_() if not self.options[options.BuildingProgression] == options.BuildingProgression.option_vanilla: return self.received(f"Progressive House", upgrade_level) if upgrade_level == 1: - return _Has("Kitchen", self.building_rules) + return Has("Kitchen", self.building_rules) if upgrade_level == 2: - return _Has("Kids Room", self.building_rules) + return Has("Kids Room", self.building_rules) # if upgrade_level == 3: - return _Has("Cellar", self.building_rules) + return Has("Cellar", self.building_rules) def can_complete_quest(self, quest: str) -> StardewRule: - return _Has(quest, self.quest_rules) + return Has(quest, self.quest_rules) def can_get_fishing_xp(self) -> StardewRule: if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: @@ -926,6 +624,11 @@ class StardewLogic: return self.can_fish() + def has_max_fishing_rod(self) -> StardewRule: + if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: + return self.received("Progressive Fishing Rod", 4) + return self.can_get_fishing_xp() + def can_fish(self, difficulty: int = 0) -> StardewRule: skill_required = max(0, int((difficulty / 10) - 1)) if difficulty <= 40: @@ -937,9 +640,33 @@ class StardewLogic: return skill_rule + def has_max_fishing(self) -> StardewRule: + skill_rule = self.has_skill_level("Fishing", 10) + return self.has_max_fishing_rod() & skill_rule + + def can_fish_chests(self) -> StardewRule: + skill_rule = self.has_skill_level("Fishing", 4) + return self.has_max_fishing_rod() & skill_rule + + def can_buy_seed(self, seed: SeedItem): + if self.options[options.SeedShuffle] == options.SeedShuffle.option_disabled or seed.name == "Rare Seed": + item_rule = True_() + else: + item_rule = self.received(seed.name) + season_rule = self.has_any_season(seed.seasons) + region_rule = self.can_reach_any_region(seed.regions) + return season_rule & region_rule & item_rule + + def can_grow_crop(self, crop: CropItem): + season_rule = self.has_any_season(crop.farm_growth_seasons) + seed_rule = self.has(crop.seed.name) + farm_rule = self.can_reach_region(SVRegion.farm) & season_rule + region_rule = farm_rule | self.can_reach_region(SVRegion.greenhouse) + return seed_rule & region_rule + def can_catch_fish(self, fish: FishItem) -> StardewRule: region_rule = self.can_reach_any_region(fish.locations) - season_rule = self.received(fish.seasons) + season_rule = self.has_any_season(fish.seasons) difficulty_rule = self.can_fish(fish.difficulty) if fish.difficulty == -1: difficulty_rule = self.can_crab_pot() @@ -947,9 +674,9 @@ class StardewLogic: def can_catch_every_fish(self) -> StardewRule: rules = [self.has_skill_level("Fishing", 10), self.has_max_fishing_rod()] - for fish in all_fish_items: + for fish in all_fish: rules.append(self.can_catch_fish(fish)) - return _And(rules) + return And(rules) def has_max_fishing_rod(self) -> StardewRule: if self.options[options.ToolProgression] == options.ToolProgression.option_progressive: @@ -966,28 +693,28 @@ class StardewLogic: if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: return self.has("Crab Pot") - return _True() + return True_() def can_do_panning(self) -> StardewRule: return self.received("Glittering Boulder Removed") # Regions def can_mine_in_the_mines_floor_1_40(self) -> StardewRule: - return self.can_reach_region("The Mines - Floor 5") + return self.can_reach_region(SVRegion.mines_floor_5) def can_mine_in_the_mines_floor_41_80(self) -> StardewRule: - return self.can_reach_region("The Mines - Floor 45") + return self.can_reach_region(SVRegion.mines_floor_45) def can_mine_in_the_mines_floor_81_120(self) -> StardewRule: - return self.can_reach_region("The Mines - Floor 85") + return self.can_reach_region(SVRegion.mines_floor_85) def can_mine_in_the_skull_cavern(self) -> StardewRule: return (self.can_progress_in_the_mines_from_floor(120) & - self.can_reach_region("Skull Cavern")) + self.can_reach_region(SVRegion.skull_cavern)) def can_mine_perfectly_in_the_skull_cavern(self) -> StardewRule: return (self.can_progress_in_the_mines_from_floor(160) & - self.can_reach_region("Skull Cavern")) + self.can_reach_region(SVRegion.skull_cavern)) def get_weapon_rule_for_floor_tier(self, tier: int): if tier >= 4: @@ -1010,7 +737,7 @@ class StardewLogic: if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: combat_tier = min(10, max(0, tier * 2)) rules.append(self.has_skill_level("Combat", combat_tier)) - return _And(rules) + return And(rules) def can_progress_easily_in_the_mines_from_floor(self, floor: int) -> StardewRule: tier = int(floor / 40) + 1 @@ -1022,7 +749,7 @@ class StardewLogic: if self.options[options.SkillProgression] == options.SkillProgression.option_progressive: combat_tier = min(10, max(0, tier * 2)) rules.append(self.has_skill_level("Combat", combat_tier)) - return _And(rules) + return And(rules) def has_mine_elevator_to_floor(self, floor: int) -> StardewRule: if (self.options[options.TheMinesElevatorsProgression] == @@ -1030,7 +757,7 @@ class StardewLogic: self.options[options.TheMinesElevatorsProgression] == options.TheMinesElevatorsProgression.option_progressive_from_previous_floor): return self.received("Progressive Mine Elevator", count=int(floor / 5)) - return _True() + return True_() def can_mine_to_floor(self, floor: int) -> StardewRule: previous_elevator = max(floor - 5, 0) @@ -1042,14 +769,14 @@ class StardewLogic: def has_jotpk_power_level(self, power_level: int) -> StardewRule: if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling: - return _True() + return True_() jotpk_buffs = ["JotPK: Progressive Boots", "JotPK: Progressive Gun", "JotPK: Progressive Ammo", "JotPK: Extra Life", "JotPK: Increased Drop Rate"] return self.received(jotpk_buffs, power_level) def has_junimo_kart_power_level(self, power_level: int) -> StardewRule: if self.options[options.ArcadeMachineLocations] != options.ArcadeMachineLocations.option_full_shuffling: - return _True() + return True_() return self.received("Junimo Kart: Extra Life", power_level) def has_traveling_merchant(self, tier: int = 1): @@ -1057,25 +784,77 @@ class StardewLogic: return self.received(traveling_merchant_days, tier) def can_get_married(self) -> StardewRule: - return self.can_reach_region("Tide Pools") & self.can_have_relationship("Bachelor", 10) & self.has_house(1) + return self.can_reach_region(SVRegion.tide_pools) & self.has_relationship("Bachelor", 10) & self.has_house(1) - def can_have_relationship(self, npc: str, hearts: int) -> StardewRule: - if npc == "Leo": - return self.can_reach_region("Ginger Island") + def can_have_two_children(self) -> StardewRule: + return self.can_get_married() & self.has_house(2) & self.has_relationship("Bachelor", 12) - if npc == "Sandy": - return self.can_reach_region("The Desert") + def has_relationship(self, npc: str, hearts: int = 1) -> StardewRule: + if hearts <= 0: + return True_() + if self.options[options.Friendsanity] == options.Friendsanity.option_none: + return self.can_earn_relationship(npc, hearts) + if npc not in all_villagers_by_name: + if npc == "Any" or npc == "Bachelor": + possible_friends = [] + for name in all_villagers_by_name: + if npc == "Any" or all_villagers_by_name[name].bachelor: + possible_friends.append(self.has_relationship(name, hearts)) + return Or(possible_friends) + if npc == "All": + mandatory_friends = [] + for name in all_villagers_by_name: + mandatory_friends.append(self.has_relationship(name, hearts)) + return And(mandatory_friends) + return self.can_earn_relationship(npc, hearts) + villager = all_villagers_by_name[npc] + if self.options[options.Friendsanity] == options.Friendsanity.option_bachelors and not villager.bachelor: + return self.can_earn_relationship(npc, hearts) + if self.options[options.Friendsanity] == options.Friendsanity.option_starting_npcs and not villager.available: + return self.can_earn_relationship(npc, hearts) + if self.options[ + options.Friendsanity] != options.Friendsanity.option_all_with_marriage and villager.bachelor and hearts > 8: + return self.received(f"{villager.name}: 1 <3", 8) & self.can_earn_relationship(npc, hearts) + return self.received(f"{villager.name}: 1 <3", hearts) + + def can_meet(self, npc: str) -> StardewRule: + if npc not in all_villagers_by_name: + return True_() + villager = all_villagers_by_name[npc] + rules = [self.can_reach_any_region(villager.locations)] if npc == "Kent": - return self.received("Year Two") + rules.append(self.has_lived_months(4)) + if npc == "Dwarf": + rules.append(self.received("Dwarvish Translation Guide")) + rules.append(self.has_tool("Pickaxe", "Iron")) - if hearts <= 3: - return self.received("Spring") - if hearts <= 6: - return self.received("Summer") - if hearts <= 9: - return self.received("Fall") - return self.received("Winter") + return And(rules) + + def can_earn_relationship(self, npc: str, hearts: int = 0) -> StardewRule: + if npc == "Pet": + return self.can_befriend_pet(hearts) + if npc in all_villagers_by_name: + villager = all_villagers_by_name[npc] + option1 = self.has_season(villager.birthday) & self.has(villager.gifts) & self.has_lived_months(1) + option2 = self.has_season(villager.birthday) & self.has(villager.gifts, 1) & self.has_lived_months( + hearts // 3) + option3 = (self.has_season(villager.birthday) | self.has(villager.gifts, 1)) & self.has_lived_months( + hearts // 2) + option4 = self.has_lived_months(hearts) + return self.can_meet(npc) & (option1 | option2 | option3 | option4) + else: + return self.has_lived_months(min(hearts // 2, 8)) + + def can_befriend_pet(self, hearts: int): + if hearts == 0: + return True_() + points = hearts * 200 + points_per_month = 12 * 14 + points_per_water_month = 18 * 14 + return self.can_reach_region(SVRegion.farm) & \ + ((self.has_tool("Watering Can") & self.has_lived_months(points // points_per_water_month)) | + self.has_lived_months(points // points_per_month)) def can_complete_bundle(self, bundle_requirements: List[BundleItem], number_required: int) -> StardewRule: item_rules = [] @@ -1109,16 +888,16 @@ class StardewLogic: # Catching every fish not expected # Shipping every item not expected self.can_get_married() & self.has_house(2), - self.received("Fall"), # 5 Friends (TODO) - self.received("Winter"), # 10 friends (TODO) - self.received("Fall"), # Max Pet takes 56 days min + self.has_lived_months(3), # 5 Friends (TODO) + self.has_lived_months(4), # 10 friends (TODO) + self.can_befriend_pet(5), # Max Pet self.can_complete_community_center(), # Community Center Completion self.can_complete_community_center(), # CC Ceremony first point self.can_complete_community_center(), # CC Ceremony second point self.received("Skull Key"), # Skull Key obtained - # Rusty key not expected + self.has_rusty_key(), # Rusty key not expected ] - return _Count(12, rules_worth_a_point) + return Count(12, rules_worth_a_point) def has_any_weapon(self) -> StardewRule: return self.has_decent_weapon() | self.received(item.name for item in all_items if Group.WEAPON in item.groups) @@ -1146,3 +925,51 @@ class StardewLogic: return (self.received(item.name for item in all_items if Group.WEAPON in item.groups and Group.GALAXY_WEAPONS in item.groups) & self.received("Adventurer's Guild")) + + def has_year_two(self) -> StardewRule: + return self.has_lived_months(4) + + def has_spring_summer_or_fall(self) -> StardewRule: + return self.has_season("Spring") | self.has_season("Summer") | self.has_season("Fall") + + def can_find_museum_item(self, item: MuseumItem) -> StardewRule: + region_rule = self.can_reach_all_regions(item.locations) + geodes_rule = self.has(item.geodes) + # monster_rule = self.can_farm_monster(item.monsters) + # extra_rule = True_() + return region_rule & geodes_rule # & monster_rule & extra_rule + + def can_complete_museum(self) -> StardewRule: + rules = [self.can_mine_perfectly_in_the_skull_cavern(), self.received("Traveling Merchant Metal Detector", 4)] + for donation in all_museum_items: + rules.append(self.can_find_museum_item(donation)) + return And(rules) + + def has_season(self, season: str) -> StardewRule: + seasons_order = ["Spring", "Summer", "Fall", "Winter"] + if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_progressive: + return self.received("Progressive Season", seasons_order.index(season)) + if self.options[options.SeasonRandomization] == options.SeasonRandomization.option_disabled: + if season == "Spring": + return True_() + return self.has_lived_months(1) + return self.received(season) + + def has_any_season(self, seasons: Iterable[str]): + if not seasons: + return True_() + return Or([self.has_season(season) for season in seasons]) + + def has_all_seasons(self, seasons: Iterable[str]): + if not seasons: + return True_() + return And([self.has_season(season) for season in seasons]) + + def has_lived_months(self, number: int) -> StardewRule: + number = max(0, min(number, 8)) + return self.received("Month End", number) + + def has_rusty_key(self) -> StardewRule: + if self.options[options.Museumsanity] == options.Museumsanity.option_none: + return True_() + return self.received("Rusty Key") diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index e7478c7dad..d8daed8fb6 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -1,12 +1,12 @@ from dataclasses import dataclass -from typing import Dict, Union, Protocol, runtime_checkable +from typing import Dict, Union, Protocol, runtime_checkable, ClassVar from Options import Option, Range, DeathLink, SpecialRange, Toggle, Choice @runtime_checkable class StardewOption(Protocol): - internal_name: str + internal_name: ClassVar[str] @dataclass @@ -22,18 +22,27 @@ class StardewOptions: class Goal(Choice): """What's your goal with this play-through? - With Community Center, the world will be completed once you complete the Community Center. - With Grandpa's Evaluation, the world will be completed once 4 candles are lit around Grandpa's Shrine. - With Bottom of the Mines, the world will be completed once you reach level 120 in the local mineshaft. - With Cryptic Note, the world will be completed once you complete the quest "Cryptic Note" where Mr Qi asks you to reach floor 100 in the Skull Cavern - With Master Angler, the world will be completed once you have caught every fish in the game. Pairs well with Fishsanity""" + Community Center: The world will be completed once you complete the Community Center. + Grandpa's Evaluation: The world will be completed once 4 candles are lit at Grandpa's Shrine. + Bottom of the Mines: The world will be completed once you reach level 120 in the mineshaft. + Cryptic Note: The world will be completed once you complete the quest "Cryptic Note" where Mr Qi asks you to + reach floor 100 in the Skull Cavern. + Master Angler: The world will be completed once you have caught every fish in the game. Pairs well with + Fishsanity. + Complete Collection: The world will be completed once you have completed the museum by donating every possible + item. Pairs well with Museumsanity. + Full House: The world will be completed once you get married and have two kids. Pairs well with Friendsanity. + """ internal_name = "goal" display_name = "Goal" + default = 0 option_community_center = 0 option_grandpa_evaluation = 1 option_bottom_of_the_mines = 2 option_cryptic_note = 3 option_master_angler = 4 + option_complete_collection = 5 + option_full_house = 6 @classmethod def get_option_name(cls, value) -> str: @@ -63,9 +72,8 @@ class StartingMoney(SpecialRange): class ResourcePackMultiplier(SpecialRange): - """How many items will be in the resource pack. A lower setting mean fewer resources in each pack. - A higher setting means more resources in each pack. Easy (200) doubles the default quantity. - This also include Friendship bonuses that replace the one from the Bulletin Board.""" + """How many items will be in the resource packs. A lower setting mean fewer resources in each pack. + A higher setting means more resources in each pack. Easy (200) doubles the default quantity""" internal_name = "resource_pack_multiplier" default = 100 range_start = 0 @@ -83,9 +91,9 @@ class ResourcePackMultiplier(SpecialRange): class BundleRandomization(Choice): """What items are needed for the community center bundles? - With Vanilla, you get the standard bundles from the game - With Thematic, every bundle will require random items within their original category - With Shuffled, every bundle will require random items without logic""" + Vanilla: Standard bundles from the vanilla game + Thematic: Every bundle will require random items compatible with their original theme + Shuffled: Every bundle will require random items and follow no particular structure""" internal_name = "bundle_randomization" display_name = "Bundle Randomization" default = 1 @@ -96,10 +104,10 @@ class BundleRandomization(Choice): class BundlePrice(Choice): """How many items are needed for the community center bundles? - With Very Cheap, every bundle will require two items fewer than usual - With Cheap, every bundle will require 1 item fewer than usual - With Normal, every bundle will require the vanilla number of items - With Expensive, every bundle will require 1 extra item""" + Very Cheap: Every bundle will require 2 items fewer than usual + Cheap: Every bundle will require 1 item fewer than usual + Normal: Every bundle will require the vanilla number of items + Expensive: Every bundle will require 1 extra item when applicable""" internal_name = "bundle_price" display_name = "Bundle Price" default = 2 @@ -111,13 +119,16 @@ class BundlePrice(Choice): class EntranceRandomization(Choice): """Should area entrances be randomized? - With Disabled, no entrance randomization is done - With Pelican Town, only buildings in the main town area are randomized with each other - With Non Progression, only buildings that are always available are randomized with each other + Disabled: No entrance randomization is done + Pelican Town: Only buildings in the main town area are randomized among each other + Non Progression: Only buildings that are always available are randomized with each other """ - # With Buildings, All buildings in the world are randomized with each other - # With Everything, All buildings and areas are randomized with each other - # With Chaos, same as everything, but the buildings are shuffled again every in-game day. You can't learn it! + # Buildings: All buildings in the world are randomized with each other + # Everything: All buildings and areas are randomized with each other + # Chaos, same as everything: but the buildings are shuffled again every in-game day. You can't learn it! + # Buildings One-way: Entrance pairs are disconnected, they aren't two-way! + # Everything One-way: Entrance pairs are disconnected, and every entrance is in the shuffle + # Chaos One-way: Entrance pairs are disconnected, and they change every day! internal_name = "entrance_randomization" display_name = "Entrance Randomization" @@ -128,14 +139,47 @@ class EntranceRandomization(Choice): # option_buildings = 3 # option_everything = 4 # option_chaos = 4 + # option_buildings_one_way = 5 + # option_everything_one_way = 6 + # option_chaos_one_way = 7 + + +class SeasonRandomization(Choice): + """Should seasons be randomized? + All settings allow you to choose which season you want to play next (from those unlocked) at the end of a season. + Disabled: You will start in Spring with all seasons unlocked. + Randomized: The seasons will be unlocked randomly as Archipelago items. + Randomized Not Winter: The seasons are randomized, but you're guaranteed not to start with winter. + Progressive: You will start in Spring and unlock the seasons in their original order. + """ + internal_name = "season_randomization" + display_name = "Season Randomization" + default = 1 + option_disabled = 0 + option_randomized = 1 + option_randomized_not_winter = 2 + option_progressive = 3 + + +class SeedShuffle(Choice): + """Should seeds be randomized? + Pierre now sells a random amount of seasonal seeds and Joja sells them without season requirements, but only in + huge packs. + Disabled: All the seeds will be unlocked from the start. + Randomized: The seeds will be unlocked as Archipelago items + """ + internal_name = "seed_shuffle" + display_name = "Seed Shuffle" + default = 1 + option_disabled = 0 + option_shuffled = 1 class BackpackProgression(Choice): """How is the backpack progression handled? - With Vanilla, you can buy them at Pierre's. - With Progressive, you will randomly find Progressive Backpack to upgrade. - With Early Progressive, you can expect you first Backpack before the second season, and the third before the forth - season. + Vanilla: You can buy them at Pierre's General Store. + Progressive: You will randomly find Progressive Backpack upgrades. + Early Progressive: You can expect your first Backpack in sphere 1. """ internal_name = "backpack_progression" display_name = "Backpack Progression" @@ -147,9 +191,8 @@ class BackpackProgression(Choice): class ToolProgression(Choice): """How is the tool progression handled? - With Vanilla, Clint will upgrade your tools with ore. - With Progressive, you will randomly find Progressive Tool to upgrade. - With World Checks, the tools of different quality will be found in the world.""" + Vanilla: Clint will upgrade your tools with ore. + Progressive: You will randomly find Progressive Tool upgrades.""" internal_name = "tool_progression" display_name = "Tool Progression" default = 1 @@ -159,11 +202,11 @@ class ToolProgression(Choice): class TheMinesElevatorsProgression(Choice): """How is The Mines' Elevator progression handled? - With Vanilla, you will unlock a new elevator floor every 5 floor in the mine. - With Progressive, you will randomly find Progressive Mine Elevator to go deeper. Location are sent for reaching + Vanilla: You will unlock a new elevator floor every 5 floor in the mine. + Progressive: You will randomly find Progressive Mine Elevator to go deeper. Location are sent for reaching every level multiple of 5. - With Progressive from previous floor, you will randomly find Progressive Mine Elevator to go deeper. Location are - sent for taking the ladder or stair to every level multiple of 5, taking the elevator does not count.""" + Progressive from previous floor: Locations are sent for taking the ladder or stairs to every 5 + levels, taking the elevator does not count.""" internal_name = "elevator_progression" display_name = "Elevator Progression" default = 2 @@ -174,9 +217,9 @@ class TheMinesElevatorsProgression(Choice): class SkillProgression(Choice): """How is the skill progression handled? - With Vanilla, you will level up and get the normal reward at each level. - With Progressive, the xp will be counted internally, locations will be sent when you gain a virtual level. Your real - levels will be scattered around the world.""" + Vanilla: You will level up and get the normal reward at each level. + Progressive: The xp will be earned internally, locations will be sent when you earn a level. Your real + levels will be scattered around the multiworld.""" internal_name = "skill_progression" display_name = "Skill Progression" default = 1 @@ -186,11 +229,10 @@ class SkillProgression(Choice): class BuildingProgression(Choice): """How is the building progression handled? - With Vanilla, you will buy each building and upgrade one at the time. - With Progressive, you will receive the buildings and will be able to build the first one of each building for free, + Vanilla: You will buy each building normally. + Progressive: You will receive the buildings and will be able to build the first one of each type for free, once it is received. If you want more of the same building, it will cost the vanilla price. - This option INCLUDES the shipping bin as a building you need to receive. - With Progressive early shipping bin, you can expect to receive the shipping bin before the end of the first season. + Progressive early shipping bin: You can expect your shipping bin in sphere 1. """ internal_name = "building_progression" display_name = "Building Progression" @@ -202,11 +244,11 @@ class BuildingProgression(Choice): class ArcadeMachineLocations(Choice): """How are the Arcade Machines handled? - With Vanilla, the arcade machines are not included in the Archipelago shuffling. - With Victories, each Arcade Machine will contain one check on victory - With Victories Easy, the arcade machines are both made considerably easier to be more accessible for the average + Vanilla: The arcade machines are not included in the Archipelago shuffling. + Victories: Each Arcade Machine will contain one check on victory + Victories Easy: The arcade machines are both made considerably easier to be more accessible for the average player. - With Full Shuffling, the arcade machines will contain multiple checks each, and different buffs that make the game + Full Shuffling: The arcade machines will contain multiple checks each, and different buffs that make the game easier are in the item pool. Junimo Kart has one check at the end of each level. Journey of the Prairie King has one check after each boss, plus one check for each vendor equipment. """ @@ -220,7 +262,7 @@ class ArcadeMachineLocations(Choice): class HelpWantedLocations(SpecialRange): - """How many "Help Wanted" quests need to be completed as ArchipelagoLocations + """How many "Help Wanted" quests need to be completed as Archipelago Locations Out of every 7 quests, 4 will be item deliveries, and then 1 of each for: Fishing, Gathering and Slaying Monsters. Choosing a multiple of 7 is recommended.""" internal_name = "help_wanted_locations" @@ -241,11 +283,11 @@ class HelpWantedLocations(SpecialRange): class Fishsanity(Choice): """Locations for catching fish? - With None, there are no locations for catching fish - With Legendaries, each of the 5 legendary fish are locations that contain items - With Special, a curated selection of strong fish are locations that contain items - With Random Selection, a random selection of fish are locations that contain items - With All, every single fish in the game is a location that contains an item. Pairs well with the Master Angler Goal + None: There are no locations for catching fish + Legendaries: Each of the 5 legendary fish are checks + Special: A curated selection of strong fish are checks + Randomized: A random selection of fish are checks + All: Every single fish in the game is a location that contains an item. Pairs well with the Master Angler Goal """ internal_name = "fishsanity" display_name = "Fishsanity" @@ -253,10 +295,45 @@ class Fishsanity(Choice): option_none = 0 option_legendaries = 1 option_special = 2 - option_random_selection = 3 + option_randomized = 3 option_all = 4 +class Museumsanity(Choice): + """Locations for museum donations? + None: There are no locations for donating artifacts and minerals to the museum + Milestones: The donation milestones from the vanilla game are checks + Randomized: A random selection of minerals and artifacts are checks + All: Every single donation will be a check + """ + internal_name = "museumsanity" + display_name = "Museumsanity" + default = 1 + option_none = 0 + option_milestones = 1 + option_randomized = 2 + option_all = 3 + + +class Friendsanity(Choice): + """Locations for friendships? + None: There are no checks for befriending villagers + Bachelors: Each heart of a bachelor is a check + Starting NPCs: Each heart for npcs that are immediately available is a check + All: Every heart with every NPC is a check, including Leo, Kent, Sandy, etc + All With Marriage: Marriage candidates must also be dated, married, and befriended up to 14 hearts. + """ + internal_name = "friendsanity" + display_name = "Friendsanity" + default = 0 + option_none = 0 + # option_marry_one_person = 1 + option_bachelors = 2 + option_starting_npcs = 3 + option_all = 4 + option_all_with_marriage = 5 + + class NumberOfPlayerBuffs(Range): """Number of buffs to the player of each type that exist as items in the pool. Buffs include movement speed (+25% multiplier, stacks additively) @@ -270,14 +347,14 @@ class NumberOfPlayerBuffs(Range): class MultipleDaySleepEnabled(Toggle): - """Should you be able to sleep automatically multiple day strait?""" + """Enable the ability to sleep automatically for multiple days straight?""" internal_name = "multiple_day_sleep_enabled" display_name = "Multiple Day Sleep Enabled" default = 1 class MultipleDaySleepCost(SpecialRange): - """How must gold it cost to sleep through multiple days? You will have to pay that amount for each day slept.""" + """How much gold it will cost to use MultiSleep. You will have to pay that amount for each day skipped.""" internal_name = "multiple_day_sleep_cost" display_name = "Multiple Day Sleep Cost" range_start = 0 @@ -293,7 +370,7 @@ class MultipleDaySleepCost(SpecialRange): class ExperienceMultiplier(SpecialRange): - """How fast do you want to level up. A lower setting mean less experience. + """How fast you want to earn skill experience. A lower setting mean less experience. A higher setting means more experience.""" internal_name = "experience_multiplier" display_name = "Experience Multiplier" @@ -311,13 +388,33 @@ class ExperienceMultiplier(SpecialRange): } +class FriendshipMultiplier(SpecialRange): + """How fast you want to earn friendship points with villagers. + A lower setting mean less friendship per action. + A higher setting means more friendship per action.""" + internal_name = "friendship_multiplier" + display_name = "Friendship Multiplier" + range_start = 25 + range_end = 400 + # step = 25 + default = 200 + + special_range_names = { + "half": 50, + "vanilla": 100, + "double": 200, + "triple": 300, + "quadruple": 400, + } + + class DebrisMultiplier(Choice): - """How much debris spawn on the player's farm? - With Vanilla, debris spawns normally - With Half, debris will spawn at half the normal rate - With Quarter, debris will spawn at one quarter of the normal rate - With None, No debris will spawn on the farm, ever - With Start Clear, debris will spawn at the normal rate, but the farm will be completely clear when starting the game + """How much debris will spawn on the player's farm? + Vanilla: debris spawns normally + Half: debris will spawn at half the normal rate + Quarter: debris will spawn at one quarter of the normal rate + None: No debris will spawn on the farm, ever + Start Clear: debris will spawn at the normal rate, but the farm will be completely clear when starting the game """ internal_name = "debris_multiplier" display_name = "Debris Multiplier" @@ -364,33 +461,37 @@ class GiftTax(SpecialRange): } -stardew_valley_options: Dict[str, type(Option)] = { - option.internal_name: option - for option in [ - StartingMoney, - ResourcePackMultiplier, - BundleRandomization, - BundlePrice, - EntranceRandomization, - BackpackProgression, - ToolProgression, - SkillProgression, - BuildingProgression, - TheMinesElevatorsProgression, - ArcadeMachineLocations, - HelpWantedLocations, - Fishsanity, - NumberOfPlayerBuffs, - Goal, - MultipleDaySleepEnabled, - MultipleDaySleepCost, - ExperienceMultiplier, - DebrisMultiplier, - QuickStart, - Gifting, - GiftTax, - ] -} +stardew_valley_option_classes = [ + StartingMoney, + ResourcePackMultiplier, + BundleRandomization, + BundlePrice, + EntranceRandomization, + SeasonRandomization, + SeedShuffle, + BackpackProgression, + ToolProgression, + SkillProgression, + BuildingProgression, + TheMinesElevatorsProgression, + ArcadeMachineLocations, + HelpWantedLocations, + Fishsanity, + Museumsanity, + Friendsanity, + NumberOfPlayerBuffs, + Goal, + MultipleDaySleepEnabled, + MultipleDaySleepCost, + ExperienceMultiplier, + FriendshipMultiplier, + DebrisMultiplier, + QuickStart, + Gifting, + GiftTax, +] +stardew_valley_options: Dict[str, type(Option)] = {option.internal_name: option for option in + stardew_valley_option_classes} default_options = {option.internal_name: option.default for option in stardew_valley_options.values()} stardew_valley_options["death_link"] = DeathLink diff --git a/worlds/stardew_valley/regions.py b/worlds/stardew_valley/regions.py index 0979d7f883..5c6dcfd923 100644 --- a/worlds/stardew_valley/regions.py +++ b/worlds/stardew_valley/regions.py @@ -5,6 +5,7 @@ from typing import Iterable, Dict, Protocol, Optional, List, Tuple from BaseClasses import Region, Entrance from . import options +from .data.region_data import SVRegion from .options import StardewOptions @@ -42,219 +43,237 @@ class ConnectionData: stardew_valley_regions = [ - RegionData("Menu", ["To Stardew Valley"]), - RegionData("Stardew Valley", ["To Farmhouse"]), - RegionData("Farmhouse", ["Outside to Farm", "Downstairs to Cellar"]), - RegionData("Cellar"), - RegionData("Farm", ["Farm to Backwoods", "Farm to Bus Stop", "Farm to Forest", "Farm to Farmcave", "Enter Greenhouse", - "Use Desert Obelisk", "Use Island Obelisk"]), - RegionData("Backwoods", ["Backwoods to Mountain"]), - RegionData("Bus Stop", ["Bus Stop to Town", "Take Bus to Desert", "Bus Stop to Tunnel Entrance"]), - RegionData("Forest", ["Forest to Town", "Enter Secret Woods", "Forest to Wizard Tower", "Forest to Marnie's Ranch", - "Forest to Leah's Cottage", "Forest to Sewers"]), - RegionData("Farmcave"), - RegionData("Greenhouse"), - RegionData("Mountain", ["Mountain to Railroad", "Mountain to Tent", "Mountain to Carpenter Shop", "Mountain to The Mines", - "Enter Quarry", "Mountain to Adventurer's Guild", "Mountain to Town"]), - RegionData("Tunnel Entrance", ["Enter Tunnel"]), - RegionData("Tunnel"), - RegionData("Town", ["Town to Community Center", "Town to Beach", "Town to Hospital", - "Town to Pierre's General Store", "Town to Saloon", "Town to Alex's House", "Town to Trailer", "Town to Mayor's Manor", - "Town to Sam's House", "Town to Haley's House", "Town to Sewers", "Town to Clint's Blacksmith", "Town to Museum", + RegionData(SVRegion.menu, ["To Stardew Valley"]), + RegionData(SVRegion.stardew_valley, ["To Farmhouse"]), + RegionData(SVRegion.farm_house, ["Outside to Farm", "Downstairs to Cellar"]), + RegionData(SVRegion.cellar), + RegionData(SVRegion.farm, + ["Farm to Backwoods", "Farm to Bus Stop", "Farm to Forest", "Farm to Farmcave", "Enter Greenhouse", + "Use Desert Obelisk", "Use Island Obelisk"]), + RegionData(SVRegion.backwoods, ["Backwoods to Mountain"]), + RegionData(SVRegion.bus_stop, ["Bus Stop to Town", "Take Bus to Desert", "Bus Stop to Tunnel Entrance"]), + RegionData(SVRegion.forest, ["Forest to Town", "Enter Secret Woods", "Forest to Wizard Tower", "Forest to Marnie's Ranch", + "Forest to Leah's Cottage", "Forest to Sewers", "Talk to Traveling Merchant"]), + RegionData(SVRegion.traveling_cart), + RegionData(SVRegion.farm_cave), + RegionData(SVRegion.greenhouse), + RegionData(SVRegion.mountain, + ["Mountain to Railroad", "Mountain to Tent", "Mountain to Carpenter Shop", "Mountain to The Mines", + "Enter Quarry", "Mountain to Adventurer's Guild", "Mountain to Town"]), + RegionData(SVRegion.tunnel_entrance, ["Enter Tunnel"]), + RegionData(SVRegion.tunnel), + RegionData(SVRegion.town, ["Town to Community Center", "Town to Beach", "Town to Hospital", + "Town to Pierre's General Store", "Town to Saloon", "Town to Alex's House", "Town to Trailer", + "Town to Mayor's Manor", + "Town to Sam's House", "Town to Haley's House", "Town to Sewers", "Town to Clint's Blacksmith", + "Town to Museum", "Town to JojaMart"]), - RegionData("Beach", ["Beach to Willy's Fish Shop", "Enter Elliott's House", "Enter Tide Pools"]), - RegionData("Railroad", ["Enter Bathhouse Entrance", "Enter Witch Warp Cave"]), # "Enter Perfection Cutscene Area" - RegionData("Marnie's Ranch"), - RegionData("Leah's Cottage"), - RegionData("Sewers", ["Enter Mutant Bug Lair"]), - RegionData("Mutant Bug Lair"), - RegionData("Wizard Tower", ["Enter Wizard Basement"]), - RegionData("Wizard Basement"), - RegionData("Tent"), - RegionData("Carpenter Shop", ["Enter Sebastian's Room"]), - RegionData("Sebastian's Room"), - RegionData("Adventurer's Guild"), - RegionData("Community Center", - ["Access Crafts Room", "Access Pantry", "Access Fish Tank", "Access Boiler Room", "Access Bulletin Board", + RegionData(SVRegion.beach, ["Beach to Willy's Fish Shop", "Enter Elliott's House", "Enter Tide Pools"]), + RegionData(SVRegion.railroad, ["Enter Bathhouse Entrance", "Enter Witch Warp Cave"]), # "Enter Perfection Cutscene Area" + RegionData(SVRegion.ranch), + RegionData(SVRegion.leah_house), + RegionData(SVRegion.sewers, ["Enter Mutant Bug Lair"]), + RegionData(SVRegion.mutant_bug_lair), + RegionData(SVRegion.wizard_tower, ["Enter Wizard Basement"]), + RegionData(SVRegion.wizard_basement), + RegionData(SVRegion.tent), + RegionData(SVRegion.carpenter, ["Enter Sebastian's Room"]), + RegionData(SVRegion.sebastian_room), + RegionData(SVRegion.adventurer_guild), + RegionData(SVRegion.community_center, + ["Access Crafts Room", "Access Pantry", "Access Fish Tank", "Access Boiler Room", + "Access Bulletin Board", "Access Vault"]), - RegionData("Crafts Room"), - RegionData("Pantry"), - RegionData("Fish Tank"), - RegionData("Boiler Room"), - RegionData("Bulletin Board"), - RegionData("Vault"), - RegionData("Hospital", ["Enter Harvey's Room"]), - RegionData("Harvey's Room"), - RegionData("Pierre's General Store", ["Enter Sunroom"]), - RegionData("Sunroom"), - RegionData("Saloon", ["Play Journey of the Prairie King", "Play Junimo Kart"]), - RegionData("Alex's House"), - RegionData("Trailer"), - RegionData("Mayor's Manor"), - RegionData("Sam's House"), - RegionData("Haley's House"), - RegionData("Clint's Blacksmith"), - RegionData("Museum"), - RegionData("JojaMart"), - RegionData("Willy's Fish Shop"), - RegionData("Elliott's House"), - RegionData("Tide Pools"), - RegionData("Bathhouse Entrance", ["Enter Locker Room"]), - RegionData("Locker Room", ["Enter Public Bath"]), - RegionData("Public Bath"), - RegionData("Witch Warp Cave", ["Enter Witch's Swamp"]), - RegionData("Witch's Swamp"), - RegionData("Quarry", ["Enter Quarry Mine Entrance"]), - RegionData("Quarry Mine Entrance", ["Enter Quarry Mine"]), - RegionData("Quarry Mine"), - RegionData("Secret Woods"), - RegionData("The Desert", ["Enter Skull Cavern Entrance"]), - RegionData("Skull Cavern Entrance", ["Enter Skull Cavern"]), - RegionData("Skull Cavern"), - RegionData("Ginger Island"), - RegionData("JotPK World 1", ["Reach JotPK World 2"]), - RegionData("JotPK World 2", ["Reach JotPK World 3"]), - RegionData("JotPK World 3"), - RegionData("Junimo Kart 1", ["Reach Junimo Kart 2"]), - RegionData("Junimo Kart 2", ["Reach Junimo Kart 3"]), - RegionData("Junimo Kart 3"), - RegionData("The Mines", ["Dig to The Mines - Floor 5", "Dig to The Mines - Floor 10", "Dig to The Mines - Floor 15", - "Dig to The Mines - Floor 20", "Dig to The Mines - Floor 25", "Dig to The Mines - Floor 30", - "Dig to The Mines - Floor 35", "Dig to The Mines - Floor 40", "Dig to The Mines - Floor 45", - "Dig to The Mines - Floor 50", "Dig to The Mines - Floor 55", "Dig to The Mines - Floor 60", - "Dig to The Mines - Floor 65", "Dig to The Mines - Floor 70", "Dig to The Mines - Floor 75", - "Dig to The Mines - Floor 80", "Dig to The Mines - Floor 85", "Dig to The Mines - Floor 90", - "Dig to The Mines - Floor 95", "Dig to The Mines - Floor 100", "Dig to The Mines - Floor 105", - "Dig to The Mines - Floor 110", "Dig to The Mines - Floor 115", "Dig to The Mines - Floor 120"]), - RegionData("The Mines - Floor 5"), - RegionData("The Mines - Floor 10"), - RegionData("The Mines - Floor 15"), - RegionData("The Mines - Floor 20"), - RegionData("The Mines - Floor 25"), - RegionData("The Mines - Floor 30"), - RegionData("The Mines - Floor 35"), - RegionData("The Mines - Floor 40"), - RegionData("The Mines - Floor 45"), - RegionData("The Mines - Floor 50"), - RegionData("The Mines - Floor 55"), - RegionData("The Mines - Floor 60"), - RegionData("The Mines - Floor 65"), - RegionData("The Mines - Floor 70"), - RegionData("The Mines - Floor 75"), - RegionData("The Mines - Floor 80"), - RegionData("The Mines - Floor 85"), - RegionData("The Mines - Floor 90"), - RegionData("The Mines - Floor 95"), - RegionData("The Mines - Floor 100"), - RegionData("The Mines - Floor 105"), - RegionData("The Mines - Floor 110"), - RegionData("The Mines - Floor 115"), - RegionData("The Mines - Floor 120"), + RegionData(SVRegion.crafts_room), + RegionData(SVRegion.pantry), + RegionData(SVRegion.fish_tank), + RegionData(SVRegion.boiler_room), + RegionData(SVRegion.bulletin_board), + RegionData(SVRegion.vault), + RegionData(SVRegion.hospital, ["Enter Harvey's Room"]), + RegionData(SVRegion.harvey_room), + RegionData(SVRegion.pierre_store, ["Enter Sunroom"]), + RegionData(SVRegion.sunroom), + RegionData(SVRegion.saloon, ["Play Journey of the Prairie King", "Play Junimo Kart"]), + RegionData(SVRegion.alex_house), + RegionData(SVRegion.trailer), + RegionData(SVRegion.mayor_house), + RegionData(SVRegion.sam_house), + RegionData(SVRegion.haley_house), + RegionData(SVRegion.blacksmith), + RegionData(SVRegion.museum), + RegionData(SVRegion.jojamart), + RegionData(SVRegion.fish_shop), + RegionData(SVRegion.elliott_house), + RegionData(SVRegion.tide_pools), + RegionData(SVRegion.bathhouse_entrance, ["Enter Locker Room"]), + RegionData(SVRegion.locker_room, ["Enter Public Bath"]), + RegionData(SVRegion.public_bath), + RegionData(SVRegion.witch_warp_cave, ["Enter Witch's Swamp"]), + RegionData(SVRegion.witch_swamp), + RegionData(SVRegion.quarry, ["Enter Quarry Mine Entrance"]), + RegionData(SVRegion.quarry_mine_entrance, ["Enter Quarry Mine"]), + RegionData(SVRegion.quarry_mine), + RegionData(SVRegion.secret_woods), + RegionData(SVRegion.desert, ["Enter Skull Cavern Entrance"]), + RegionData(SVRegion.skull_cavern_entrance, ["Enter Skull Cavern"]), + RegionData(SVRegion.skull_cavern, ["Mine to Skull Cavern Floor 100"]), + RegionData(SVRegion.perfect_skull_cavern), + RegionData(SVRegion.ginger_island), + RegionData(SVRegion.jotpk_world_1, ["Reach JotPK World 2"]), + RegionData(SVRegion.jotpk_world_2, ["Reach JotPK World 3"]), + RegionData(SVRegion.jotpk_world_3), + RegionData(SVRegion.junimo_kart_1, ["Reach Junimo Kart 2"]), + RegionData(SVRegion.junimo_kart_2, ["Reach Junimo Kart 3"]), + RegionData(SVRegion.junimo_kart_3), + RegionData(SVRegion.mines, ["Dig to The Mines - Floor 5", "Dig to The Mines - Floor 10", "Dig to The Mines - Floor 15", + "Dig to The Mines - Floor 20", "Dig to The Mines - Floor 25", + "Dig to The Mines - Floor 30", + "Dig to The Mines - Floor 35", "Dig to The Mines - Floor 40", + "Dig to The Mines - Floor 45", + "Dig to The Mines - Floor 50", "Dig to The Mines - Floor 55", + "Dig to The Mines - Floor 60", + "Dig to The Mines - Floor 65", "Dig to The Mines - Floor 70", + "Dig to The Mines - Floor 75", + "Dig to The Mines - Floor 80", "Dig to The Mines - Floor 85", + "Dig to The Mines - Floor 90", + "Dig to The Mines - Floor 95", "Dig to The Mines - Floor 100", + "Dig to The Mines - Floor 105", + "Dig to The Mines - Floor 110", "Dig to The Mines - Floor 115", + "Dig to The Mines - Floor 120"]), + RegionData(SVRegion.mines_floor_5), + RegionData(SVRegion.mines_floor_10), + RegionData(SVRegion.mines_floor_15), + RegionData(SVRegion.mines_floor_20), + RegionData(SVRegion.mines_floor_25), + RegionData(SVRegion.mines_floor_30), + RegionData(SVRegion.mines_floor_35), + RegionData(SVRegion.mines_floor_40), + RegionData(SVRegion.mines_floor_45), + RegionData(SVRegion.mines_floor_50), + RegionData(SVRegion.mines_floor_55), + RegionData(SVRegion.mines_floor_60), + RegionData(SVRegion.mines_floor_65), + RegionData(SVRegion.mines_floor_70), + RegionData(SVRegion.mines_floor_75), + RegionData(SVRegion.mines_floor_80), + RegionData(SVRegion.mines_floor_85), + RegionData(SVRegion.mines_floor_90), + RegionData(SVRegion.mines_floor_95), + RegionData(SVRegion.mines_floor_100), + RegionData(SVRegion.mines_floor_105), + RegionData(SVRegion.mines_floor_110), + RegionData(SVRegion.mines_floor_115), + RegionData(SVRegion.mines_floor_120), ] # Exists and where they lead mandatory_connections = [ - ConnectionData("To Stardew Valley", "Stardew Valley"), - ConnectionData("To Farmhouse", "Farmhouse"), - ConnectionData("Outside to Farm", "Farm"), - ConnectionData("Downstairs to Cellar", "Cellar"), - ConnectionData("Farm to Backwoods", "Backwoods"), - ConnectionData("Farm to Bus Stop", "Bus Stop"), - ConnectionData("Farm to Forest", "Forest"), - ConnectionData("Farm to Farmcave", "Farmcave", flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData("Enter Greenhouse", "Greenhouse"), - ConnectionData("Use Desert Obelisk", "The Desert"), - ConnectionData("Use Island Obelisk", "Ginger Island"), - ConnectionData("Backwoods to Mountain", "Mountain"), - ConnectionData("Bus Stop to Town", "Town"), - ConnectionData("Bus Stop to Tunnel Entrance", "Tunnel Entrance"), - ConnectionData("Take Bus to Desert", "The Desert"), - ConnectionData("Enter Tunnel", "Tunnel"), - ConnectionData("Forest to Town", "Town"), - ConnectionData("Forest to Wizard Tower", "Wizard Tower", flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData("Enter Wizard Basement", "Wizard Basement"), - ConnectionData("Forest to Marnie's Ranch", "Marnie's Ranch", flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData("Forest to Leah's Cottage", "Leah's Cottage"), - ConnectionData("Enter Secret Woods", "Secret Woods"), - ConnectionData("Forest to Sewers", "Sewers"), - ConnectionData("Town to Sewers", "Sewers"), - ConnectionData("Enter Mutant Bug Lair", "Mutant Bug Lair"), - ConnectionData("Mountain to Railroad", "Railroad"), - ConnectionData("Mountain to Tent", "Tent", flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData("Mountain to Carpenter Shop", "Carpenter Shop", flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData("Enter Sebastian's Room", "Sebastian's Room"), - ConnectionData("Mountain to Adventurer's Guild", "Adventurer's Guild"), - ConnectionData("Enter Quarry", "Quarry"), - ConnectionData("Enter Quarry Mine Entrance", "Quarry Mine Entrance"), - ConnectionData("Enter Quarry Mine", "Quarry Mine"), - ConnectionData("Mountain to Town", "Town"), - ConnectionData("Town to Community Center", "Community Center", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Access Crafts Room", "Crafts Room"), - ConnectionData("Access Pantry", "Pantry"), - ConnectionData("Access Fish Tank", "Fish Tank"), - ConnectionData("Access Boiler Room", "Boiler Room"), - ConnectionData("Access Bulletin Board", "Bulletin Board"), - ConnectionData("Access Vault", "Vault"), - ConnectionData("Town to Hospital", "Hospital", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Enter Harvey's Room", "Harvey's Room"), - ConnectionData("Town to Pierre's General Store", "Pierre's General Store", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Enter Sunroom", "Sunroom"), - ConnectionData("Town to Clint's Blacksmith", "Clint's Blacksmith", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to Saloon", "Saloon", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Play Journey of the Prairie King", "JotPK World 1"), - ConnectionData("Reach JotPK World 2", "JotPK World 2"), - ConnectionData("Reach JotPK World 3", "JotPK World 3"), - ConnectionData("Play Junimo Kart", "Junimo Kart 1"), - ConnectionData("Reach Junimo Kart 2", "Junimo Kart 2"), - ConnectionData("Reach Junimo Kart 3", "Junimo Kart 3"), - ConnectionData("Town to Sam's House", "Sam's House", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to Haley's House", "Haley's House", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to Mayor's Manor", "Mayor's Manor", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to Alex's House", "Alex's House", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to Trailer", "Trailer", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to Museum", "Museum", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to JojaMart", "JojaMart", flag=RandomizationFlag.PELICAN_TOWN), - ConnectionData("Town to Beach", "Beach"), - ConnectionData("Enter Elliott's House", "Elliott's House"), - ConnectionData("Beach to Willy's Fish Shop", "Willy's Fish Shop", flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData("Enter Tide Pools", "Tide Pools"), - ConnectionData("Mountain to The Mines", "The Mines", flag=RandomizationFlag.NON_PROGRESSION), - ConnectionData("Dig to The Mines - Floor 5", "The Mines - Floor 5"), - ConnectionData("Dig to The Mines - Floor 10", "The Mines - Floor 10"), - ConnectionData("Dig to The Mines - Floor 15", "The Mines - Floor 15"), - ConnectionData("Dig to The Mines - Floor 20", "The Mines - Floor 20"), - ConnectionData("Dig to The Mines - Floor 25", "The Mines - Floor 25"), - ConnectionData("Dig to The Mines - Floor 30", "The Mines - Floor 30"), - ConnectionData("Dig to The Mines - Floor 35", "The Mines - Floor 35"), - ConnectionData("Dig to The Mines - Floor 40", "The Mines - Floor 40"), - ConnectionData("Dig to The Mines - Floor 45", "The Mines - Floor 45"), - ConnectionData("Dig to The Mines - Floor 50", "The Mines - Floor 50"), - ConnectionData("Dig to The Mines - Floor 55", "The Mines - Floor 55"), - ConnectionData("Dig to The Mines - Floor 60", "The Mines - Floor 60"), - ConnectionData("Dig to The Mines - Floor 65", "The Mines - Floor 65"), - ConnectionData("Dig to The Mines - Floor 70", "The Mines - Floor 70"), - ConnectionData("Dig to The Mines - Floor 75", "The Mines - Floor 75"), - ConnectionData("Dig to The Mines - Floor 80", "The Mines - Floor 80"), - ConnectionData("Dig to The Mines - Floor 85", "The Mines - Floor 85"), - ConnectionData("Dig to The Mines - Floor 90", "The Mines - Floor 90"), - ConnectionData("Dig to The Mines - Floor 95", "The Mines - Floor 95"), - ConnectionData("Dig to The Mines - Floor 100", "The Mines - Floor 100"), - ConnectionData("Dig to The Mines - Floor 105", "The Mines - Floor 105"), - ConnectionData("Dig to The Mines - Floor 110", "The Mines - Floor 110"), - ConnectionData("Dig to The Mines - Floor 115", "The Mines - Floor 115"), - ConnectionData("Dig to The Mines - Floor 120", "The Mines - Floor 120"), - ConnectionData("Enter Skull Cavern Entrance", "Skull Cavern Entrance"), - ConnectionData("Enter Skull Cavern", "Skull Cavern"), - ConnectionData("Enter Witch Warp Cave", "Witch Warp Cave"), - ConnectionData("Enter Witch's Swamp", "Witch's Swamp"), - ConnectionData("Enter Bathhouse Entrance", "Bathhouse Entrance"), - ConnectionData("Enter Locker Room", "Locker Room"), - ConnectionData("Enter Public Bath", "Public Bath"), + ConnectionData("To Stardew Valley", SVRegion.stardew_valley), + ConnectionData("To Farmhouse", SVRegion.farm_house), + ConnectionData("Outside to Farm", SVRegion.farm), + ConnectionData("Downstairs to Cellar", SVRegion.cellar), + ConnectionData("Farm to Backwoods", SVRegion.backwoods), + ConnectionData("Farm to Bus Stop", SVRegion.bus_stop), + ConnectionData("Farm to Forest", SVRegion.forest), + ConnectionData("Farm to Farmcave", SVRegion.farm_cave, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Enter Greenhouse", SVRegion.greenhouse), + ConnectionData("Use Desert Obelisk", SVRegion.desert), + ConnectionData("Use Island Obelisk", SVRegion.ginger_island), + ConnectionData("Backwoods to Mountain", SVRegion.mountain), + ConnectionData("Bus Stop to Town", SVRegion.town), + ConnectionData("Bus Stop to Tunnel Entrance", SVRegion.tunnel_entrance), + ConnectionData("Take Bus to Desert", SVRegion.desert), + ConnectionData("Enter Tunnel", SVRegion.tunnel), + ConnectionData("Forest to Town", SVRegion.town), + ConnectionData("Forest to Wizard Tower", SVRegion.wizard_tower, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Enter Wizard Basement", SVRegion.wizard_basement), + ConnectionData("Forest to Marnie's Ranch", SVRegion.ranch, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Forest to Leah's Cottage", SVRegion.leah_house), + ConnectionData("Enter Secret Woods", SVRegion.secret_woods), + ConnectionData("Forest to Sewers", SVRegion.sewers), + ConnectionData("Talk to Traveling Merchant", SVRegion.traveling_cart), + ConnectionData("Town to Sewers", SVRegion.sewers), + ConnectionData("Enter Mutant Bug Lair", SVRegion.mutant_bug_lair), + ConnectionData("Mountain to Railroad", SVRegion.railroad), + ConnectionData("Mountain to Tent", SVRegion.tent, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Mountain to Carpenter Shop", SVRegion.carpenter, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Enter Sebastian's Room", SVRegion.sebastian_room), + ConnectionData("Mountain to Adventurer's Guild", SVRegion.adventurer_guild), + ConnectionData("Enter Quarry", SVRegion.quarry), + ConnectionData("Enter Quarry Mine Entrance", SVRegion.quarry_mine_entrance), + ConnectionData("Enter Quarry Mine", SVRegion.quarry_mine), + ConnectionData("Mountain to Town", SVRegion.town), + ConnectionData("Town to Community Center", SVRegion.community_center, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Access Crafts Room", SVRegion.crafts_room), + ConnectionData("Access Pantry", SVRegion.pantry), + ConnectionData("Access Fish Tank", SVRegion.fish_tank), + ConnectionData("Access Boiler Room", SVRegion.boiler_room), + ConnectionData("Access Bulletin Board", SVRegion.bulletin_board), + ConnectionData("Access Vault", SVRegion.vault), + ConnectionData("Town to Hospital", SVRegion.hospital, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Enter Harvey's Room", SVRegion.harvey_room), + ConnectionData("Town to Pierre's General Store", SVRegion.pierre_store, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Enter Sunroom", SVRegion.sunroom), + ConnectionData("Town to Clint's Blacksmith", SVRegion.blacksmith, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Saloon", SVRegion.saloon, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Play Journey of the Prairie King", SVRegion.jotpk_world_1), + ConnectionData("Reach JotPK World 2", SVRegion.jotpk_world_2), + ConnectionData("Reach JotPK World 3", SVRegion.jotpk_world_3), + ConnectionData("Play Junimo Kart", SVRegion.junimo_kart_1), + ConnectionData("Reach Junimo Kart 2", SVRegion.junimo_kart_2), + ConnectionData("Reach Junimo Kart 3", SVRegion.junimo_kart_3), + ConnectionData("Town to Sam's House", SVRegion.sam_house, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Haley's House", SVRegion.haley_house, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Mayor's Manor", SVRegion.mayor_house, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Alex's House", SVRegion.alex_house, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Trailer", SVRegion.trailer, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Museum", SVRegion.museum, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to JojaMart", SVRegion.jojamart, flag=RandomizationFlag.PELICAN_TOWN), + ConnectionData("Town to Beach", SVRegion.beach), + ConnectionData("Enter Elliott's House", SVRegion.elliott_house), + ConnectionData("Beach to Willy's Fish Shop", SVRegion.fish_shop, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Enter Tide Pools", SVRegion.tide_pools), + ConnectionData("Mountain to The Mines", SVRegion.mines, flag=RandomizationFlag.NON_PROGRESSION), + ConnectionData("Dig to The Mines - Floor 5", SVRegion.mines_floor_5), + ConnectionData("Dig to The Mines - Floor 10", SVRegion.mines_floor_10), + ConnectionData("Dig to The Mines - Floor 15", SVRegion.mines_floor_15), + ConnectionData("Dig to The Mines - Floor 20", SVRegion.mines_floor_20), + ConnectionData("Dig to The Mines - Floor 25", SVRegion.mines_floor_25), + ConnectionData("Dig to The Mines - Floor 30", SVRegion.mines_floor_30), + ConnectionData("Dig to The Mines - Floor 35", SVRegion.mines_floor_35), + ConnectionData("Dig to The Mines - Floor 40", SVRegion.mines_floor_40), + ConnectionData("Dig to The Mines - Floor 45", SVRegion.mines_floor_45), + ConnectionData("Dig to The Mines - Floor 50", SVRegion.mines_floor_50), + ConnectionData("Dig to The Mines - Floor 55", SVRegion.mines_floor_55), + ConnectionData("Dig to The Mines - Floor 60", SVRegion.mines_floor_60), + ConnectionData("Dig to The Mines - Floor 65", SVRegion.mines_floor_65), + ConnectionData("Dig to The Mines - Floor 70", SVRegion.mines_floor_70), + ConnectionData("Dig to The Mines - Floor 75", SVRegion.mines_floor_75), + ConnectionData("Dig to The Mines - Floor 80", SVRegion.mines_floor_80), + ConnectionData("Dig to The Mines - Floor 85", SVRegion.mines_floor_85), + ConnectionData("Dig to The Mines - Floor 90", SVRegion.mines_floor_90), + ConnectionData("Dig to The Mines - Floor 95", SVRegion.mines_floor_95), + ConnectionData("Dig to The Mines - Floor 100", SVRegion.mines_floor_100), + ConnectionData("Dig to The Mines - Floor 105", SVRegion.mines_floor_105), + ConnectionData("Dig to The Mines - Floor 110", SVRegion.mines_floor_110), + ConnectionData("Dig to The Mines - Floor 115", SVRegion.mines_floor_115), + ConnectionData("Dig to The Mines - Floor 120", SVRegion.mines_floor_120), + ConnectionData("Enter Skull Cavern Entrance", SVRegion.skull_cavern_entrance), + ConnectionData("Enter Skull Cavern", SVRegion.skull_cavern), + ConnectionData("Mine to Skull Cavern Floor 100", SVRegion.perfect_skull_cavern), + ConnectionData("Enter Witch Warp Cave", SVRegion.witch_warp_cave), + ConnectionData("Enter Witch's Swamp", SVRegion.witch_swamp), + ConnectionData("Enter Bathhouse Entrance", SVRegion.bathhouse_entrance), + ConnectionData("Enter Locker Room", SVRegion.locker_room), + ConnectionData("Enter Public Bath", SVRegion.public_bath), ] -def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewOptions) -> Tuple[Iterable[Region], Dict[str, str]]: - regions: Dict[str: Region] = {region.name: region_factory(region.name, region.exits) for region in stardew_valley_regions} +def create_regions(region_factory: RegionFactory, random: Random, world_options: StardewOptions) -> Tuple[ + Iterable[Region], Dict[str, str]]: + regions: Dict[str: Region] = {region.name: region_factory(region.name, region.exits) for region in + stardew_valley_regions} entrances: Dict[str: Entrance] = {entrance.name: entrance for region in regions.values() for entrance in region.exits} @@ -272,9 +291,11 @@ def create_regions(region_factory: RegionFactory, random: Random, world_options: def randomize_connections(random: Random, world_options: StardewOptions) -> Tuple[List[ConnectionData], Dict[str, str]]: connections_to_randomize = [] if world_options[options.EntranceRandomization] == options.EntranceRandomization.option_pelican_town: - connections_to_randomize = [connection for connection in mandatory_connections if RandomizationFlag.PELICAN_TOWN in connection.flag] + connections_to_randomize = [connection for connection in mandatory_connections if + RandomizationFlag.PELICAN_TOWN in connection.flag] elif world_options[options.EntranceRandomization] == options.EntranceRandomization.option_non_progression: - connections_to_randomize = [connection for connection in mandatory_connections if RandomizationFlag.NON_PROGRESSION in connection.flag] + connections_to_randomize = [connection for connection in mandatory_connections if + RandomizationFlag.NON_PROGRESSION in connection.flag] random.shuffle(connections_to_randomize) destination_pool = list(connections_to_randomize) diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index f9ba31cc19..cbefe64810 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -1,49 +1,51 @@ import itertools -from typing import Dict +from typing import Dict, List from BaseClasses import MultiWorld from worlds.generic import Rules as MultiWorldRules from . import options, locations from .bundles import Bundle +from .data.museum_data import all_museum_items, all_mineral_items, all_artifact_items, \ + dwarf_scrolls, skeleton_front, \ + skeleton_middle, skeleton_back, all_museum_items_by_name from .locations import LocationTags -from .logic import StardewLogic, _And, season_per_skill_level, tool_prices, week_days - -help_wanted_per_season = { - 1: "Spring", - 2: "Summer", - 3: "Fall", - 4: "Winter", - 5: "Year Two", - 6: "Year Two", - 7: "Year Two", - 8: "Year Two", - 9: "Year Two", - 10: "Year Two", -} +from .logic import StardewLogic, And, month_end_per_skill_level, tool_prices, week_days +from .options import StardewOptions -def set_rules(multi_world: MultiWorld, player: int, world_options: options.StardewOptions, logic: StardewLogic, +def set_rules(multi_world: MultiWorld, player: int, world_options: StardewOptions, logic: StardewLogic, current_bundles: Dict[str, Bundle]): - summer = multi_world.get_location("Summer", player) all_location_names = list(location.name for location in multi_world.get_locations(player)) for floor in range(5, 120 + 5, 5): - MultiWorldRules.add_rule(multi_world.get_entrance(f"Dig to The Mines - Floor {floor}", player), + MultiWorldRules.set_rule(multi_world.get_entrance(f"Dig to The Mines - Floor {floor}", player), logic.can_mine_to_floor(floor).simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance("Enter Quarry", player), + MultiWorldRules.set_rule(multi_world.get_entrance("Enter Tide Pools", player), + logic.received("Beach Bridge").simplify()) + MultiWorldRules.set_rule(multi_world.get_entrance("Enter Quarry", player), logic.received("Bridge Repair").simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance("Enter Secret Woods", player), + MultiWorldRules.set_rule(multi_world.get_entrance("Enter Secret Woods", player), logic.has_tool("Axe", "Iron").simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance("Take Bus to Desert", player), + MultiWorldRules.set_rule(multi_world.get_entrance("Forest to Sewers", player), + logic.has_rusty_key().simplify()) + MultiWorldRules.set_rule(multi_world.get_entrance("Town to Sewers", player), + logic.has_rusty_key().simplify()) + MultiWorldRules.set_rule(multi_world.get_entrance("Take Bus to Desert", player), logic.received("Bus Repair").simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance("Enter Skull Cavern", player), + MultiWorldRules.set_rule(multi_world.get_entrance("Enter Skull Cavern", player), logic.received("Skull Key").simplify()) + MultiWorldRules.set_rule(multi_world.get_entrance("Mine to Skull Cavern Floor 100", player), + logic.can_mine_perfectly_in_the_skull_cavern().simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance("Use Desert Obelisk", player), + MultiWorldRules.set_rule(multi_world.get_entrance("Use Desert Obelisk", player), logic.received("Desert Obelisk").simplify()) - MultiWorldRules.add_rule(multi_world.get_entrance("Use Island Obelisk", player), + MultiWorldRules.set_rule(multi_world.get_entrance("Use Island Obelisk", player), logic.received("Island Obelisk").simplify()) + MultiWorldRules.set_rule(multi_world.get_entrance("Talk to Traveling Merchant", player), + logic.has_traveling_merchant()) + MultiWorldRules.set_rule(multi_world.get_entrance("Enter Greenhouse", player), + logic.received("Greenhouse")) # Those checks do not exist if ToolProgression is vanilla if world_options[options.ToolProgression] != options.ToolProgression.option_vanilla: @@ -68,43 +70,46 @@ def set_rules(multi_world: MultiWorld, player: int, world_options: options.Stard if world_options[options.SkillProgression] != options.SkillProgression.option_vanilla: for i in range(1, 11): MultiWorldRules.set_rule(multi_world.get_location(f"Level {i} Farming", player), - (logic.received(season_per_skill_level["Farming", i])).simplify()) + (logic.received("Month End", month_end_per_skill_level["Farming", i])).simplify()) MultiWorldRules.set_rule(multi_world.get_location(f"Level {i} Fishing", player), (logic.can_get_fishing_xp() & - logic.received(season_per_skill_level["Fishing", i])).simplify()) + logic.received("Month End", month_end_per_skill_level["Fishing", i])).simplify()) MultiWorldRules.add_rule(multi_world.get_location(f"Level {i} Foraging", player), - logic.received(season_per_skill_level["Foraging", i]).simplify()) + logic.received("Month End", month_end_per_skill_level["Foraging", i]).simplify()) if i >= 6: MultiWorldRules.add_rule(multi_world.get_location(f"Level {i} Foraging", player), logic.has_tool("Axe", "Iron").simplify()) MultiWorldRules.set_rule(multi_world.get_location(f"Level {i} Mining", player), - logic.received(season_per_skill_level["Mining", i]).simplify()) + logic.received("Month End", month_end_per_skill_level["Mining", i]).simplify()) MultiWorldRules.set_rule(multi_world.get_location(f"Level {i} Combat", player), - (logic.received(season_per_skill_level["Combat", i]) & + (logic.received("Month End", month_end_per_skill_level["Combat", i]) & logic.has_any_weapon()).simplify()) # Bundles for bundle in current_bundles.values(): - MultiWorldRules.set_rule(multi_world.get_location(bundle.get_name_with_bundle(), player), - logic.can_complete_bundle(bundle.requirements, bundle.number_required).simplify()) + location = multi_world.get_location(bundle.get_name_with_bundle(), player) + rules = logic.can_complete_bundle(bundle.requirements, bundle.number_required) + simplified_rules = rules.simplify() + MultiWorldRules.set_rule(location, simplified_rules) MultiWorldRules.add_rule(multi_world.get_location("Complete Crafts Room", player), - _And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.CRAFTS_ROOM_BUNDLE]).simplify()) + And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.CRAFTS_ROOM_BUNDLE]).simplify()) MultiWorldRules.add_rule(multi_world.get_location("Complete Pantry", player), - _And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.PANTRY_BUNDLE]).simplify()) + And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.PANTRY_BUNDLE]).simplify()) MultiWorldRules.add_rule(multi_world.get_location("Complete Fish Tank", player), - _And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.FISH_TANK_BUNDLE]).simplify()) + And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.FISH_TANK_BUNDLE]).simplify()) MultiWorldRules.add_rule(multi_world.get_location("Complete Boiler Room", player), - _And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.BOILER_ROOM_BUNDLE]).simplify()) + And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.BOILER_ROOM_BUNDLE]).simplify()) MultiWorldRules.add_rule(multi_world.get_location("Complete Bulletin Board", player), - _And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.BULLETIN_BOARD_BUNDLE]).simplify()) + And(logic.can_reach_location(bundle.name) + for bundle + in locations.locations_by_tag[LocationTags.BULLETIN_BOARD_BUNDLE]).simplify()) MultiWorldRules.add_rule(multi_world.get_location("Complete Vault", player), - _And(logic.can_reach_location(bundle.name) - for bundle in locations.locations_by_tag[LocationTags.VAULT_BUNDLE]).simplify()) + And(logic.can_reach_location(bundle.name) + for bundle in locations.locations_by_tag[LocationTags.VAULT_BUNDLE]).simplify()) # Buildings if world_options[options.BuildingProgression] != options.BuildingProgression.option_vanilla: @@ -122,7 +127,7 @@ def set_rules(multi_world: MultiWorld, player: int, world_options: options.Stard for i in range(1, desired_number_help_wanted + 1): prefix = "Help Wanted:" delivery = "Item Delivery" - rule = logic.received(help_wanted_per_season[min(5, i)]) + rule = logic.received("Month End", i - 1) fishing_rule = rule & logic.can_fish() slay_rule = rule & logic.has_any_weapon() for j in range(i, i + 4): @@ -136,6 +141,21 @@ def set_rules(multi_world: MultiWorld, player: int, world_options: options.Stard MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Slay Monsters {i}", player), slay_rule.simplify()) + set_fishsanity_rules(all_location_names, logic, multi_world, player) + set_museumsanity_rules(all_location_names, logic, multi_world, player, world_options) + set_friendsanity_rules(all_location_names, logic, multi_world, player) + set_backpack_rules(logic, multi_world, player, world_options) + + MultiWorldRules.add_rule(multi_world.get_location("Old Master Cannoli", player), + logic.has("Sweet Gem Berry").simplify()) + MultiWorldRules.add_rule(multi_world.get_location("Galaxy Sword Shrine", player), + logic.has("Prismatic Shard").simplify()) + + set_traveling_merchant_rules(logic, multi_world, player) + set_arcade_machine_rules(logic, multi_world, player, world_options) + + +def set_fishsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int): fish_prefix = "Fishsanity: " for fish_location in locations.locations_by_tag[LocationTags.FISHSANITY]: if fish_location.name in all_location_names: @@ -143,27 +163,78 @@ def set_rules(multi_world: MultiWorld, player: int, world_options: options.Stard MultiWorldRules.set_rule(multi_world.get_location(fish_location.name, player), logic.has(fish_name).simplify()) - if world_options[options.BuildingProgression] == options.BuildingProgression.option_progressive_early_shipping_bin: - summer.access_rule = summer.access_rule & logic.received("Shipping Bin") - # Backpacks +def set_museumsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int, + world_options: StardewOptions): + museum_prefix = "Museumsanity: " + if world_options[options.Museumsanity] == options.Museumsanity.option_milestones: + for museum_milestone in locations.locations_by_tag[LocationTags.MUSEUM_MILESTONES]: + set_museum_milestone_rule(logic, multi_world, museum_milestone, museum_prefix, player) + elif world_options[options.Museumsanity] != options.Museumsanity.option_none: + set_museum_individual_donations_rules(all_location_names, logic, multi_world, museum_prefix, player) + + +def set_museum_individual_donations_rules(all_location_names, logic, multi_world, museum_prefix, player): + all_donations = sorted(locations.locations_by_tag[LocationTags.MUSEUM_DONATIONS], + key=lambda x: all_museum_items_by_name[x.name[len(museum_prefix):]].difficulty, reverse=True) + counter = 0 + number_donations = len(all_donations) + for museum_location in all_donations: + if museum_location.name in all_location_names: + donation_name = museum_location.name[len(museum_prefix):] + required_detectors = counter * 5 // number_donations + rule = logic.has(donation_name) & logic.received("Traveling Merchant Metal Detector", required_detectors) + MultiWorldRules.set_rule(multi_world.get_location(museum_location.name, player), + rule.simplify()) + counter += 1 + + +def set_museum_milestone_rule(logic: StardewLogic, multi_world: MultiWorld, museum_milestone, museum_prefix: str, + player: int): + milestone_name = museum_milestone.name[len(museum_prefix):] + donations_suffix = " Donations" + minerals_suffix = " Minerals" + artifacts_suffix = " Artifacts" + metal_detector = "Traveling Merchant Metal Detector" + rule = None + if milestone_name.endswith(donations_suffix): + rule = get_museum_item_count_rule(logic, donations_suffix, milestone_name, all_museum_items) + elif milestone_name.endswith(minerals_suffix): + rule = get_museum_item_count_rule(logic, minerals_suffix, milestone_name, all_mineral_items) + elif milestone_name.endswith(artifacts_suffix): + rule = get_museum_item_count_rule(logic, artifacts_suffix, milestone_name, all_artifact_items) + elif milestone_name == "Dwarf Scrolls": + rule = logic.has([item.name for item in dwarf_scrolls]) & logic.received(metal_detector, 4) + elif milestone_name == "Skeleton Front": + rule = logic.has([item.name for item in skeleton_front]) & logic.received(metal_detector, 4) + elif milestone_name == "Skeleton Middle": + rule = logic.has([item.name for item in skeleton_middle]) & logic.received(metal_detector, 4) + elif milestone_name == "Skeleton Back": + rule = logic.has([item.name for item in skeleton_back]) & logic.received(metal_detector, 4) + elif milestone_name == "Ancient Seed": + rule = logic.has("Ancient Seed") & logic.received(metal_detector, 4) + if rule is None: + return + MultiWorldRules.set_rule(multi_world.get_location(museum_milestone.name, player), rule.simplify()) + + +def get_museum_item_count_rule(logic, suffix, milestone_name, accepted_items): + metal_detector = "Traveling Merchant Metal Detector" + num = int(milestone_name[:milestone_name.index(suffix)]) + required_detectors = (num - 1) * 5 // len(accepted_items) + rule = logic.has([item.name for item in accepted_items], num) & logic.received(metal_detector, required_detectors) + return rule + + +def set_backpack_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options): if world_options[options.BackpackProgression] != options.BackpackProgression.option_vanilla: - MultiWorldRules.add_rule(multi_world.get_location("Large Pack", player), + MultiWorldRules.set_rule(multi_world.get_location("Large Pack", player), logic.can_spend_money(2000).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Deluxe Pack", player), - logic.can_spend_money(10000).simplify()) + MultiWorldRules.set_rule(multi_world.get_location("Deluxe Pack", player), + (logic.can_spend_money(10000) & logic.received("Progressive Backpack")).simplify()) - if world_options[options.BackpackProgression] == options.BackpackProgression.option_early_progressive: - summer.access_rule = summer.access_rule & logic.received("Progressive Backpack") - MultiWorldRules.add_rule(multi_world.get_location("Winter", player), - logic.received("Progressive Backpack", 2).simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Old Master Cannoli", player), - logic.has("Sweet Gem Berry").simplify()) - MultiWorldRules.add_rule(multi_world.get_location("Galaxy Sword Shrine", player), - logic.has("Prismatic Shard").simplify()) - - # Traveling Merchant +def set_traveling_merchant_rules(logic: StardewLogic, multi_world: MultiWorld, player: int): for day in week_days: item_for_day = f"Traveling Merchant: {day}" for i in range(1, 4): @@ -171,6 +242,8 @@ def set_rules(multi_world: MultiWorld, player: int, world_options: options.Stard MultiWorldRules.set_rule(multi_world.get_location(location_name, player), logic.received(item_for_day)) + +def set_arcade_machine_rules(logic: StardewLogic, multi_world: MultiWorld, player: int, world_options): if world_options[options.ArcadeMachineLocations] == options.ArcadeMachineLocations.option_full_shuffling: MultiWorldRules.add_rule(multi_world.get_entrance("Play Junimo Kart", player), (logic.received("Skull Key") & logic.has("Junimo Kart Small Buff")).simplify()) @@ -188,3 +261,18 @@ def set_rules(multi_world: MultiWorld, player: int, world_options: options.Stard logic.has("JotPK Big Buff").simplify()) MultiWorldRules.add_rule(multi_world.get_location("Journey of the Prairie King Victory", player), logic.has("JotPK Max Buff").simplify()) + + +def set_friendsanity_rules(all_location_names: List[str], logic: StardewLogic, multi_world: MultiWorld, player: int): + friend_prefix = "Friendsanity: " + friend_suffix = " <3" + for friend_location in locations.locations_by_tag[LocationTags.FRIENDSANITY]: + if not friend_location.name in all_location_names: + continue + friend_location_without_prefix = friend_location.name[len(friend_prefix):] + friend_location_trimmed = friend_location_without_prefix[:friend_location_without_prefix.index(friend_suffix)] + parts = friend_location_trimmed.split(" ") + friend_name = parts[0] + num_hearts = int(parts[1]) + MultiWorldRules.set_rule(multi_world.get_location(friend_location.name, player), + logic.can_earn_relationship(friend_name, num_hearts).simplify()) diff --git a/worlds/stardew_valley/stardew_rule.py b/worlds/stardew_valley/stardew_rule.py new file mode 100644 index 0000000000..7f568f9fc9 --- /dev/null +++ b/worlds/stardew_valley/stardew_rule.py @@ -0,0 +1,360 @@ +from __future__ import annotations + +from dataclasses import dataclass +from typing import Iterable, Dict, List, Union, FrozenSet + +from BaseClasses import CollectionState, ItemClassification +from .items import item_table + +MISSING_ITEM = "THIS ITEM IS MISSING" + + +class StardewRule: + def __call__(self, state: CollectionState) -> bool: + raise NotImplementedError + + def __or__(self, other) -> StardewRule: + if isinstance(other, Or): + return Or(self, *other.rules) + + return Or(self, other) + + def __and__(self, other) -> StardewRule: + if isinstance(other, And): + return And(other.rules.union({self})) + + return And(self, other) + + def get_difficulty(self): + raise NotImplementedError + + def simplify(self) -> StardewRule: + return self + + +class True_(StardewRule): # noqa + + def __new__(cls, _cache=[]): # noqa + # Only one single instance will be ever created. + if not _cache: + _cache.append(super(True_, cls).__new__(cls)) + return _cache[0] + + def __call__(self, state: CollectionState) -> bool: + return True + + def __or__(self, other) -> StardewRule: + return self + + def __and__(self, other) -> StardewRule: + return other + + def __repr__(self): + return "True" + + def get_difficulty(self): + return 0 + + +class False_(StardewRule): # noqa + + def __new__(cls, _cache=[]): # noqa + # Only one single instance will be ever created. + if not _cache: + _cache.append(super(False_, cls).__new__(cls)) + return _cache[0] + + def __call__(self, state: CollectionState) -> bool: + return False + + def __or__(self, other) -> StardewRule: + return other + + def __and__(self, other) -> StardewRule: + return self + + def __repr__(self): + return "False" + + def get_difficulty(self): + return 999999999 + + +class Or(StardewRule): + rules: FrozenSet[StardewRule] + + def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): + rules_list = set() + if isinstance(rule, Iterable): + rules_list.update(rule) + else: + rules_list.add(rule) + + if rules is not None: + rules_list.update(rules) + + assert rules_list, "Can't create a Or conditions without rules" + + new_rules = set() + for rule in rules_list: + if isinstance(rule, Or): + new_rules.update(rule.rules) + else: + new_rules.add(rule) + rules_list = new_rules + + self.rules = frozenset(rules_list) + + def __call__(self, state: CollectionState) -> bool: + return any(rule(state) for rule in self.rules) + + def __repr__(self): + return f"({' | '.join(repr(rule) for rule in self.rules)})" + + def __or__(self, other): + if isinstance(other, True_): + return other + if isinstance(other, False_): + return self + if isinstance(other, Or): + return Or(self.rules.union(other.rules)) + + return Or(self.rules.union({other})) + + def __eq__(self, other): + return isinstance(other, self.__class__) and other.rules == self.rules + + def __hash__(self): + return hash(self.rules) + + def get_difficulty(self): + return min(rule.get_difficulty() for rule in self.rules) + + def simplify(self) -> StardewRule: + if any(isinstance(rule, True_) for rule in self.rules): + return True_() + + simplified_rules = {rule.simplify() for rule in self.rules} + simplified_rules = {rule for rule in simplified_rules if rule is not False_()} + + if not simplified_rules: + return False_() + + if len(simplified_rules) == 1: + return next(iter(simplified_rules)) + + return Or(simplified_rules) + + +class And(StardewRule): + rules: FrozenSet[StardewRule] + + def __init__(self, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): + rules_list = set() + if isinstance(rule, Iterable): + rules_list.update(rule) + else: + rules_list.add(rule) + + if rules is not None: + rules_list.update(rules) + + assert rules_list, "Can't create a And conditions without rules" + + new_rules = set() + for rule in rules_list: + if isinstance(rule, And): + new_rules.update(rule.rules) + else: + new_rules.add(rule) + rules_list = new_rules + + self.rules = frozenset(rules_list) + + def __call__(self, state: CollectionState) -> bool: + return all(rule(state) for rule in self.rules) + + def __repr__(self): + return f"({' & '.join(repr(rule) for rule in self.rules)})" + + def __and__(self, other): + if isinstance(other, True_): + return self + if isinstance(other, False_): + return other + if isinstance(other, And): + return And(self.rules.union(other.rules)) + + return And(self.rules.union({other})) + + def __eq__(self, other): + return isinstance(other, self.__class__) and other.rules == self.rules + + def __hash__(self): + return hash(self.rules) + + def get_difficulty(self): + return max(rule.get_difficulty() for rule in self.rules) + + def simplify(self) -> StardewRule: + if any(isinstance(rule, False_) for rule in self.rules): + return False_() + + simplified_rules = {rule.simplify() for rule in self.rules} + simplified_rules = {rule for rule in simplified_rules if rule is not True_()} + + if not simplified_rules: + return True_() + + if len(simplified_rules) == 1: + return next(iter(simplified_rules)) + + return And(simplified_rules) + + +class Count(StardewRule): + count: int + rules: List[StardewRule] + + def __init__(self, count: int, rule: Union[StardewRule, Iterable[StardewRule]], *rules: StardewRule): + rules_list = [] + if isinstance(rule, Iterable): + rules_list.extend(rule) + else: + rules_list.append(rule) + + if rules is not None: + rules_list.extend(rules) + + assert rules_list, "Can't create a Count conditions without rules" + assert len(rules_list) >= count, "Count need at least as many rules at the count" + + self.rules = rules_list + self.count = count + + def __call__(self, state: CollectionState) -> bool: + c = 0 + for r in self.rules: + if r(state): + c += 1 + if c >= self.count: + return True + return False + + def __repr__(self): + return f"Received {self.count} {repr(self.rules)}" + + def get_difficulty(self): + rules_sorted_by_difficulty = sorted(self.rules, key=lambda x: x.get_difficulty()) + easiest_n_rules = rules_sorted_by_difficulty[0:self.count] + return max(rule.get_difficulty() for rule in easiest_n_rules) + + def simplify(self): + return Count(self.count, [rule.simplify() for rule in self.rules]) + + +class TotalReceived(StardewRule): + count: int + items: Iterable[str] + player: int + + def __init__(self, count: int, items: Union[str, Iterable[str]], player: int): + items_list = [] + if isinstance(items, Iterable): + items_list.extend(items) + else: + items_list.append(items) + + assert items_list, "Can't create a Total Received conditions without items" + for item in items_list: + assert item_table[item].classification & ItemClassification.progression, \ + "Item has to be progression to be used in logic" + + self.player = player + self.items = items_list + self.count = count + + def __call__(self, state: CollectionState) -> bool: + c = 0 + for item in self.items: + c += state.count(item, self.player) + if c >= self.count: + return True + return False + + def __repr__(self): + return f"Received {self.count} {self.items}" + + def get_difficulty(self): + return self.count + + +@dataclass(frozen=True) +class Received(StardewRule): + item: str + player: int + count: int + + def __post_init__(self): + assert item_table[self.item].classification & ItemClassification.progression, \ + "Item has to be progression to be used in logic" + + def __call__(self, state: CollectionState) -> bool: + return state.has(self.item, self.player, self.count) + + def __repr__(self): + if self.count == 1: + return f"Received {self.item}" + return f"Received {self.count} {self.item}" + + def get_difficulty(self): + if self.item == "Spring": + return 0 + if self.item == "Summer": + return 1 + if self.item == "Fall": + return 2 + if self.item == "Winter": + return 3 + return self.count + + +@dataclass(frozen=True) +class Reach(StardewRule): + spot: str + resolution_hint: str + player: int + + def __call__(self, state: CollectionState) -> bool: + return state.can_reach(self.spot, self.resolution_hint, self.player) + + def __repr__(self): + return f"Reach {self.resolution_hint} {self.spot}" + + def get_difficulty(self): + return 1 + + +@dataclass(frozen=True) +class Has(StardewRule): + item: str + # For sure there is a better way than just passing all the rules everytime + other_rules: Dict[str, StardewRule] + + def __call__(self, state: CollectionState) -> bool: + if isinstance(self.item, str): + return self.other_rules[self.item](state) + + def __repr__(self): + if not self.item in self.other_rules: + return f"Has {self.item} -> {MISSING_ITEM}" + return f"Has {self.item} -> {repr(self.other_rules[self.item])}" + + def get_difficulty(self): + return self.other_rules[self.item].get_difficulty() + 1 + + def __hash__(self): + return hash(self.item) + + def simplify(self) -> StardewRule: + return self.other_rules[self.item].simplify() diff --git a/worlds/stardew_valley/test/TestAllLogic.py b/worlds/stardew_valley/test/TestAllLogic.py deleted file mode 100644 index de1c004913..0000000000 --- a/worlds/stardew_valley/test/TestAllLogic.py +++ /dev/null @@ -1,53 +0,0 @@ -import unittest - -from test.general import setup_solo_multiworld -from .. import StardewValleyWorld -from ..bundle_data import all_bundle_items_except_money -from ..logic import MISSING_ITEM, _False - - -class TestAllLogicalItem(unittest.TestCase): - multi_world = setup_solo_multiworld(StardewValleyWorld) - world = multi_world.worlds[1] - logic = world.logic - - def setUp(self) -> None: - for item in self.multi_world.get_items(): - self.multi_world.state.collect(item, event=True) - - def test_given_bundle_item_then_is_available_in_logic(self): - for bundle_item in all_bundle_items_except_money: - with self.subTest(bundle_item=bundle_item): - assert bundle_item.item.name in self.logic.item_rules - - def test_given_item_rule_then_can_be_resolved(self): - for item in self.logic.item_rules.keys(): - with self.subTest(item=item): - rule = self.logic.item_rules[item] - - assert MISSING_ITEM not in repr(rule) - assert rule == _False() or rule(self.multi_world.state), f"Could not resolve rule for {item} {rule}" - - def test_given_building_rule_then_can_be_resolved(self): - for item in self.logic.building_rules.keys(): - with self.subTest(item=item): - rule = self.logic.building_rules[item] - - assert MISSING_ITEM not in repr(rule) - assert rule == _False() or rule(self.multi_world.state), f"Could not resolve rule for {item} {rule}" - - def test_given_quest_rule_then_can_be_resolved(self): - for item in self.logic.quest_rules.keys(): - with self.subTest(item=item): - rule = self.logic.quest_rules[item] - - assert MISSING_ITEM not in repr(rule) - assert rule == _False() or rule(self.multi_world.state), f"Could not resolve rule for {item} {rule}" - - def test_given_location_rule_then_can_be_resolved(self): - for location in self.multi_world.get_locations(1): - with self.subTest(location=location): - rule = location.access_rule - - assert MISSING_ITEM not in repr(rule) - assert rule == _False() or rule(self.multi_world.state), f"Could not resolve rule for {location} {rule}" diff --git a/worlds/stardew_valley/test/TestBundles.py b/worlds/stardew_valley/test/TestBundles.py index 5801737709..1e75bd9bb0 100644 --- a/worlds/stardew_valley/test/TestBundles.py +++ b/worlds/stardew_valley/test/TestBundles.py @@ -1,6 +1,6 @@ import unittest -from ..bundle_data import all_bundle_items +from ..data.bundle_data import all_bundle_items class TestBundles(unittest.TestCase): diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py index 840052d30b..9bf4fbcb09 100644 --- a/worlds/stardew_valley/test/TestGeneration.py +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -1,6 +1,7 @@ from BaseClasses import ItemClassification from . import SVTestBase from .. import locations, items, location_table, options +from ..data.villagers_data import all_villagers_by_name from ..items import items_by_group, Group from ..locations import LocationTags @@ -113,15 +114,157 @@ class TestLocationGeneration(SVTestBase): class TestLocationAndItemCount(SVTestBase): options = { + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + options.SeedShuffle.internal_name: options.SeedShuffle.option_shuffled, options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla, options.ToolProgression.internal_name: options.ToolProgression.option_vanilla, - options.TheMinesElevatorsProgression.internal_name: options.TheMinesElevatorsProgression.option_vanilla, options.SkillProgression.internal_name: options.SkillProgression.option_vanilla, options.BuildingProgression.internal_name: options.BuildingProgression.option_vanilla, + options.TheMinesElevatorsProgression.internal_name: options.TheMinesElevatorsProgression.option_vanilla, options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_disabled, options.HelpWantedLocations.internal_name: 0, + options.Fishsanity.internal_name: options.Fishsanity.option_none, + options.Museumsanity.internal_name: options.Museumsanity.option_none, + options.Friendsanity.internal_name: options.Museumsanity.option_none, options.NumberOfPlayerBuffs.internal_name: 12, } def test_minimal_location_maximal_items_still_valid(self): assert len(self.multiworld.get_locations()) >= len(self.multiworld.get_items()) + + +class TestFriendsanityNone(SVTestBase): + options = { + options.Friendsanity.internal_name: options.Friendsanity.option_none, + } + + def test_no_friendsanity_items(self): + for item in self.multiworld.get_items(): + assert not item.name.endswith(": 1 <3") + + def test_no_friendsanity_locations(self): + for location in self.multiworld.get_locations(): + assert not location.name.startswith("Friendsanity") + + +class TestFriendsanityBachelors(SVTestBase): + options = { + options.Friendsanity.internal_name: options.Friendsanity.option_bachelors, + } + bachelors = {"Harvey", "Elliott", "Sam", "Alex", "Shane", "Sebastian", "Emily", "Haley", "Leah", "Abigail", "Penny", + "Maru"} + + def test_friendsanity_only_bachelor_items(self): + suffix = ": 1 <3" + for item in self.multiworld.get_items(): + if item.name.endswith(suffix): + villager_name = item.name[:item.name.index(suffix)] + assert villager_name in self.bachelors + + def test_friendsanity_only_bachelor_locations(self): + prefix = "Friendsanity: " + suffix = " <3" + for location in self.multiworld.get_locations(): + if location.name.startswith(prefix): + name_no_prefix = location.name[len(prefix):] + name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] + parts = name_trimmed.split(" ") + name = parts[0] + hearts = parts[1] + assert name in self.bachelors + assert int(hearts) <= 8 + + +class TestFriendsanityStartingNpcs(SVTestBase): + options = { + options.Friendsanity.internal_name: options.Friendsanity.option_starting_npcs, + } + excluded_npcs = {"Leo", "Krobus", "Dwarf", "Sandy", "Kent"} + + def test_friendsanity_only_starting_npcs_items(self): + suffix = ": 1 <3" + for item in self.multiworld.get_items(): + if item.name.endswith(suffix): + villager_name = item.name[:item.name.index(suffix)] + assert villager_name not in self.excluded_npcs + + def test_friendsanity_only_starting_npcs_locations(self): + prefix = "Friendsanity: " + suffix = " <3" + for location in self.multiworld.get_locations(): + if location.name.startswith(prefix): + name_no_prefix = location.name[len(prefix):] + name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] + parts = name_trimmed.split(" ") + name = parts[0] + hearts = parts[1] + assert name not in self.excluded_npcs + assert name in all_villagers_by_name or name == "Pet" + if name == "Pet": + assert int(hearts) <= 5 + elif all_villagers_by_name[name].bachelor: + assert int(hearts) <= 8 + else: + assert int(hearts) <= 10 + + +class TestFriendsanityAllNpcs(SVTestBase): + options = { + options.Friendsanity.internal_name: options.Friendsanity.option_all, + } + + def test_friendsanity_all_items(self): + suffix = ": 1 <3" + for item in self.multiworld.get_items(): + if item.name.endswith(suffix): + villager_name = item.name[:item.name.index(suffix)] + assert villager_name in all_villagers_by_name or villager_name == "Pet" + + def test_friendsanity_all_locations(self): + prefix = "Friendsanity: " + suffix = " <3" + for location in self.multiworld.get_locations(): + if location.name.startswith(prefix): + name_no_prefix = location.name[len(prefix):] + name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] + parts = name_trimmed.split(" ") + name = parts[0] + hearts = parts[1] + assert name in all_villagers_by_name or name == "Pet" + if name == "Pet": + assert int(hearts) <= 5 + elif all_villagers_by_name[name].bachelor: + assert int(hearts) <= 8 + else: + assert int(hearts) <= 10 + + +class TestFriendsanityAllNpcsWithMarriage(SVTestBase): + options = { + options.Friendsanity.internal_name: options.Friendsanity.option_all_with_marriage, + } + + def test_friendsanity_all_with_marriage_items(self): + suffix = ": 1 <3" + for item in self.multiworld.get_items(): + if item.name.endswith(suffix): + villager_name = item.name[:item.name.index(suffix)] + assert villager_name in all_villagers_by_name or villager_name == "Pet" + + def test_friendsanity_all_with_marriage_locations(self): + prefix = "Friendsanity: " + suffix = " <3" + for location in self.multiworld.get_locations(): + if location.name.startswith(prefix): + name_no_prefix = location.name[len(prefix):] + name_trimmed = name_no_prefix[:name_no_prefix.index(suffix)] + parts = name_trimmed.split(" ") + name = parts[0] + hearts = parts[1] + assert name in all_villagers_by_name or name == "Pet" + if name == "Pet": + assert int(hearts) <= 5 + elif all_villagers_by_name[name].bachelor: + assert int(hearts) <= 14 + else: + assert int(hearts) <= 10 diff --git a/worlds/stardew_valley/test/TestItems.py b/worlds/stardew_valley/test/TestItems.py index 98d251eb58..8c31818441 100644 --- a/worlds/stardew_valley/test/TestItems.py +++ b/worlds/stardew_valley/test/TestItems.py @@ -1,8 +1,11 @@ +import itertools +import math import unittest -from BaseClasses import MultiWorld -from .. import StardewValleyWorld -from ..items import item_table + +from BaseClasses import ItemClassification, MultiWorld +from .. import ItemData, StardewValleyWorld +from ..items import Group, ResourcePackData, item_table class TestItems(unittest.TestCase): @@ -24,3 +27,70 @@ class TestItems(unittest.TestCase): assert item_with_lowest_id.code >= 717000 assert item_with_highest_id.code < 727000 + + +class TestResourcePacks: + def test_can_transform_resource_pack_data_into_idem_data(self): + resource_pack = ResourcePackData("item name", 1, 1, ItemClassification.filler, frozenset()) + + items = resource_pack.as_item_data(itertools.count()) + + assert ItemData(0, "Resource Pack: 1 item name", ItemClassification.filler, {Group.RESOURCE_PACK}) in items + assert ItemData(1, "Resource Pack: 2 item name", ItemClassification.filler, {Group.RESOURCE_PACK}) in items + assert len(items) == 2 + + def test_when_scale_quantity_then_generate_a_possible_quantity_from_minimal_scaling_to_double(self): + resource_pack = ResourcePackData("item name", default_amount=4, scaling_factor=2) + + quantities = resource_pack.scale_quantity.items() + + assert (50, 2) in quantities + assert (100, 4) in quantities + assert (150, 6) in quantities + assert (200, 8) in quantities + assert len(quantities) == (4 / 2) * 2 + + def test_given_scaling_not_multiple_of_default_amount_when_scale_quantity_then_double_is_added_at_200_scaling(self): + resource_pack = ResourcePackData("item name", default_amount=5, scaling_factor=3) + + quantities = resource_pack.scale_quantity.items() + + assert (40, 2) in quantities + assert (100, 5) in quantities + assert (160, 8) in quantities + assert (200, 10) in quantities + assert len(quantities) == math.ceil(5 / 3) * 2 + + def test_given_large_default_amount_multiple_of_scaling_factor_when_scale_quantity_then_scaled_amount_multiple(self): + resource_pack = ResourcePackData("item name", default_amount=500, scaling_factor=50) + + quantities = resource_pack.scale_quantity.items() + + assert (10, 50) in quantities + assert (20, 100) in quantities + assert (30, 150) in quantities + assert (40, 200) in quantities + assert (50, 250) in quantities + assert (60, 300) in quantities + assert (70, 350) in quantities + assert (80, 400) in quantities + assert (90, 450) in quantities + assert (100, 500) in quantities + assert (110, 550) in quantities + assert (120, 600) in quantities + assert (130, 650) in quantities + assert (140, 700) in quantities + assert (150, 750) in quantities + assert (160, 800) in quantities + assert (170, 850) in quantities + assert (180, 900) in quantities + assert (190, 950) in quantities + assert (200, 1000) in quantities + assert len(quantities) == math.ceil(500 / 50) * 2 + + def test_given_smallest_multiplier_possible_when_generate_resource_pack_name_then_quantity_is_not_0(self): + resource_pack = ResourcePackData("item name", default_amount=10, scaling_factor=5) + + name = resource_pack.create_name_from_multiplier(1) + + assert name == "Resource Pack: 5 item name" diff --git a/worlds/stardew_valley/test/TestLogic.py b/worlds/stardew_valley/test/TestLogic.py index 83129a56b5..beb610230b 100644 --- a/worlds/stardew_valley/test/TestLogic.py +++ b/worlds/stardew_valley/test/TestLogic.py @@ -1,293 +1,57 @@ -from . import SVTestBase -from .. import options +import pytest + +from test.general import setup_solo_multiworld +from .. import StardewValleyWorld, StardewLocation +from ..data.bundle_data import BundleItem, all_bundle_items_except_money +from ..stardew_rule import MISSING_ITEM, False_ + +multi_world = setup_solo_multiworld(StardewValleyWorld) +world = multi_world.worlds[1] +logic = world.logic -class TestProgressiveToolsLogic(SVTestBase): - options = { - options.ToolProgression.internal_name: options.ToolProgression.option_progressive, - } - - def test_sturgeon(self): - assert not self.world.logic.has("Sturgeon")(self.multiworld.state) - - summer = self.get_item_by_name("Summer") - self.multiworld.state.collect(summer, event=True) - assert not self.world.logic.has("Sturgeon")(self.multiworld.state) - - fishing_rod = self.get_item_by_name("Progressive Fishing Rod") - self.multiworld.state.collect(fishing_rod, event=True) - self.multiworld.state.collect(fishing_rod, event=True) - assert not self.world.logic.has("Sturgeon")(self.multiworld.state) - - fishing_level = self.get_item_by_name("Fishing Level") - self.multiworld.state.collect(fishing_level, event=True) - assert not self.world.logic.has("Sturgeon")(self.multiworld.state) - - self.multiworld.state.collect(fishing_level, event=True) - self.multiworld.state.collect(fishing_level, event=True) - self.multiworld.state.collect(fishing_level, event=True) - self.multiworld.state.collect(fishing_level, event=True) - self.multiworld.state.collect(fishing_level, event=True) - assert self.world.logic.has("Sturgeon")(self.multiworld.state) - - self.remove(summer) - assert not self.world.logic.has("Sturgeon")(self.multiworld.state) - - winter = self.get_item_by_name("Winter") - self.multiworld.state.collect(winter, event=True) - assert self.world.logic.has("Sturgeon")(self.multiworld.state) - - self.remove(fishing_rod) - assert not self.world.logic.has("Sturgeon")(self.multiworld.state) - - def test_old_master_cannoli(self): - self.multiworld.state.collect(self.get_item_by_name("Progressive Axe"), event=True) - self.multiworld.state.collect(self.get_item_by_name("Progressive Axe"), event=True) - - assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) - - fall = self.get_item_by_name("Fall") - self.multiworld.state.collect(fall, event=True) - assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) - - tuesday = self.get_item_by_name("Traveling Merchant: Tuesday") - self.multiworld.state.collect(tuesday, event=True) - assert self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) - - self.remove(fall) - assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) - self.remove(tuesday) - - green_house = self.get_item_by_name("Greenhouse") - self.multiworld.state.collect(green_house, event=True) - assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) - - friday = self.get_item_by_name("Traveling Merchant: Friday") - self.multiworld.state.collect(friday, event=True) - assert self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) - - self.remove(green_house) - assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) - self.remove(friday) +def collect_all(mw): + for item in mw.get_items(): + mw.state.collect(item, event=True) -class TestBundlesLogic(SVTestBase): - options = { - } - - def test_vault_2500g_bundle(self): - assert not self.world.logic.can_reach_location("2,500g Bundle")(self.multiworld.state) - - summer = self.get_item_by_name("Summer") - self.multiworld.state.collect(summer, event=True) - assert self.world.logic.can_reach_location("2,500g Bundle")(self.multiworld.state) +collect_all(multi_world) -class TestBuildingLogic(SVTestBase): - options = { - options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_early_shipping_bin - } - - def test_coop_blueprint(self): - assert not self.world.logic.can_reach_location("Coop Blueprint")(self.multiworld.state) - - summer = self.get_item_by_name("Summer") - self.multiworld.state.collect(summer, event=True) - assert self.world.logic.can_reach_location("Coop Blueprint")(self.multiworld.state) - - def test_big_coop_blueprint(self): - assert not self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state), \ - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}" - - self.multiworld.state.collect(self.get_item_by_name("Fall"), event=True) - assert not self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state), \ - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}" - - self.multiworld.state.collect(self.get_item_by_name("Progressive Coop"), event=True) - assert self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state), \ - f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}" - - def test_deluxe_big_coop_blueprint(self): - assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) - - self.multiworld.state.collect(self.get_item_by_name("Year Two"), event=True) - assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) - - self.multiworld.state.collect(self.get_item_by_name("Progressive Coop"), event=True) - assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) - - self.multiworld.state.collect(self.get_item_by_name("Progressive Coop"), event=True) - assert self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) - - def test_big_shed_blueprint(self): - assert not self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state), \ - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}" - - self.multiworld.state.collect(self.get_item_by_name("Year Two"), event=True) - assert not self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state), \ - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}" - - self.multiworld.state.collect(self.get_item_by_name("Progressive Shed"), event=True) - assert self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state), \ - f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}" +@pytest.mark.parametrize("bundle_item", all_bundle_items_except_money, + ids=[i.item.name for i in all_bundle_items_except_money]) +def test_given_bundle_item_then_is_available_in_logic(bundle_item: BundleItem): + assert bundle_item.item.name in logic.item_rules -class TestArcadeMachinesLogic(SVTestBase): - options = { - options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, - } +@pytest.mark.parametrize("item", logic.item_rules.keys(), ids=logic.item_rules.keys()) +def test_given_item_rule_then_can_be_resolved(item: str): + rule = logic.item_rules[item] - def test_prairie_king(self): - assert not self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) - assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) - assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) - assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) - - boots = self.get_item_by_name("JotPK: Progressive Boots") - gun = self.get_item_by_name("JotPK: Progressive Gun") - ammo = self.get_item_by_name("JotPK: Progressive Ammo") - life = self.get_item_by_name("JotPK: Extra Life") - drop = self.get_item_by_name("JotPK: Increased Drop Rate") - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) - assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) - assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) - assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) - self.remove(boots) - self.remove(gun) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(boots, event=True) - assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) - assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) - assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) - assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) - self.remove(boots) - self.remove(boots) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(life, event=True) - assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) - assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) - assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) - assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) - self.remove(boots) - self.remove(gun) - self.remove(ammo) - self.remove(life) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(life, event=True) - self.multiworld.state.collect(drop, event=True) - assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) - assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) - assert self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) - assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) - self.remove(boots) - self.remove(gun) - self.remove(gun) - self.remove(ammo) - self.remove(ammo) - self.remove(life) - self.remove(drop) - - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(boots, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(gun, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(ammo, event=True) - self.multiworld.state.collect(life, event=True) - self.multiworld.state.collect(drop, event=True) - assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) - assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) - assert self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) - assert self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) - self.remove(boots) - self.remove(boots) - self.remove(gun) - self.remove(gun) - self.remove(gun) - self.remove(gun) - self.remove(ammo) - self.remove(ammo) - self.remove(ammo) - self.remove(life) - self.remove(drop) + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve rule for {item} {rule}" -class TestWeaponsLogic(SVTestBase): - options = { - options.ToolProgression.internal_name: options.ToolProgression.option_progressive, - options.SkillProgression.internal_name: options.SkillProgression.option_progressive, - } +@pytest.mark.parametrize("item", logic.building_rules.keys(), ids=logic.building_rules.keys()) +def test_given_building_rule_then_can_be_resolved(item: str): + rule = logic.building_rules[item] - def test_mine(self): - self.collect(self.get_item_by_name("Adventurer's Guild")) - self.multiworld.state.collect(self.get_item_by_name("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.get_item_by_name("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.get_item_by_name("Progressive Pickaxe"), event=True) - self.multiworld.state.collect(self.get_item_by_name("Progressive Pickaxe"), event=True) - self.collect([self.get_item_by_name("Combat Level")] * 10) - self.collect([self.get_item_by_name("Progressive Mine Elevator")] * 24) - self.multiworld.state.collect(self.get_item_by_name("Bus Repair"), event=True) - self.multiworld.state.collect(self.get_item_by_name("Skull Key"), event=True) + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve rule for {item} {rule}" - self.GiveItemAndCheckReachableMine("Rusty Sword", 1) - self.GiveItemAndCheckReachableMine("Wooden Blade", 1) - self.GiveItemAndCheckReachableMine("Elf Blade", 1) - self.GiveItemAndCheckReachableMine("Silver Saber", 2) - self.GiveItemAndCheckReachableMine("Crystal Dagger", 2) +@pytest.mark.parametrize("item", logic.quest_rules.keys(), ids=logic.quest_rules.keys()) +def test_given_quest_rule_then_can_be_resolved(item: str): + rule = logic.quest_rules[item] - self.GiveItemAndCheckReachableMine("Claymore", 3) - self.GiveItemAndCheckReachableMine("Obsidian Edge", 3) - self.GiveItemAndCheckReachableMine("Bone Sword", 3) + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve rule for {item} {rule}" - self.GiveItemAndCheckReachableMine("The Slammer", 4) - self.GiveItemAndCheckReachableMine("Lava Katana", 4) - self.GiveItemAndCheckReachableMine("Galaxy Sword", 5) - self.GiveItemAndCheckReachableMine("Galaxy Hammer", 5) - self.GiveItemAndCheckReachableMine("Galaxy Dagger", 5) +@pytest.mark.parametrize("location", multi_world.get_locations(1), + ids=[loc.name for loc in multi_world.get_locations(1)]) +def test_given_location_rule_then_can_be_resolved(location: StardewLocation): + rule = location.access_rule - def GiveItemAndCheckReachableMine(self, item_name: str, reachable_level: int): - item = self.multiworld.create_item(item_name, self.player) - self.multiworld.state.collect(item, event=True) - if reachable_level > 0: - assert self.world.logic.can_mine_in_the_mines_floor_1_40()(self.multiworld.state) - else: - assert not self.world.logic.can_mine_in_the_mines_floor_1_40()(self.multiworld.state) - - if reachable_level > 1: - assert self.world.logic.can_mine_in_the_mines_floor_41_80()(self.multiworld.state) - else: - assert not self.world.logic.can_mine_in_the_mines_floor_41_80()(self.multiworld.state) - - if reachable_level > 2: - assert self.world.logic.can_mine_in_the_mines_floor_81_120()(self.multiworld.state) - else: - assert not self.world.logic.can_mine_in_the_mines_floor_81_120()(self.multiworld.state) - - if reachable_level > 3: - assert self.world.logic.can_mine_in_the_skull_cavern()(self.multiworld.state) - else: - assert not self.world.logic.can_mine_in_the_skull_cavern()(self.multiworld.state) - - if reachable_level > 4: - assert self.world.logic.can_mine_perfectly_in_the_skull_cavern()(self.multiworld.state) - else: - assert not self.world.logic.can_mine_perfectly_in_the_skull_cavern()(self.multiworld.state) - - self.remove(item) + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve rule for {location} {rule}" diff --git a/worlds/stardew_valley/test/TestLogicSimplification.py b/worlds/stardew_valley/test/TestLogicSimplification.py index 1a3d5a1dca..c37f5782bf 100644 --- a/worlds/stardew_valley/test/TestLogicSimplification.py +++ b/worlds/stardew_valley/test/TestLogicSimplification.py @@ -1,52 +1,65 @@ -import unittest - -from .. import _True -from ..logic import _Received, _Has, _False, _And, _Or +from .. import True_ +from ..logic import Received, Has, False_, And, Or, StardewLogic +from ..options import default_options, StardewOptions -class TestLogicSimplification(unittest.TestCase): - def test_simplify_true_in_and(self): - rules = { - "Wood": _True(), - "Rock": _True(), - } - summer = _Received("Summer", 0, 1) - assert (_Has("Wood", rules) & summer & _Has("Rock", rules)).simplify() == summer +def test_simplify_true_in_and(): + rules = { + "Wood": True_(), + "Rock": True_(), + } + summer = Received("Summer", 0, 1) + assert (Has("Wood", rules) & summer & Has("Rock", rules)).simplify() == summer - def test_simplify_false_in_or(self): - rules = { - "Wood": _False(), - "Rock": _False(), - } - summer = _Received("Summer", 0, 1) - assert (_Has("Wood", rules) | summer | _Has("Rock", rules)).simplify() == summer - def test_simplify_and_in_and(self): - rule = _And(_And(_Received("Summer", 0, 1), _Received("Fall", 0, 1)), - _And(_Received("Winter", 0, 1), _Received("Spring", 0, 1))) - assert rule.simplify() == _And(_Received("Summer", 0, 1), _Received("Fall", 0, 1), _Received("Winter", 0, 1), - _Received("Spring", 0, 1)) +def test_simplify_false_in_or(): + rules = { + "Wood": False_(), + "Rock": False_(), + } + summer = Received("Summer", 0, 1) + assert (Has("Wood", rules) | summer | Has("Rock", rules)).simplify() == summer - def test_simplify_duplicated_and(self): - rule = _And(_And(_Received("Summer", 0, 1), _Received("Fall", 0, 1)), - _And(_Received("Summer", 0, 1), _Received("Fall", 0, 1))) - assert rule.simplify() == _And(_Received("Summer", 0, 1), _Received("Fall", 0, 1)) - def test_simplify_or_in_or(self): - rule = _Or(_Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1)), - _Or(_Received("Winter", 0, 1), _Received("Spring", 0, 1))) - assert rule.simplify() == _Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1), _Received("Winter", 0, 1), - _Received("Spring", 0, 1)) +def test_simplify_and_in_and(): + rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), + And(Received('Winter', 0, 1), Received('Spring', 0, 1))) + assert rule.simplify() == And(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1), + Received('Spring', 0, 1)) - def test_simplify_duplicated_or(self): - rule = _And(_Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1)), - _Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1))) - assert rule.simplify() == _Or(_Received("Summer", 0, 1), _Received("Fall", 0, 1)) - def test_simplify_true_in_or(self): - rule = _Or(_True(), _Received("Summer", 0, 1)) - assert rule.simplify() == _True() +def test_simplify_duplicated_and(): + rule = And(And(Received('Summer', 0, 1), Received('Fall', 0, 1)), + And(Received('Summer', 0, 1), Received('Fall', 0, 1))) + assert rule.simplify() == And(Received('Summer', 0, 1), Received('Fall', 0, 1)) - def test_simplify_false_in_and(self): - rule = _And(_False(), _Received("Summer", 0, 1)) - assert rule.simplify() == _False() + +def test_simplify_or_in_or(): + rule = Or(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), + Or(Received('Winter', 0, 1), Received('Spring', 0, 1))) + assert rule.simplify() == Or(Received('Summer', 0, 1), Received('Fall', 0, 1), Received('Winter', 0, 1), + Received('Spring', 0, 1)) + + +def test_simplify_duplicated_or(): + rule = And(Or(Received('Summer', 0, 1), Received('Fall', 0, 1)), + Or(Received('Summer', 0, 1), Received('Fall', 0, 1))) + assert rule.simplify() == Or(Received('Summer', 0, 1), Received('Fall', 0, 1)) + + +def test_simplify_true_in_or(): + rule = Or(True_(), Received('Summer', 0, 1)) + assert rule.simplify() == True_() + + +def test_simplify_false_in_and(): + rule = And(False_(), Received('Summer', 0, 1)) + assert rule.simplify() == False_() + + +def test_simplify_coffee(): + logic = StardewLogic(1, StardewOptions(default_options)) + + simplified_coffee = logic.has("Coffee").simplify() + + assert simplified_coffee == True_() diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 063d9c2be9..9907b45dee 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,8 +1,159 @@ -from worlds.stardew_valley.test import SVTestBase +import itertools + +import pytest + +from BaseClasses import ItemClassification, MultiWorld +from Options import SpecialRange +from . import setup_solo_multiworld +from .. import StardewItem, options +from ..options import StardewOption, stardew_valley_option_classes + +SEASONS = {"Spring", "Summer", "Fall", "Winter"} +TOOLS = {"Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can", "Fishing Rod"} -class TestMasterAnglerVanillaTools(SVTestBase): - options = { - "goal": "master_angler", - "tool_progression": "vanilla", - } +def basic_checks(multi_world: MultiWorld): + assert StardewItem("Victory", ItemClassification.progression, None, 1) in multi_world.get_items() + assert_can_win(multi_world) + assert len(multi_world.itempool) == len( + [location for location in multi_world.get_locations() if not location.event]) + + +def assert_can_win(multi_world: MultiWorld): + for item in multi_world.get_items(): + multi_world.state.collect(item) + + assert multi_world.find_item("Victory", 1).can_reach(multi_world.state) + + +@pytest.mark.parametrize("option, value", [(option, value) + for option in stardew_valley_option_classes + if issubclass(option, SpecialRange) + for value in option.special_range_names]) +def test_given_special_range_when_generate_then_basic_checks(option: (SpecialRange, StardewOption), value): + multi_world = setup_solo_multiworld({option.internal_name: option.special_range_names[value]}) + + basic_checks(multi_world) + + +@pytest.mark.parametrize("option, value", [(option, value) + for option in stardew_valley_option_classes + if option.options + for value in option.options]) +def test_given_choice_when_generate_then_basic_checks(option, value): + multi_world = setup_solo_multiworld({option.internal_name: option.options[value]}) + + basic_checks(multi_world) + + +@pytest.mark.parametrize("option_combination", + [{options.Goal.internal_name: options.Goal.option_master_angler, + options.ToolProgression.internal_name: options.ToolProgression.option_vanilla}], + ids=["Master Angler + Vanilla tools"]) +def test_given_option_combination_when_generate_then_basic_checks(option_combination): + multi_world = setup_solo_multiworld(option_combination) + + basic_checks(multi_world) + + +class TestGoal: + @pytest.mark.parametrize("goal,location", [("community_center", "Complete Community Center"), + ("grandpa_evaluation", "Succeed Grandpa's Evaluation"), + ("bottom_of_the_mines", "Reach the Bottom of The Mines"), + ("cryptic_note", "Complete Quest Cryptic Note"), + ("master_angler", "Catch Every Fish")]) + def test_given_goal_when_generate_then_victory_is_in_correct_location(self, goal, location): + multi_world = setup_solo_multiworld({options.Goal.internal_name: options.Goal.options[goal]}) + victory = multi_world.find_item("Victory", 1) + + assert victory.name == location + + +class TestSeasonRandomization: + def test_given_disabled_when_generate_then_all_seasons_are_precollected(self): + multi_world = setup_solo_multiworld({options.SeasonRandomization.internal_name: + options.SeasonRandomization.option_disabled}) + + precollected_items = {item.name for item in multi_world.precollected_items[1]} + assert all([season in precollected_items for season in SEASONS]) + + @pytest.mark.parametrize("value", [value for value in options.SeasonRandomization.options if "randomized" in value]) + def test_given_randomized_when_generate_then_all_seasons_are_in_the_pool_or_precollected(self, value): + multi_world = setup_solo_multiworld({options.SeasonRandomization.internal_name: + options.SeasonRandomization.options[value]}) + + precollected_items = {item.name for item in multi_world.precollected_items[1]} + items = {item.name for item in multi_world.get_items()} | precollected_items + assert all([season in items for season in SEASONS]) + assert len(SEASONS.intersection(precollected_items)) == 1 + + def test_given_progressive_when_generate_then_3_progressive_seasons_are_in_the_pool(self): + multi_world = setup_solo_multiworld({options.SeasonRandomization.internal_name: + options.SeasonRandomization.option_progressive}) + + items = [item.name for item in multi_world.get_items()] + assert items.count("Progressive Season") == 3 + + +class TestBackpackProgression: + def test_given_vanilla_when_generate_then_no_backpack_in_pool(self): + multi_world = setup_solo_multiworld({options.BackpackProgression.internal_name: + options.BackpackProgression.option_vanilla}) + + assert "Progressive Backpack" not in {item.name for item in multi_world.get_items()} + + @pytest.mark.parametrize("value", + [value for value in options.BackpackProgression.options if "progressive" in value]) + def test_given_progressive_when_generate_then_progressive_backpack_is_in_pool_two_times(self, value): + multi_world = setup_solo_multiworld({options.BackpackProgression.internal_name: + options.BackpackProgression.options[value]}) + + items = [item.name for item in multi_world.get_items()] + assert items.count("Progressive Backpack") == 2 + + @pytest.mark.parametrize("value", + [value for value in options.BackpackProgression.options if "progressive" in value]) + def test_given_progressive_when_generate_then_backpack_upgrades_are_locations(self, value): + multi_world = setup_solo_multiworld({options.BackpackProgression.internal_name: + options.BackpackProgression.options[value]}) + + locations = {locations.name for locations in multi_world.get_locations(1)} + assert "Large Pack" in locations + assert "Deluxe Pack" in locations + + def test_given_early_progressive_when_generate_then_progressive_backpack_is_in_early_pool(self): + multi_world = setup_solo_multiworld({options.BackpackProgression.internal_name: + options.BackpackProgression.option_early_progressive}) + + assert "Progressive Backpack" in multi_world.early_items[1] + + +class TestToolProgression: + def test_given_vanilla_when_generate_then_no_tool_in_pool(self): + multi_world = setup_solo_multiworld({options.ToolProgression.internal_name: + options.ToolProgression.option_vanilla}) + + items = {item.name for item in multi_world.get_items()} + for tool in TOOLS: + assert tool not in items + + def test_given_progressive_when_generate_then_progressive_tool_of_each_is_in_pool_four_times(self): + multi_world = setup_solo_multiworld({options.ToolProgression.internal_name: + options.ToolProgression.option_progressive}) + + items = [item.name for item in multi_world.get_items()] + for tool in TOOLS: + assert items.count("Progressive " + tool) == 4 + + def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): + multi_world = setup_solo_multiworld({options.ToolProgression.internal_name: + options.ToolProgression.option_progressive}) + + locations = {locations.name for locations in multi_world.get_locations(1)} + for material, tool in itertools.product(["Copper", "Iron", "Gold", "Iridium"], + ["Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can"]): + assert f"{material} {tool} Upgrade" in locations + assert "Purchase Training Rod" in locations + assert "Bamboo Pole Cutscene" in locations + assert "Purchase Fiberglass Rod" in locations + assert "Purchase Iridium Rod" in locations diff --git a/worlds/stardew_valley/test/TestResourcePack.py b/worlds/stardew_valley/test/TestResourcePack.py deleted file mode 100644 index d25505bbdc..0000000000 --- a/worlds/stardew_valley/test/TestResourcePack.py +++ /dev/null @@ -1,76 +0,0 @@ -import itertools -import math -import unittest - -from BaseClasses import ItemClassification -from .. import ItemData -from ..items import Group, ResourcePackData - - -class TestResourcePack(unittest.TestCase): - - def test_can_transform_resource_pack_data_into_idem_data(self): - resource_pack = ResourcePackData("item name", 1, 1, ItemClassification.filler, frozenset()) - - items = resource_pack.as_item_data(itertools.count()) - - assert ItemData(0, "Resource Pack: 1 item name", ItemClassification.filler, {Group.RESOURCE_PACK}) in items - assert ItemData(1, "Resource Pack: 2 item name", ItemClassification.filler, {Group.RESOURCE_PACK}) in items - assert len(items) == 2 - - def test_when_scale_quantity_then_generate_a_possible_quantity_from_minimal_scaling_to_double(self): - resource_pack = ResourcePackData("item name", default_amount=4, scaling_factor=2) - - quantities = resource_pack.scale_quantity.items() - - assert (50, 2) in quantities - assert (100, 4) in quantities - assert (150, 6) in quantities - assert (200, 8) in quantities - assert len(quantities) == (4 / 2) * 2 - - def test_given_scaling_not_multiple_of_default_amount_when_scale_quantity_then_double_is_added_at_200_scaling(self): - resource_pack = ResourcePackData("item name", default_amount=5, scaling_factor=3) - - quantities = resource_pack.scale_quantity.items() - - assert (40, 2) in quantities - assert (100, 5) in quantities - assert (160, 8) in quantities - assert (200, 10) in quantities - assert len(quantities) == math.ceil(5 / 3) * 2 - - def test_given_large_default_amount_multiple_of_scaling_factor_when_scale_quantity_then_scaled_amount_multiple( - self): - resource_pack = ResourcePackData("item name", default_amount=500, scaling_factor=50) - - quantities = resource_pack.scale_quantity.items() - - assert (10, 50) in quantities - assert (20, 100) in quantities - assert (30, 150) in quantities - assert (40, 200) in quantities - assert (50, 250) in quantities - assert (60, 300) in quantities - assert (70, 350) in quantities - assert (80, 400) in quantities - assert (90, 450) in quantities - assert (100, 500) in quantities - assert (110, 550) in quantities - assert (120, 600) in quantities - assert (130, 650) in quantities - assert (140, 700) in quantities - assert (150, 750) in quantities - assert (160, 800) in quantities - assert (170, 850) in quantities - assert (180, 900) in quantities - assert (190, 950) in quantities - assert (200, 1000) in quantities - assert len(quantities) == math.ceil(500 / 50) * 2 - - def test_given_smallest_multiplier_possible_when_generate_resource_pack_name_then_quantity_is_not_0(self): - resource_pack = ResourcePackData("item name", default_amount=10, scaling_factor=5) - - name = resource_pack.create_name_from_multiplier(1) - - assert name == "Resource Pack: 5 item name" diff --git a/worlds/stardew_valley/test/TestRules.py b/worlds/stardew_valley/test/TestRules.py new file mode 100644 index 0000000000..1963801255 --- /dev/null +++ b/worlds/stardew_valley/test/TestRules.py @@ -0,0 +1,309 @@ +from collections import Counter + +from . import SVTestBase +from .. import options + + +class TestProgressiveToolsLogic(SVTestBase): + options = { + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized, + } + + def setUp(self): + super().setUp() + self.multiworld.state.prog_items = Counter() + + def test_sturgeon(self): + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + summer = self.world.create_item("Summer") + self.multiworld.state.collect(summer, event=True) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + fishing_rod = self.world.create_item("Progressive Fishing Rod") + self.multiworld.state.collect(fishing_rod, event=True) + self.multiworld.state.collect(fishing_rod, event=True) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + fishing_level = self.world.create_item("Fishing Level") + self.multiworld.state.collect(fishing_level, event=True) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + self.multiworld.state.collect(fishing_level, event=True) + self.multiworld.state.collect(fishing_level, event=True) + self.multiworld.state.collect(fishing_level, event=True) + self.multiworld.state.collect(fishing_level, event=True) + self.multiworld.state.collect(fishing_level, event=True) + assert self.world.logic.has("Sturgeon")(self.multiworld.state) + + self.remove(summer) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + winter = self.world.create_item("Winter") + self.multiworld.state.collect(winter, event=True) + assert self.world.logic.has("Sturgeon")(self.multiworld.state) + + self.remove(fishing_rod) + assert not self.world.logic.has("Sturgeon")(self.multiworld.state) + + def test_old_master_cannoli(self): + self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=True) + self.multiworld.state.collect(self.world.create_item("Progressive Axe"), event=True) + self.multiworld.state.collect(self.world.create_item("Summer"), event=True) + + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + + fall = self.world.create_item("Fall") + self.multiworld.state.collect(fall, event=True) + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + + tuesday = self.world.create_item("Traveling Merchant: Tuesday") + self.multiworld.state.collect(tuesday, event=True) + assert self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + + self.remove(fall) + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + self.remove(tuesday) + + green_house = self.world.create_item("Greenhouse") + self.multiworld.state.collect(green_house, event=True) + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + + friday = self.world.create_item("Traveling Merchant: Friday") + self.multiworld.state.collect(friday, event=True) + assert self.multiworld.get_location("Old Master Cannoli", 1).access_rule(self.multiworld.state) + + self.remove(green_house) + assert not self.world.logic.can_reach_location("Old Master Cannoli")(self.multiworld.state) + self.remove(friday) + + +class TestBundlesLogic(SVTestBase): + options = { + } + + def test_vault_2500g_bundle(self): + assert self.world.logic.can_reach_location("2,500g Bundle")(self.multiworld.state) + + +class TestBuildingLogic(SVTestBase): + options = { + options.BuildingProgression.internal_name: options.BuildingProgression.option_progressive_early_shipping_bin + } + + def test_coop_blueprint(self): + assert not self.world.logic.can_reach_location("Coop Blueprint")(self.multiworld.state) + + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + assert self.world.logic.can_reach_location("Coop Blueprint")(self.multiworld.state) + + def test_big_coop_blueprint(self): + assert not self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state), \ + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}" + + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + assert not self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state), \ + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}" + + self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=True) + assert self.world.logic.can_reach_location("Big Coop Blueprint")(self.multiworld.state), \ + f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}" + + def test_deluxe_coop_blueprint(self): + assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) + + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) + + self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=True) + assert not self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) + + self.multiworld.state.collect(self.world.create_item("Progressive Coop"), event=True) + assert self.world.logic.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state) + + def test_big_shed_blueprint(self): + assert not self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state), \ + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}" + + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + self.multiworld.state.collect(self.world.create_item("Month End"), event=True) + assert not self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state), \ + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}" + + self.multiworld.state.collect(self.world.create_item("Progressive Shed"), event=True) + assert self.world.logic.can_reach_location("Big Shed Blueprint")(self.multiworld.state), \ + f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}" + + +class TestArcadeMachinesLogic(SVTestBase): + options = { + options.ArcadeMachineLocations.internal_name: options.ArcadeMachineLocations.option_full_shuffling, + } + + def test_prairie_king(self): + assert not self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) + + boots = self.world.create_item("JotPK: Progressive Boots") + gun = self.world.create_item("JotPK: Progressive Gun") + ammo = self.world.create_item("JotPK: Progressive Ammo") + life = self.world.create_item("JotPK: Extra Life") + drop = self.world.create_item("JotPK: Increased Drop Rate") + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) + self.remove(boots) + self.remove(gun) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(boots, event=True) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) + self.remove(boots) + self.remove(boots) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(life, event=True) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert not self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) + self.remove(boots) + self.remove(gun) + self.remove(ammo) + self.remove(life) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(life, event=True) + self.multiworld.state.collect(drop, event=True) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert not self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) + self.remove(boots) + self.remove(gun) + self.remove(gun) + self.remove(ammo) + self.remove(ammo) + self.remove(life) + self.remove(drop) + + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(boots, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(gun, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(ammo, event=True) + self.multiworld.state.collect(life, event=True) + self.multiworld.state.collect(drop, event=True) + assert self.world.logic.can_reach_region("JotPK World 1")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 2")(self.multiworld.state) + assert self.world.logic.can_reach_region("JotPK World 3")(self.multiworld.state) + assert self.world.logic.can_reach_location("Journey of the Prairie King Victory")(self.multiworld.state) + self.remove(boots) + self.remove(boots) + self.remove(gun) + self.remove(gun) + self.remove(gun) + self.remove(gun) + self.remove(ammo) + self.remove(ammo) + self.remove(ammo) + self.remove(life) + self.remove(drop) + + +class TestWeaponsLogic(SVTestBase): + options = { + options.ToolProgression.internal_name: options.ToolProgression.option_progressive, + options.SkillProgression.internal_name: options.SkillProgression.option_progressive, + } + + def test_mine(self): + self.collect(self.world.create_item("Adventurer's Guild")) + self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) + self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) + self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) + self.multiworld.state.collect(self.world.create_item("Progressive Pickaxe"), event=True) + self.collect([self.world.create_item("Combat Level")] * 10) + self.collect([self.world.create_item("Progressive Mine Elevator")] * 24) + self.multiworld.state.collect(self.world.create_item("Bus Repair"), event=True) + self.multiworld.state.collect(self.world.create_item("Skull Key"), event=True) + + self.GiveItemAndCheckReachableMine("Rusty Sword", 1) + self.GiveItemAndCheckReachableMine("Wooden Blade", 1) + self.GiveItemAndCheckReachableMine("Elf Blade", 1) + + self.GiveItemAndCheckReachableMine("Silver Saber", 2) + self.GiveItemAndCheckReachableMine("Crystal Dagger", 2) + + self.GiveItemAndCheckReachableMine("Claymore", 3) + self.GiveItemAndCheckReachableMine("Obsidian Edge", 3) + self.GiveItemAndCheckReachableMine("Bone Sword", 3) + + self.GiveItemAndCheckReachableMine("The Slammer", 4) + self.GiveItemAndCheckReachableMine("Lava Katana", 4) + + self.GiveItemAndCheckReachableMine("Galaxy Sword", 5) + self.GiveItemAndCheckReachableMine("Galaxy Hammer", 5) + self.GiveItemAndCheckReachableMine("Galaxy Dagger", 5) + + def GiveItemAndCheckReachableMine(self, item_name: str, reachable_level: int): + item = self.multiworld.create_item(item_name, self.player) + self.multiworld.state.collect(item, event=True) + if reachable_level > 0: + assert self.world.logic.can_mine_in_the_mines_floor_1_40()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_in_the_mines_floor_1_40()(self.multiworld.state) + + if reachable_level > 1: + assert self.world.logic.can_mine_in_the_mines_floor_41_80()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_in_the_mines_floor_41_80()(self.multiworld.state) + + if reachable_level > 2: + assert self.world.logic.can_mine_in_the_mines_floor_81_120()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_in_the_mines_floor_81_120()(self.multiworld.state) + + if reachable_level > 3: + assert self.world.logic.can_mine_in_the_skull_cavern()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_in_the_skull_cavern()(self.multiworld.state) + + if reachable_level > 4: + assert self.world.logic.can_mine_perfectly_in_the_skull_cavern()(self.multiworld.state) + else: + assert not self.world.logic.can_mine_perfectly_in_the_skull_cavern()(self.multiworld.state) + + self.remove(item) diff --git a/worlds/stardew_valley/test/__init__.py b/worlds/stardew_valley/test/__init__.py index c9a8c74667..9d2fac02d9 100644 --- a/worlds/stardew_valley/test/__init__.py +++ b/worlds/stardew_valley/test/__init__.py @@ -1,7 +1,11 @@ -from typing import ClassVar +from argparse import Namespace +from typing import Dict, FrozenSet, Tuple, Any, ClassVar +from BaseClasses import MultiWorld from test.TestBase import WorldTestBase +from test.general import gen_steps from .. import StardewValleyWorld +from ...AutoWorld import call_all class SVTestBase(WorldTestBase): @@ -12,9 +16,41 @@ class SVTestBase(WorldTestBase): def world_setup(self, *args, **kwargs): super().world_setup(*args, **kwargs) if self.constructed: - self.world = self.multiworld.worlds[self.player] + self.world = self.multiworld.worlds[self.player] # noqa @property def run_default_tests(self) -> bool: # world_setup is overridden, so it'd always run default tests when importing SVTestBase return type(self) is not SVTestBase and super().run_default_tests + + +pre_generated_worlds = {} + + +# Mostly a copy of test.general.setup_solo_multiworld, I just don't want to change the core. +def setup_solo_multiworld(test_options=None, + _cache: Dict[FrozenSet[Tuple[str, Any]], MultiWorld] = {}) -> MultiWorld: # noqa + if test_options is None: + test_options = {} + + # Yes I reuse the worlds generated between tests, its speeds the execution by a couple seconds + frozen_options = frozenset(test_options.items()) + if frozen_options in _cache: + return _cache[frozen_options] + + multiworld = MultiWorld(1) + multiworld.game[1] = StardewValleyWorld.game + multiworld.player_name = {1: "Tester"} + multiworld.set_seed() + args = Namespace() + for name, option in StardewValleyWorld.option_definitions.items(): + value = option(test_options[name]) if name in test_options else option.from_any(option.default) + setattr(args, name, {1: value}) + multiworld.set_options(args) + multiworld.set_default_common_options() + for step in gen_steps: + call_all(multiworld, step) + + _cache[frozen_options] = multiworld + + return multiworld From 05d398a51dd7b832cd00b5680a91fcf54ab62c5e Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Mon, 10 Apr 2023 21:16:38 -0400 Subject: [PATCH 028/489] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20Missing=20game=20?= =?UTF-8?q?corner=20logic=20(#1693)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Game corner logic fix * Fix for the fix --- worlds/pokemon_rb/rules.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index c69df00366..e9e64e218e 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -63,17 +63,17 @@ def set_rules(world, player): "Celadon Game Corner - West Gambler's Gift (Coin Case)": lambda state: state.has("Coin Case", player), "Celadon Game Corner - Center Gambler's Gift (Coin Case)": lambda state: state.has("Coin Case", player), "Celadon Game Corner - East Gambler's Gift (Coin Case)": lambda state: state.has("Coin Case", player), - "Celadon Game Corner - Hidden Item Northwest By Counter (Coin Case)": lambda state: state.has("Coin Case", player), - "Celadon Game Corner - Hidden Item Southwest Corner (Coin Case)": lambda state: state.has("Coin Case", player), - "Celadon Game Corner - Hidden Item Near Rumor Man (Coin Case)": lambda state: state.has("Coin Case", player), - "Celadon Game Corner - Hidden Item Near Speculating Woman (Coin Case)": lambda state: state.has("Coin Case", player), - "Celadon Game Corner - Hidden Item Near West Gifting Gambler (Coin Case)": lambda state: state.has("Coin Case", player), - "Celadon Game Corner - Hidden Item Near Wonderful Time Woman (Coin Case)": lambda state: state.has("Coin Case", player), - "Celadon Game Corner - Hidden Item Near Failing Gym Information Guy (Coin Case)": lambda state: state.has( "Coin Case", player), - "Celadon Game Corner - Hidden Item Near East Gifting Gambler (Coin Case)": lambda state: state.has("Coin Case", player), - "Celadon Game Corner - Hidden Item Near Hooked Guy (Coin Case)": lambda state: state.has("Coin Case", player), - "Celadon Game Corner - Hidden Item at End of Horizontal Machine Row (Coin Case)": lambda state: state.has("Coin Case", player), - "Celadon Game Corner - Hidden Item in Front of Horizontal Machine Row (Coin Case)": lambda state: state.has("Coin Case", player), + "Celadon Game Corner - Hidden Item Northwest By Counter (Coin Case)": lambda state: state.has("Coin Case", player) and state.pokemon_rb_can_get_hidden_items(player), + "Celadon Game Corner - Hidden Item Southwest Corner (Coin Case)": lambda state: state.has("Coin Case", player) and state.pokemon_rb_can_get_hidden_items(player), + "Celadon Game Corner - Hidden Item Near Rumor Man (Coin Case)": lambda state: state.has("Coin Case", player) and state.pokemon_rb_can_get_hidden_items(player), + "Celadon Game Corner - Hidden Item Near Speculating Woman (Coin Case)": lambda state: state.has("Coin Case", player) and state.pokemon_rb_can_get_hidden_items(player), + "Celadon Game Corner - Hidden Item Near West Gifting Gambler (Coin Case)": lambda state: state.has("Coin Case", player) and state.pokemon_rb_can_get_hidden_items(player), + "Celadon Game Corner - Hidden Item Near Wonderful Time Woman (Coin Case)": lambda state: state.has("Coin Case", player) and state.pokemon_rb_can_get_hidden_items(player), + "Celadon Game Corner - Hidden Item Near Failing Gym Information Guy (Coin Case)": lambda state: state.has( "Coin Case", player) and state.pokemon_rb_can_get_hidden_items(player), + "Celadon Game Corner - Hidden Item Near East Gifting Gambler (Coin Case)": lambda state: state.has("Coin Case", player) and state.pokemon_rb_can_get_hidden_items(player), + "Celadon Game Corner - Hidden Item Near Hooked Guy (Coin Case)": lambda state: state.has("Coin Case", player) and state.pokemon_rb_can_get_hidden_items(player), + "Celadon Game Corner - Hidden Item at End of Horizontal Machine Row (Coin Case)": lambda state: state.has("Coin Case", player) and state.pokemon_rb_can_get_hidden_items(player), + "Celadon Game Corner - Hidden Item in Front of Horizontal Machine Row (Coin Case)": lambda state: state.has("Coin Case", player) and state.pokemon_rb_can_get_hidden_items(player), "Silph Co 11F - Silph Co Liberated": lambda state: state.has("Card Key", player), From 3c3954f5e888e704bc9c370a729f8bb04993728b Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Mon, 10 Apr 2023 20:18:29 -0500 Subject: [PATCH 029/489] Core: Band-aid fixes `start_inventory_from_pool` causing generation failures if any world doesn't utilize it. (#1694) * Core: Band-aid fixes `start_inventory_from_pool` causing generation failures if any world does utilize it. * Core: Slightly better(?) solution * Set default so it doesn't fail on WebHost. --- BaseClasses.py | 6 +++--- Main.py | 26 ++++++++++++++------------ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 11f9160c1f..1368d1625f 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -7,9 +7,9 @@ import random import secrets import typing # this can go away when Python 3.8 support is dropped from argparse import Namespace -from collections import OrderedDict, Counter, deque, ChainMap +from collections import ChainMap, Counter, OrderedDict, deque from enum import IntEnum, IntFlag -from typing import List, Dict, Optional, Set, Iterable, Union, Any, Tuple, TypedDict, Callable, NamedTuple +from typing import Any, Callable, Dict, Iterable, List, NamedTuple, Optional, Set, Tuple, TypedDict, Union import NetUtils import Options @@ -121,7 +121,7 @@ class MultiWorld(): self.early_items = {player: {} for player in self.player_ids} self.local_early_items = {player: {} for player in self.player_ids} self.indirect_connections = {} - self.start_inventory_from_pool = {player: Options.StartInventoryPool({}) for player in range(1, players + 1)} + self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {} self.fix_trock_doors = self.AttributeProxy( lambda player: self.shuffle[player] != 'vanilla' or self.mode[player] == 'inverted') self.fix_skullwoods_exit = self.AttributeProxy( diff --git a/Main.py b/Main.py index 6eda78f5b4..635e7df20f 100644 --- a/Main.py +++ b/Main.py @@ -1,23 +1,24 @@ import collections +import concurrent.futures import logging import os -import time -import zlib -import concurrent.futures import pickle import tempfile +import time import zipfile -from typing import Dict, List, Tuple, Optional, Set +import zlib +from typing import Dict, List, Optional, Set, Tuple -from BaseClasses import Item, MultiWorld, CollectionState, Region, LocationProgressType, Location import worlds -from worlds.alttp.SubClasses import LTTPRegionType -from worlds.alttp.Regions import is_main_entrance -from Fill import distribute_items_restrictive, flood_items, balance_multiworld_progression, distribute_planned -from worlds.alttp.Shops import FillDisabledShopSlots -from Utils import output_path, get_options, __version__, version_tuple -from worlds.generic.Rules import locality_rules, exclusion_rules +from BaseClasses import CollectionState, Item, Location, LocationProgressType, MultiWorld, Region +from Fill import balance_multiworld_progression, distribute_items_restrictive, distribute_planned, flood_items +from Options import StartInventoryPool +from Utils import __version__, get_options, output_path, version_tuple from worlds import AutoWorld +from worlds.alttp.Regions import is_main_entrance +from worlds.alttp.Shops import FillDisabledShopSlots +from worlds.alttp.SubClasses import LTTPRegionType +from worlds.generic.Rules import exclusion_rules, locality_rules ordered_areas = ( 'Light World', 'Dark World', 'Hyrule Castle', 'Agahnims Tower', 'Eastern Palace', 'Desert Palace', @@ -115,7 +116,8 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for item_name, count in world.start_inventory[player].value.items(): for _ in range(count): world.push_precollected(world.create_item(item_name, player)) - for item_name, count in world.start_inventory_from_pool[player].value.items(): + + for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items(): for _ in range(count): world.push_precollected(world.create_item(item_name, player)) From c711d803f8d03646ecb2ef10555a1de904103915 Mon Sep 17 00:00:00 2001 From: toasterparty Date: Mon, 10 Apr 2023 18:43:29 -0700 Subject: [PATCH 030/489] [OC2] Enabled DLC Option (#1688) - New OC2 option `DLCOptionSet`, which is a list of DLCs whose levels should or shouldn't be used for entrance randomizer (and mention in documentation). By default, DLC owners now need to enable DLCs in weighted settings. - Throw user-friendly exceptions when contradictory settings are enabled - Slightly relax generation requirements for sphere 1/2 level permutations - Write entrance randomizer info in spoiler log - Skip adding "Dark Green Ramp" to item pool if Kevin Levels are disabled --- worlds/overcooked2/Items.py | 14 ++++- worlds/overcooked2/Logic.py | 61 +++++++++++++++++----- worlds/overcooked2/Options.py | 12 ++++- worlds/overcooked2/Overcooked2Levels.py | 27 +--------- worlds/overcooked2/__init__.py | 50 ++++++++++++++---- worlds/overcooked2/docs/setup_en.md | 5 +- worlds/overcooked2/test/TestOvercooked2.py | 32 +++++------- 7 files changed, 130 insertions(+), 71 deletions(-) diff --git a/worlds/overcooked2/Items.py b/worlds/overcooked2/Items.py index e9cd741992..64575ef2b6 100644 --- a/worlds/overcooked2/Items.py +++ b/worlds/overcooked2/Items.py @@ -1,6 +1,6 @@ from BaseClasses import Item from typing import NamedTuple, Dict - +from .Overcooked2Levels import Overcooked2Dlc class ItemData(NamedTuple): code: int @@ -77,6 +77,18 @@ item_frequencies = { "Ok Emote": 0, } +dlc_exclusives = { + "Wood" : {Overcooked2Dlc.CAMPFIRE_COOK_OFF}, + "Coal Bucket" : {Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE}, + "Bellows" : {Overcooked2Dlc.SURF_N_TURF}, + "Control Stick Batteries" : {Overcooked2Dlc.STORY, Overcooked2Dlc.SURF_N_TURF, Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE, Overcooked2Dlc.CARNIVAL_OF_CHAOS, Overcooked2Dlc.SEASONAL}, + "Wok Wheels" : {Overcooked2Dlc.SEASONAL}, + "Lightweight Backpack" : {Overcooked2Dlc.CAMPFIRE_COOK_OFF}, + "Faster Condiment/Drink Switch" : {Overcooked2Dlc.SEASONAL, Overcooked2Dlc.CARNIVAL_OF_CHAOS}, + "Calmer Unbread" : {Overcooked2Dlc.SEASONAL, Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE}, + "Coin Purse" : {Overcooked2Dlc.SEASONAL, Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE}, +} + item_name_to_config_name = { "Wood" : "DisableWood" , "Coal Bucket" : "DisableCoal" , diff --git a/worlds/overcooked2/Logic.py b/worlds/overcooked2/Logic.py index b86dc86d3f..d8468cb59a 100644 --- a/worlds/overcooked2/Logic.py +++ b/worlds/overcooked2/Logic.py @@ -1,6 +1,6 @@ from BaseClasses import CollectionState from .Overcooked2Levels import Overcooked2GenericLevel, Overcooked2Dlc, Overcooked2Level, OverworldRegion, overworld_region_by_level -from typing import Dict +from typing import Dict, Set from random import Random def has_requirements_for_level_access(state: CollectionState, level_name: str, previous_level_completed_event_name: str, @@ -132,11 +132,18 @@ def level_shuffle_factory( rng: Random, shuffle_prep_levels: bool, shuffle_horde_levels: bool, + kevin_levels: bool, + enabled_dlc: Set[Overcooked2Dlc], + player_name: str, ) -> Dict[int, Overcooked2GenericLevel]: # return + # Create a list of all valid levels for selection # (excludes tutorial, throne and sometimes horde/prep levels) pool = list() for dlc in Overcooked2Dlc: + if dlc not in enabled_dlc: + continue + for level_id in range(dlc.start_level_id, dlc.end_level_id): if level_id in dlc.excluded_levels(): continue @@ -151,25 +158,55 @@ def level_shuffle_factory( Overcooked2GenericLevel(level_id, dlc) ) + if kevin_levels: + level_count = 43 + else: + level_count = 35 + + if len(pool) < level_count: + if shuffle_prep_levels: + prep_text = "" + else: + prep_text = " NON-PREP" + + raise Exception(f"Invalid OC2 settings({player_name}): OC2 needs at least {level_count}{prep_text} levels in the level pool (currently has {len(pool)})") + # Sort the pool to eliminate risk pool.sort(key=lambda x: int(x.dlc)*1000 + x.level_id) result: Dict[int, Overcooked2GenericLevel] = dict() story = Overcooked2Dlc.STORY + attempts = 0 + while len(result) == 0 or not meets_minimum_sphere_one_requirements(result): + if attempts >= 15: + raise Exception("Failed to create valid Overcooked2 level shuffle permutation in a reasonable amount of attempts") + result.clear() # Shuffle the pool, using the provided RNG rng.shuffle(pool) - # Return the first 44 levels and assign those to each level - for level_id in range(story.start_level_id, story.end_level_id): - if level_id not in story.excluded_levels(): - result[level_id] = pool[level_id-1] - elif level_id == 36: - # Level 6-6 is exempt from shuffling - result[level_id] = Overcooked2GenericLevel(level_id) + # Handle level assignment + + level_id = 0 + placed = 0 + for level in pool: + level_id += 1 + while level_id in story.excluded_levels(): + level_id += 1 + + result[level_id] = level + placed += 1 + + if placed >= level_count: + break + + # Level 6-6 is exempt from shuffling + result[36] = Overcooked2GenericLevel(36) + + attempts += 1 return result @@ -178,12 +215,12 @@ def meets_minimum_sphere_one_requirements( levels: Dict[int, Overcooked2GenericLevel], ) -> bool: - # 1-1, 2-1, and 4-1 are garunteed to be accessible on + # 1-1, 2-1, and 4-1 are guaranteed to be accessible on # the overworld without requiring a ramp or additional stars sphere_one = [1, 7, 19] - # 1-2, 2-2, 3-1 and 5-1 are almost always the next thing unlocked - sphere_twoish = [2, 8, 13, 25] + # 1-2, 2-2, 3-1, 5-1 and 6-1 are almost always the next thing unlocked + sphere_twoish = [2, 8, 13, 25, 31] # Peek the logic for sphere one and see how many are possible # with no items @@ -199,7 +236,7 @@ def meets_minimum_sphere_one_requirements( return sphere_one_count >= 2 and \ sphere_twoish_count >= 2 and \ - sphere_one_count + sphere_twoish_count >= 6 + sphere_one_count + sphere_twoish_count >= 5 def is_completable_no_items(level: Overcooked2GenericLevel) -> bool: diff --git a/worlds/overcooked2/Options.py b/worlds/overcooked2/Options.py index cb4e43d25d..9ddcf5e85f 100644 --- a/worlds/overcooked2/Options.py +++ b/worlds/overcooked2/Options.py @@ -1,7 +1,7 @@ from enum import IntEnum from typing import TypedDict -from Options import Toggle, DefaultOnToggle, Range, Choice - +from Options import DefaultOnToggle, Toggle, Range, Choice, OptionSet +from .Overcooked2Levels import Overcooked2Dlc class LocationBalancingMode(IntEnum): disabled = 0 @@ -87,6 +87,13 @@ class ShuffleLevelOrder(OC2OnToggle): display_name = "Shuffle Level Order" +class DLCOptionSet(OptionSet): + """Which DLCs should be included when 'Shuffle Level Order' is enabled?'""" + display_name = "Enabled DLC" + default = {"Story", "Seasonal"} + valid_keys = [dlc.value for dlc in Overcooked2Dlc] + + class IncludeHordeLevels(OC2OnToggle): """Includes "Horde Defense" levels in the pool of possible kitchens when Shuffle Level Order is enabled. Also adds two horde-specific items into the item pool.""" @@ -170,6 +177,7 @@ overcooked_options = { # randomization options "shuffle_level_order": ShuffleLevelOrder, + "include_dlcs": DLCOptionSet, "include_horde_levels": IncludeHordeLevels, "prep_levels": PrepLevels, "kevin_levels": KevinLevels, diff --git a/worlds/overcooked2/Overcooked2Levels.py b/worlds/overcooked2/Overcooked2Levels.py index 816e1e514a..9f2a225e40 100644 --- a/worlds/overcooked2/Overcooked2Levels.py +++ b/worlds/overcooked2/Overcooked2Levels.py @@ -4,11 +4,11 @@ from typing import List class Overcooked2Dlc(Enum): STORY = "Story" + SEASONAL = "Seasonal" SURF_N_TURF = "Surf 'n' Turf" CAMPFIRE_COOK_OFF = "Campfire Cook Off" NIGHT_OF_THE_HANGRY_HORDE = "Night of the Hangry Horde" CARNIVAL_OF_CHAOS = "Carnival of Chaos" - SEASONAL = "Seasonal" # CHRISTMAS = "Christmas" # CHINESE_NEW_YEAR = "Chinese New Year" # WINTER_WONDERLAND = "Winter Wonderland" @@ -87,31 +87,6 @@ class Overcooked2Dlc(Enum): return [] - def exclusive_items(self) -> List[str]: - """Returns list of items exclusive to this DLC""" - if self == Overcooked2Dlc.SURF_N_TURF: - return ["Bellows"] - if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF: - return ["Wood"] - if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE: - return ["Coal Bucket"] - if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS: - return ["Faster Condiment/Drink Switch"] - if self == Overcooked2Dlc.SEASONAL: - return ["Wok Wheels"] - - return [] - -ITEMS_TO_EXCLUDE_IF_NO_DLC = [ - "Wood", - "Coal Bucket", - "Bellows", - "Coin Purse", - "Wok Wheels", - "Lightweight Backpack", - "Faster Condiment/Drink Switch", - "Calmer Unbread", -] class Overcooked2GameWorld(IntEnum): ONE = 1 diff --git a/worlds/overcooked2/__init__.py b/worlds/overcooked2/__init__.py index d28fc23947..afffa744fa 100644 --- a/worlds/overcooked2/__init__.py +++ b/worlds/overcooked2/__init__.py @@ -1,13 +1,13 @@ from enum import IntEnum -from typing import Callable, Dict, Any, List, Optional +from typing import Any, List, Dict, Set, Callable, Optional, TextIO from BaseClasses import ItemClassification, CollectionState, Region, Entrance, Location, Tutorial, LocationProgressType from worlds.AutoWorld import World, WebWorld -from .Overcooked2Levels import Overcooked2Level, Overcooked2GenericLevel, ITEMS_TO_EXCLUDE_IF_NO_DLC +from .Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, Overcooked2GenericLevel from .Locations import Overcooked2Location, oc2_location_name_to_id, oc2_location_id_to_name from .Options import overcooked_options, OC2Options, OC2OnToggle, LocationBalancingMode, DeathLinkMode -from .Items import item_table, Overcooked2Item, item_name_to_id, item_id_to_name, item_to_unlock_event, item_frequencies +from .Items import item_table, Overcooked2Item, item_name_to_id, item_id_to_name, item_to_unlock_event, item_frequencies, dlc_exclusives from .Logic import has_requirements_for_level_star, has_requirements_for_level_access, level_shuffle_factory, is_item_progression, is_useful @@ -222,17 +222,23 @@ class Overcooked2World(World): # Helper Data + player_name: str level_unlock_counts: Dict[int, int] # level_id, stars to purchase level_mapping: Dict[int, Overcooked2GenericLevel] # level_id, level + enabled_dlc: Set[Overcooked2Dlc] # Autoworld Hooks def generate_early(self): + self.player_name = self.multiworld.player_name[self.player] self.options = self.get_options() # 0.0 to 1.0 where 1.0 is World Record self.star_threshold_scale = self.options["StarThresholdScale"] / 100.0 + # Parse DLCOptionSet back into enums + self.enabled_dlc = {Overcooked2Dlc(x) for x in self.options["DLCOptionSet"]} + # Generate level unlock requirements such that the levels get harder to unlock # the further the game has progressed, and levels progress radially rather than linearly self.level_unlock_counts = level_unlock_requirement_factory(self.options["StarsToWin"]) @@ -244,9 +250,16 @@ class Overcooked2World(World): self.multiworld.random, self.options["PrepLevels"] != PrepLevelMode.excluded, self.options["IncludeHordeLevels"], + self.options["KevinLevels"], + self.enabled_dlc, + self.player_name, ) else: self.level_mapping = None + if Overcooked2Dlc.STORY not in self.enabled_dlc: + raise Exception(f"Invalid OC2 settings({self.player_name}) Need either Level Shuffle disabled or 'Story' DLC enabled") + + self.enabled_dlc = {Overcooked2Dlc.STORY} def set_location_priority(self) -> None: priority_locations = self.get_priority_locations() @@ -351,17 +364,23 @@ class Overcooked2World(World): # not used continue - if not self.options["ShuffleLevelOrder"] and item_name in ITEMS_TO_EXCLUDE_IF_NO_DLC: - # skip DLC items if no DLC - continue + if item_name in dlc_exclusives: + if not any(x in dlc_exclusives[item_name] for x in self.enabled_dlc): + # Item is always useless with these settings + continue if not self.options["IncludeHordeLevels"] and item_name in ["Calmer Unbread", "Coin Purse"]: # skip horde-specific items if no horde levels continue - if not self.options["KevinLevels"] and item_name.startswith("Kevin"): - # skip kevin items if no kevin levels - continue + if not self.options["KevinLevels"]: + if item_name.startswith("Kevin"): + # skip kevin items if no kevin levels + continue + + if item_name == "Dark Green Ramp": + # skip dark green ramp if there's no Kevin-1 to reveal it + continue if is_item_progression(item_name, self.level_mapping, self.options["KevinLevels"]): # progression.append(item_name) @@ -425,7 +444,7 @@ class Overcooked2World(World): # Items get distributed to locations def fill_json_data(self) -> Dict[str, Any]: - mod_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.multiworld.player_name[self.player]}" + mod_name = f"AP-{self.multiworld.seed_name}-P{self.player}-{self.player_name}" # Serialize Level Order story_level_order = dict() @@ -578,6 +597,17 @@ class Overcooked2World(World): def fill_slot_data(self) -> Dict[str, Any]: return self.fill_json_data() + def write_spoiler(self, spoiler_handle: TextIO) -> None: + if not self.options["ShuffleLevelOrder"]: + return + + world: Overcooked2World = self.multiworld.worlds[self.player] + spoiler_handle.write(f"\n\n{self.player_name}'s Level Order:\n\n") + for overworld_id in world.level_mapping: + overworld_name = Overcooked2GenericLevel(overworld_id).shortname.split("Story ")[1] + kitchen_name = world.level_mapping[overworld_id].shortname + spoiler_handle.write(f'{overworld_name} | {kitchen_name}\n') + def level_unlock_requirement_factory(stars_to_win: int) -> Dict[int, int]: level_unlock_counts = dict() diff --git a/worlds/overcooked2/docs/setup_en.md b/worlds/overcooked2/docs/setup_en.md index de7bbb5bc8..f04fe92ab0 100644 --- a/worlds/overcooked2/docs/setup_en.md +++ b/worlds/overcooked2/docs/setup_en.md @@ -51,8 +51,11 @@ To completely remove *OC2-Modding*, navigate to your game's installation folder 1. Visit the [Player Settings](../../../../games/Overcooked!%202/player-settings) page and configure the game-specific settings to taste +*By default, these settings will only use levels from the base game and the "Seasonal" free DLC updates. If you own any of the paid DLC, you may select individual DLC packs to include/exclude on the [Weighted Settings](../../../../weighted-settings) page* + 2. Export your yaml file and use it to generate a new randomized game -- (For instructions on how to generate an Archipelago game, refer to the [Archipelago Web Guide](../../../../tutorial/Archipelago/using_website/en)) + +*For instructions on how to generate an Archipelago game, refer to the [Archipelago Web Guide](../../../../tutorial/Archipelago/using_website/en)* ## Joining a MultiWorld Game diff --git a/worlds/overcooked2/test/TestOvercooked2.py b/worlds/overcooked2/test/TestOvercooked2.py index 4cb12d9d9b..ee0b44a86e 100644 --- a/worlds/overcooked2/test/TestOvercooked2.py +++ b/worlds/overcooked2/test/TestOvercooked2.py @@ -5,7 +5,7 @@ from worlds.AutoWorld import AutoWorldRegister from test.general import setup_solo_multiworld from worlds.overcooked2.Items import * -from worlds.overcooked2.Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, OverworldRegion, overworld_region_by_level, level_id_to_shortname, ITEMS_TO_EXCLUDE_IF_NO_DLC +from worlds.overcooked2.Overcooked2Levels import Overcooked2Dlc, Overcooked2Level, OverworldRegion, overworld_region_by_level, level_id_to_shortname from worlds.overcooked2.Logic import level_logic, overworld_region_logic, level_shuffle_factory from worlds.overcooked2.Locations import oc2_location_name_to_id @@ -67,26 +67,24 @@ class Overcooked2Test(unittest.TestCase): def testOvercooked2ShuffleFactory(self): previous_runs = set() + + # Test uniqueness for seed in range(0, 5): - levels = level_shuffle_factory(Random(seed), True, False) + levels = level_shuffle_factory(Random(seed), True, False, True, {x for x in Overcooked2Dlc}, "test") self.assertEqual(len(levels), 44) - previous_level_id = None - for level_id in levels.keys(): - if previous_level_id is not None: - self.assertEqual(previous_level_id+1, level_id) - previous_level_id = level_id - self.assertNotIn(levels[15], previous_runs) - previous_runs.add(levels[15]) + self.assertNotIn((levels[5], levels[15]), previous_runs) + previous_runs.add((levels[5], levels[15])) - levels = level_shuffle_factory(Random(123), False, True) - self.assertEqual(len(levels), 44) + # Test kevin = false + levels = level_shuffle_factory(Random(123), False, True, False, {x for x in Overcooked2Dlc}, "test") + self.assertEqual(len(levels), 36) def testLevelNameRepresentation(self): shortnames = [level.as_generic_level.shortname for level in Overcooked2Level()] for shortname in shortnames: - self.assertIn(shortname, level_logic.keys()) + self.assertIn(shortname, level_logic) self.assertEqual(len(level_logic), len(level_id_to_shortname)) @@ -142,13 +140,9 @@ class Overcooked2Test(unittest.TestCase): self.assertLessEqual(number_of_items, len(oc2_location_name_to_id), "Too many items (before fillers placed)") - def testExclusiveItems(self): - for dlc in Overcooked2Dlc: - for item in dlc.exclusive_items(): - self.assertIn(item, item_table.keys()) - - for item in ITEMS_TO_EXCLUDE_IF_NO_DLC: - self.assertIn(item, item_table.keys()) + def testDlcExclusives(self): + for item in dlc_exclusives: + self.assertIn(item, item_table) def testLevelCounts(self): for dlc in Overcooked2Dlc: From 8b7ffaf67175d4b7965bf925b58b7c38485795ff Mon Sep 17 00:00:00 2001 From: Nyx-Edelstein <91574183+Nyx-Edelstein@users.noreply.github.com> Date: Mon, 10 Apr 2023 19:31:57 -0700 Subject: [PATCH 031/489] ALTTP: Add "oof" sound customization option (#709) Co-authored-by: Fabian Dill Co-authored-by: Fabian Dill Co-authored-by: Zach Parks --- LttPAdjuster.py | 89 +++++++++++++++++++++++++++++-- worlds/alttp/Rom.py | 57 +++++++++++++++++++- worlds/alttp/__init__.py | 12 ++++- worlds/alttp/docs/oof_sound_en.md | 20 +++++++ 4 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 worlds/alttp/docs/oof_sound_en.md diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 205a76813a..db0f760e22 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -107,6 +107,12 @@ def main(): Alternatively, can be a ALttP Rom patched with a Link sprite that will be extracted. ''') + parser.add_argument('--oof', help='''\ + Path to a sound effect to replace Link's "oof" sound. + Needs to be in a .brr format and have a length of no + more than 2673 bytes, created from a 16-bit signed PCM + .wav at 12khz. https://github.com/boldowa/snesbrr + ''') parser.add_argument('--names', default='', type=str) parser.add_argument('--update_sprites', action='store_true', help='Update Sprite Database, then exit.') args = parser.parse_args() @@ -126,6 +132,13 @@ def main(): if args.sprite is not None and not os.path.isfile(args.sprite) and not Sprite.get_sprite_from_name(args.sprite): input('Could not find link sprite sheet at given location. \nPress Enter to exit.') sys.exit(1) + if args.oof is not None and not os.path.isfile(args.oof): + input('Could not find oof sound effect at given location. \nPress Enter to exit.') + sys.exit(1) + if args.oof is not None and os.path.getsize(args.oof) > 2673: + input('"oof" sound effect cannot exceed 2673 bytes. \nPress Enter to exit.') + sys.exit(1) + args, path = adjust(args=args) if isinstance(args.sprite, Sprite): @@ -165,7 +178,7 @@ def adjust(args): world = getattr(args, "world") apply_rom_settings(rom, args.heartbeep, args.heartcolor, args.quickswap, args.menuspeed, args.music, - args.sprite, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world, + args.sprite, args.oof, palettes_options, reduceflashing=args.reduceflashing or racerom, world=world, deathlink=args.deathlink, allowcollect=args.allowcollect) path = output_path(f'{os.path.basename(args.rom)[:-4]}_adjusted.sfc') rom.write_to_file(path) @@ -227,6 +240,7 @@ def adjustGUI(): guiargs.sprite = rom_vars.sprite if rom_vars.sprite_pool: guiargs.world = AdjusterWorld(rom_vars.sprite_pool) + guiargs.oof = rom_vars.oof try: guiargs, path = adjust(args=guiargs) @@ -265,6 +279,7 @@ def adjustGUI(): else: guiargs.sprite = rom_vars.sprite guiargs.sprite_pool = rom_vars.sprite_pool + guiargs.oof = rom_vars.oof persistent_store("adjuster", GAME_ALTTP, guiargs) messagebox.showinfo(title="Success", message="Settings saved to persistent storage") @@ -481,6 +496,36 @@ class BackgroundTaskProgressNullWindow(BackgroundTask): self.stop() +class AttachTooltip(object): + + def __init__(self, parent, text): + self._parent = parent + self._text = text + self._window = None + parent.bind('', lambda event : self.show()) + parent.bind('', lambda event : self.hide()) + + def show(self): + if self._window or not self._text: + return + self._window = Toplevel(self._parent) + #remove window bar controls + self._window.wm_overrideredirect(1) + #adjust positioning + x, y, *_ = self._parent.bbox("insert") + x = x + self._parent.winfo_rootx() + 20 + y = y + self._parent.winfo_rooty() + 20 + self._window.wm_geometry("+{0}+{1}".format(x,y)) + #show text + label = Label(self._window, text=self._text, justify=LEFT) + label.pack(ipadx=1) + + def hide(self): + if self._window: + self._window.destroy() + self._window = None + + def get_rom_frame(parent=None): adjuster_settings = get_adjuster_settings(GAME_ALTTP) if not adjuster_settings: @@ -522,6 +567,7 @@ def get_rom_options_frame(parent=None): "reduceflashing": True, "deathlink": False, "sprite": None, + "oof": None, "quickswap": True, "menuspeed": 'normal', "heartcolor": 'red', @@ -598,12 +644,50 @@ def get_rom_options_frame(parent=None): spriteEntry.pack(side=LEFT) spriteSelectButton.pack(side=LEFT) + oofDialogFrame = Frame(romOptionsFrame) + oofDialogFrame.grid(row=1, column=1) + baseOofLabel = Label(oofDialogFrame, text='"OOF" Sound:') + + vars.oofNameVar = StringVar() + vars.oof = adjuster_settings.oof + + def set_oof(oof_param): + nonlocal vars + if isinstance(oof_param, str) and os.path.isfile(oof_param) and os.path.getsize(oof_param) <= 2673: + vars.oof = oof_param + vars.oofNameVar.set(oof_param.rsplit('/',1)[-1]) + else: + vars.oof = None + vars.oofNameVar.set('(unchanged)') + + set_oof(adjuster_settings.oof) + oofEntry = Label(oofDialogFrame, textvariable=vars.oofNameVar) + + def OofSelect(): + nonlocal vars + oof_file = filedialog.askopenfilename( + filetypes=[("BRR files", ".brr"), + ("All Files", "*")]) + try: + set_oof(oof_file) + except Exception: + set_oof(None) + + oofSelectButton = Button(oofDialogFrame, text='...', command=OofSelect) + AttachTooltip(oofSelectButton, + text="Select a .brr file no more than 2673 bytes.\n" + \ + "This can be created from a <=0.394s 16-bit signed PCM .wav file at 12khz using snesbrr.") + + baseOofLabel.pack(side=LEFT) + oofEntry.pack(side=LEFT) + oofSelectButton.pack(side=LEFT) + vars.quickSwapVar = IntVar(value=adjuster_settings.quickswap) quickSwapCheckbutton = Checkbutton(romOptionsFrame, text="L/R Quickswapping", variable=vars.quickSwapVar) quickSwapCheckbutton.grid(row=1, column=0, sticky=E) menuspeedFrame = Frame(romOptionsFrame) - menuspeedFrame.grid(row=1, column=1, sticky=E) + menuspeedFrame.grid(row=6, column=1, sticky=E) menuspeedLabel = Label(menuspeedFrame, text='Menu speed') menuspeedLabel.pack(side=LEFT) vars.menuspeedVar = StringVar() @@ -1056,7 +1140,6 @@ class SpriteSelector(): def custom_sprite_dir(self): return user_path("data", "sprites", "custom") - def get_image_for_sprite(sprite, gif_only: bool = False): if not sprite.valid: return None diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index 99826157d8..e1cbb5c0cd 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -189,7 +189,7 @@ def check_enemizer(enemizercli): # some time may have passed since the lock was acquired, as such a quick re-check doesn't hurt if getattr(check_enemizer, "done", None): return - wanted_version = (7, 0, 1) + wanted_version = (7, 1, 0) # version info is saved on the lib, for some reason library_info = os.path.join(os.path.dirname(enemizercli), "EnemizerCLI.Core.deps.json") with open(library_info) as f: @@ -1775,8 +1775,57 @@ def hud_format_text(text): output += b'\x7f\x00' return output[:32] +def apply_oof_sfx(rom, oof: str): + with open(oof, 'rb') as stream: + oof_bytes = bytearray(stream.read()) -def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, palettes_options, + oof_len_bytes = len(oof_bytes).to_bytes(2, byteorder='little') + + # Credit to kan for this method, and Nyx for initial C# implementation + # this is ported from, with both of their permission for use by AP + # Original C# implementation: + # https://github.com/Nyx-Edelstein/The-Unachievable-Ideal-of-Chibi-Elf-Grunting-Noises-When-They-Get-Punched-A-Z3-Rom-Patcher + + # Jump execution from the SPC load routine to new code + rom.write_bytes(0x8CF, [0x5C, 0x00, 0x80, 0x25]) + + # Change the pointer for instrument 9 in SPC memory to point to the new data we'll be inserting: + rom.write_bytes(0x1A006C, [0x88, 0x31, 0x00, 0x00]) + + # Insert a sigil so we can branch on it later + # We will recover the value it overwrites after we're done with insertion + rom.write_bytes(0x1AD38C, [0xBE, 0xBE]) + + # Change the "oof" sound effect to use instrument 9: + rom.write_byte(0x1A9C4E, 0x09) + + # Correct the pitch shift value: + rom.write_byte(0x1A9C51, 0xB6) + + # Modify parameters of instrument 9 + # (I don't actually understand this part, they're just magic values to me) + rom.write_bytes(0x1A9CAE, [0x7F, 0x7F, 0x00, 0x10, 0x1A, 0x00, 0x00, 0x7F, 0x01]) + + # Hook from SPC load routine: + # * Check for the read of the sigil + # * Once we find it, change the SPC load routine's data pointer to read from the location containing the new sample + # * Note: XXXX in the string below is a placeholder for the number of bytes in the .brr sample (little endian) + # * Another sigil "$EBEB" is inserted at the end of the data + # * When the second sigil is read, we know we're done inserting our data so we can change the data pointer back + # * Effect: The new data gets loaded into SPC memory without having to relocate the SPC load routine + # Slight variation from VT-compatible algorithm: We need to change the data pointer to $00 00 35 and load 538E into Y to pick back up where we left off + rom.write_bytes(0x128000, [0xB7, 0x00, 0xC8, 0xC8, 0xC9, 0xBE, 0xBE, 0xF0, 0x09, 0xC9, 0xEB, 0xEB, 0xF0, 0x1B, 0x5C, 0xD3, 0x88, 0x00, 0xA2, oof_len_bytes[0], oof_len_bytes[1], 0xA9, 0x80, 0x25, 0x85, 0x01, 0xA9, 0x3A, 0x80, 0x85, 0x00, 0xA0, 0x00, 0x00, 0xA9, 0x88, 0x31, 0x5C, 0xD8, 0x88, 0x00, 0xA9, 0x80, 0x35, 0x64, 0x00, 0x85, 0x01, 0xA2, 0x00, 0x00, 0xA0, 0x8E, 0x53, 0x5C, 0xD4, 0x88, 0x00]) + + # The new sample data + # (We need to insert the second sigil at the end) + rom.write_bytes(0x12803A, oof_bytes) + rom.write_bytes(0x12803A + len(oof_bytes), [0xEB, 0xEB]) + + #Enemizer patch: prevent Enemizer from overwriting $3188 in SPC memory with an unused sound effect ("WHAT") + rom.write_bytes(0x13000D, [0x00, 0x00, 0x00, 0x08]) + + +def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options, world=None, player=1, allow_random_on_event=False, reduceflashing=False, triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False): local_random = random if not world else world.per_slot_randoms[player] @@ -1918,6 +1967,10 @@ def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, spri apply_random_sprite_on_event(rom, sprite, local_random, allow_random_on_event, world.sprite_pool[player] if world else []) + + if oof is not None: + apply_oof_sfx(rom, oof) + if isinstance(rom, LocalRom): rom.write_crc() diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 8ca82d43d5..bd54cbd23b 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -103,7 +103,16 @@ class ALTTPWeb(WebWorld): ["Berserker"] ) - tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando] + oof_sound = Tutorial( + "'OOF' Sound Replacement", + "A guide to customizing Link's 'oof' sound", + "English", + "oof_sound_en.md", + "oof_sound/en", + ["Nyx Edelstein"] + ) + + tutorials = [setup_en, setup_de, setup_es, setup_fr, msu, msu_es, msu_fr, plando, oof_sound] class ALTTPWorld(World): @@ -485,6 +494,7 @@ class ALTTPWorld(World): world.menuspeed[player].current_key, world.music[player], world.sprite[player], + None, palettes_options, world, player, True, reduceflashing=world.reduceflashing[player] or world.is_race, triforcehud=world.triforcehud[player].current_key, diff --git a/worlds/alttp/docs/oof_sound_en.md b/worlds/alttp/docs/oof_sound_en.md new file mode 100644 index 0000000000..8cbbc7a561 --- /dev/null +++ b/worlds/alttp/docs/oof_sound_en.md @@ -0,0 +1,20 @@ +# "OOF" sound customization guide + +## What does this feature do? + +It replaces the sound effect when Link takes damage. The intended use case for this is custom sprites, but you can use it with any sprite, including the default one. + +Due to technical restrictions resulting from limited available memory, there is a limit to how long the sound can be. Using the current method, this limit is **0.394 seconds**. This means that many ideas won't work, and any intelligible speech or anything other than a grunt or simple noise will be too long. + +Some examples of what is possible: https://www.youtube.com/watch?v=TYs322kHlc0 + +## How do I create my own custom sound? + +1. Obtain a .wav file with the following specifications: 16-bit signed PCM at 12khz, no longer than 0.394 seconds. You can do this by editing an existing sample using a program like Audacity, or by recording your own. Note that samples can be shrinked or truncated to meet the length requirement, at the expense of sound quality. +2. Use the `--encode` function of the snesbrr tool (https://github.com/boldowa/snesbrr) to encode your .wav file in the proper format (.brr). The .brr file **cannot** exceed 2673 bytes. As long as the input file meets the above specifications, the .brr file should be this size or smaller. If your file is too large, go back to step 1 and make the sample shorter. +3. When running the adjuster GUI, simply select the .brr file you wish to use after clicking the `"OOF" Sound` menu option. +4. You can also do the patch via command line: `python .\LttPAdjuster.py --baserom .\baserom.sfc --oof .\oof.brr .\romtobeadjusted.sfc`, replacing the file names with your files. + +## Can I use multiple sounds for composite sprites? + +No, this is not technically feasible. You can only use one sound. From b02b32918149f8829aca852311f0a7fcbb4e7d0e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 11 Apr 2023 08:47:19 +0200 Subject: [PATCH 032/489] DLCQuest: fix data_version --- worlds/dlcquest/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index 9afde0ea26..c30e4a8f45 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -32,7 +32,7 @@ class DLCqworld(World): item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = location_table - data_version = 0 + data_version = 1 option_definitions = DLCQuest_options From 70ff19ac8c5b637794527a69b34364f63d44c16c Mon Sep 17 00:00:00 2001 From: zig-for Date: Tue, 11 Apr 2023 00:18:33 -0700 Subject: [PATCH 033/489] LADX: AP egg title screen (#1683) --- worlds/ladx/LADXR/generator.py | 20 ++--- worlds/ladx/LADXR/patches/aesthetics.py | 12 +++ worlds/ladx/LADXR/patches/endscreen.py | 2 +- worlds/ladx/LADXR/patches/titleScreen.py | 73 ++++++++++++++++-- worlds/ladx/LADXR/patches/title_screen.bdiff4 | Bin 0 -> 990 bytes worlds/ladx/LADXR/rom.py | 9 ++- worlds/ladx/LADXR/romTables.py | 4 +- worlds/ladx/Options.py | 9 ++- worlds/ladx/__init__.py | 17 ++-- worlds/ladx/docs/en_Links Awakening DX.md | 4 +- 10 files changed, 119 insertions(+), 31 deletions(-) create mode 100644 worlds/ladx/LADXR/patches/title_screen.bdiff4 diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index cb9de281b9..08552081cc 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -2,6 +2,7 @@ import binascii import importlib.util import importlib.machinery import os +import pkgutil from .romTables import ROMWithTables from . import assembler @@ -61,7 +62,12 @@ from ..Options import TrendyGame, Palette, MusicChangeCondition # Function to generate a final rom, this patches the rom with all required patches def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, multiworld=None, player_name=None, player_names=[], player_id = 0): - rom = ROMWithTables(args.input_filename) + rom_patches = [] + + if ap_settings["ap_title_screen"]: + rom_patches.append(pkgutil.get_data(__name__, "patches/title_screen.bdiff4")) + + rom = ROMWithTables(args.input_filename, rom_patches) rom.player_names = player_names pymods = [] if args.pymod: @@ -271,6 +277,8 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m patches.core.warpHome(rom) # Needs to be done after setting the start location. patches.titleScreen.setRomInfo(rom, auth, seed_name, settings, player_name, player_id) + if ap_settings["ap_title_screen"]: + patches.titleScreen.setTitleGraphics(rom) patches.endscreen.updateEndScreen(rom) patches.aesthetics.updateSpriteData(rom) if args.doubletrouble: @@ -363,15 +371,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m if x > max: return max return x - def bin_to_rgb(word): - red = word & 0b11111 - word >>= 5 - green = word & 0b11111 - word >>= 5 - blue = word & 0b11111 - return (red, green, blue) - def rgb_to_bin(r, g, b): - return (b << 10) | (g << 5) | r + from patches.aesthetics import rgb_to_bin, bin_to_rgb for address in range(start, end, 2): packed = (rom.banks[bank][address + 1] << 8) | rom.banks[bank][address] diff --git a/worlds/ladx/LADXR/patches/aesthetics.py b/worlds/ladx/LADXR/patches/aesthetics.py index ff8cd5d856..2975c804d4 100644 --- a/worlds/ladx/LADXR/patches/aesthetics.py +++ b/worlds/ladx/LADXR/patches/aesthetics.py @@ -434,3 +434,15 @@ noChange: rom.room_sprite_data_overworld[room_nr] = data else: rom.room_sprite_data_indoor[room_nr - 0x100] = data + + +def bin_to_rgb(word): + red = word & 0b11111 + word >>= 5 + green = word & 0b11111 + word >>= 5 + blue = word & 0b11111 + return (red, green, blue) + +def rgb_to_bin(r, g, b): + return (b << 10) | (g << 5) | r diff --git a/worlds/ladx/LADXR/patches/endscreen.py b/worlds/ladx/LADXR/patches/endscreen.py index 3a8b4c2df7..94a03fc6fc 100644 --- a/worlds/ladx/LADXR/patches/endscreen.py +++ b/worlds/ladx/LADXR/patches/endscreen.py @@ -136,4 +136,4 @@ loadLoop2: addr = 0x1000 data = pkgutil.get_data(__name__, "nyan.bin") rom.banks[0x3F][addr : addr + len(data)] = data - + diff --git a/worlds/ladx/LADXR/patches/titleScreen.py b/worlds/ladx/LADXR/patches/titleScreen.py index b81f1460eb..3a4dade218 100644 --- a/worlds/ladx/LADXR/patches/titleScreen.py +++ b/worlds/ladx/LADXR/patches/titleScreen.py @@ -1,11 +1,9 @@ from ..backgroundEditor import BackgroundEditor -import subprocess -import binascii - - +from .aesthetics import rgb_to_bin, bin_to_rgb, prepatch +import copy +import pkgutil CHAR_MAP = {'z': 0x3E, '-': 0x3F, '.': 0x39, ':': 0x42, '?': 0x3C, '!': 0x3D} - def _encode(s): result = bytearray() for char in s: @@ -82,3 +80,68 @@ def setRomInfo(rom, seed, seed_name, settings, player_name, player_id): ba.tiles[0x9820 + n] = 0x08 | pal be.store(rom) ba.store(rom) + +def setTitleGraphics(rom): + BASE = 0x9800 + ROW_SIZE = 0x20 + + be = BackgroundEditor(rom, 0x11, attributes=True) + for tile in be.tiles: + if be.tiles[tile] == 7: + be.tiles[tile] = 3 + + be.tiles[BASE + 10 * ROW_SIZE + 8] = 7 + be.tiles[BASE + 10 * ROW_SIZE + 10] = 2 + be.tiles[BASE + 10 * ROW_SIZE + 11] = 5 + be.tiles[BASE + 11 * ROW_SIZE + 10] = 6 + be.tiles[BASE + 11 * ROW_SIZE + 11] = 6 + be.tiles[BASE + 12 * ROW_SIZE + 11] = 6 + be.tiles[BASE + 11 * ROW_SIZE + 9] = 1 + be.tiles[BASE + 12 * ROW_SIZE + 9] = 1 + be.tiles[BASE + 12 * ROW_SIZE + 10] = 1 + be.tiles[BASE + 13 * ROW_SIZE + 9] = 1 + be.tiles[BASE + 13 * ROW_SIZE + 10] = 1 + + be.store(rom) + + SKIP_INTRO = True + if SKIP_INTRO: + # Skip intro as it's causing problems + rom.banks[1][0x2F5B : 0x2F5B + 3] = [0xC3, 0x39, 0x6E] + # Disable initial music + rom.banks[1][0x2F03 : 0x2F03 + 5] = [0] * 5 + # Disable music fade on reset + rom.banks[1][0x3436 : 0x3436 + 3] = [0] * 3 + + + # Set egg palette + BASE = 0x3DEE + palettes = [] + BANK = 0x21 + for i in range(8): + palette = [] + for c in range(4): + address = BASE + i * 8 + c * 2 + packed = (rom.banks[BANK][address + 1] << 8) | rom.banks[BANK][address] + r,g,b = bin_to_rgb(packed) + palette.append([r, g, b]) + palettes.append(palette) + + for i in [1, 2, 5, 6, 7]: + palettes[i] = copy.copy(palettes[3]) + + def to_5_bit(r, g, b): + return [r >> 3, g >> 3, b >> 3] + + palettes[1][3] = to_5_bit(0xFF, 0x80, 145) + palettes[2][2] = to_5_bit(119, 198, 155) + palettes[5][3] = to_5_bit(119, 198, 155) + palettes[6][3] = to_5_bit(192, 139, 215) + palettes[7][3] = to_5_bit(229, 196, 139) + + for i in range(8): + for c in range(4): + address = BASE + i * 8 + c * 2 + packed = rgb_to_bin(*palettes[i][c]) + rom.banks[BANK][address] = packed & 0xFF + rom.banks[BANK][address + 1] = packed >> 8 diff --git a/worlds/ladx/LADXR/patches/title_screen.bdiff4 b/worlds/ladx/LADXR/patches/title_screen.bdiff4 new file mode 100644 index 0000000000000000000000000000000000000000..89e9d64482aa27c5ec815aceaafa927f5a01bfac GIT binary patch literal 990 zcmV<410noEQ$$HdMl>)>000000000(0ssI20000001yBG0000&T4*^jL0KkKSv&;q>;mZ>u4$_WxMG3 zyOJrwgn{MYZ^%MgXgM)KSte6iH6agJ00tL-|NrmUY467TQ&t>fG<^{k@qI+9dty&0^Nuy0njDQB30NOwR42>R;4Lwa71|S1KGzLt7G{|}r1}H^O z)IijFn2j_ZlNl3CfHcvekOL-zMw$Zz4Lu+OL7-`nXaMydpcyHtqy~Tu0MHs5X`?^@ z000^q000dd000000NLzh^Qo%Iw`^iVW=y{Bo&~32Rwe2JK?IT)LIE#O5D6s1t_fy@ ze4>y503ZMaM)If_uI{WjYSo=Ae|v}i!4GFtB#4~kPGqFTz<>0^059t6G)!z1Y5;Oe z-#v1{(murkHp&Wwfqq58%Ccy_cTdcvtowJsK>z>%{+}%pkxF1GkQJA1pzLliWL+!$ zD%QJ2f^tU=1g!;*y!IAZm=I&5k(gA{F_ArT1u;dl#RB)eSbBuh-B^rvi&rO70sQIW zjd}+^e@YN&KCknX!Od_|kf9GqBw*0tk{}owGG7pPwB9f5+IyL5|7O^ zuw@}>h*}*P)H?R@W@z26O({fyt&I^Q5ZseOi1kh#i@9{s)J1%e-NqKabaM}odmhsP zukE_X1yuWuSplX(2B$TGNaF z9_}G&Cv_k*gy^6K5feV`)8G<6ei(X&2ga!lTWGovF@UoTQG~fgU*32pr znM~}1!4wXGoFOT}0y5wr*0~w#!aA+A#bbvLocations From f0324e60f86bf12a33c503b9148319d1ff3e5c81 Mon Sep 17 00:00:00 2001 From: Magnemania <89949176+Magnemania@users.noreply.github.com> Date: Tue, 11 Apr 2023 12:50:26 -0400 Subject: [PATCH 034/489] SC2: Removed extra space from location (#1697) --- worlds/sc2wol/Locations.py | 2 +- worlds/sc2wol/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index b476fc5e20..e91068c4f2 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -225,7 +225,7 @@ def get_locations(multiworld: Optional[MultiWorld], player: Optional[int]) -> Tu lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)), LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104, lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)), - LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105, + LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk", SC2WOL_LOC_ID_OFFSET + 2105, lambda state: state._sc2wol_has_mm_upgrade(multiworld, player)), LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200), LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201), diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index 60de200804..822d36fc78 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -34,7 +34,7 @@ class SC2WoLWorld(World): game = "Starcraft 2 Wings of Liberty" web = Starcraft2WoLWebWorld() - data_version = 3 + data_version = 4 item_name_to_id = {name: data.code for name, data in item_table.items()} location_name_to_id = {location.name: location.code for location in get_locations(None, None)} From 054d14baa4c3ae4f592ef949b5d5e35ede66e0ca Mon Sep 17 00:00:00 2001 From: axe-y <58866768+axe-y@users.noreply.github.com> Date: Fri, 14 Apr 2023 01:09:50 -0400 Subject: [PATCH 035/489] Fix DLCQuest Errors Generating on Latest Source, without any DLCQuest YAMLs (#1704) --- worlds/dlcquest/Items.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/dlcquest/Items.py b/worlds/dlcquest/Items.py index a740ecf549..61f4cd30fa 100644 --- a/worlds/dlcquest/Items.py +++ b/worlds/dlcquest/Items.py @@ -1,7 +1,7 @@ import csv import enum import math -from typing import Protocol, Union, Dict, List +from typing import Protocol, Union, Dict, List, Set from BaseClasses import Item, ItemClassification from . import Options, data from dataclasses import dataclass, field @@ -29,7 +29,7 @@ class ItemData: code_without_offset: offset name: str classification: ItemClassification - groups: set[Group] = field(default_factory=frozenset) + groups: Set[Group] = field(default_factory=frozenset) def __post_init__(self): if not isinstance(self.groups, frozenset): From 8ada91939c41d21f7547ed5840fb2c1d08b6658f Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 14 Apr 2023 13:04:20 -0500 Subject: [PATCH 036/489] LTTP: Fix Location Name Groups (#1705) --- worlds/alttp/__init__.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index bd54cbd23b..d1a44df12f 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -147,7 +147,7 @@ class ALTTPWorld(World): "Eastern Palace - Cannonball Chest", "Eastern Palace - Big Key Chest", "Eastern Palace - Map Chest", "Eastern Palace - Boss"}, "Desert Palace": {"Desert Palace - Big Chest", "Desert Palace - Torch", "Desert Palace - Map Chest", - "Desert Palace - Compass Chest", "Desert Palace Big Key Chest", "Desert Palace - Boss"}, + "Desert Palace - Compass Chest", "Desert Palace - Big Key Chest", "Desert Palace - Boss"}, "Tower of Hera": {"Tower of Hera - Basement Cage", "Tower of Hera - Map Chest", "Tower of Hera - Big Key Chest", "Tower of Hera - Compass Chest", "Tower of Hera - Big Chest", "Tower of Hera - Boss"}, "Palace of Darkness": {"Palace of Darkness - Shooter Room", "Palace of Darkness - The Arena - Bridge", @@ -157,10 +157,10 @@ class ALTTPWorld(World): "Palace of Darkness - Dark Basement - Right", "Palace of Darkness - Dark Maze - Top", "Palace of Darkness - Dark Maze - Bottom", "Palace of Darkness - Big Chest", "Palace of Darkness - Harmless Hellway", "Palace of Darkness - Boss"}, - "Swamp Palace": {"Swamp Palace - Entrance", "Swamp Palace - Swamp Palace - Map Chest", - "Swamp Palace - Big Chest", "Swamp Palace - Compass Chest", "Swamp Palace - Big Key Chest", - "Swamp Palace - West Chest", "Swamp Palace - Flooded Room - Left", - "Swamp Palace - Flooded Room - Right", "Swamp Palace - Waterfall Room", "Swamp Palace - Boss"}, + "Swamp Palace": {"Swamp Palace - Entrance", "Swamp Palace - Map Chest", "Swamp Palace - Big Chest", + "Swamp Palace - Compass Chest", "Swamp Palace - Big Key Chest", "Swamp Palace - West Chest", + "Swamp Palace - Flooded Room - Left", "Swamp Palace - Flooded Room - Right", + "Swamp Palace - Waterfall Room", "Swamp Palace - Boss"}, "Thieves' Town": {"Thieves' Town - Big Key Chest", "Thieves' Town - Map Chest", "Thieves' Town - Compass Chest", "Thieves' Town - Ambush Chest", "Thieves' Town - Attic", "Thieves' Town - Big Chest", "Thieves' Town - Blind's Cell", "Thieves' Town - Boss"}, @@ -175,11 +175,12 @@ class ALTTPWorld(World): "Misery Mire - Bridge Chest", "Misery Mire - Spike Chest", "Misery Mire - Compass Chest", "Misery Mire - Big Key Chest", "Misery Mire - Boss"}, "Turtle Rock": {"Turtle Rock - Compass Chest", "Turtle Rock - Roller Room - Left", - "Turtle Rock - Roller Room - Right", "Turtle Room - Chain Chomps", "Turtle Rock - Big Key Chest", + "Turtle Rock - Roller Room - Right", "Turtle Rock - Chain Chomps", "Turtle Rock - Big Key Chest", "Turtle Rock - Big Chest", "Turtle Rock - Crystaroller Room", "Turtle Rock - Eye Bridge - Bottom Left", "Turtle Rock - Eye Bridge - Bottom Right", - "Turtle Rock - Eye Bridge - Top Left", "Turtle Rock - Eye Bridge - Top Right", "Turtle Rock - Boss"}, - "Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganon's Tower - Hope Room - Left", + "Turtle Rock - Eye Bridge - Top Left", "Turtle Rock - Eye Bridge - Top Right", + "Turtle Rock - Boss"}, + "Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganons Tower - Hope Room - Left", "Ganons Tower - Hope Room - Right", "Ganons Tower - Tile Room", "Ganons Tower - Compass Room - Top Left", "Ganons Tower - Compass Room - Top Right", "Ganons Tower - Compass Room - Bottom Left", "Ganons Tower - Compass Room - Bottom Left", @@ -191,9 +192,9 @@ class ALTTPWorld(World): "Ganons Tower - Bob's Chest", "Ganons Tower - Big Chest", "Ganons Tower - Big Key Room - Left", "Ganons Tower - Big Key Room - Right", "Ganons Tower - Big Key Chest", "Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right", - "Ganons Tower - Pre-Moldorm Room", "Ganons Tower - Validation Chest"}, + "Ganons Tower - Pre-Moldorm Chest", "Ganons Tower - Validation Chest"}, "Ganons Tower Climb": {"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right", - "Ganons Tower - Pre-Moldorm Room", "Ganons Tower - Validation Chest"}, + "Ganons Tower - Pre-Moldorm Chest", "Ganons Tower - Validation Chest"}, } hint_blacklist = {"Triforce"} From 469807ba01c343401609649fb385edb847a489bf Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 14 Apr 2023 02:09:44 +0200 Subject: [PATCH 037/489] Core: remove outdated assert on push_item --- BaseClasses.py | 1 - 1 file changed, 1 deletion(-) diff --git a/BaseClasses.py b/BaseClasses.py index 1368d1625f..29b2c3f687 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -445,7 +445,6 @@ class MultiWorld(): self.state.collect(item, True) def push_item(self, location: Location, item: Item, collect: bool = True): - assert location.can_fill(self.state, item, False), f"Cannot place {item} into {location}." location.item = item item.location = location if collect: From f52ca2571fa3d2eede8eb6258fbbdd8896aadead Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 14 Apr 2023 13:11:01 -0500 Subject: [PATCH 038/489] Tests: Add tests for location name groups (#1706) --- test/general/TestLocations.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/general/TestLocations.py b/test/general/TestLocations.py index 8e4e3ab1be..e77e7a6332 100644 --- a/test/general/TestLocations.py +++ b/test/general/TestLocations.py @@ -59,3 +59,13 @@ class TestBase(unittest.TestCase): f"{game_name} modified region count during pre_fill") self.assertGreaterEqual(location_count, len(multiworld.get_locations()), f"{game_name} modified locations count during pre_fill") + + def testLocationGroup(self): + """Test that all location name groups contain valid locations and don't share names.""" + for game_name, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game_name, game_name=game_name): + for group_name, locations in world_type.location_name_groups.items(): + with self.subTest(group_name, group_name=group_name): + for location in locations: + self.assertIn(location, world_type.location_name_to_id) + self.assertNotIn(group_name, world_type.location_name_to_id) From d3baca9251dfbc07f76cacd841595ae4482377ec Mon Sep 17 00:00:00 2001 From: t3hf1gm3nt <59876300+t3hf1gm3nt@users.noreply.github.com> Date: Fri, 14 Apr 2023 23:40:48 -0400 Subject: [PATCH 039/489] [TLOZ] Add FileNotFoundError handling for base rom (#1708) --- worlds/tloz/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/worlds/tloz/__init__.py b/worlds/tloz/__init__.py index 311215c157..356f9e5ff5 100644 --- a/worlds/tloz/__init__.py +++ b/worlds/tloz/__init__.py @@ -77,6 +77,12 @@ class TLoZWorld(World): self.levels = None self.filler_items = None + @classmethod + def stage_assert_generate(cls, multiworld: MultiWorld): + rom_file = get_base_rom_path() + if not os.path.exists(rom_file): + raise FileNotFoundError(rom_file) + def create_item(self, name: str): return TLoZItem(name, item_table[name].classification, self.item_name_to_id[name], self.player) From 3fdf07677cc956d62785d0ae30f8148e583dd9dd Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Fri, 14 Apr 2023 23:42:02 -0400 Subject: [PATCH 040/489] Stardew Valley: Removed Pytest Requirement from everything (#1696) --- worlds/stardew_valley/logic.py | 29 ++-- worlds/stardew_valley/requirements.txt | 2 +- worlds/stardew_valley/test/TestLogic.py | 89 ++++++++---- worlds/stardew_valley/test/TestOptions.py | 157 +++++++++++----------- 4 files changed, 158 insertions(+), 119 deletions(-) diff --git a/worlds/stardew_valley/logic.py b/worlds/stardew_valley/logic.py index c4d157b827..41f8357249 100644 --- a/worlds/stardew_valley/logic.py +++ b/worlds/stardew_valley/logic.py @@ -103,7 +103,7 @@ class StardewLogic: item_rules: Dict[str, StardewRule] = field(default_factory=dict) tree_fruit_rules: Dict[str, StardewRule] = field(default_factory=dict) seed_rules: Dict[str, StardewRule] = field(default_factory=dict) - crops_rules: Dict[str, StardewRule] = field(default_factory=dict) + crop_rules: Dict[str, StardewRule] = field(default_factory=dict) fish_rules: Dict[str, StardewRule] = field(default_factory=dict) museum_rules: Dict[str, StardewRule] = field(default_factory=dict) building_rules: Dict[str, StardewRule] = field(default_factory=dict) @@ -127,8 +127,8 @@ class StardewLogic: }) self.seed_rules.update({seed.name: self.can_buy_seed(seed) for seed in all_purchasable_seeds}) - self.crops_rules.update({crop.name: self.can_grow_crop(crop) for crop in all_crops}) - self.crops_rules.update({ + self.crop_rules.update({crop.name: self.can_grow_crop(crop) for crop in all_crops}) + self.crop_rules.update({ "Coffee Bean": (self.has_season("Spring") | self.has_season("Summer")) & self.has_traveling_merchant(), }) @@ -422,7 +422,7 @@ class StardewLogic: self.item_rules.update(self.museum_rules) self.item_rules.update(self.tree_fruit_rules) self.item_rules.update(self.seed_rules) - self.item_rules.update(self.crops_rules) + self.item_rules.update(self.crop_rules) self.building_rules.update({ "Barn": self.can_spend_money(6000) & self.has(["Wood", "Stone"]), @@ -795,6 +795,10 @@ class StardewLogic: if self.options[options.Friendsanity] == options.Friendsanity.option_none: return self.can_earn_relationship(npc, hearts) if npc not in all_villagers_by_name: + if npc == "Pet": + if self.options[options.Friendsanity] == options.Friendsanity.option_bachelors: + return self.can_befriend_pet(hearts) + return self.received(f"Pet: 1 <3", hearts) if npc == "Any" or npc == "Bachelor": possible_friends = [] for name in all_villagers_by_name: @@ -806,6 +810,11 @@ class StardewLogic: for name in all_villagers_by_name: mandatory_friends.append(self.has_relationship(name, hearts)) return And(mandatory_friends) + if npc.isnumeric(): + possible_friends = [] + for name in all_villagers_by_name: + possible_friends.append(self.has_relationship(name, hearts)) + return Count(int(npc), possible_friends) return self.can_earn_relationship(npc, hearts) villager = all_villagers_by_name[npc] @@ -888,9 +897,9 @@ class StardewLogic: # Catching every fish not expected # Shipping every item not expected self.can_get_married() & self.has_house(2), - self.has_lived_months(3), # 5 Friends (TODO) - self.has_lived_months(4), # 10 friends (TODO) - self.can_befriend_pet(5), # Max Pet + self.has_relationship("5", 8), # 5 Friends + self.has_relationship("10", 8), # 10 friends + self.has_relationship("Pet", 5), # Max Pet self.can_complete_community_center(), # Community Center Completion self.can_complete_community_center(), # CC Ceremony first point self.can_complete_community_center(), # CC Ceremony second point @@ -940,7 +949,11 @@ class StardewLogic: return region_rule & geodes_rule # & monster_rule & extra_rule def can_complete_museum(self) -> StardewRule: - rules = [self.can_mine_perfectly_in_the_skull_cavern(), self.received("Traveling Merchant Metal Detector", 4)] + rules = [self.can_mine_perfectly_in_the_skull_cavern()] + + if self.options[options.Museumsanity] != options.Museumsanity.option_none: + rules.append(self.received("Traveling Merchant Metal Detector", 4)) + for donation in all_museum_items: rules.append(self.can_find_museum_item(donation)) return And(rules) diff --git a/worlds/stardew_valley/requirements.txt b/worlds/stardew_valley/requirements.txt index b0922176e4..a7141f6aa8 100644 --- a/worlds/stardew_valley/requirements.txt +++ b/worlds/stardew_valley/requirements.txt @@ -1 +1 @@ -importlib_resources; python_version <= '3.8' +importlib_resources; python_version <= '3.8' \ No newline at end of file diff --git a/worlds/stardew_valley/test/TestLogic.py b/worlds/stardew_valley/test/TestLogic.py index beb610230b..e1c5d2150e 100644 --- a/worlds/stardew_valley/test/TestLogic.py +++ b/worlds/stardew_valley/test/TestLogic.py @@ -1,4 +1,4 @@ -import pytest +import unittest from test.general import setup_solo_multiworld from .. import StardewValleyWorld, StardewLocation @@ -18,40 +18,71 @@ def collect_all(mw): collect_all(multi_world) -@pytest.mark.parametrize("bundle_item", all_bundle_items_except_money, - ids=[i.item.name for i in all_bundle_items_except_money]) -def test_given_bundle_item_then_is_available_in_logic(bundle_item: BundleItem): - assert bundle_item.item.name in logic.item_rules +class TestLogic(unittest.TestCase): + def test_given_bundle_item_then_is_available_in_logic(self): + for bundle_item in all_bundle_items_except_money: + with self.subTest(msg=bundle_item.item.name): + assert bundle_item.item.name in logic.item_rules + def test_given_item_rule_then_can_be_resolved(self): + for item in logic.item_rules.keys(): + with self.subTest(msg=item): + rule = logic.item_rules[item] + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve item rule for {item} {rule}" -@pytest.mark.parametrize("item", logic.item_rules.keys(), ids=logic.item_rules.keys()) -def test_given_item_rule_then_can_be_resolved(item: str): - rule = logic.item_rules[item] + def test_given_building_rule_then_can_be_resolved(self): + for building in logic.building_rules.keys(): + with self.subTest(msg=building): + rule = logic.building_rules[building] + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve building rule for {building} {rule}" - assert MISSING_ITEM not in repr(rule) - assert rule == False_() or rule(multi_world.state), f"Could not resolve rule for {item} {rule}" + def test_given_quest_rule_then_can_be_resolved(self): + for quest in logic.quest_rules.keys(): + with self.subTest(msg=quest): + rule = logic.quest_rules[quest] + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve quest rule for {quest} {rule}" + def test_given_tree_fruit_rule_then_can_be_resolved(self): + for tree_fruit in logic.tree_fruit_rules.keys(): + with self.subTest(msg=tree_fruit): + rule = logic.tree_fruit_rules[tree_fruit] + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve tree fruit rule for {tree_fruit} {rule}" -@pytest.mark.parametrize("item", logic.building_rules.keys(), ids=logic.building_rules.keys()) -def test_given_building_rule_then_can_be_resolved(item: str): - rule = logic.building_rules[item] + def test_given_seed_rule_then_can_be_resolved(self): + for seed in logic.seed_rules.keys(): + with self.subTest(msg=seed): + rule = logic.seed_rules[seed] + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve seed rule for {seed} {rule}" - assert MISSING_ITEM not in repr(rule) - assert rule == False_() or rule(multi_world.state), f"Could not resolve rule for {item} {rule}" + def test_given_crop_rule_then_can_be_resolved(self): + for crop in logic.crop_rules.keys(): + with self.subTest(msg=crop): + rule = logic.crop_rules[crop] + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve crop rule for {crop} {rule}" + def test_given_fish_rule_then_can_be_resolved(self): + for fish in logic.fish_rules.keys(): + with self.subTest(msg=fish): + rule = logic.fish_rules[fish] + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve fish rule for {fish} {rule}" -@pytest.mark.parametrize("item", logic.quest_rules.keys(), ids=logic.quest_rules.keys()) -def test_given_quest_rule_then_can_be_resolved(item: str): - rule = logic.quest_rules[item] + def test_given_museum_rule_then_can_be_resolved(self): + for donation in logic.museum_rules.keys(): + with self.subTest(msg=donation): + rule = logic.museum_rules[donation] + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve museum rule for {donation} {rule}" - assert MISSING_ITEM not in repr(rule) - assert rule == False_() or rule(multi_world.state), f"Could not resolve rule for {item} {rule}" - - -@pytest.mark.parametrize("location", multi_world.get_locations(1), - ids=[loc.name for loc in multi_world.get_locations(1)]) -def test_given_location_rule_then_can_be_resolved(location: StardewLocation): - rule = location.access_rule - - assert MISSING_ITEM not in repr(rule) - assert rule == False_() or rule(multi_world.state), f"Could not resolve rule for {location} {rule}" + def test_given_location_rule_then_can_be_resolved(self): + for location in multi_world.get_locations(1): + with self.subTest(msg=location.name): + rule = location.access_rule + assert MISSING_ITEM not in repr(rule) + assert rule == False_() or rule(multi_world.state), f"Could not resolve location rule for {location} {rule}" diff --git a/worlds/stardew_valley/test/TestOptions.py b/worlds/stardew_valley/test/TestOptions.py index 9907b45dee..2d946e2ff9 100644 --- a/worlds/stardew_valley/test/TestOptions.py +++ b/worlds/stardew_valley/test/TestOptions.py @@ -1,10 +1,9 @@ import itertools - -import pytest +import unittest from BaseClasses import ItemClassification, MultiWorld from Options import SpecialRange -from . import setup_solo_multiworld +from . import setup_solo_multiworld, SVTestBase from .. import StardewItem, options from ..options import StardewOption, stardew_valley_option_classes @@ -12,142 +11,138 @@ SEASONS = {"Spring", "Summer", "Fall", "Winter"} TOOLS = {"Hoe", "Pickaxe", "Axe", "Watering Can", "Trash Can", "Fishing Rod"} -def basic_checks(multi_world: MultiWorld): - assert StardewItem("Victory", ItemClassification.progression, None, 1) in multi_world.get_items() - assert_can_win(multi_world) - assert len(multi_world.itempool) == len( - [location for location in multi_world.get_locations() if not location.event]) +def assert_can_win(multiworld: MultiWorld): + for item in multiworld.get_items(): + multiworld.state.collect(item) + + assert multiworld.find_item("Victory", 1).can_reach(multiworld.state) -def assert_can_win(multi_world: MultiWorld): - for item in multi_world.get_items(): - multi_world.state.collect(item) - - assert multi_world.find_item("Victory", 1).can_reach(multi_world.state) +def basic_checks(multiworld: MultiWorld): + assert StardewItem("Victory", ItemClassification.progression, None, 1) in multiworld.get_items() + assert_can_win(multiworld) + assert len(multiworld.itempool) == len( + [location for location in multiworld.get_locations() if not location.event]) -@pytest.mark.parametrize("option, value", [(option, value) - for option in stardew_valley_option_classes - if issubclass(option, SpecialRange) - for value in option.special_range_names]) -def test_given_special_range_when_generate_then_basic_checks(option: (SpecialRange, StardewOption), value): - multi_world = setup_solo_multiworld({option.internal_name: option.special_range_names[value]}) +class TestGenerateDynamicOptions(SVTestBase): + def test_given_special_range_when_generate_then_basic_checks(self): + for option in stardew_valley_option_classes: + if not issubclass(option, SpecialRange): + continue + with self.subTest(msg=option.internal_name): + for value in option.special_range_names: + multiworld = setup_solo_multiworld({option.internal_name: option.special_range_names[value]}) + basic_checks(multiworld) - basic_checks(multi_world) + def test_given_choice_when_generate_then_basic_checks(self): + for option in stardew_valley_option_classes: + if not option.options: + continue + with self.subTest(msg=option.internal_name): + for value in option.options: + multiworld = setup_solo_multiworld({option.internal_name: option.options[value]}) + basic_checks(multiworld) + + def test_given_option_combination_when_generate_then_basic_checks(self): + option_combinations = [{options.Goal.internal_name: options.Goal.option_master_angler, + options.ToolProgression.internal_name: options.ToolProgression.option_vanilla}] + ids = ["Master Angler + Vanilla tools"] + + for i in range(0, len(option_combinations)): + option_combination = option_combinations[i] + id = ids[i] + with self.subTest(msg=f"{id}"): + multi_world = setup_solo_multiworld(option_combination) + basic_checks(multi_world) -@pytest.mark.parametrize("option, value", [(option, value) - for option in stardew_valley_option_classes - if option.options - for value in option.options]) -def test_given_choice_when_generate_then_basic_checks(option, value): - multi_world = setup_solo_multiworld({option.internal_name: option.options[value]}) - - basic_checks(multi_world) - - -@pytest.mark.parametrize("option_combination", - [{options.Goal.internal_name: options.Goal.option_master_angler, - options.ToolProgression.internal_name: options.ToolProgression.option_vanilla}], - ids=["Master Angler + Vanilla tools"]) -def test_given_option_combination_when_generate_then_basic_checks(option_combination): - multi_world = setup_solo_multiworld(option_combination) - - basic_checks(multi_world) - - -class TestGoal: - @pytest.mark.parametrize("goal,location", [("community_center", "Complete Community Center"), +class TestGoal(SVTestBase): + def test_given_goal_when_generate_then_victory_is_in_correct_location(self): + for goal, location in [("community_center", "Complete Community Center"), ("grandpa_evaluation", "Succeed Grandpa's Evaluation"), ("bottom_of_the_mines", "Reach the Bottom of The Mines"), ("cryptic_note", "Complete Quest Cryptic Note"), - ("master_angler", "Catch Every Fish")]) - def test_given_goal_when_generate_then_victory_is_in_correct_location(self, goal, location): - multi_world = setup_solo_multiworld({options.Goal.internal_name: options.Goal.options[goal]}) - victory = multi_world.find_item("Victory", 1) - - assert victory.name == location + ("master_angler", "Catch Every Fish")]: + with self.subTest(msg=f"Goal: {goal}, Location: {location}"): + world_options = {options.Goal.internal_name: options.Goal.options[goal]} + multi_world = setup_solo_multiworld(world_options) + victory = multi_world.find_item("Victory", 1) + assert victory.name == location -class TestSeasonRandomization: +class TestSeasonRandomization(SVTestBase): def test_given_disabled_when_generate_then_all_seasons_are_precollected(self): - multi_world = setup_solo_multiworld({options.SeasonRandomization.internal_name: - options.SeasonRandomization.option_disabled}) + world_options = {options.SeasonRandomization.internal_name: options.SeasonRandomization.option_disabled} + multi_world = setup_solo_multiworld(world_options) precollected_items = {item.name for item in multi_world.precollected_items[1]} assert all([season in precollected_items for season in SEASONS]) - @pytest.mark.parametrize("value", [value for value in options.SeasonRandomization.options if "randomized" in value]) - def test_given_randomized_when_generate_then_all_seasons_are_in_the_pool_or_precollected(self, value): - multi_world = setup_solo_multiworld({options.SeasonRandomization.internal_name: - options.SeasonRandomization.options[value]}) - + def test_given_randomized_when_generate_then_all_seasons_are_in_the_pool_or_precollected(self): + world_options = {options.SeasonRandomization.internal_name: options.SeasonRandomization.option_randomized} + multi_world = setup_solo_multiworld(world_options) precollected_items = {item.name for item in multi_world.precollected_items[1]} items = {item.name for item in multi_world.get_items()} | precollected_items assert all([season in items for season in SEASONS]) assert len(SEASONS.intersection(precollected_items)) == 1 def test_given_progressive_when_generate_then_3_progressive_seasons_are_in_the_pool(self): - multi_world = setup_solo_multiworld({options.SeasonRandomization.internal_name: - options.SeasonRandomization.option_progressive}) + world_options = {options.SeasonRandomization.internal_name: options.SeasonRandomization.option_progressive} + multi_world = setup_solo_multiworld(world_options) items = [item.name for item in multi_world.get_items()] assert items.count("Progressive Season") == 3 -class TestBackpackProgression: +class TestBackpackProgression(SVTestBase): def test_given_vanilla_when_generate_then_no_backpack_in_pool(self): - multi_world = setup_solo_multiworld({options.BackpackProgression.internal_name: - options.BackpackProgression.option_vanilla}) + world_options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_vanilla} + multi_world = setup_solo_multiworld(world_options) assert "Progressive Backpack" not in {item.name for item in multi_world.get_items()} - @pytest.mark.parametrize("value", - [value for value in options.BackpackProgression.options if "progressive" in value]) - def test_given_progressive_when_generate_then_progressive_backpack_is_in_pool_two_times(self, value): - multi_world = setup_solo_multiworld({options.BackpackProgression.internal_name: - options.BackpackProgression.options[value]}) - + def test_given_progressive_when_generate_then_progressive_backpack_is_in_pool_two_times(self): + world_options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive} + multi_world = setup_solo_multiworld(world_options) items = [item.name for item in multi_world.get_items()] assert items.count("Progressive Backpack") == 2 - @pytest.mark.parametrize("value", - [value for value in options.BackpackProgression.options if "progressive" in value]) - def test_given_progressive_when_generate_then_backpack_upgrades_are_locations(self, value): - multi_world = setup_solo_multiworld({options.BackpackProgression.internal_name: - options.BackpackProgression.options[value]}) + def test_given_progressive_when_generate_then_backpack_upgrades_are_locations(self): + world_options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive} + multi_world = setup_solo_multiworld(world_options) locations = {locations.name for locations in multi_world.get_locations(1)} assert "Large Pack" in locations assert "Deluxe Pack" in locations def test_given_early_progressive_when_generate_then_progressive_backpack_is_in_early_pool(self): - multi_world = setup_solo_multiworld({options.BackpackProgression.internal_name: - options.BackpackProgression.option_early_progressive}) + world_options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_early_progressive} + multi_world = setup_solo_multiworld(world_options) assert "Progressive Backpack" in multi_world.early_items[1] -class TestToolProgression: +class TestToolProgression(SVTestBase): def test_given_vanilla_when_generate_then_no_tool_in_pool(self): - multi_world = setup_solo_multiworld({options.ToolProgression.internal_name: - options.ToolProgression.option_vanilla}) + world_options = {options.ToolProgression.internal_name: options.ToolProgression.option_vanilla} + multi_world = setup_solo_multiworld(world_options) items = {item.name for item in multi_world.get_items()} for tool in TOOLS: assert tool not in items def test_given_progressive_when_generate_then_progressive_tool_of_each_is_in_pool_four_times(self): - multi_world = setup_solo_multiworld({options.ToolProgression.internal_name: - options.ToolProgression.option_progressive}) + world_options = {options.ToolProgression.internal_name:options.ToolProgression.option_progressive} + multi_world = setup_solo_multiworld(world_options) items = [item.name for item in multi_world.get_items()] for tool in TOOLS: assert items.count("Progressive " + tool) == 4 def test_given_progressive_when_generate_then_tool_upgrades_are_locations(self): - multi_world = setup_solo_multiworld({options.ToolProgression.internal_name: - options.ToolProgression.option_progressive}) + world_options = {options.ToolProgression.internal_name: options.ToolProgression.option_progressive} + multi_world = setup_solo_multiworld(world_options) locations = {locations.name for locations in multi_world.get_locations(1)} for material, tool in itertools.product(["Copper", "Iron", "Gold", "Iridium"], From e716b50f8ce96a8508ba66d95a75efe2490ef284 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Sat, 15 Apr 2023 00:15:04 -0400 Subject: [PATCH 041/489] KH2: Fixed Non Deterministic Generation (#1707) --- worlds/kh2/Locations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/worlds/kh2/Locations.py b/worlds/kh2/Locations.py index 9b5cc55252..9046dfc67b 100644 --- a/worlds/kh2/Locations.py +++ b/worlds/kh2/Locations.py @@ -1157,7 +1157,7 @@ exclusion_table = { LocationName.Crit_6, LocationName.Crit_7, }, - "Hitlist": { + "Hitlist": [ LocationName.XemnasDataPowerBoost, LocationName.AxelDataMagicBoost, LocationName.RoxasDataMagicBoost, @@ -1182,7 +1182,7 @@ exclusion_table = { LocationName.TransporttoRemembrance, LocationName.OrichalcumPlusGoddessofFateCup, LocationName.HadesCupTrophyParadoxCups, - }, + ], "Cups": { LocationName.ProtectBeltPainandPanicCup, LocationName.SerenityGemPainandPanicCup, From 02ef6cee471815e19bd73d2992bfa8d6654dbb32 Mon Sep 17 00:00:00 2001 From: zig-for Date: Fri, 14 Apr 2023 21:47:36 -0700 Subject: [PATCH 042/489] LADX: Support magpie tracker's sendfull button (#1701) --- worlds/ladx/Tracker.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/worlds/ladx/Tracker.py b/worlds/ladx/Tracker.py index b85086298a..29dce401b8 100644 --- a/worlds/ladx/Tracker.py +++ b/worlds/ladx/Tracker.py @@ -148,7 +148,7 @@ class MagpieBridge: checks = None item_tracker = None ws = None - + features = [] async def handler(self, websocket): self.ws = websocket while True: @@ -156,9 +156,12 @@ class MagpieBridge: if message["type"] == "handshake": logger.info( f"Connected, supported features: {message['features']}") - if "items" in message["features"]: + self.features = message["features"] + + if message["type"] in ("handshake", "sendFull"): + if "items" in self.features: await self.send_all_inventory() - if "checks" in message["features"]: + if "checks" in self.features: await self.send_all_checks() # Translate renamed IDs back to LADXR IDs @staticmethod From d8f79b4a42dfe0d66f9e58ea1f37ee998b4888d7 Mon Sep 17 00:00:00 2001 From: zig-for Date: Fri, 14 Apr 2023 21:48:05 -0700 Subject: [PATCH 043/489] LADX: Fix useless item being marked as progression (#1700) --- worlds/ladx/Items.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ladx/Items.py b/worlds/ladx/Items.py index ff5db6950a..b8be03d67d 100644 --- a/worlds/ladx/Items.py +++ b/worlds/ladx/Items.py @@ -202,7 +202,7 @@ links_awakening_items = [ ItemData(ItemName.RUPEES_200, "RUPEES_200", ItemClassification.progression_skip_balancing), ItemData(ItemName.RUPEES_500, "RUPEES_500", ItemClassification.progression_skip_balancing), ItemData(ItemName.SEASHELL, "SEASHELL", ItemClassification.progression_skip_balancing), - ItemData(ItemName.MESSAGE, "MESSAGE", ItemClassification.progression), + ItemData(ItemName.MESSAGE, "MESSAGE", ItemClassification.filler), ItemData(ItemName.GEL, "GEL", ItemClassification.trap), ItemData(ItemName.BOOMERANG, "BOOMERANG", ItemClassification.progression), ItemData(ItemName.HEART_PIECE, "HEART_PIECE", ItemClassification.filler), From 808203a50f6fd9383c89e29fdee223fb0248d5ec Mon Sep 17 00:00:00 2001 From: zig-for Date: Fri, 14 Apr 2023 22:47:45 -0700 Subject: [PATCH 044/489] LADX: fix egg sprite in non-patched gfx mode (#1699) --- worlds/ladx/LADXR/generator.py | 5 ----- worlds/ladx/__init__.py | 17 ++++++++++++++--- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index 08552081cc..90670c0258 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -64,9 +64,6 @@ from ..Options import TrendyGame, Palette, MusicChangeCondition def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, multiworld=None, player_name=None, player_names=[], player_id = 0): rom_patches = [] - if ap_settings["ap_title_screen"]: - rom_patches.append(pkgutil.get_data(__name__, "patches/title_screen.bdiff4")) - rom = ROMWithTables(args.input_filename, rom_patches) rom.player_names = player_names pymods = [] @@ -416,9 +413,7 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m assert(len(auth) == 12) rom.patch(0x00, SEED_LOCATION, None, binascii.hexlify(auth)) - for pymod in pymods: pymod.postPatch(rom) - return rom diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index 0ec22e38cc..d64b950e38 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -1,5 +1,8 @@ import binascii +import bsdiff4 import os +import pkgutil +import tempfile from BaseClasses import Entrance, Item, ItemClassification, Location, Tutorial from Fill import fill_restrictive @@ -406,10 +409,18 @@ class LinksAwakeningWorld(World): player_names=all_names, player_id = self.player) - handle = open(out_path, "wb") - rom.save(handle, name="LADXR") + with open(out_path, "wb") as handle: + rom.save(handle, name="LADXR") + + # Write title screen after everything else is done - full gfxmods may stomp over the egg tiles + if self.player_options["ap_title_screen"]: + with tempfile.NamedTemporaryFile(delete=False) as title_patch: + title_patch.write(pkgutil.get_data(__name__, "LADXR/patches/title_screen.bdiff4")) + + bsdiff4.file_patch_inplace(out_path, title_patch.name) + os.unlink(title_patch.name) + - handle.close() patch = LADXDeltaPatch(os.path.splitext(out_path)[0]+LADXDeltaPatch.patch_file_ending, player=self.player, player_name=self.multiworld.player_name[self.player], patched_path=out_path) patch.write() From 5a7d20d39332d06ae73fc85912e866aafe2133f4 Mon Sep 17 00:00:00 2001 From: zig-for Date: Sat, 15 Apr 2023 00:17:33 -0700 Subject: [PATCH 045/489] Lua: Further centralize code, fix Bizhawk 2.9 (#1685) * Fixes the socket library for bizhawk 2.9/lua 5.4 by including another one in parallel * Fixes lua 5.4 support by making socket.lua into a "modern" module (the `module` keyword is gone) * Adds the linux version and 32 bit windows socket dlls because why not * Merges common functions into `common.lua` - the only functional change of this should be that: * Some things that were locals are globals now - this can be changed, I just was lazy and it likely doesn't matter * `drawText` now uses middle/bottom for all prints - feel free to do what you like with that change --- AdventureClient.py | 6 +- FF1Client.py | 6 +- OoTClient.py | 6 +- Zelda1Client.py | 6 +- data/lua/ADVENTURE/socket.lua | 132 ------ data/lua/FF1/core.dll | Bin 29184 -> 0 bytes data/lua/FF1/json.lua | 380 ----------------- data/lua/FF1/socket.lua | 132 ------ data/lua/OOT/core.dll | Bin 29184 -> 0 bytes data/lua/OOT/json.lua | 380 ----------------- data/lua/OOT/socket.lua | 132 ------ data/lua/PKMN_RB/core.dll | Bin 29184 -> 0 bytes data/lua/PKMN_RB/json.lua | 389 ------------------ data/lua/PKMN_RB/socket.lua | 132 ------ data/lua/TLoZ/core.dll | Bin 29184 -> 0 bytes data/lua/TLoZ/json.lua | 380 ----------------- data/lua/TLoZ/socket.lua | 132 ------ data/lua/common.lua | 102 +++++ ..._connector.lua => connector_adventure.lua} | 117 +----- .../ff1_connector.lua => connector_ff1.lua} | 108 +---- .../oot_connector.lua => connector_oot.lua} | 8 +- .../pkmn_rb.lua => connector_pkmn_rb.lua} | 55 +-- ...fZeldaConnector.lua => connector_tloz.lua} | 97 +---- data/lua/core.dll | Bin 29184 -> 0 bytes data/lua/{ADVENTURE => }/json.lua | 0 data/lua/lua_5_3_compat.lua | 12 + data/lua/socket.lua | 53 ++- data/lua/x64/luasocket.LICENSE.txt | 20 + data/lua/x64/socket-linux-5-1.so | Bin 0 -> 70808 bytes data/lua/x64/socket-linux-5-4.so | Bin 0 -> 81104 bytes data/lua/x64/socket-windows-5-1.dll | Bin 0 -> 56832 bytes data/lua/x64/socket-windows-5-4.dll | Bin 0 -> 51712 bytes data/lua/x86/luasocket.LICENSE.txt | 20 + data/lua/x86/socket-windows-5-1.dll | Bin 0 -> 40448 bytes host.yaml | 2 +- worlds/adventure/docs/setup_en.md | 6 +- worlds/ff1/docs/multiworld_en.md | 2 +- worlds/oot/docs/setup_en.md | 2 +- worlds/oot/docs/setup_fr.md | 4 +- worlds/pokemon_rb/docs/setup_en.md | 2 +- worlds/tloz/docs/multiworld_en.md | 2 +- 41 files changed, 255 insertions(+), 2570 deletions(-) delete mode 100644 data/lua/ADVENTURE/socket.lua delete mode 100644 data/lua/FF1/core.dll delete mode 100644 data/lua/FF1/json.lua delete mode 100644 data/lua/FF1/socket.lua delete mode 100644 data/lua/OOT/core.dll delete mode 100644 data/lua/OOT/json.lua delete mode 100644 data/lua/OOT/socket.lua delete mode 100644 data/lua/PKMN_RB/core.dll delete mode 100644 data/lua/PKMN_RB/json.lua delete mode 100644 data/lua/PKMN_RB/socket.lua delete mode 100644 data/lua/TLoZ/core.dll delete mode 100644 data/lua/TLoZ/json.lua delete mode 100644 data/lua/TLoZ/socket.lua create mode 100644 data/lua/common.lua rename data/lua/{ADVENTURE/adventure_connector.lua => connector_adventure.lua} (91%) rename data/lua/{FF1/ff1_connector.lua => connector_ff1.lua} (85%) rename data/lua/{OOT/oot_connector.lua => connector_oot.lua} (99%) rename data/lua/{PKMN_RB/pkmn_rb.lua => connector_pkmn_rb.lua} (89%) rename data/lua/{TLoZ/TheLegendOfZeldaConnector.lua => connector_tloz.lua} (87%) delete mode 100644 data/lua/core.dll rename data/lua/{ADVENTURE => }/json.lua (100%) create mode 100644 data/lua/lua_5_3_compat.lua create mode 100644 data/lua/x64/luasocket.LICENSE.txt create mode 100644 data/lua/x64/socket-linux-5-1.so create mode 100644 data/lua/x64/socket-linux-5-4.so create mode 100644 data/lua/x64/socket-windows-5-1.dll create mode 100644 data/lua/x64/socket-windows-5-4.dll create mode 100644 data/lua/x86/luasocket.LICENSE.txt create mode 100644 data/lua/x86/socket-windows-5-1.dll diff --git a/AdventureClient.py b/AdventureClient.py index 06eea5215c..0977203e2b 100644 --- a/AdventureClient.py +++ b/AdventureClient.py @@ -25,11 +25,11 @@ from worlds.adventure.Offsets import static_item_element_size, connector_port_of SYSTEM_MESSAGE_ID = 0 CONNECTION_TIMING_OUT_STATUS = \ - "Connection timing out. Please restart your emulator, then restart adventure_connector.lua" + "Connection timing out. Please restart your emulator, then restart connector_adventure.lua" CONNECTION_REFUSED_STATUS = \ - "Connection Refused. Please start your emulator and make sure adventure_connector.lua is running" + "Connection Refused. Please start your emulator and make sure connector_adventure.lua is running" CONNECTION_RESET_STATUS = \ - "Connection was reset. Please restart your emulator, then restart adventure_connector.lua" + "Connection was reset. Please restart your emulator, then restart connector_adventure.lua" CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" CONNECTION_CONNECTED_STATUS = "Connected" CONNECTION_INITIAL_STATUS = "Connection has not been initiated" diff --git a/FF1Client.py b/FF1Client.py index 83c2484682..6256744222 100644 --- a/FF1Client.py +++ b/FF1Client.py @@ -13,9 +13,9 @@ from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandP SYSTEM_MESSAGE_ID = 0 -CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart ff1_connector.lua" -CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure ff1_connector.lua is running" -CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart ff1_connector.lua" +CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_ff1.lua" +CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure connector_ff1.lua is running" +CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_ff1.lua" CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" CONNECTION_CONNECTED_STATUS = "Connected" CONNECTION_INITIAL_STATUS = "Connection has not been initiated" diff --git a/OoTClient.py b/OoTClient.py index f8a052402f..c05151a285 100644 --- a/OoTClient.py +++ b/OoTClient.py @@ -17,9 +17,9 @@ from worlds.oot.N64Patch import apply_patch_file from worlds.oot.Utils import data_path -CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart oot_connector.lua" -CONNECTION_REFUSED_STATUS = "Connection refused. Please start your emulator and make sure oot_connector.lua is running" -CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart oot_connector.lua" +CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_oot.lua" +CONNECTION_REFUSED_STATUS = "Connection refused. Please start your emulator and make sure connector_oot.lua is running" +CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_oot.lua" CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" CONNECTION_CONNECTED_STATUS = "Connected" CONNECTION_INITIAL_STATUS = "Connection has not been initiated" diff --git a/Zelda1Client.py b/Zelda1Client.py index a325e4aebe..b30bad5138 100644 --- a/Zelda1Client.py +++ b/Zelda1Client.py @@ -23,9 +23,9 @@ from worlds.tloz import Items, Locations, Rom SYSTEM_MESSAGE_ID = 0 -CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart Zelda_connector.lua" -CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure Zelda_connector.lua is running" -CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart Zelda_connector.lua" +CONNECTION_TIMING_OUT_STATUS = "Connection timing out. Please restart your emulator, then restart connector_tloz.lua" +CONNECTION_REFUSED_STATUS = "Connection Refused. Please start your emulator and make sure connector_tloz.lua is running" +CONNECTION_RESET_STATUS = "Connection was reset. Please restart your emulator, then restart connector_tloz.lua" CONNECTION_TENTATIVE_STATUS = "Initial Connection Made" CONNECTION_CONNECTED_STATUS = "Connected" CONNECTION_INITIAL_STATUS = "Connection has not been initiated" diff --git a/data/lua/ADVENTURE/socket.lua b/data/lua/ADVENTURE/socket.lua deleted file mode 100644 index a98e952115..0000000000 --- a/data/lua/ADVENTURE/socket.lua +++ /dev/null @@ -1,132 +0,0 @@ ------------------------------------------------------------------------------ --- LuaSocket helper module --- Author: Diego Nehab --- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ ------------------------------------------------------------------------------ - ------------------------------------------------------------------------------ --- Declare module and import dependencies ------------------------------------------------------------------------------ -local base = _G -local string = require("string") -local math = require("math") -local socket = require("socket.core") -module("socket") - ------------------------------------------------------------------------------ --- Exported auxiliar functions ------------------------------------------------------------------------------ -function connect(address, port, laddress, lport) - local sock, err = socket.tcp() - if not sock then return nil, err end - if laddress then - local res, err = sock:bind(laddress, lport, -1) - if not res then return nil, err end - end - local res, err = sock:connect(address, port) - if not res then return nil, err end - return sock -end - -function bind(host, port, backlog) - local sock, err = socket.tcp() - if not sock then return nil, err end - sock:setoption("reuseaddr", true) - local res, err = sock:bind(host, port) - if not res then return nil, err end - res, err = sock:listen(backlog) - if not res then return nil, err end - return sock -end - -try = newtry() - -function choose(table) - return function(name, opt1, opt2) - if base.type(name) ~= "string" then - name, opt1, opt2 = "default", name, opt1 - end - local f = table[name or "nil"] - if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) - else return f(opt1, opt2) end - end -end - ------------------------------------------------------------------------------ --- Socket sources and sinks, conforming to LTN12 ------------------------------------------------------------------------------ --- create namespaces inside LuaSocket namespace -sourcet = {} -sinkt = {} - -BLOCKSIZE = 2048 - -sinkt["close-when-done"] = function(sock) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function(self, chunk, err) - if not chunk then - sock:close() - return 1 - else return sock:send(chunk) end - end - }) -end - -sinkt["keep-open"] = function(sock) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function(self, chunk, err) - if chunk then return sock:send(chunk) - else return 1 end - end - }) -end - -sinkt["default"] = sinkt["keep-open"] - -sink = choose(sinkt) - -sourcet["by-length"] = function(sock, length) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function() - if length <= 0 then return nil end - local size = math.min(socket.BLOCKSIZE, length) - local chunk, err = sock:receive(size) - if err then return nil, err end - length = length - string.len(chunk) - return chunk - end - }) -end - -sourcet["until-closed"] = function(sock) - local done - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function() - if done then return nil end - local chunk, err, partial = sock:receive(socket.BLOCKSIZE) - if not err then return chunk - elseif err == "closed" then - sock:close() - done = 1 - return partial - else return nil, err end - end - }) -end - - -sourcet["default"] = sourcet["until-closed"] - -source = choose(sourcet) diff --git a/data/lua/FF1/core.dll b/data/lua/FF1/core.dll deleted file mode 100644 index 3e9569571ab0947dcb7bcd789dc9c06c009d072d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29184 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 B{xARl diff --git a/data/lua/FF1/json.lua b/data/lua/FF1/json.lua deleted file mode 100644 index 0833bf6fb4..0000000000 --- a/data/lua/FF1/json.lua +++ /dev/null @@ -1,380 +0,0 @@ --- --- json.lua --- --- Copyright (c) 2015 rxi --- --- This library is free software; you can redistribute it and/or modify it --- under the terms of the MIT license. See LICENSE for details. --- - -local json = { _version = "0.1.0" } - -------------------------------------------------------------------------------- --- Encode -------------------------------------------------------------------------------- - -local encode - -local escape_char_map = { - [ "\\" ] = "\\\\", - [ "\"" ] = "\\\"", - [ "\b" ] = "\\b", - [ "\f" ] = "\\f", - [ "\n" ] = "\\n", - [ "\r" ] = "\\r", - [ "\t" ] = "\\t", -} - -local escape_char_map_inv = { [ "\\/" ] = "/" } -for k, v in pairs(escape_char_map) do - escape_char_map_inv[v] = k -end - - -local function escape_char(c) - return escape_char_map[c] or string.format("\\u%04x", c:byte()) -end - - -local function encode_nil(val) - return "null" -end - - -local function encode_table(val, stack) - local res = {} - stack = stack or {} - - -- Circular reference? - if stack[val] then error("circular reference") end - - stack[val] = true - - if val[1] ~= nil or next(val) == nil then - -- Treat as array -- check keys are valid and it is not sparse - local n = 0 - for k in pairs(val) do - if type(k) ~= "number" then - error("invalid table: mixed or invalid key types") - end - n = n + 1 - end - if n ~= #val then - error("invalid table: sparse array") - end - -- Encode - for i, v in ipairs(val) do - table.insert(res, encode(v, stack)) - end - stack[val] = nil - return "[" .. table.concat(res, ",") .. "]" - - else - -- Treat as an object - for k, v in pairs(val) do - if type(k) ~= "string" then - error("invalid table: mixed or invalid key types") - end - table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) - end - stack[val] = nil - return "{" .. table.concat(res, ",") .. "}" - end -end - - -local function encode_string(val) - return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' -end - - -local function encode_number(val) - -- Check for NaN, -inf and inf - if val ~= val or val <= -math.huge or val >= math.huge then - error("unexpected number value '" .. tostring(val) .. "'") - end - return string.format("%.14g", val) -end - - -local type_func_map = { - [ "nil" ] = encode_nil, - [ "table" ] = encode_table, - [ "string" ] = encode_string, - [ "number" ] = encode_number, - [ "boolean" ] = tostring, -} - - -encode = function(val, stack) - local t = type(val) - local f = type_func_map[t] - if f then - return f(val, stack) - end - error("unexpected type '" .. t .. "'") -end - - -function json.encode(val) - return ( encode(val) ) -end - - -------------------------------------------------------------------------------- --- Decode -------------------------------------------------------------------------------- - -local parse - -local function create_set(...) - local res = {} - for i = 1, select("#", ...) do - res[ select(i, ...) ] = true - end - return res -end - -local space_chars = create_set(" ", "\t", "\r", "\n") -local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") -local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") -local literals = create_set("true", "false", "null") - -local literal_map = { - [ "true" ] = true, - [ "false" ] = false, - [ "null" ] = nil, -} - - -local function next_char(str, idx, set, negate) - for i = idx, #str do - if set[str:sub(i, i)] ~= negate then - return i - end - end - return #str + 1 -end - - -local function decode_error(str, idx, msg) - --local line_count = 1 - --local col_count = 1 - --for i = 1, idx - 1 do - -- col_count = col_count + 1 - -- if str:sub(i, i) == "\n" then - -- line_count = line_count + 1 - -- col_count = 1 - -- end - -- end - -- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) ) -end - - -local function codepoint_to_utf8(n) - -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa - local f = math.floor - if n <= 0x7f then - return string.char(n) - elseif n <= 0x7ff then - return string.char(f(n / 64) + 192, n % 64 + 128) - elseif n <= 0xffff then - return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) - elseif n <= 0x10ffff then - return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, - f(n % 4096 / 64) + 128, n % 64 + 128) - end - error( string.format("invalid unicode codepoint '%x'", n) ) -end - - -local function parse_unicode_escape(s) - local n1 = tonumber( s:sub(3, 6), 16 ) - local n2 = tonumber( s:sub(9, 12), 16 ) - -- Surrogate pair? - if n2 then - return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) - else - return codepoint_to_utf8(n1) - end -end - - -local function parse_string(str, i) - local has_unicode_escape = false - local has_surrogate_escape = false - local has_escape = false - local last - for j = i + 1, #str do - local x = str:byte(j) - - if x < 32 then - decode_error(str, j, "control character in string") - end - - if last == 92 then -- "\\" (escape char) - if x == 117 then -- "u" (unicode escape sequence) - local hex = str:sub(j + 1, j + 5) - if not hex:find("%x%x%x%x") then - decode_error(str, j, "invalid unicode escape in string") - end - if hex:find("^[dD][89aAbB]") then - has_surrogate_escape = true - else - has_unicode_escape = true - end - else - local c = string.char(x) - if not escape_chars[c] then - decode_error(str, j, "invalid escape char '" .. c .. "' in string") - end - has_escape = true - end - last = nil - - elseif x == 34 then -- '"' (end of string) - local s = str:sub(i + 1, j - 1) - if has_surrogate_escape then - s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) - end - if has_unicode_escape then - s = s:gsub("\\u....", parse_unicode_escape) - end - if has_escape then - s = s:gsub("\\.", escape_char_map_inv) - end - return s, j + 1 - - else - last = x - end - end - decode_error(str, i, "expected closing quote for string") -end - - -local function parse_number(str, i) - local x = next_char(str, i, delim_chars) - local s = str:sub(i, x - 1) - local n = tonumber(s) - if not n then - decode_error(str, i, "invalid number '" .. s .. "'") - end - return n, x -end - - -local function parse_literal(str, i) - local x = next_char(str, i, delim_chars) - local word = str:sub(i, x - 1) - if not literals[word] then - decode_error(str, i, "invalid literal '" .. word .. "'") - end - return literal_map[word], x -end - - -local function parse_array(str, i) - local res = {} - local n = 1 - i = i + 1 - while 1 do - local x - i = next_char(str, i, space_chars, true) - -- Empty / end of array? - if str:sub(i, i) == "]" then - i = i + 1 - break - end - -- Read token - x, i = parse(str, i) - res[n] = x - n = n + 1 - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "]" then break end - if chr ~= "," then decode_error(str, i, "expected ']' or ','") end - end - return res, i -end - - -local function parse_object(str, i) - local res = {} - i = i + 1 - while 1 do - local key, val - i = next_char(str, i, space_chars, true) - -- Empty / end of object? - if str:sub(i, i) == "}" then - i = i + 1 - break - end - -- Read key - if str:sub(i, i) ~= '"' then - decode_error(str, i, "expected string for key") - end - key, i = parse(str, i) - -- Read ':' delimiter - i = next_char(str, i, space_chars, true) - if str:sub(i, i) ~= ":" then - decode_error(str, i, "expected ':' after key") - end - i = next_char(str, i + 1, space_chars, true) - -- Read value - val, i = parse(str, i) - -- Set - res[key] = val - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "}" then break end - if chr ~= "," then decode_error(str, i, "expected '}' or ','") end - end - return res, i -end - - -local char_func_map = { - [ '"' ] = parse_string, - [ "0" ] = parse_number, - [ "1" ] = parse_number, - [ "2" ] = parse_number, - [ "3" ] = parse_number, - [ "4" ] = parse_number, - [ "5" ] = parse_number, - [ "6" ] = parse_number, - [ "7" ] = parse_number, - [ "8" ] = parse_number, - [ "9" ] = parse_number, - [ "-" ] = parse_number, - [ "t" ] = parse_literal, - [ "f" ] = parse_literal, - [ "n" ] = parse_literal, - [ "[" ] = parse_array, - [ "{" ] = parse_object, -} - - -parse = function(str, idx) - local chr = str:sub(idx, idx) - local f = char_func_map[chr] - if f then - return f(str, idx) - end - decode_error(str, idx, "unexpected character '" .. chr .. "'") -end - - -function json.decode(str) - if type(str) ~= "string" then - error("expected argument of type string, got " .. type(str)) - end - return ( parse(str, next_char(str, 1, space_chars, true)) ) -end - - -return json \ No newline at end of file diff --git a/data/lua/FF1/socket.lua b/data/lua/FF1/socket.lua deleted file mode 100644 index a98e952115..0000000000 --- a/data/lua/FF1/socket.lua +++ /dev/null @@ -1,132 +0,0 @@ ------------------------------------------------------------------------------ --- LuaSocket helper module --- Author: Diego Nehab --- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ ------------------------------------------------------------------------------ - ------------------------------------------------------------------------------ --- Declare module and import dependencies ------------------------------------------------------------------------------ -local base = _G -local string = require("string") -local math = require("math") -local socket = require("socket.core") -module("socket") - ------------------------------------------------------------------------------ --- Exported auxiliar functions ------------------------------------------------------------------------------ -function connect(address, port, laddress, lport) - local sock, err = socket.tcp() - if not sock then return nil, err end - if laddress then - local res, err = sock:bind(laddress, lport, -1) - if not res then return nil, err end - end - local res, err = sock:connect(address, port) - if not res then return nil, err end - return sock -end - -function bind(host, port, backlog) - local sock, err = socket.tcp() - if not sock then return nil, err end - sock:setoption("reuseaddr", true) - local res, err = sock:bind(host, port) - if not res then return nil, err end - res, err = sock:listen(backlog) - if not res then return nil, err end - return sock -end - -try = newtry() - -function choose(table) - return function(name, opt1, opt2) - if base.type(name) ~= "string" then - name, opt1, opt2 = "default", name, opt1 - end - local f = table[name or "nil"] - if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) - else return f(opt1, opt2) end - end -end - ------------------------------------------------------------------------------ --- Socket sources and sinks, conforming to LTN12 ------------------------------------------------------------------------------ --- create namespaces inside LuaSocket namespace -sourcet = {} -sinkt = {} - -BLOCKSIZE = 2048 - -sinkt["close-when-done"] = function(sock) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function(self, chunk, err) - if not chunk then - sock:close() - return 1 - else return sock:send(chunk) end - end - }) -end - -sinkt["keep-open"] = function(sock) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function(self, chunk, err) - if chunk then return sock:send(chunk) - else return 1 end - end - }) -end - -sinkt["default"] = sinkt["keep-open"] - -sink = choose(sinkt) - -sourcet["by-length"] = function(sock, length) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function() - if length <= 0 then return nil end - local size = math.min(socket.BLOCKSIZE, length) - local chunk, err = sock:receive(size) - if err then return nil, err end - length = length - string.len(chunk) - return chunk - end - }) -end - -sourcet["until-closed"] = function(sock) - local done - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function() - if done then return nil end - local chunk, err, partial = sock:receive(socket.BLOCKSIZE) - if not err then return chunk - elseif err == "closed" then - sock:close() - done = 1 - return partial - else return nil, err end - end - }) -end - - -sourcet["default"] = sourcet["until-closed"] - -source = choose(sourcet) diff --git a/data/lua/OOT/core.dll b/data/lua/OOT/core.dll deleted file mode 100644 index 3e9569571ab0947dcb7bcd789dc9c06c009d072d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29184 zcmeIbe|(hHwKw`CnS=ocGU9-vjyU3gQ9_)_OhPh~UqFIU1D#}&84x6dWWprmS0~RP zC}MDkB@R(;ORcB17OQVBr}eb7$D^@wG>{ZfX-m=4P(bM^S2qoMQsro@=-lsG&+|+Y z#GZ5S`~Gp?`#A&q_u6}}wf0(Tuf3mVCQI+$DWyn~q(g|uC8-Z7eM&g~`;kENv>Sdo zO?rOvuW#&2s`&Md)uEMUq~U?nI4>P-QjpMuas8l%ssI z!XK(2KJjNSViac0O`>Le07$TjR4E>fNKz$gEp3w2K+QWP>A57zT=Lm1NiC_8bfyll zmo$wpo@u#cZPrNAzRQiLcFK~28)f9h9f$}&qBTJT^7vRmZC1FUPR86f&P2r;1T(@i zgmIq|Or52GNlyY-sSAO|YD5_KDUqc9tZ-+z9(7DBXl5ogj{`!sgvJX8TiOA*m&V(T zkcI#n$A3yBY0>!df9L<#aljvwZTdiLv&|Ur3umz;g>-8rqhHKMwi*BYVmeh`tfR`Q z$O0^l+CKM-4~rxTJuUdhpfv6mF@`WlM^huSRrHssARAPWwF(F@xFuv}sxwjJ7o}Wp z=p(gS9jmZeokzqp&>S7K4bbGX(C+OmwHZSu^zz1MY+EB4ql2d23Y)GHOvCK|QApqh zZ^*#oo>8Ju9)MZbsKNVFmvFd^Ns+m=cf2|MU5lU1q-5xo+Zo&i)D09|3y$ zShdaI>}SsQEV1+~G50Jp_V`UbOY}WU4B~G$Nz&P+hFt>?yZg*B|Xq8mL5tz9tqjLBKW|ogX{{o)3WuiVEm z_OzK5EENoat2qhAAkbSq&06IZ?=AQ;c4<9r7IgERmcJSO?2CxW`I@J~W~hZeoJqbX zWnd+#3nNMy#xF32RUG{5!35!qufPnFN1DsHZDHJ&m}700d7^if-6|J8qPwABikw|A zRh~n*_HH-8o6gsVr+URg;EFozJi3_i5yK7Jr$nWs=%FOf=i%3eb%<|C@Oc80(Fe9L zeOrJT%;=-nSHsrc!!XJcY(1X=1hg$@o6Uxjuf`vH-d2EIAhu6Q#S^(ePC!x2-cSsR zNEYm;RJRPPGbFxEas=_O3V{9+@hG*F+FV|&UWKnxtU;N~rW+~yPU+qM4Egegw$o5( zsOJV_SKh6W+a4)NDrkvJ$8Z@{Vl&hb3^_Ld@PazjLaDvt;O9#1KcP0%p^ouJ4y&00 zk%j>S;y9`T;;7Of_FfOH;NC!ytd-)MC_#~Iq(}j=iy?o3IJU!93eurZwX5g-iZ{+A z47PQU7&QQbGAaqPS=#0ZPN9})t3~P;|E!kY zV_0h+78!1UU);uJokHt(l>%smRO^8F-s|89i<8+zo8maDcyqfH!ch_UdBro_(KzPy zFn>b(AhLe(Q*>9(98YmBWaeIe(TZNQ8Y{SWCgGs1`PTD#>|bO0szZsi)rd4CA%a}# zy$84149H6&=73w$|G;g2@dl;{_4B;BD%wmc#GS@~dw3mY+d4?Yla%Y=fKLS=+G%Wv zR>G725Y;Lm&MOuT8Q2C;Z;JsRnf5uGz7jfYtnwbh20}-1k*;8Km0uJRHg7RDdr*n3 zksJ#l>@W&`@oiK=eP&|PD)AfCrN2ntf)2(Qo2Ig<{+VE)jNm^ZMzn|(OqeCXEHLf= z5q=R&1#SVq?_>0kVEpvs@GDpF`wt`u{IV{O-{Q;Q_Y&%G1RRf_<{uRV6iL{H(fs08 z>O30K*F#LO+f<7v&6on#+=mRjsA2$wbkr?Oa(CT|0NQdXp1sYxiXQFCK^;Hs;8zweJ%Sj zkSINtnNHWu$rxoWMiHOPCkPYqF?!wv&k{Z;%RPk7LPBS$LeWCIkEPOiGGt^vuCitH ztb)r4HwwXpV2jTv;G--~XT$}=FR=2`4AguNfTq-g+^hQc_6D{$}EaLO=krAtHU z-o?Pxy!CIKUjY0pTR-LeJfM={`AEKc{~3MCA&L%AwBIXk$bvtWR=b+591mTq&7tWw zrC1pv$4KG;Q+FM)cof8#y#cJK1#?S6k3E2x-Y?HgXj!waN*v7~wit$|F)spQgv?lz3iyx}3k0Ol@mavr=2n})D!>IF%r?>?dC6~cp zL|_t}@%+M_=pf(d?2mQrqOS3yI0^LXJ|{B3)o35JOOZd-CVZ`l_dg^mrNoj_x^60k z4T7ip9ZVdC4nJ~@``XC0+fK^UA6b#)zB?&0ExJEBdLVh*$;esfYs0D6 zy%s%zBd$CJ_af1Q`tD)^ve+(ISH z@B(Dn;(?kK17j}X<5W+38rP4P4-BZ>Pd4j9Wct10KDNFF)g>EiPiju{ihCwBVxX~h zz1rVB0c5P*sJ37G7425F{cmhtzdS!}k>d1(M$8{QZ=m%w5!*tFy@fk0o0?Po;vtm1 z)X>QdrqE7i($y$YZ84?rq8;3do8=_g7?J@(jjOrRK<&378zS}clU+26Kfv-4NSso% zmocan_QDvdR=eLY*4>IM4pP(TATrpHM}L#9epDid4$V%^xp$JiXI6OR|QdE=*DLH7&IDL(OQ#d@ks;}h?p zjHU63A#N~*GJ=xPCpM!*wa-!-_lcd{ifcizRtE@b8{|4eug>O${sL%)QsEUxKjt$i zL!ae^@QKHj#?TPTK4C$L#weu}UG3bejQ_maqe1E6hle8WoC7$5q=n~QLkyz|+>DJR zcFWq;auzFR6%F}@((VH8$(9^14cJAGdo~daJ~{xZlGXU+)ta zF$}MZHMOYvNV}rGIhFL~0YzU{CTuF{qtc#6?e|=!9f}t6 zs^_}_re@sv4*C`dxv&)e=x^iV2=Jw&<^@Wj3WPuQGwc`3sCp%OHz(2N&`UW0T*Kp$ zJ360=IusMS0SYwLpi2flE*bQ&(v(l5(QfC#^|3>>328oZBoHbfWSntPqT&)#ak)Ez z%MH}~w^YrzB&oOr7{C%6rRKlD2LS>YJi#h9lZo?-KQ`i{Lq`c84L(=E=~{|zZpJKa zCI?6-VfUJfkE6N}BybNu3MYOgWk%<@g- zu#Pt?=+Ld{1BUQ3${BJz0^0m}KIOpuauRWT7JB=|^N6uX{QQAD!7h{uJ-=`vM=LcK zDT{~PaYEo4G4J%a!*nG9VLiH$;)wnv5;RxCUuWiJE7jDB2MjoUR)C@ksOo9ln=*tE zO^|aO#|N#-d@ms7`67?g$U!dZYsX#jZ$0ly)*g;1_qKF-8Bk<|wy|HM4=~Mi#!;fX zgr0r2B~jD`U4oQ>2k`jk>ki0NG3qm-d#)j~oZHwbu+iVSKjHO=u}JdI@D#SV(m z&q!V&7#44H63T)P2sr-%*0e=;;)7uw7-3PL;G8!C9Wv}L5M3#TC-Kd39`?~Gl2qgC z-Dg_^^j-!P62^KGWA|=M!v0Z;?W}i)Erdoo;)kjNqIL!$hDY%amPK(kmmVyFx>*!o zM0nwtV=duC_AX9Zxpk2EG!QB!Ybacaw!Re1qqTwr6DhNoo^zE3D#bF=L>v{HrJCMd z%(g(4$fruuH}Ny%eBvJw<7yTY@h+qdrbr#1FG6eRWU#7{roR83cw8xaxx7A6F6Ht{ zD(4}q!k;{&l@<6m5@iMcWt5|X<#hbmN_UR%$Cxa1A@I0lTjO zQefzrR7)E$xXrun#QC41#R0!XH^`&IDC{hl|5x1O0QCuosQj%pn6`B1!WeU?<+b3QB^Lv<>e9Z1jb^01AlPfp&A8S3D3K5!Ye_=!>;V z?3@W`m~VfmV;SVDt6VbnZqbE0ZpTDDY2xXTcwBqRYJpMG_dZ3lf^|Morh|D6L=T$% z;!YMgD{fk#iPGv70|C(vdi`hkeX%J>S=vh~bJxOa_8<(j=D8 z!$`1Gc%l?R4$RM@V?mbY_t?Q|JKTRex&LVhNwHgJGx zTgs>yaOp2ci5U7Mv zp^bD5_jkuy$0HB?C(7R}VD1&=SC8A*#E+G}s_-%V*jhXQR3HNoJw<7hRp~(&F&&6u zd519=aoP-YXZeMUrhsU~<6_A5>1)O>J>DH+A@+;!oQlV3V^!4f78Uu+iT@Gyy=!o| zP}t)W!{`g+wjC%nC;PBJ<)M3QB>k0X*chYd*2<3PIiq|H?q?z^Ufl=l{t~#18=8-P z6uG%MX}I9kL8{YJ9d7O-S5*z)@+u9M0F$o{btr?29>$-Tj@#dX=Hwc&6e-rwp91pW%OBoN7{iI}Ta@;|m~oxhz8dYRBv2S+Emr!=78~}04y4U^ zW8rrKIzTU;z#!Nf-pWSriXW2P><;!XV5PJ8{kORR#eM+BAcZ z4X-qySME ztFcQpa-8^06Iuw>)usoh4}%o=Ehh9miUG;9`WdRyUqN?kRIHV6i3+29byOtF(?fkK zM7~1*@YSGIhnY>FcX$dHaHKtbjV4y`;T1?lC9r%6r}=-5e#}WUXl#Hf_lh4wqQU$5k*oE}=OaCSgzw45Fi7MyQRwdqeSF`oEf=ywE~dY=cN1E*Fi z9pziZC>*RyVRR?<4=11KKQ&7keem>&(?h4zRu7y$G0`FTpXXnKdCSL%k*e*gy$!8_ zIE8ksBe&#xzbF5W${V>HqjC$fYWo#`(8@~tXU9=qHP$|yC@bwJQC9rBjE5Be?rZGz zs{S45Ba>-TOeUe=6MdKhJ_9cK@yyKEyG4&fDK4>K!;hF?tzkpo1=pcD_5kdFy|Svt zFRBe>Rq=4Y`(wG>C%zBBC@ze@_#$#`-;)^C>l1M}8<2|H-X%Id(HaKuq#70kaA^IL z+NycMBo!oCP)a=f*VIxum-xgS9-p^2bpuoH-=gO;#-3l;=tLvi@@O|uxN#cX;O9;J z#u(@#J0yte#I`p($9RKhD_c$|!BAk2$Mk{4-`T)?lMk>^lA#~Xy@2&=%aYA^Qe zlA1<{cxn2Jzr&N5Qb66=TdNnl{!Gh&qXur;o&`V10Nl+a8vt#rq{l^gK|~aRAwIg7 zyj5(4*rLtm!-Eg-x(W2!hc#L_g@4a1n zZe+u4_yz{dfYbG%QDMDbG%A1zUx6kC=v^u(R>6W*!2QC+Rq%@+pd)}w$VT%kqHGt$ zbhRT1d7sG&{vJe)wk+FhMpP+hO&fBwI3(ZjuOaHEQ~QvINWj*R^DrbTFbURRHm$*I zZ4GSEL1XWFy1FvKrkJp8;mN9m(NHrzocbzdYeezIc$}WH*}RIN`ozEDjHr1Z1%C1I zm5P{xZ6=Lv%eju&Hk=(0N76Mel_>SR7Y);+1Aa(McNrGupK>mU?XX!fx55sgN6Xeh zVu5Cm`!Y}vFQ9&mAH)TPAGB4E39J3B%kTqol8Kd*gbVn5nJ-{U4S0|Nu4I5K8OYBS z3vdX=)~|3#;|}XL^f!}E!>5fOdSX?VL=i!DV7xO_BTv%+cco1v> z<%9`fvY^(LB8a;57mq^f>HdWzhEGlRv78xtZ%-MX?-$z?sUtz1#2g=<41ac-#^jUC zZap&@taO1NDl7jL9q|qoDl}HI4b}V|MS|Mi9DiP;?{5HGWm8mp!QN39K0YXY+~}jY zsfz#)j!t|f%lcnCuPI}FaEzQyzwj~{rU^4ECSSvM2TS}iz=MsSum{A4X{}uu?ZH#?vhJ#VD_z^v>C)XOK#=tT!C z@}6JkrGk$L6NOtszO(0>`j38u8k0O3XVf8jeb&S`DzcLv2b@DEF!owf;cFo)niliB z&M=u~vY6)d7x$C)4(FfvKr!>wb(zvt(S9iXWxC>dclwJ5sB14SzvvO34G)PIqwg;AT!- zuMg12E6h+KI<6?!toJG1R9V1aW9L_8Pr3u0wDaqF0Ap)_+4~pdz_b72`L&Fw9Xr3` zg+e+6_F_|J+kCn6>%S!$RchF2)XoFRUa=oiIsQCQ(QL(ggoI$6Swv@n&ws;b0ZXOm zOw+CrVH&MC{=Z5su@0d__}~ck*>%RurHrj@q+PQE&p(es?mic-%Gn*~YlbDVs9AQ^3WxKX9oX3=Iv z(z}n7j}N5D(_%(?sRJ)jn0s-ffKlxj)hqr0X2RV)fo+oA*ZXWI`o%9mIrPr>#|E46 z)YN`xuK0mcW$MM9i)I=a9~AG|@8t|v)O!NKm_Yp6~{?CAhRHmR>JcfLs6Ax^OD0xz$3)x6LL<(4)x`xm#y)O;Y z1K7&uG->adM(XZ8JdLMiXUg=Mj7*vnBpQk zl^*`kfGWmcdVh((6O>6WYGjh_qHjhazqX6^qtCkUSVucA=C{iJ_&N9$_GJ@|y z*l>M=6mD0*=M}%gwIUeOQzk|7#dlF)qCs#O5uAY$jFt41@@MeJKp)s+ip>o8s>F^& zNBIyI4DarcG-9^jf}$wB=}7CtHuey>04kaBlN81eC{NA^aZ9sAh%CZ*U_uV4v-N(D zI*2P!#ppzCuAzhYyaWzc$EJA$col3aciQrMl9wLH4c>1b+ZO-s{6LsbPZnaQnefO6 z_;A+`MR^70kT4N3WR&?Gm>{!?aQHvjA0s|oADafeF^R3lTPb?D)0Q_bi;1$Je;1!G z@uNt=!a@9y^F$n1TPiL!v>rDk@VHUqg@VDGgaP{va(+*Y~rcjx~*{P-oN20s?a zi^Qe`79NEoz~tM1(8nq?!1IGfJ5eA@m{++n;LC8laMkqtL z3!xGrfN&2&Jwg*gJ3<8EeuS+E-$3X^_!h!HAp8K~e<1t>;pYf1BK#}D0K#hszeV^x z!byZbA-sq1KEg)`pCZH&Qty(CS0H2{NORRsQnH#!nXA+4=FUw`o1~vSEenS1@LxpQx@&b@IAbV6?SO*XYWN4@W1D<;o{9oJW~)bcCtY_6Vw z2N1*eJ${JvP#R2me9RV+e5(5oy`rmAB)$?7kF5ZvZWZ;V_d(OJ53S>YFojN;f)s;Z z#Fs=4S`r(CHXh#sD+4;?m3Yo;5>u7o_+`@AG;E;a8r0wxV3K$N1K}%NX*e3{tHvnc z;g3_>p;~o@zccop{YlQa#cUq|=Z}Y5(abi6D}b(^8P=QZ+=?l+6@h*;ca=e4% z?Hq68xQpY>9B^T*2}49G7!k%5e$D1svyb z?BdwYaVEzS$2yMn92+<`a%|$*%&~=IE5|mDGZ1^G()aQs$sY}zyJc+Gi5BkahLp(j z>BW-s`smqFoR~gL&(zso8_?RWjGot>D6lPWjGlS0F*^F7{3Ziiqi07>^q;`-7neQ8 zckp%(w28D8_oZ^d+vmEEZJKq$Lf-P$ze4Z3w(f@%Js3U8N*6sQ4QHyYO8t;h|3_(< zd|A^c2M9XScLKg1h1V24qHitg<{iGS|C6IP9gYpok?_#hb2vODwtvO0nAD@Q4a}!& zmu_kJoIlisY2!0WArP?GTk4qSnm&TAti|gZKu#qv3+kQLJWY5YeGjY9~O^$d1#B74jqueg&#R{RFiSktjvm zik=*T&2O<;Rz&o8NqL4IIOWfO0+sTt5G7&_dmSs}^tf!Z_$i9dpoVxV5qj(YQi^;icq7 zg%_^y1Sn$Vi9{?gzEgb90fW?R3_Z~bGFyR6?9)QICa z{GgJ~v0ZA_JJ2jJyI~XHZ0gXQgQHqI9V2A{~?kj!$?Q z?5ubm5Cex&iC9#}>GnyV6iF3tq8@DKZzMqf4C2@5wgPN}uH#F#kkV(CN5P<})fCV0 zQha>{#X02^7nUM!+%cnrlc^1yTwlUTY5^y6@;I6ALh|HesUak>GU=IBoa&xggS59y z|4a*&yU$?%Je0ZzXePnUeVQ7dS%uP}R4?MmI;sCqYBjg2*E2ocL+J8!0qT0XPazwh zVa57Cvl|u7N#YyCnPv!u1(l&8EMt6z8QEwd0Z0)%f)@1z6QZ%>GfZg1TmD8QuMIRC zLj5E<*;@sSsk-FHb4N+bjL-+(Wpug-{oP`0v6H18hId z&VC={t*7ilPuZyN$>p)bIg(_Ryx)KF?s>nHZ>apfvp@ZZ)OwHTsX5=XbcC;kP*sCI zqboltyNKy^@-(p;v_dAxmeIY#%GU$X+qLu30bEQ9q(t0goRCoT{?NSD@3G-m){^-m3a_{o-FnJS#Up3q$5}Pwku&k*(Loa{>qMbEC2jyZ1DNasq$>tU3`G?vpf`>_ z8Cp?3;1Bd|2imVvSz$cXc`#;qI8S0ttH+8?8@%J2tvAHO$Ika9Tv#fA8$Uiyn)PjepY z{uu76-n?NJPTZzgxxw>kcyesHKIMexv#=q0ASv3P;W-|D7f1Qkm%d>BU4NqeYB`@E~2KbiIQ8w!Q6&O0w@C-He1O=Eyxe$U;6o4)8#6WxvUhSMubzY@~ z7#8I(FnMAnw*BIUPmV^@yg$N{}6TiPDOVUAvvk0RI2GnODEJ3&%VFSWWgxv_s z08gLoNS~s<2(KU9)>6#T?Ah)V86nrK!Q%zP347FIzhR8Eml1?be&?A{(1pn(D$?_3qkQxxG^k zH?`3heb6twt!N^*N5i+mF>ghSlcIUdH^Sl84-^P*oz5QC!M_`1&a`t=j) zKwurwPowc9$j^ynY5(W6p^R%F@Tncd<36aVHP{}JrAXWQw)T75tif=&9m@{>McP{0 z>zLxs^$KpC!4@J#iZpaca(xH*8AJ&(5NT`-qEm2VeXs*H8aJtpK3(;ak$x3oBhob% z$w=QLlky~Ehgu(^6!i^AJM4f#>UB%Txi?E-$@0g$wp8mctMc9PwPn;+TUxfL=5|RUh7s10w$4aLM|&8HYHjb3 zo7&r~Yj~}(>YYKEz)+oFwbT}AT^kHrZ&vwrv$e4)*wQd27;D?xTY`0M)|=bgzSPzZ zrfV{Cb5k2+qp_|&c(b*s!&=wS5Dt>SDk5hc-+ajoG=|z0^d!b>jkJIj80PZhjcfkW zc!@TQ2l;BG#cpT{%fw7g4qBiHw6<99`EB@}%a*!LQaBjt4APW4vcyA8v^0LQIHF#YW2#qJufX&CoxwJiIaQaWmZnZQh!JT`+9;N4K&_g*S6^aE#DaoZwQ7T;QIQOreK?_ z5Q>N**g|>X_dUZts}i8YG$Q43T!C4l6RDit2cY>w)k5miBhS zVM3iO!$M*FFch7DkZ)*jlfy6wyq;JK8Jm>47LDQdR;&Zrfcr@inE|i`37%0t(x!4g z*pO%+UlR^CLU`@GbPobzvFk5YC%mmpY*O?cr9?A82YL z8`q|rqz~28dpam2o7Omz-O`Yqqn4**1ATB zzE!cJ2_68($99zvBw7{986yU?_b z*rmygX1o_FM%=1V2#Ek*lBCr)*0naZY+}ZYZ8}5`e1rD-_7?0i1R!}QmM3-qO<#lf zKoNjH@i;M*#$yuUOImPj(#D+FMOswXFgCwxg%hy%hTGfLu}VPGwAGfRrI4?>b+iGu zTU!7@d@hChG_@&Msr+CLHHFE$NXSKd0(HioWuKjgF`CA44~$u-Y>N|UzSlZY*Q$b@ z?U6792daUsye>>!{wn^ZQ3Kwk;#4aysaAZv=}6}yX!Urt zG6DCk8p-%G1nRG~9~sw9^(WP{g4T{_Mv4A+EXQ+81OxUn?NfjaamO0$HRI|Qk%v)- zI`(1|$PTIl+`e&j-$Y#j>Mr;B@AK#XzUR+?FT2*6P1`bjB?(eri=6^)Dy%P;IwGB+ zdU7fwVOaQkxGr3GFKs8%ZCt9yP9g_o-b(z5K8k-YNmo-%MJ0G_E}0zM>yD_9&^`l50qQrvq;OiL|Ec(Arh2sP~lO59SmnPDNU)N z-gIomFX1!qfTcFaLLLJSVUT>65SK{mloeabocq{&K|DWcOM9o4QF zSEupeRvtsyuI}VK1TY~?KCU4eG>m{@PL@N_g4dI756V9FKda#tF6>-_O_XwyKgGu< zo4d-YmX%d#TH{ac?7;Pj+$i0h+SyrO*VYKn!RKFGm1pMx!%4M491KIC@ZGhuGuAea(mpn~$*U~CoIhL;tO4ldV z*0px7s|{`h!=giO^f{wWw+iOX_JCQ}NwmR5|k5c%eq<+2h4}gZ*Xm8_5A4;wTLF=WAlv=gs ze$c}gB(a3~z$-L`R&uw))^=oYIK9?+{+qP4(-! z0{5@u@Kgr61HR(oakVP;RIPo~zj2X_^&=le3!}KZy@(*8{-_PU2U5rmpGH{RAj;(W z)hbnBIoa+1{`~)$1H0DZ4iP`zx&vtm?iDp!pQrMT@S&0Jw&M9J(*AMnUfeetZIZE+ z?j4b8{pcR@Xx3QSj;~LiaY;rS(%txLDi%t!C8G%`F71s5wOxnQ3OM?8o;7HvpA0(V zkc^{L&XtVkk#-?HgVc-qx2TNt2vTE?WTbC3TT!NOJdfaB^m(esID3%pMg5c1j`4RQ zrFpj@-Hq`!Qi}c>{%|pj9dj`!QvBMJu^OotX$8^|q#7LEzaB+@t^7nI=D|JfPNc^C zv3jE4xM+-iqL<1zg=NhC$kG&9c<0ZqAB89G_xaTcLdACQQuOI!0 zzAnHMeP@soeI*#5=-Z33hF<~t?VO7-k?uvR(Pu#!zc^;pBRz`x3&0PT-o`UXt>f;K z>7Mxv#?{87Ctg3l=2iNQ8~$kZ|DFEt$pIasoy33k2d^1*X;f(S$*8bjrXDXhNOZlM#60xRTF6_NYpd6WBrS~3rkDySa$pJ zQRZ0VcsBd@*{8Bi&N63(^O*BPXO3&L>*btK?zeKU&M(b>EdR~?EuKd`w->A{c(~yE z1&0d0D5x&nQFx&+v*@3Sjuic&=s$`jD_;e`=v;8*W}nTT>-@<1DZXm7%2nrTaec$} zP1jFd@3_vn;x1E8H0P0=(VXiSlq^`j;K2pw7Oc(d%ll)VKHr@0&R>=Pc>b^RZ+8dX zAG&|U4D_shH^dB^k4=KUq_)4Ut= zm*zj1|F?XDd#2mwzSW)U_PbZQ8{O;O8{GH1A9lywkGsF;{*n79?w8$fyIr39Jo8|8&7S1E!FjK9kIR?Sl=EoLrrhUqXS;84XSrQ&x4Xo>#9iU8axZtUb~m`2-5tPT zv-=_UcK4(1UGDF?cLR^-+`n-5xnFS)x?gu6bsu-1a{rrKxIb`TaR1FMdD1)v&vef% zp4&V*9*?KQQ|c-ARCv6eYR_`dD$g2EgD2!^@pO1(&qmK?PnTz#XS-*IXQyYE=Lye~ zp535rkLNkhUeEKMKF>Z+zh}^M*mJ~l)bo~S$n(>}cM9Jv94`E8;U|Tk7p4?VE;1FF zi!4RfB3n^LQD#wB(ZfYMioRL&MA1`4&ldf-Xm8OAMf-{l6df-5P0?FLe=Isv^ls7l zq7REki@qq*6;CNP7SAlU6wfZsD9$Q&6}yW|ikB2u6jv25FJ4{TP~2SHQM{pebMZsP z+lwD9-c|hF;@!nRDt@l`7sY+WuM`g!zg~Q__;~TD;(sd^#UB)3DE=GCl^4(E!F$P` zW;fWU+s*b_cANcHyWO5^FR)kH+w4w90d(X}$7)BjqubHzc---{;|0g7jz2j5-J#2# zoSmM1YxZr~p6olbz1e~6j_eKDJ=u?De-E#;l z3^}`;Z#pGcIkfB{*LK&Vu3fJ0x}J9JasABog6m&hgRVDRZ@Nyp{_Og9*Qo1rS4z&* zoS8W{=FHE@&RLXmd(P4vl7x<&O*xsluG~erOLKj>>vA{eek1n>x&M-TAotVUFLJM6 zFnhtl1#c`kz2LJ2>3K8qw&ZQgdn9jX-uLr!96yT!;T}4oz7j(C!9|@cRQbT?r}cn-0OVa+2`En z>~{`24?B-IhYC&=SPHF$dkgyt_Z7ZXI8-=VNNX*1;T;ByA=!0yJveN(TkKYFIK!T4 z&jW``?4@?Ez1qIU-T>Kev3J;I`$qd_dzXD1G+~E*C$!-S`;+$FuvmNS&)N6dpNDqr zv-jHv?T770>__cy*@x_>>}Tv}?ZSTEK4QNBJ8;o1Idl%A!{jhKEDo!~=E!hlI`XiR z<&Fx6*HI0d*Wd^_S{xk?*|E{F+0o_L2FtO-vD2~3@r2_^$8N{7jy;a&9D5zl!=CJO z^uwkQAC5ZSatt|6!M>b#j5sbhMjaO&QnoHzpKZuCW}C9j*_P~#?96N;`TuL5{~I$@ B{xARl diff --git a/data/lua/OOT/json.lua b/data/lua/OOT/json.lua deleted file mode 100644 index 0833bf6fb4..0000000000 --- a/data/lua/OOT/json.lua +++ /dev/null @@ -1,380 +0,0 @@ --- --- json.lua --- --- Copyright (c) 2015 rxi --- --- This library is free software; you can redistribute it and/or modify it --- under the terms of the MIT license. See LICENSE for details. --- - -local json = { _version = "0.1.0" } - -------------------------------------------------------------------------------- --- Encode -------------------------------------------------------------------------------- - -local encode - -local escape_char_map = { - [ "\\" ] = "\\\\", - [ "\"" ] = "\\\"", - [ "\b" ] = "\\b", - [ "\f" ] = "\\f", - [ "\n" ] = "\\n", - [ "\r" ] = "\\r", - [ "\t" ] = "\\t", -} - -local escape_char_map_inv = { [ "\\/" ] = "/" } -for k, v in pairs(escape_char_map) do - escape_char_map_inv[v] = k -end - - -local function escape_char(c) - return escape_char_map[c] or string.format("\\u%04x", c:byte()) -end - - -local function encode_nil(val) - return "null" -end - - -local function encode_table(val, stack) - local res = {} - stack = stack or {} - - -- Circular reference? - if stack[val] then error("circular reference") end - - stack[val] = true - - if val[1] ~= nil or next(val) == nil then - -- Treat as array -- check keys are valid and it is not sparse - local n = 0 - for k in pairs(val) do - if type(k) ~= "number" then - error("invalid table: mixed or invalid key types") - end - n = n + 1 - end - if n ~= #val then - error("invalid table: sparse array") - end - -- Encode - for i, v in ipairs(val) do - table.insert(res, encode(v, stack)) - end - stack[val] = nil - return "[" .. table.concat(res, ",") .. "]" - - else - -- Treat as an object - for k, v in pairs(val) do - if type(k) ~= "string" then - error("invalid table: mixed or invalid key types") - end - table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) - end - stack[val] = nil - return "{" .. table.concat(res, ",") .. "}" - end -end - - -local function encode_string(val) - return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' -end - - -local function encode_number(val) - -- Check for NaN, -inf and inf - if val ~= val or val <= -math.huge or val >= math.huge then - error("unexpected number value '" .. tostring(val) .. "'") - end - return string.format("%.14g", val) -end - - -local type_func_map = { - [ "nil" ] = encode_nil, - [ "table" ] = encode_table, - [ "string" ] = encode_string, - [ "number" ] = encode_number, - [ "boolean" ] = tostring, -} - - -encode = function(val, stack) - local t = type(val) - local f = type_func_map[t] - if f then - return f(val, stack) - end - error("unexpected type '" .. t .. "'") -end - - -function json.encode(val) - return ( encode(val) ) -end - - -------------------------------------------------------------------------------- --- Decode -------------------------------------------------------------------------------- - -local parse - -local function create_set(...) - local res = {} - for i = 1, select("#", ...) do - res[ select(i, ...) ] = true - end - return res -end - -local space_chars = create_set(" ", "\t", "\r", "\n") -local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") -local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") -local literals = create_set("true", "false", "null") - -local literal_map = { - [ "true" ] = true, - [ "false" ] = false, - [ "null" ] = nil, -} - - -local function next_char(str, idx, set, negate) - for i = idx, #str do - if set[str:sub(i, i)] ~= negate then - return i - end - end - return #str + 1 -end - - -local function decode_error(str, idx, msg) - --local line_count = 1 - --local col_count = 1 - --for i = 1, idx - 1 do - -- col_count = col_count + 1 - -- if str:sub(i, i) == "\n" then - -- line_count = line_count + 1 - -- col_count = 1 - -- end - -- end - -- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) ) -end - - -local function codepoint_to_utf8(n) - -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa - local f = math.floor - if n <= 0x7f then - return string.char(n) - elseif n <= 0x7ff then - return string.char(f(n / 64) + 192, n % 64 + 128) - elseif n <= 0xffff then - return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) - elseif n <= 0x10ffff then - return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, - f(n % 4096 / 64) + 128, n % 64 + 128) - end - error( string.format("invalid unicode codepoint '%x'", n) ) -end - - -local function parse_unicode_escape(s) - local n1 = tonumber( s:sub(3, 6), 16 ) - local n2 = tonumber( s:sub(9, 12), 16 ) - -- Surrogate pair? - if n2 then - return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) - else - return codepoint_to_utf8(n1) - end -end - - -local function parse_string(str, i) - local has_unicode_escape = false - local has_surrogate_escape = false - local has_escape = false - local last - for j = i + 1, #str do - local x = str:byte(j) - - if x < 32 then - decode_error(str, j, "control character in string") - end - - if last == 92 then -- "\\" (escape char) - if x == 117 then -- "u" (unicode escape sequence) - local hex = str:sub(j + 1, j + 5) - if not hex:find("%x%x%x%x") then - decode_error(str, j, "invalid unicode escape in string") - end - if hex:find("^[dD][89aAbB]") then - has_surrogate_escape = true - else - has_unicode_escape = true - end - else - local c = string.char(x) - if not escape_chars[c] then - decode_error(str, j, "invalid escape char '" .. c .. "' in string") - end - has_escape = true - end - last = nil - - elseif x == 34 then -- '"' (end of string) - local s = str:sub(i + 1, j - 1) - if has_surrogate_escape then - s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) - end - if has_unicode_escape then - s = s:gsub("\\u....", parse_unicode_escape) - end - if has_escape then - s = s:gsub("\\.", escape_char_map_inv) - end - return s, j + 1 - - else - last = x - end - end - decode_error(str, i, "expected closing quote for string") -end - - -local function parse_number(str, i) - local x = next_char(str, i, delim_chars) - local s = str:sub(i, x - 1) - local n = tonumber(s) - if not n then - decode_error(str, i, "invalid number '" .. s .. "'") - end - return n, x -end - - -local function parse_literal(str, i) - local x = next_char(str, i, delim_chars) - local word = str:sub(i, x - 1) - if not literals[word] then - decode_error(str, i, "invalid literal '" .. word .. "'") - end - return literal_map[word], x -end - - -local function parse_array(str, i) - local res = {} - local n = 1 - i = i + 1 - while 1 do - local x - i = next_char(str, i, space_chars, true) - -- Empty / end of array? - if str:sub(i, i) == "]" then - i = i + 1 - break - end - -- Read token - x, i = parse(str, i) - res[n] = x - n = n + 1 - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "]" then break end - if chr ~= "," then decode_error(str, i, "expected ']' or ','") end - end - return res, i -end - - -local function parse_object(str, i) - local res = {} - i = i + 1 - while 1 do - local key, val - i = next_char(str, i, space_chars, true) - -- Empty / end of object? - if str:sub(i, i) == "}" then - i = i + 1 - break - end - -- Read key - if str:sub(i, i) ~= '"' then - decode_error(str, i, "expected string for key") - end - key, i = parse(str, i) - -- Read ':' delimiter - i = next_char(str, i, space_chars, true) - if str:sub(i, i) ~= ":" then - decode_error(str, i, "expected ':' after key") - end - i = next_char(str, i + 1, space_chars, true) - -- Read value - val, i = parse(str, i) - -- Set - res[key] = val - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "}" then break end - if chr ~= "," then decode_error(str, i, "expected '}' or ','") end - end - return res, i -end - - -local char_func_map = { - [ '"' ] = parse_string, - [ "0" ] = parse_number, - [ "1" ] = parse_number, - [ "2" ] = parse_number, - [ "3" ] = parse_number, - [ "4" ] = parse_number, - [ "5" ] = parse_number, - [ "6" ] = parse_number, - [ "7" ] = parse_number, - [ "8" ] = parse_number, - [ "9" ] = parse_number, - [ "-" ] = parse_number, - [ "t" ] = parse_literal, - [ "f" ] = parse_literal, - [ "n" ] = parse_literal, - [ "[" ] = parse_array, - [ "{" ] = parse_object, -} - - -parse = function(str, idx) - local chr = str:sub(idx, idx) - local f = char_func_map[chr] - if f then - return f(str, idx) - end - decode_error(str, idx, "unexpected character '" .. chr .. "'") -end - - -function json.decode(str) - if type(str) ~= "string" then - error("expected argument of type string, got " .. type(str)) - end - return ( parse(str, next_char(str, 1, space_chars, true)) ) -end - - -return json \ No newline at end of file diff --git a/data/lua/OOT/socket.lua b/data/lua/OOT/socket.lua deleted file mode 100644 index a98e952115..0000000000 --- a/data/lua/OOT/socket.lua +++ /dev/null @@ -1,132 +0,0 @@ ------------------------------------------------------------------------------ --- LuaSocket helper module --- Author: Diego Nehab --- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ ------------------------------------------------------------------------------ - ------------------------------------------------------------------------------ --- Declare module and import dependencies ------------------------------------------------------------------------------ -local base = _G -local string = require("string") -local math = require("math") -local socket = require("socket.core") -module("socket") - ------------------------------------------------------------------------------ --- Exported auxiliar functions ------------------------------------------------------------------------------ -function connect(address, port, laddress, lport) - local sock, err = socket.tcp() - if not sock then return nil, err end - if laddress then - local res, err = sock:bind(laddress, lport, -1) - if not res then return nil, err end - end - local res, err = sock:connect(address, port) - if not res then return nil, err end - return sock -end - -function bind(host, port, backlog) - local sock, err = socket.tcp() - if not sock then return nil, err end - sock:setoption("reuseaddr", true) - local res, err = sock:bind(host, port) - if not res then return nil, err end - res, err = sock:listen(backlog) - if not res then return nil, err end - return sock -end - -try = newtry() - -function choose(table) - return function(name, opt1, opt2) - if base.type(name) ~= "string" then - name, opt1, opt2 = "default", name, opt1 - end - local f = table[name or "nil"] - if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) - else return f(opt1, opt2) end - end -end - ------------------------------------------------------------------------------ --- Socket sources and sinks, conforming to LTN12 ------------------------------------------------------------------------------ --- create namespaces inside LuaSocket namespace -sourcet = {} -sinkt = {} - -BLOCKSIZE = 2048 - -sinkt["close-when-done"] = function(sock) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function(self, chunk, err) - if not chunk then - sock:close() - return 1 - else return sock:send(chunk) end - end - }) -end - -sinkt["keep-open"] = function(sock) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function(self, chunk, err) - if chunk then return sock:send(chunk) - else return 1 end - end - }) -end - -sinkt["default"] = sinkt["keep-open"] - -sink = choose(sinkt) - -sourcet["by-length"] = function(sock, length) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function() - if length <= 0 then return nil end - local size = math.min(socket.BLOCKSIZE, length) - local chunk, err = sock:receive(size) - if err then return nil, err end - length = length - string.len(chunk) - return chunk - end - }) -end - -sourcet["until-closed"] = function(sock) - local done - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function() - if done then return nil end - local chunk, err, partial = sock:receive(socket.BLOCKSIZE) - if not err then return chunk - elseif err == "closed" then - sock:close() - done = 1 - return partial - else return nil, err end - end - }) -end - - -sourcet["default"] = sourcet["until-closed"] - -source = choose(sourcet) diff --git a/data/lua/PKMN_RB/core.dll b/data/lua/PKMN_RB/core.dll deleted file mode 100644 index 3e9569571ab0947dcb7bcd789dc9c06c009d072d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29184 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 B{xARl diff --git a/data/lua/PKMN_RB/json.lua b/data/lua/PKMN_RB/json.lua deleted file mode 100644 index a1f6e4ede2..0000000000 --- a/data/lua/PKMN_RB/json.lua +++ /dev/null @@ -1,389 +0,0 @@ --- --- json.lua --- --- Copyright (c) 2015 rxi --- --- This library is free software; you can redistribute it and/or modify it --- under the terms of the MIT license. See LICENSE for details. --- - -local json = { _version = "0.1.0" } - -------------------------------------------------------------------------------- --- Encode -------------------------------------------------------------------------------- - -local encode - -function error(err) - print(err) -end - -local escape_char_map = { - [ "\\" ] = "\\\\", - [ "\"" ] = "\\\"", - [ "\b" ] = "\\b", - [ "\f" ] = "\\f", - [ "\n" ] = "\\n", - [ "\r" ] = "\\r", - [ "\t" ] = "\\t", -} - -local escape_char_map_inv = { [ "\\/" ] = "/" } -for k, v in pairs(escape_char_map) do - escape_char_map_inv[v] = k -end - - -local function escape_char(c) - return escape_char_map[c] or string.format("\\u%04x", c:byte()) -end - - -local function encode_nil(val) - return "null" -end - - -local function encode_table(val, stack) - local res = {} - stack = stack or {} - - -- Circular reference? - if stack[val] then error("circular reference") end - - stack[val] = true - - if val[1] ~= nil or next(val) == nil then - -- Treat as array -- check keys are valid and it is not sparse - local n = 0 - for k in pairs(val) do - if type(k) ~= "number" then - error("invalid table: mixed or invalid key types") - end - n = n + 1 - end - if n ~= #val then - print("invalid table: sparse array") - print(n) - print("VAL:") - print(val) - print("STACK:") - print(stack) - end - -- Encode - for i, v in ipairs(val) do - table.insert(res, encode(v, stack)) - end - stack[val] = nil - return "[" .. table.concat(res, ",") .. "]" - - else - -- Treat as an object - for k, v in pairs(val) do - if type(k) ~= "string" then - error("invalid table: mixed or invalid key types") - end - table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) - end - stack[val] = nil - return "{" .. table.concat(res, ",") .. "}" - end -end - - -local function encode_string(val) - return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' -end - - -local function encode_number(val) - -- Check for NaN, -inf and inf - if val ~= val or val <= -math.huge or val >= math.huge then - error("unexpected number value '" .. tostring(val) .. "'") - end - return string.format("%.14g", val) -end - - -local type_func_map = { - [ "nil" ] = encode_nil, - [ "table" ] = encode_table, - [ "string" ] = encode_string, - [ "number" ] = encode_number, - [ "boolean" ] = tostring, -} - - -encode = function(val, stack) - local t = type(val) - local f = type_func_map[t] - if f then - return f(val, stack) - end - error("unexpected type '" .. t .. "'") -end - - -function json.encode(val) - return ( encode(val) ) -end - - -------------------------------------------------------------------------------- --- Decode -------------------------------------------------------------------------------- - -local parse - -local function create_set(...) - local res = {} - for i = 1, select("#", ...) do - res[ select(i, ...) ] = true - end - return res -end - -local space_chars = create_set(" ", "\t", "\r", "\n") -local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") -local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") -local literals = create_set("true", "false", "null") - -local literal_map = { - [ "true" ] = true, - [ "false" ] = false, - [ "null" ] = nil, -} - - -local function next_char(str, idx, set, negate) - for i = idx, #str do - if set[str:sub(i, i)] ~= negate then - return i - end - end - return #str + 1 -end - - -local function decode_error(str, idx, msg) - --local line_count = 1 - --local col_count = 1 - --for i = 1, idx - 1 do - -- col_count = col_count + 1 - -- if str:sub(i, i) == "\n" then - -- line_count = line_count + 1 - -- col_count = 1 - -- end - -- end - -- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) ) -end - - -local function codepoint_to_utf8(n) - -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa - local f = math.floor - if n <= 0x7f then - return string.char(n) - elseif n <= 0x7ff then - return string.char(f(n / 64) + 192, n % 64 + 128) - elseif n <= 0xffff then - return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) - elseif n <= 0x10ffff then - return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, - f(n % 4096 / 64) + 128, n % 64 + 128) - end - error( string.format("invalid unicode codepoint '%x'", n) ) -end - - -local function parse_unicode_escape(s) - local n1 = tonumber( s:sub(3, 6), 16 ) - local n2 = tonumber( s:sub(9, 12), 16 ) - -- Surrogate pair? - if n2 then - return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) - else - return codepoint_to_utf8(n1) - end -end - - -local function parse_string(str, i) - local has_unicode_escape = false - local has_surrogate_escape = false - local has_escape = false - local last - for j = i + 1, #str do - local x = str:byte(j) - - if x < 32 then - decode_error(str, j, "control character in string") - end - - if last == 92 then -- "\\" (escape char) - if x == 117 then -- "u" (unicode escape sequence) - local hex = str:sub(j + 1, j + 5) - if not hex:find("%x%x%x%x") then - decode_error(str, j, "invalid unicode escape in string") - end - if hex:find("^[dD][89aAbB]") then - has_surrogate_escape = true - else - has_unicode_escape = true - end - else - local c = string.char(x) - if not escape_chars[c] then - decode_error(str, j, "invalid escape char '" .. c .. "' in string") - end - has_escape = true - end - last = nil - - elseif x == 34 then -- '"' (end of string) - local s = str:sub(i + 1, j - 1) - if has_surrogate_escape then - s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) - end - if has_unicode_escape then - s = s:gsub("\\u....", parse_unicode_escape) - end - if has_escape then - s = s:gsub("\\.", escape_char_map_inv) - end - return s, j + 1 - - else - last = x - end - end - decode_error(str, i, "expected closing quote for string") -end - - -local function parse_number(str, i) - local x = next_char(str, i, delim_chars) - local s = str:sub(i, x - 1) - local n = tonumber(s) - if not n then - decode_error(str, i, "invalid number '" .. s .. "'") - end - return n, x -end - - -local function parse_literal(str, i) - local x = next_char(str, i, delim_chars) - local word = str:sub(i, x - 1) - if not literals[word] then - decode_error(str, i, "invalid literal '" .. word .. "'") - end - return literal_map[word], x -end - - -local function parse_array(str, i) - local res = {} - local n = 1 - i = i + 1 - while 1 do - local x - i = next_char(str, i, space_chars, true) - -- Empty / end of array? - if str:sub(i, i) == "]" then - i = i + 1 - break - end - -- Read token - x, i = parse(str, i) - res[n] = x - n = n + 1 - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "]" then break end - if chr ~= "," then decode_error(str, i, "expected ']' or ','") end - end - return res, i -end - - -local function parse_object(str, i) - local res = {} - i = i + 1 - while 1 do - local key, val - i = next_char(str, i, space_chars, true) - -- Empty / end of object? - if str:sub(i, i) == "}" then - i = i + 1 - break - end - -- Read key - if str:sub(i, i) ~= '"' then - decode_error(str, i, "expected string for key") - end - key, i = parse(str, i) - -- Read ':' delimiter - i = next_char(str, i, space_chars, true) - if str:sub(i, i) ~= ":" then - decode_error(str, i, "expected ':' after key") - end - i = next_char(str, i + 1, space_chars, true) - -- Read value - val, i = parse(str, i) - -- Set - res[key] = val - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "}" then break end - if chr ~= "," then decode_error(str, i, "expected '}' or ','") end - end - return res, i -end - - -local char_func_map = { - [ '"' ] = parse_string, - [ "0" ] = parse_number, - [ "1" ] = parse_number, - [ "2" ] = parse_number, - [ "3" ] = parse_number, - [ "4" ] = parse_number, - [ "5" ] = parse_number, - [ "6" ] = parse_number, - [ "7" ] = parse_number, - [ "8" ] = parse_number, - [ "9" ] = parse_number, - [ "-" ] = parse_number, - [ "t" ] = parse_literal, - [ "f" ] = parse_literal, - [ "n" ] = parse_literal, - [ "[" ] = parse_array, - [ "{" ] = parse_object, -} - - -parse = function(str, idx) - local chr = str:sub(idx, idx) - local f = char_func_map[chr] - if f then - return f(str, idx) - end - decode_error(str, idx, "unexpected character '" .. chr .. "'") -end - - -function json.decode(str) - if type(str) ~= "string" then - error("expected argument of type string, got " .. type(str)) - end - return ( parse(str, next_char(str, 1, space_chars, true)) ) -end - - -return json \ No newline at end of file diff --git a/data/lua/PKMN_RB/socket.lua b/data/lua/PKMN_RB/socket.lua deleted file mode 100644 index a98e952115..0000000000 --- a/data/lua/PKMN_RB/socket.lua +++ /dev/null @@ -1,132 +0,0 @@ ------------------------------------------------------------------------------ --- LuaSocket helper module --- Author: Diego Nehab --- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ ------------------------------------------------------------------------------ - ------------------------------------------------------------------------------ --- Declare module and import dependencies ------------------------------------------------------------------------------ -local base = _G -local string = require("string") -local math = require("math") -local socket = require("socket.core") -module("socket") - ------------------------------------------------------------------------------ --- Exported auxiliar functions ------------------------------------------------------------------------------ -function connect(address, port, laddress, lport) - local sock, err = socket.tcp() - if not sock then return nil, err end - if laddress then - local res, err = sock:bind(laddress, lport, -1) - if not res then return nil, err end - end - local res, err = sock:connect(address, port) - if not res then return nil, err end - return sock -end - -function bind(host, port, backlog) - local sock, err = socket.tcp() - if not sock then return nil, err end - sock:setoption("reuseaddr", true) - local res, err = sock:bind(host, port) - if not res then return nil, err end - res, err = sock:listen(backlog) - if not res then return nil, err end - return sock -end - -try = newtry() - -function choose(table) - return function(name, opt1, opt2) - if base.type(name) ~= "string" then - name, opt1, opt2 = "default", name, opt1 - end - local f = table[name or "nil"] - if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) - else return f(opt1, opt2) end - end -end - ------------------------------------------------------------------------------ --- Socket sources and sinks, conforming to LTN12 ------------------------------------------------------------------------------ --- create namespaces inside LuaSocket namespace -sourcet = {} -sinkt = {} - -BLOCKSIZE = 2048 - -sinkt["close-when-done"] = function(sock) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function(self, chunk, err) - if not chunk then - sock:close() - return 1 - else return sock:send(chunk) end - end - }) -end - -sinkt["keep-open"] = function(sock) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function(self, chunk, err) - if chunk then return sock:send(chunk) - else return 1 end - end - }) -end - -sinkt["default"] = sinkt["keep-open"] - -sink = choose(sinkt) - -sourcet["by-length"] = function(sock, length) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function() - if length <= 0 then return nil end - local size = math.min(socket.BLOCKSIZE, length) - local chunk, err = sock:receive(size) - if err then return nil, err end - length = length - string.len(chunk) - return chunk - end - }) -end - -sourcet["until-closed"] = function(sock) - local done - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function() - if done then return nil end - local chunk, err, partial = sock:receive(socket.BLOCKSIZE) - if not err then return chunk - elseif err == "closed" then - sock:close() - done = 1 - return partial - else return nil, err end - end - }) -end - - -sourcet["default"] = sourcet["until-closed"] - -source = choose(sourcet) diff --git a/data/lua/TLoZ/core.dll b/data/lua/TLoZ/core.dll deleted file mode 100644 index 3e9569571ab0947dcb7bcd789dc9c06c009d072d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29184 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 B{xARl diff --git a/data/lua/TLoZ/json.lua b/data/lua/TLoZ/json.lua deleted file mode 100644 index 0833bf6fb4..0000000000 --- a/data/lua/TLoZ/json.lua +++ /dev/null @@ -1,380 +0,0 @@ --- --- json.lua --- --- Copyright (c) 2015 rxi --- --- This library is free software; you can redistribute it and/or modify it --- under the terms of the MIT license. See LICENSE for details. --- - -local json = { _version = "0.1.0" } - -------------------------------------------------------------------------------- --- Encode -------------------------------------------------------------------------------- - -local encode - -local escape_char_map = { - [ "\\" ] = "\\\\", - [ "\"" ] = "\\\"", - [ "\b" ] = "\\b", - [ "\f" ] = "\\f", - [ "\n" ] = "\\n", - [ "\r" ] = "\\r", - [ "\t" ] = "\\t", -} - -local escape_char_map_inv = { [ "\\/" ] = "/" } -for k, v in pairs(escape_char_map) do - escape_char_map_inv[v] = k -end - - -local function escape_char(c) - return escape_char_map[c] or string.format("\\u%04x", c:byte()) -end - - -local function encode_nil(val) - return "null" -end - - -local function encode_table(val, stack) - local res = {} - stack = stack or {} - - -- Circular reference? - if stack[val] then error("circular reference") end - - stack[val] = true - - if val[1] ~= nil or next(val) == nil then - -- Treat as array -- check keys are valid and it is not sparse - local n = 0 - for k in pairs(val) do - if type(k) ~= "number" then - error("invalid table: mixed or invalid key types") - end - n = n + 1 - end - if n ~= #val then - error("invalid table: sparse array") - end - -- Encode - for i, v in ipairs(val) do - table.insert(res, encode(v, stack)) - end - stack[val] = nil - return "[" .. table.concat(res, ",") .. "]" - - else - -- Treat as an object - for k, v in pairs(val) do - if type(k) ~= "string" then - error("invalid table: mixed or invalid key types") - end - table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) - end - stack[val] = nil - return "{" .. table.concat(res, ",") .. "}" - end -end - - -local function encode_string(val) - return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' -end - - -local function encode_number(val) - -- Check for NaN, -inf and inf - if val ~= val or val <= -math.huge or val >= math.huge then - error("unexpected number value '" .. tostring(val) .. "'") - end - return string.format("%.14g", val) -end - - -local type_func_map = { - [ "nil" ] = encode_nil, - [ "table" ] = encode_table, - [ "string" ] = encode_string, - [ "number" ] = encode_number, - [ "boolean" ] = tostring, -} - - -encode = function(val, stack) - local t = type(val) - local f = type_func_map[t] - if f then - return f(val, stack) - end - error("unexpected type '" .. t .. "'") -end - - -function json.encode(val) - return ( encode(val) ) -end - - -------------------------------------------------------------------------------- --- Decode -------------------------------------------------------------------------------- - -local parse - -local function create_set(...) - local res = {} - for i = 1, select("#", ...) do - res[ select(i, ...) ] = true - end - return res -end - -local space_chars = create_set(" ", "\t", "\r", "\n") -local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") -local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") -local literals = create_set("true", "false", "null") - -local literal_map = { - [ "true" ] = true, - [ "false" ] = false, - [ "null" ] = nil, -} - - -local function next_char(str, idx, set, negate) - for i = idx, #str do - if set[str:sub(i, i)] ~= negate then - return i - end - end - return #str + 1 -end - - -local function decode_error(str, idx, msg) - --local line_count = 1 - --local col_count = 1 - --for i = 1, idx - 1 do - -- col_count = col_count + 1 - -- if str:sub(i, i) == "\n" then - -- line_count = line_count + 1 - -- col_count = 1 - -- end - -- end - -- emu.message( string.format("%s at line %d col %d", msg, line_count, col_count) ) -end - - -local function codepoint_to_utf8(n) - -- http://scripts.sil.org/cms/scripts/page.php?site_id=nrsi&id=iws-appendixa - local f = math.floor - if n <= 0x7f then - return string.char(n) - elseif n <= 0x7ff then - return string.char(f(n / 64) + 192, n % 64 + 128) - elseif n <= 0xffff then - return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) - elseif n <= 0x10ffff then - return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, - f(n % 4096 / 64) + 128, n % 64 + 128) - end - error( string.format("invalid unicode codepoint '%x'", n) ) -end - - -local function parse_unicode_escape(s) - local n1 = tonumber( s:sub(3, 6), 16 ) - local n2 = tonumber( s:sub(9, 12), 16 ) - -- Surrogate pair? - if n2 then - return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) - else - return codepoint_to_utf8(n1) - end -end - - -local function parse_string(str, i) - local has_unicode_escape = false - local has_surrogate_escape = false - local has_escape = false - local last - for j = i + 1, #str do - local x = str:byte(j) - - if x < 32 then - decode_error(str, j, "control character in string") - end - - if last == 92 then -- "\\" (escape char) - if x == 117 then -- "u" (unicode escape sequence) - local hex = str:sub(j + 1, j + 5) - if not hex:find("%x%x%x%x") then - decode_error(str, j, "invalid unicode escape in string") - end - if hex:find("^[dD][89aAbB]") then - has_surrogate_escape = true - else - has_unicode_escape = true - end - else - local c = string.char(x) - if not escape_chars[c] then - decode_error(str, j, "invalid escape char '" .. c .. "' in string") - end - has_escape = true - end - last = nil - - elseif x == 34 then -- '"' (end of string) - local s = str:sub(i + 1, j - 1) - if has_surrogate_escape then - s = s:gsub("\\u[dD][89aAbB]..\\u....", parse_unicode_escape) - end - if has_unicode_escape then - s = s:gsub("\\u....", parse_unicode_escape) - end - if has_escape then - s = s:gsub("\\.", escape_char_map_inv) - end - return s, j + 1 - - else - last = x - end - end - decode_error(str, i, "expected closing quote for string") -end - - -local function parse_number(str, i) - local x = next_char(str, i, delim_chars) - local s = str:sub(i, x - 1) - local n = tonumber(s) - if not n then - decode_error(str, i, "invalid number '" .. s .. "'") - end - return n, x -end - - -local function parse_literal(str, i) - local x = next_char(str, i, delim_chars) - local word = str:sub(i, x - 1) - if not literals[word] then - decode_error(str, i, "invalid literal '" .. word .. "'") - end - return literal_map[word], x -end - - -local function parse_array(str, i) - local res = {} - local n = 1 - i = i + 1 - while 1 do - local x - i = next_char(str, i, space_chars, true) - -- Empty / end of array? - if str:sub(i, i) == "]" then - i = i + 1 - break - end - -- Read token - x, i = parse(str, i) - res[n] = x - n = n + 1 - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "]" then break end - if chr ~= "," then decode_error(str, i, "expected ']' or ','") end - end - return res, i -end - - -local function parse_object(str, i) - local res = {} - i = i + 1 - while 1 do - local key, val - i = next_char(str, i, space_chars, true) - -- Empty / end of object? - if str:sub(i, i) == "}" then - i = i + 1 - break - end - -- Read key - if str:sub(i, i) ~= '"' then - decode_error(str, i, "expected string for key") - end - key, i = parse(str, i) - -- Read ':' delimiter - i = next_char(str, i, space_chars, true) - if str:sub(i, i) ~= ":" then - decode_error(str, i, "expected ':' after key") - end - i = next_char(str, i + 1, space_chars, true) - -- Read value - val, i = parse(str, i) - -- Set - res[key] = val - -- Next token - i = next_char(str, i, space_chars, true) - local chr = str:sub(i, i) - i = i + 1 - if chr == "}" then break end - if chr ~= "," then decode_error(str, i, "expected '}' or ','") end - end - return res, i -end - - -local char_func_map = { - [ '"' ] = parse_string, - [ "0" ] = parse_number, - [ "1" ] = parse_number, - [ "2" ] = parse_number, - [ "3" ] = parse_number, - [ "4" ] = parse_number, - [ "5" ] = parse_number, - [ "6" ] = parse_number, - [ "7" ] = parse_number, - [ "8" ] = parse_number, - [ "9" ] = parse_number, - [ "-" ] = parse_number, - [ "t" ] = parse_literal, - [ "f" ] = parse_literal, - [ "n" ] = parse_literal, - [ "[" ] = parse_array, - [ "{" ] = parse_object, -} - - -parse = function(str, idx) - local chr = str:sub(idx, idx) - local f = char_func_map[chr] - if f then - return f(str, idx) - end - decode_error(str, idx, "unexpected character '" .. chr .. "'") -end - - -function json.decode(str) - if type(str) ~= "string" then - error("expected argument of type string, got " .. type(str)) - end - return ( parse(str, next_char(str, 1, space_chars, true)) ) -end - - -return json \ No newline at end of file diff --git a/data/lua/TLoZ/socket.lua b/data/lua/TLoZ/socket.lua deleted file mode 100644 index a98e952115..0000000000 --- a/data/lua/TLoZ/socket.lua +++ /dev/null @@ -1,132 +0,0 @@ ------------------------------------------------------------------------------ --- LuaSocket helper module --- Author: Diego Nehab --- RCS ID: $Id: socket.lua,v 1.22 2005/11/22 08:33:29 diego Exp $ ------------------------------------------------------------------------------ - ------------------------------------------------------------------------------ --- Declare module and import dependencies ------------------------------------------------------------------------------ -local base = _G -local string = require("string") -local math = require("math") -local socket = require("socket.core") -module("socket") - ------------------------------------------------------------------------------ --- Exported auxiliar functions ------------------------------------------------------------------------------ -function connect(address, port, laddress, lport) - local sock, err = socket.tcp() - if not sock then return nil, err end - if laddress then - local res, err = sock:bind(laddress, lport, -1) - if not res then return nil, err end - end - local res, err = sock:connect(address, port) - if not res then return nil, err end - return sock -end - -function bind(host, port, backlog) - local sock, err = socket.tcp() - if not sock then return nil, err end - sock:setoption("reuseaddr", true) - local res, err = sock:bind(host, port) - if not res then return nil, err end - res, err = sock:listen(backlog) - if not res then return nil, err end - return sock -end - -try = newtry() - -function choose(table) - return function(name, opt1, opt2) - if base.type(name) ~= "string" then - name, opt1, opt2 = "default", name, opt1 - end - local f = table[name or "nil"] - if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) - else return f(opt1, opt2) end - end -end - ------------------------------------------------------------------------------ --- Socket sources and sinks, conforming to LTN12 ------------------------------------------------------------------------------ --- create namespaces inside LuaSocket namespace -sourcet = {} -sinkt = {} - -BLOCKSIZE = 2048 - -sinkt["close-when-done"] = function(sock) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function(self, chunk, err) - if not chunk then - sock:close() - return 1 - else return sock:send(chunk) end - end - }) -end - -sinkt["keep-open"] = function(sock) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function(self, chunk, err) - if chunk then return sock:send(chunk) - else return 1 end - end - }) -end - -sinkt["default"] = sinkt["keep-open"] - -sink = choose(sinkt) - -sourcet["by-length"] = function(sock, length) - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function() - if length <= 0 then return nil end - local size = math.min(socket.BLOCKSIZE, length) - local chunk, err = sock:receive(size) - if err then return nil, err end - length = length - string.len(chunk) - return chunk - end - }) -end - -sourcet["until-closed"] = function(sock) - local done - return base.setmetatable({ - getfd = function() return sock:getfd() end, - dirty = function() return sock:dirty() end - }, { - __call = function() - if done then return nil end - local chunk, err, partial = sock:receive(socket.BLOCKSIZE) - if not err then return chunk - elseif err == "closed" then - sock:close() - done = 1 - return partial - else return nil, err end - end - }) -end - - -sourcet["default"] = sourcet["until-closed"] - -source = choose(sourcet) diff --git a/data/lua/common.lua b/data/lua/common.lua new file mode 100644 index 0000000000..4df2ab8470 --- /dev/null +++ b/data/lua/common.lua @@ -0,0 +1,102 @@ +local lua_major, lua_minor = _VERSION:match("Lua (%d+)%.(%d+)") +lua_major = tonumber(lua_major) +lua_minor = tonumber(lua_minor) +-- lua compat shims +if lua_major > 5 or (lua_major == 5 and lua_minor >= 3) then + require("lua_5_3_compat") +end + +function table.empty (self) + for _, _ in pairs(self) do + return false + end + return true +end + +local bizhawk_version = client.getversion() +local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)") +bizhawk_major = tonumber(bizhawk_major) +bizhawk_minor = tonumber(bizhawk_minor) +if bizhawk_patch == "" then + bizhawk_patch = 0 +else + bizhawk_patch = tonumber(bizhawk_patch) +end + +local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_major == 2 and bizhawk_minor >= 3 and bizhawk_minor <= 5) +local isGreaterOrEqualTo26 = bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor >= 6) +local isUntestedBizhawk = bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor > 9) +local untestedBizhawkMessage = "Warning: this version of bizhawk is newer than we know about. If it doesn't work, consider downgrading to 2.9" + +u8 = memory.read_u8 +wU8 = memory.write_u8 +u16 = memory.read_u16_le + +function getMaxMessageLength() + local denominator = 12 + if is23Or24Or25 then + denominator = 11 + end + return math.floor(client.screenwidth()/denominator) +end + +function drawText(x, y, message, color) + if is23Or24Or25 then + gui.addmessage(message) + elseif isGreaterOrEqualTo26 then + gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", "middle", "bottom", nil, "client") + end +end + +function clearScreen() + if is23Or24Or25 then + return + elseif isGreaterOrEqualTo26 then + drawText(0, 0, "", "black") + end +end + +itemMessages = {} + +function drawMessages() + if table.empty(itemMessages) then + clearScreen() + return + end + local y = 10 + found = false + maxMessageLength = getMaxMessageLength() + for k, v in pairs(itemMessages) do + if v["TTL"] > 0 then + message = v["message"] + while true do + drawText(5, y, message:sub(1, maxMessageLength), v["color"]) + y = y + 16 + + message = message:sub(maxMessageLength + 1, message:len()) + if message:len() == 0 then + break + end + end + newTTL = 0 + if isGreaterOrEqualTo26 then + newTTL = itemMessages[k]["TTL"] - 1 + end + itemMessages[k]["TTL"] = newTTL + found = true + end + end + if found == false then + clearScreen() + end +end + +function checkBizhawkVersion() + if not is23Or24Or25 and not isGreaterOrEqualTo26 then + print("Must use a version of bizhawk 2.3.1 or higher") + return false + elseif isUntestedBizhawk then + print(untestedBizhawkMessage) + end + return true +end \ No newline at end of file diff --git a/data/lua/ADVENTURE/adventure_connector.lua b/data/lua/connector_adventure.lua similarity index 91% rename from data/lua/ADVENTURE/adventure_connector.lua rename to data/lua/connector_adventure.lua index 598d6d74ff..8ad62bf5cb 100644 --- a/data/lua/ADVENTURE/adventure_connector.lua +++ b/data/lua/connector_adventure.lua @@ -1,6 +1,7 @@ local socket = require("socket") local json = require('json') local math = require('math') +require("common") local STATE_OK = "Ok" local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" @@ -32,8 +33,6 @@ local frames_with_no_item = 0 local ItemTableStart = 0xfe9d local PlayerSlotAddress = 0xfff9 -local itemMessages = {} - local nullObjectId = 0xB4 local ItemsReceived = nil local sha256hash = nil @@ -101,17 +100,6 @@ local current_bat_ap_item = nil local was_in_number_room = false -local u8 = nil -local wU8 = nil -local u16 - -local bizhawk_version = client.getversion() -local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5") -local is26To28 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7") or (bizhawk_version:sub(1,3)=="2.8") - -u8 = memory.read_u8 -wU8 = memory.write_u8 -u16 = memory.read_u16_le function uRangeRam(address, bytes) data = memory.read_bytes_as_array(address, bytes, "Main RAM") return data @@ -125,23 +113,6 @@ function uRangeAddress(address, bytes) return data end - -function table.empty (self) - for _, _ in pairs(self) do - return false - end - return true -end - -function slice (tbl, s, e) - local pos, new = 1, {} - for i = s + 1, e do - new[pos] = tbl[i] - pos = pos + 1 - end - return new -end - local function createForeignItemsByRoom() foreign_items_by_room = {} if foreign_items == nil then @@ -294,94 +265,11 @@ function processBlock(block) end end -local function clearScreen() - if is23Or24Or25 then - return - elseif is26To28 then - drawText(0, 0, "", "black") - end -end - -local function getMaxMessageLength() - if is23Or24Or25 then - return client.screenwidth()/11 - elseif is26To28 then - return client.screenwidth()/12 - end -end - -function drawText(x, y, message, color) - if is23Or24Or25 then - gui.addmessage(message) - elseif is26To28 then - gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", nil, nil, nil, "client") - end -end - -local function drawMessages() - if table.empty(itemMessages) then - clearScreen() - return - end - local y = 10 - found = false - maxMessageLength = getMaxMessageLength() - for k, v in pairs(itemMessages) do - if v["TTL"] > 0 then - message = v["message"] - while true do - drawText(5, y, message:sub(1, maxMessageLength), v["color"]) - y = y + 16 - - message = message:sub(maxMessageLength + 1, message:len()) - if message:len() == 0 then - break - end - end - newTTL = 0 - if is26To28 then - newTTL = itemMessages[k]["TTL"] - 1 - end - itemMessages[k]["TTL"] = newTTL - found = true - end - end - if found == false then - clearScreen() - end -end - -function difference(a, b) - local aa = {} - for k,v in pairs(a) do aa[v]=true end - for k,v in pairs(b) do aa[v]=nil end - local ret = {} - local n = 0 - for k,v in pairs(a) do - if aa[v] then n=n+1 ret[n]=v end - end - return ret -end - function getAllRam() uRangeRAM(0,128); return data end -local function arrayEqual(a1, a2) - if #a1 ~= #a2 then - return false - end - - for i, v in ipairs(a1) do - if v ~= a2[i] then - return false - end - end - - return true -end - local function alive_mode() return (u8(PlayerRoomAddr) ~= 0x00 and u8(WinAddr) == 0x00) end @@ -569,8 +457,7 @@ end function main() memory.usememorydomain("System Bus") - if (is23Or24Or25 or is26To28) == false then - print("Must use a version of bizhawk 2.3.1 or higher") + if not checkBizhawkVersion() then return end local playerSlot = memory.read_u8(PlayerSlotAddress) diff --git a/data/lua/FF1/ff1_connector.lua b/data/lua/connector_ff1.lua similarity index 85% rename from data/lua/FF1/ff1_connector.lua rename to data/lua/connector_ff1.lua index 6b2eec269a..95f94a06dc 100644 --- a/data/lua/FF1/ff1_connector.lua +++ b/data/lua/connector_ff1.lua @@ -1,6 +1,7 @@ local socket = require("socket") local json = require('json') local math = require('math') +require("common") local STATE_OK = "Ok" local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" @@ -102,15 +103,12 @@ local noOverworldItemsLookup = { [500] = 0x12, } -local itemMessages = {} local consumableStacks = nil local prevstate = "" local curstate = STATE_UNINITIALIZED local ff1Socket = nil local frame = 0 -local u8 = nil -local wU8 = nil local isNesHawk = false @@ -134,9 +132,6 @@ local function defineMemoryFunctions() end local memDomain = defineMemoryFunctions() -u8 = memory.read_u8 -wU8 = memory.write_u8 -uRange = memory.readbyterange local function StateOKForMainLoop() memDomain.saveram() @@ -146,83 +141,6 @@ local function StateOKForMainLoop() return A ~= 0x00 and not (A== 0xF2 and B == 0xF2 and C == 0xF2) end -function table.empty (self) - for _, _ in pairs(self) do - return false - end - return true -end - -function slice (tbl, s, e) - local pos, new = 1, {} - for i = s + 1, e do - new[pos] = tbl[i] - pos = pos + 1 - end - return new -end - -local bizhawk_version = client.getversion() -local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5") -local is26To28 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7") or (bizhawk_version:sub(1,3)=="2.8") - -local function getMaxMessageLength() - if is23Or24Or25 then - return client.screenwidth()/11 - elseif is26To28 then - return client.screenwidth()/12 - end -end - -local function drawText(x, y, message, color) - if is23Or24Or25 then - gui.addmessage(message) - elseif is26To28 then - gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", nil, nil, nil, "client") - end -end - -local function clearScreen() - if is23Or24Or25 then - return - elseif is26To28 then - drawText(0, 0, "", "black") - end -end - -local function drawMessages() - if table.empty(itemMessages) then - clearScreen() - return - end - local y = 10 - found = false - maxMessageLength = getMaxMessageLength() - for k, v in pairs(itemMessages) do - if v["TTL"] > 0 then - message = v["message"] - while true do - drawText(5, y, message:sub(1, maxMessageLength), v["color"]) - y = y + 16 - - message = message:sub(maxMessageLength + 1, message:len()) - if message:len() == 0 then - break - end - end - newTTL = 0 - if is26To28 then - newTTL = itemMessages[k]["TTL"] - 1 - end - itemMessages[k]["TTL"] = newTTL - found = true - end - end - if found == false then - clearScreen() - end -end - function generateLocationChecked() memDomain.saveram() data = uRange(0x01FF, 0x101) @@ -316,7 +234,14 @@ function getEmptyArmorSlots() end return ret end - +local function slice (tbl, s, e) + local pos, new = 1, {} + for i = s + 1, e do + new[pos] = tbl[i] + pos = pos + 1 + end + return new +end function processBlock(block) local msgBlock = block['messages'] if msgBlock ~= nil then @@ -448,18 +373,6 @@ function processBlock(block) end end -function difference(a, b) - local aa = {} - for k,v in pairs(a) do aa[v]=true end - for k,v in pairs(b) do aa[v]=nil end - local ret = {} - local n = 0 - for k,v in pairs(a) do - if aa[v] then n=n+1 ret[n]=v end - end - return ret -end - function receive() l, e = ff1Socket:receive() if e == 'closed' then @@ -501,8 +414,7 @@ function receive() end function main() - if (is23Or24Or25 or is26To28) == false then - print("Must use a version of bizhawk 2.3.1 or higher") + if not checkBizhawkVersion() then return end server, error = socket.bind('localhost', 52980) diff --git a/data/lua/OOT/oot_connector.lua b/data/lua/connector_oot.lua similarity index 99% rename from data/lua/OOT/oot_connector.lua rename to data/lua/connector_oot.lua index cfcf6e334d..a5248e5ba4 100644 --- a/data/lua/OOT/oot_connector.lua +++ b/data/lua/connector_oot.lua @@ -1,8 +1,9 @@ local socket = require("socket") local json = require('json') local math = require('math') +require('common.lua') -local last_modified_date = '2022-11-27' -- Should be the last modified date +local last_modified_date = '2022-4-9' -- Should be the last modified date local script_version = 3 -------------------------------------------------- @@ -1861,8 +1862,7 @@ function receive() end function main() - if (is23Or24Or25 or is26To27) == false then - print("Must use a version of bizhawk 2.3.1 or higher") + if not checkBizhawkVersion() then return end server, error = socket.bind('localhost', 28921) @@ -1886,7 +1886,7 @@ function main() ootSocket = client ootSocket:settimeout(0) else - print('Connection failed, ensure OoTClient is running and rerun oot_connector.lua') + print('Connection failed, ensure OoTClient is running and rerun connector_oot.lua') return end end diff --git a/data/lua/PKMN_RB/pkmn_rb.lua b/data/lua/connector_pkmn_rb.lua similarity index 89% rename from data/lua/PKMN_RB/pkmn_rb.lua rename to data/lua/connector_pkmn_rb.lua index 036f7a6255..1c214c4a74 100644 --- a/data/lua/PKMN_RB/pkmn_rb.lua +++ b/data/lua/connector_pkmn_rb.lua @@ -1,7 +1,7 @@ local socket = require("socket") local json = require('json') local math = require('math') - +require("common") local STATE_OK = "Ok" local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made" @@ -32,9 +32,6 @@ local curstate = STATE_UNINITIALIZED local gbSocket = nil local frame = 0 -local u8 = nil -local wU8 = nil -local u16 local compat = nil local function defineMemoryFunctions() @@ -55,35 +52,6 @@ function uRange(address, bytes) return data end - -function table.empty (self) - for _, _ in pairs(self) do - return false - end - return true -end - -function slice (tbl, s, e) - local pos, new = 1, {} - for i = s + 1, e do - new[pos] = tbl[i] - pos = pos + 1 - end - return new -end - -function difference(a, b) - local aa = {} - for k,v in pairs(a) do aa[v]=true end - for k,v in pairs(b) do aa[v]=nil end - local ret = {} - local n = 0 - for k,v in pairs(a) do - if aa[v] then n=n+1 ret[n]=v end - end - return ret -end - function generateLocationsChecked() memDomain.wram() events = uRange(EventFlagAddress, 0x140) @@ -106,17 +74,17 @@ function generateLocationsChecked() end local function arrayEqual(a1, a2) - if #a1 ~= #a2 then - return false - end - - for i, v in ipairs(a1) do - if v ~= a2[i] then + if #a1 ~= #a2 then return false end - end - - return true + + for i, v in ipairs(a1) do + if v ~= a2[i] then + return false + end + end + + return true end function receive() @@ -196,8 +164,7 @@ function receive() end function main() - if (is23Or24Or25 or is26To28) == false then - print("Must use a version of bizhawk 2.3.1 or higher") + if not checkBizhawkVersion() then return end server, error = socket.bind('localhost', 17242) diff --git a/data/lua/TLoZ/TheLegendOfZeldaConnector.lua b/data/lua/connector_tloz.lua similarity index 87% rename from data/lua/TLoZ/TheLegendOfZeldaConnector.lua rename to data/lua/connector_tloz.lua index ac33ed3cc4..1764f5487c 100644 --- a/data/lua/TLoZ/TheLegendOfZeldaConnector.lua +++ b/data/lua/connector_tloz.lua @@ -3,13 +3,12 @@ local socket = require("socket") local json = require('json') local math = require('math') - +require("common") local STATE_OK = "Ok" local STATE_TENTATIVELY_CONNECTED = "Tentatively Connected" local STATE_INITIAL_CONNECTION_MADE = "Initial Connection Made" local STATE_UNINITIALIZED = "Uninitialized" -local itemMessages = {} local consumableStacks = nil local prevstate = "" local curstate = STATE_UNINITIALIZED @@ -21,8 +20,6 @@ local cave_index local triforce_byte local game_state -local u8 = nil -local wU8 = nil local isNesHawk = false local shopsChecked = {} @@ -420,83 +417,6 @@ local function checkCaveItemObtained() return returnTable end -function table.empty (self) - for _, _ in pairs(self) do - return false - end - return true -end - -function slice (tbl, s, e) - local pos, new = 1, {} - for i = s + 1, e do - new[pos] = tbl[i] - pos = pos + 1 - end - return new -end - -local bizhawk_version = client.getversion() -local is23Or24Or25 = (bizhawk_version=="2.3.1") or (bizhawk_version:sub(1,3)=="2.4") or (bizhawk_version:sub(1,3)=="2.5") -local is26To28 = (bizhawk_version:sub(1,3)=="2.6") or (bizhawk_version:sub(1,3)=="2.7") or (bizhawk_version:sub(1,3)=="2.8") - -local function getMaxMessageLength() - if is23Or24Or25 then - return client.screenwidth()/11 - elseif is26To28 then - return client.screenwidth()/12 - end -end - -local function drawText(x, y, message, color) - if is23Or24Or25 then - gui.addmessage(message) - elseif is26To28 then - gui.drawText(x, y, message, color, 0xB0000000, 18, "Courier New", "middle", "bottom", nil, "client") - end -end - -local function clearScreen() - if is23Or24Or25 then - return - elseif is26To28 then - drawText(0, 0, "", "black") - end -end - -local function drawMessages() - if table.empty(itemMessages) then - clearScreen() - return - end - local y = 10 - found = false - maxMessageLength = getMaxMessageLength() - for k, v in pairs(itemMessages) do - if v["TTL"] > 0 then - message = v["message"] - while true do - drawText(5, y, message:sub(1, maxMessageLength), v["color"]) - y = y + 16 - - message = message:sub(maxMessageLength + 1, message:len()) - if message:len() == 0 then - break - end - end - newTTL = 0 - if is26To28 then - newTTL = itemMessages[k]["TTL"] - 1 - end - itemMessages[k]["TTL"] = newTTL - found = true - end - end - if found == false then - clearScreen() - end -end - function generateOverworldLocationChecked() memDomain.ram() data = uRange(0x067E, 0x81) @@ -589,18 +509,6 @@ function processBlock(block) end end -function difference(a, b) - local aa = {} - for k,v in pairs(a) do aa[v]=true end - for k,v in pairs(b) do aa[v]=nil end - local ret = {} - local n = 0 - for k,v in pairs(a) do - if aa[v] then n=n+1 ret[n]=v end - end - return ret -end - function receive() l, e = zeldaSocket:receive() if e == 'closed' then @@ -653,8 +561,7 @@ function receive() end function main() - if (is23Or24Or25 or is26To28) == false then - print("Must use a version of bizhawk 2.3.1 or higher") + if not checkBizhawkVersion() then return end server, error = socket.bind('localhost', 52980) diff --git a/data/lua/core.dll b/data/lua/core.dll deleted file mode 100644 index 3e9569571ab0947dcb7bcd789dc9c06c009d072d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29184 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 B{xARl diff --git a/data/lua/ADVENTURE/json.lua b/data/lua/json.lua similarity index 100% rename from data/lua/ADVENTURE/json.lua rename to data/lua/json.lua diff --git a/data/lua/lua_5_3_compat.lua b/data/lua/lua_5_3_compat.lua new file mode 100644 index 0000000000..0d9990a430 --- /dev/null +++ b/data/lua/lua_5_3_compat.lua @@ -0,0 +1,12 @@ +function bit.rshift(a, b) + return a >> b +end +function bit.lshift(a, b) + return a << b +end +function bit.bor(a, b) + return a | b +end +function bit.band(a, b) + return a & b +end \ No newline at end of file diff --git a/data/lua/socket.lua b/data/lua/socket.lua index a98e952115..696f53a327 100644 --- a/data/lua/socket.lua +++ b/data/lua/socket.lua @@ -10,8 +10,53 @@ local base = _G local string = require("string") local math = require("math") -local socket = require("socket.core") -module("socket") + +function get_lua_version() + local major, minor = _VERSION:match("Lua (%d+)%.(%d+)") + assert(tonumber(major) == 5) + if tonumber(minor) >= 4 then + return "5-4" + end + return "5-1" +end + +function get_os() + local the_os, ext, arch + if package.config:sub(1,1) == "\\" then + the_os, ext = "windows", "dll" + arch = os.getenv"PROCESSOR_ARCHITECTURE" + else + -- TODO: macos? + the_os, ext = "linux", "so" + arch = "x86_64" -- TODO: read ELF header from /proc/$PID/exe to get arch + end + + if arch:find("64") ~= nil then + arch = "x64" + else + arch = "x86" + end + + return the_os, ext, arch +end + +function get_socket_path() + local the_os, ext, arch = get_os() + -- for some reason ./ isn't working, so use a horrible hack to get the pwd + local pwd = (io.popen and io.popen("cd"):read'*l') or "." + return pwd .. "/" .. arch .. "/socket-" .. the_os .. "-" .. get_lua_version() .. "." .. ext +end + +local socket_path = get_socket_path() +local socket = assert(package.loadlib(socket_path, "luaopen_socket_core"))() + +-- http://lua-users.org/wiki/ModulesTutorial +local M = {} +if setfenv then + setfenv(1, M) -- for 5.1 +else + _ENV = M -- for 5.2 +end ----------------------------------------------------------------------------- -- Exported auxiliar functions @@ -39,7 +84,7 @@ function bind(host, port, backlog) return sock end -try = newtry() +try = socket.newtry() function choose(table) return function(name, opt1, opt2) @@ -130,3 +175,5 @@ end sourcet["default"] = sourcet["until-closed"] source = choose(sourcet) + +return M diff --git a/data/lua/x64/luasocket.LICENSE.txt b/data/lua/x64/luasocket.LICENSE.txt new file mode 100644 index 0000000000..ff5c6a73c0 --- /dev/null +++ b/data/lua/x64/luasocket.LICENSE.txt @@ -0,0 +1,20 @@ +LuaSocket 3.0 license +Copyright � 2004-2013 Diego Nehab + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/data/lua/x64/socket-linux-5-1.so b/data/lua/x64/socket-linux-5-1.so new file mode 100644 index 0000000000000000000000000000000000000000..df95403fcdb5c354197b1bd6d5a37f20cbaa51bc GIT binary patch literal 70808 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 literal 0 HcmV?d00001 diff --git a/data/lua/x64/socket-linux-5-4.so b/data/lua/x64/socket-linux-5-4.so new file mode 100644 index 0000000000000000000000000000000000000000..059899d21c106fc3c7ea8ad68d466297240bb786 GIT binary patch literal 81104 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 OqC^!eoV-3XGw>hb3TzMn literal 0 HcmV?d00001 diff --git a/data/lua/x64/socket-windows-5-4.dll b/data/lua/x64/socket-windows-5-4.dll new file mode 100644 index 0000000000000000000000000000000000000000..896b3cbacd5e05ed2d5f5b4e02b61d33dd8a3e82 GIT binary patch literal 51712 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 G1^y1ZM6uBT literal 0 HcmV?d00001 diff --git a/data/lua/x86/luasocket.LICENSE.txt b/data/lua/x86/luasocket.LICENSE.txt new file mode 100644 index 0000000000..ff5c6a73c0 --- /dev/null +++ b/data/lua/x86/luasocket.LICENSE.txt @@ -0,0 +1,20 @@ +LuaSocket 3.0 license +Copyright � 2004-2013 Diego Nehab + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/data/lua/x86/socket-windows-5-1.dll b/data/lua/x86/socket-windows-5-1.dll new file mode 100644 index 0000000000000000000000000000000000000000..2255fed700af24f8e0ab3ad71582ff7ab4ffd835 GIT binary patch literal 40448 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 Z;;_uKEU+xHcq~1je*jVOXxsn* literal 0 HcmV?d00001 diff --git a/host.yaml b/host.yaml index fd5c759032..463ffc39b3 100644 --- a/host.yaml +++ b/host.yaml @@ -181,7 +181,7 @@ adventure_options: # For example, this can be used to autoload the connector script in BizHawk # (see BizHawk --lua= option) # Windows example: - # rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/ADVENTURE/adventure_connector.lua" + # rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/connector_adventure.lua" rom_args: " " # Set this to true to display item received messages in Emuhawk display_msgs: true diff --git a/worlds/adventure/docs/setup_en.md b/worlds/adventure/docs/setup_en.md index 658162d8c2..3afa4544b0 100644 --- a/worlds/adventure/docs/setup_en.md +++ b/worlds/adventure/docs/setup_en.md @@ -27,10 +27,10 @@ Once Bizhawk has been installed, open Bizhawk and change the following settings: BizHawk is running in the background. - It is recommended that you provide a path to BizHawk in your host.yaml for Adventure so the client can start it automatically -- At the same time, you can set an option to automatically load the adventure_connector.lua script when launching BizHawk +- At the same time, you can set an option to automatically load the connector_adventure.lua script when launching BizHawk from AdventureClient. Default Windows install example: -```rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/ADVENTURE/adventure_connector.lua"``` +```rom_args: "--lua=C:/ProgramData/Archipelago/data/lua/connector_adventure.lua"``` ## Configuring your YAML file @@ -66,7 +66,7 @@ path as recommended). Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools" menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script. -Navigate to your Archipelago install folder and open `data/lua/ADVENTURE/adventure_connector.lua`, if it is not +Navigate to your Archipelago install folder and open `data/lua/connector_adventure.lua`, if it is not configured to do this automatically. To connect the client to the multiserver simply put `
:` on the textfield on top and press enter (if the diff --git a/worlds/ff1/docs/multiworld_en.md b/worlds/ff1/docs/multiworld_en.md index 1c943fcf66..a827fb6e7d 100644 --- a/worlds/ff1/docs/multiworld_en.md +++ b/worlds/ff1/docs/multiworld_en.md @@ -58,7 +58,7 @@ Once the Archipelago server has been hosted: extension `*.nes` 2. Click on the Tools menu and click on **Lua Console** 3. Click the folder button to open a new Lua script. (CTL-O or **Script** -> **Open Script**) -4. Navigate to the location you installed Archipelago to. Open data/lua/FF1/ff1_connector.lua +4. Navigate to the location you installed Archipelago to. Open data/lua/connector_ff1.lua 1. If it gives a `NLua.Exceptions.LuaScriptException: .\socket.lua:13: module 'socket.core' not found:` exception close your emulator entirely, restart it and re-run these steps 2. If it says `Must use a version of bizhawk 2.3.1 or higher`, double-check your Bizhawk version by clicking ** diff --git a/worlds/oot/docs/setup_en.md b/worlds/oot/docs/setup_en.md index 2c652ff62f..414271d8e6 100644 --- a/worlds/oot/docs/setup_en.md +++ b/worlds/oot/docs/setup_en.md @@ -414,7 +414,7 @@ to the emulator as recommended). Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools" menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script. -Navigate to your Archipelago install folder and open `data/lua/OOT/oot_connector.lua`. +Navigate to your Archipelago install folder and open `data/lua/connector_oot.lua`. To connect the client to the multiserver simply put `
:` on the textfield on top and press enter (if the server uses password, type in the bottom textfield `/connect
: [password]`) diff --git a/worlds/oot/docs/setup_fr.md b/worlds/oot/docs/setup_fr.md index 6248f8c44b..37df146def 100644 --- a/worlds/oot/docs/setup_fr.md +++ b/worlds/oot/docs/setup_fr.md @@ -414,9 +414,9 @@ Double-cliquez sur votre fichier `.apz5` pour démarrer votre client et démarre 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. -Accédez à votre dossier d'installation Archipelago et ouvrez `data/lua/OOT/oot_connector.lua`. +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 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. \ No newline at end of file +Vous êtes maintenant prêt à commencer votre aventure à Hyrule. diff --git a/worlds/pokemon_rb/docs/setup_en.md b/worlds/pokemon_rb/docs/setup_en.md index 3c54cf9008..92c12de1bd 100644 --- a/worlds/pokemon_rb/docs/setup_en.md +++ b/worlds/pokemon_rb/docs/setup_en.md @@ -86,7 +86,7 @@ to the emulator as recommended). Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools" menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script. -Navigate to your Archipelago install folder and open `data/lua/PKMN_RB/pkmr_rb.lua`. +Navigate to your Archipelago install folder and open `data/lua/connector_pkmn_rb.lua`. To connect the client to the multiserver simply put `
:` on the textfield on top and press enter (if the server uses password, type in the bottom textfield `/connect
: [password]`) diff --git a/worlds/tloz/docs/multiworld_en.md b/worlds/tloz/docs/multiworld_en.md index 031d4eee87..ab3acf0b29 100644 --- a/worlds/tloz/docs/multiworld_en.md +++ b/worlds/tloz/docs/multiworld_en.md @@ -80,7 +80,7 @@ Once the Archipelago server has been hosted: extension `*.nes`. 2. Click on the Tools menu and click on **Lua Console**. 3. Click the folder button to open a new Lua script. (CTL-O or **Script** -> **Open Script**) -4. Navigate to the location you installed Archipelago to. Open `data/lua/TLOZ/tloz_connector.lua`. +4. Navigate to the location you installed Archipelago to. Open `data/lua/connector_tloz.lua`. 1. If it gives a `NLua.Exceptions.LuaScriptException: .\socket.lua:13: module 'socket.core' not found:` exception close your emulator entirely, restart it and re-run these steps. 2. If it says `Must use a version of bizhawk 2.3.1 or higher`, double-check your Bizhawk version by clicking ** From b0e8c8db6b2bb8bc6a6cb0c239dc6f9008bbd09d Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sat, 15 Apr 2023 17:10:33 +0200 Subject: [PATCH 046/489] MultiServer: fix broken ping (#1711) Ping argument seems to have changed in an update of websockets. This uses the lib's default of 20s now. --- MultiServer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 57f93d6713..59c2975e12 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -2272,8 +2272,7 @@ async def main(args: argparse.Namespace): ssl_context = load_server_cert(args.cert, args.cert_key) if args.cert else None - ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ping_timeout=None, - ping_interval=None, ssl=ssl_context) + ctx.server = websockets.serve(functools.partial(server, ctx=ctx), host=ctx.host, port=ctx.port, ssl=ssl_context) ip = args.host if args.host else Utils.get_public_ipv4() logging.info('Hosting game at %s:%d (%s)' % (ip, ctx.port, 'No password' if not ctx.password else 'Password: %s' % ctx.password)) From 27cb93d3194a84efe2e90c2de95ad114db2cea45 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Sat, 15 Apr 2023 08:37:31 -0700 Subject: [PATCH 047/489] Asset: make icon 512 x 512 (#1710) --- data/icon.png | Bin 109228 -> 37921 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/data/icon.png b/data/icon.png index 4fd9334dff842e24b3de9d24ffd3fd6b6bdd2db5..1d660434736b5fa681b664a4f0e2da8f42a9cf39 100644 GIT binary patch literal 37921 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

Vx&N*+6IkU@hR{|;#_EqfvVAtt|^9Tf*mz-0l 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 M+2*I*A6dWuAJ?zx-2eap From d85c13ef0ede7ac78a0a23da3567dfe95e5c8116 Mon Sep 17 00:00:00 2001 From: JusticePS <5125765+JusticePS@users.noreply.github.com> Date: Sat, 15 Apr 2023 07:28:37 -0700 Subject: [PATCH 048/489] Adventure: Fix error in connector lua that was fairly harmless in BizHawk 2.8 but throws in 2.9 --- data/lua/connector_adventure.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/lua/connector_adventure.lua b/data/lua/connector_adventure.lua index 8ad62bf5cb..d5b1898284 100644 --- a/data/lua/connector_adventure.lua +++ b/data/lua/connector_adventure.lua @@ -598,7 +598,7 @@ function main() if ( localItemLocations ~= nil and localItemLocations[tostring(carry_item)] ~= nil ) then pending_local_items_collected[localItemLocations[tostring(carry_item)]] = localItemLocations[tostring(carry_item)] - table.remove(localItemLocations, tostring(carry_item)) + localItemLocations[tostring(carry_item)] = nil skip_inventory_items[carry_item] = carry_item end end From 3d8bc0bb678fe4ac68b875f6fb685150b5b31ee0 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Sat, 15 Apr 2023 15:17:23 -0400 Subject: [PATCH 049/489] KH2: Init Cleanup and Keyblade Fix (#1713) --- worlds/kh2/__init__.py | 61 +++++++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index 06036c6803..cc50c86eb1 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -33,6 +33,7 @@ class KH2World(World): game: str = "Kingdom Hearts 2" web = KingdomHearts2Web() data_version = 1 + required_client_version = (0, 4, 0) option_definitions = KH2_Options item_name_to_id = {name: data.code for name, data in item_dictionary_table.items()} location_name_to_id = {item_name: data.code for item_name, data in all_locations.items() if data.code} @@ -40,6 +41,7 @@ class KH2World(World): def __init__(self, multiworld: "MultiWorld", player: int): super().__init__(multiworld, player) + self.valid_abilities = None self.visitlocking_dict = None self.plando_locations = None self.luckyemblemamount = None @@ -81,8 +83,6 @@ class KH2World(World): return created_item def create_items(self) -> None: - itempool: typing.List[Item] = [] - self.visitlocking_dict = Progression_Dicts["AllVisitLocking"].copy() if self.multiworld.Schmovement[self.player] != "level_0": for _ in range(self.multiworld.Schmovement[self.player].value): @@ -128,9 +128,7 @@ class KH2World(World): self.visitlocking_dict.pop(item) self.multiworld.push_precollected(self.create_item(item)) - for item in item_dictionary_table: - data = self.item_quantity_dict[item] - itempool += [self.create_item(item) for _ in range(data)] + itempool = [self.create_item(item) for item, data in self.item_quantity_dict.items() for _ in range(data)] # Creating filler for unfilled locations itempool += [self.create_filler() @@ -139,14 +137,12 @@ class KH2World(World): def generate_early(self) -> None: # Item Quantity dict because Abilities can be a problem for KH2's Software. - for item, data in item_dictionary_table.items(): - self.item_quantity_dict[item] = data.quantity + self.item_quantity_dict = {item: data.quantity for item, data in item_dictionary_table.items()} # Dictionary to mark locations with their plandoed item # Example. Final Xemnas: Victory self.plando_locations = dict() self.hitlist = [] self.starting_invo_verify() - # Option to turn off Promise Charm Item if not self.multiworld.Promise_Charm[self.player]: self.item_quantity_dict[ItemName.PromiseCharm] = 0 @@ -243,43 +239,64 @@ class KH2World(World): def keyblade_fill(self): if self.multiworld.KeybladeAbilities[self.player] == "support": - self.sora_keyblade_ability_pool.extend(SupportAbility_Table.keys()) + self.sora_keyblade_ability_pool = { + **{item: data for item, data in self.item_quantity_dict.items() if item in SupportAbility_Table}, + **{ItemName.NegativeCombo: 1, ItemName.AirComboPlus: 1, ItemName.ComboPlus: 1, + ItemName.FinishingPlus: 1}} + elif self.multiworld.KeybladeAbilities[self.player] == "action": - self.sora_keyblade_ability_pool.extend(ActionAbility_Table.keys()) + self.sora_keyblade_ability_pool = {item: data for item, data in self.item_quantity_dict.items() if item in ActionAbility_Table} + # there are too little action abilities so 2 random support abilities are placed + for _ in range(3): + randomSupportAbility = self.multiworld.per_slot_randoms[self.player].choice(list(SupportAbility_Table.keys())) + while randomSupportAbility in self.sora_keyblade_ability_pool: + randomSupportAbility = self.multiworld.per_slot_randoms[self.player].choice( + list(SupportAbility_Table.keys())) + self.sora_keyblade_ability_pool[randomSupportAbility] = 1 else: # both action and support on keyblades. # TODO: make option to just exclude scom - self.sora_keyblade_ability_pool.extend(ActionAbility_Table.keys()) - self.sora_keyblade_ability_pool.extend(SupportAbility_Table.keys()) + self.sora_keyblade_ability_pool = { + **{item: data for item, data in self.item_quantity_dict.items() if item in SupportAbility_Table}, + **{item: data for item, data in self.item_quantity_dict.items() if item in ActionAbility_Table}, + **{ItemName.NegativeCombo: 1, ItemName.AirComboPlus: 1, ItemName.ComboPlus: 1, ItemName.FinishingPlus: 1}} for ability in self.multiworld.BlacklistKeyblade[self.player].value: if ability in self.sora_keyblade_ability_pool: - self.sora_keyblade_ability_pool.remove(ability) + self.sora_keyblade_ability_pool.pop(ability) - while len(self.sora_keyblade_ability_pool) < len(self.keyblade_slot_copy): - self.sora_keyblade_ability_pool.append( - self.multiworld.per_slot_randoms[self.player].choice(list(SupportAbility_Table.keys()))) + # magic number for amount of keyblades + if sum(self.sora_keyblade_ability_pool.values()) < 28: + raise Exception(f"{self.multiworld.get_file_safe_player_name(self.player)} has too little Keyblade Abilities in the Keyblade Pool") + self.valid_abilities = list(self.sora_keyblade_ability_pool.keys()) # Kingdom Key cannot have No Experience so plandoed here instead of checking 26 times if its kingdom key - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.valid_abilities) while random_ability == ItemName.NoExperience: - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.validAbilitys) self.plando_locations[LocationName.KingdomKeySlot] = random_ability self.item_quantity_dict[random_ability] -= 1 - self.sora_keyblade_ability_pool.remove(random_ability) + self.sora_keyblade_ability_pool[random_ability] -= 1 + if self.sora_keyblade_ability_pool[random_ability] == 0: + self.valid_abilities.remove(random_ability) + self.sora_keyblade_ability_pool.pop(random_ability) # plando keyblades because they can only have abilities for keyblade in self.keyblade_slot_copy: - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.sora_keyblade_ability_pool) + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.valid_abilities) self.plando_locations[keyblade] = random_ability self.item_quantity_dict[random_ability] -= 1 - self.sora_keyblade_ability_pool.remove(random_ability) + self.sora_keyblade_ability_pool[random_ability] -= 1 + if self.sora_keyblade_ability_pool[random_ability] == 0: + self.valid_abilities.remove(random_ability) + self.sora_keyblade_ability_pool.pop(random_ability) self.totalLocations -= 1 def starting_invo_verify(self): for item, value in self.multiworld.start_inventory[self.player].value.items(): if item in ActionAbility_Table \ - or item in SupportAbility_Table or exclusionItem_table["StatUps"]: + or item in SupportAbility_Table or exclusionItem_table["StatUps"] \ + or item in DonaldAbility_Table or item in GoofyAbility_Table: # cannot have more than the quantity for abilties if value > item_dictionary_table[item].quantity: logging.info( From 0122eb38ab0ca6904b4fea39bb18c61895d6ba3c Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Sat, 15 Apr 2023 16:04:08 -0400 Subject: [PATCH 050/489] KH2: Fix validAbilitys (#1714) --- worlds/kh2/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index cc50c86eb1..30ace74ee1 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -273,7 +273,7 @@ class KH2World(World): # Kingdom Key cannot have No Experience so plandoed here instead of checking 26 times if its kingdom key random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.valid_abilities) while random_ability == ItemName.NoExperience: - random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.validAbilitys) + random_ability = self.multiworld.per_slot_randoms[self.player].choice(self.valid_abilities) self.plando_locations[LocationName.KingdomKeySlot] = random_ability self.item_quantity_dict[random_ability] -= 1 self.sora_keyblade_ability_pool[random_ability] -= 1 From ef211da27f3ee7d53b9b9b14493a530f03e709f7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 16 Apr 2023 01:34:00 +0200 Subject: [PATCH 051/489] Core: update modules --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 5b50664475..7d9f84ee85 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ colorama>=0.4.5 -websockets>=10.3 +websockets>=11.0.1 PyYAML>=6.0 -jellyfish>=0.11.0 +jellyfish>=0.11.2 jinja2>=3.1.2 schema>=0.7.5 kivy>=2.1.0 From 89ec31708ec020d54b3bbaa6cea3f5e0f2323eb4 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Sat, 15 Apr 2023 17:45:31 -0600 Subject: [PATCH 052/489] oot: bugfixes (#1709) * oot client: check types of tables coming from lua script for safety There was a reported bug with corrupted (?) slot data preventing locations sending. This should safeguard against any instances of that happening in the future, if it ever happens again. * oot: repair minor hint issues SMW has # in some location names which breaks ootr's poor text formatting system, so those need to be filtered out. Also replaces "[X] for [player Y]" with "[player Y]'s X" as frequently requested. * oot: update required client version * oot client: fix patching filename bug * oot: fix broken poachers saw item how was I this stupid, seriously * oot: sanitize player, location, and item names everywhere --- OoTClient.py | 19 +++++++++++++++---- worlds/oot/Hints.py | 10 +++++----- worlds/oot/Options.py | 2 +- worlds/oot/Patches.py | 8 ++++---- worlds/oot/TextBox.py | 7 +++++++ worlds/oot/__init__.py | 2 +- 6 files changed, 33 insertions(+), 15 deletions(-) diff --git a/OoTClient.py b/OoTClient.py index c05151a285..f940ce1214 100644 --- a/OoTClient.py +++ b/OoTClient.py @@ -179,6 +179,12 @@ async def parse_payload(payload: dict, ctx: OoTContext, force: bool): locations = payload['locations'] collectibles = payload['collectibles'] + # The Lua JSON library serializes an empty table into a list instead of a dict. Verify types for safety: + if isinstance(locations, list): + locations = {} + if isinstance(collectibles, list): + collectibles = {} + if ctx.location_table != locations or ctx.collectible_table != collectibles: ctx.location_table = locations ctx.collectible_table = collectibles @@ -293,10 +299,15 @@ async def patch_and_run_game(apz5_file): if not os.path.exists(rom_file_name): rom_file_name = Utils.user_path(rom_file_name) rom = Rom(rom_file_name) - apply_patch_file(rom, apz5_file, - sub_file=(os.path.basename(base_name) + '.zpf' - if zipfile.is_zipfile(apz5_file) - else None)) + + sub_file = None + if zipfile.is_zipfile(apz5_file): + for name in zipfile.ZipFile(apz5_file).namelist(): + if name.endswith('.zpf'): + sub_file = name + break + + apply_patch_file(rom, apz5_file, sub_file=sub_file) rom.write_to_file(decomp_path) os.chdir(data_path("Compress")) compress_rom_file(decomp_path, comp_path) diff --git a/worlds/oot/Hints.py b/worlds/oot/Hints.py index 35451f3d8f..4627abd00f 100644 --- a/worlds/oot/Hints.py +++ b/worlds/oot/Hints.py @@ -15,7 +15,7 @@ from .Items import OOTItem from .HintList import getHint, getHintGroup, Hint, hintExclusions, \ misc_item_hint_table, misc_location_hint_table from .Messages import COLOR_MAP, update_message_by_id -from .TextBox import line_wrap +from .TextBox import line_wrap, character_table, rom_safe_text from .Utils import data_path, read_json @@ -149,11 +149,11 @@ def isRestrictedDungeonItem(dungeon, item): # Attach a player name to the item or location text. # If the associated player of the item/location and the world are the same, does nothing. -# Otherwise, attaches the object's player's name to the string. +# Otherwise, attaches the object's player's name to the string, calling rom_safe_text for foreign items/locations. def attach_name(text, hinted_object, world): if hinted_object.player == world.player: return text - return f"{text} for {world.multiworld.get_player_name(hinted_object.player)}" + return rom_safe_text(f"{world.multiworld.get_player_name(hinted_object.player)}'s {text}") def add_hint(world, groups, gossip_text, count, location=None, force_reachable=False): @@ -1144,7 +1144,7 @@ def buildMiscItemHints(world, messages): area = HintArea.at(location, use_alt_hint=data['use_alt_hint']).text(world.clearer_hints, world=None) else: area = location.name - text = data['default_item_text'].format(area=(player_text + area)) + text = data['default_item_text'].format(area=rom_safe_text(player_text + area)) elif 'default_item_fallback' in data: text = data['default_item_fallback'] else: @@ -1167,7 +1167,7 @@ def buildMiscLocationHints(world, messages): item_text = getHint(getItemGenericName(item), world.clearer_hints).text if item.player != world.player: item_text += f' for {world.multiworld.get_player_name(item.player)}' - text = data['location_text'].format(item=item_text) + text = data['location_text'].format(item=rom_safe_text(item_text)) update_message_by_id(messages, data['id'], str(GossipText(text, ['Green'], prefix='')), 0x23) diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 35b477ae58..03f5346cee 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -1037,7 +1037,7 @@ class AdultTradeStart(OptionSet): "Pocket Cucco", "Cojiro", "Odd Mushroom", - "Poacher's Saw", + "Poachers Saw", "Broken Sword", "Prescription", "Eyeball Frog", diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index c02512a788..ab1e75d1b9 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -25,7 +25,7 @@ from .Rom import Rom from .SaveContext import SaveContext, Scenes, FlagType from .SceneFlags import get_alt_list_bytes, get_collectible_flag_table, get_collectible_flag_table_bytes, \ get_collectible_flag_addresses -from .TextBox import character_table, NORMAL_LINE_WIDTH +from .TextBox import character_table, NORMAL_LINE_WIDTH, rom_safe_text from .texture_util import ci4_rgba16patch_to_ci8, rgba16_patch from .Utils import __version__ @@ -2771,7 +2771,7 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F split_item_name[0] = create_fake_name(split_item_name[0]) if len(world.multiworld.worlds) > 1: # OOTWorld.MultiWorld.AutoWorld[] - description_text = '\x08\x05\x41%s %d Rupees\x01%s\x01\x05\x42%s\x05\x40\x01Special deal! ONE LEFT!\x09\x0A\x02' % (split_item_name[0], location.price, split_item_name[1], world.multiworld.get_player_name(location.item.player)) + description_text = '\x08\x05\x41%s %d Rupees\x01%s\x01\x05\x42%s\x05\x40\x01Special deal! ONE LEFT!\x09\x0A\x02' % (split_item_name[0], location.price, split_item_name[1], rom_safe_text(world.multiworld.get_player_name(location.item.player))) else: description_text = '\x08\x05\x41%s %d Rupees\x01%s\x01\x05\x40Special deal! ONE LEFT!\x01Get it while it lasts!\x09\x0A\x02' % (split_item_name[0], location.price, split_item_name[1]) purchase_text = '\x08%s %d Rupees\x09\x01%s\x01\x1B\x05\x42Buy\x01Don\'t buy\x05\x40\x02' % (split_item_name[0], location.price, split_item_name[1]) @@ -2785,9 +2785,9 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F shop_item_name = create_fake_name(shop_item_name) if len(world.multiworld.worlds) > 1: - shop_item_name = ''.join(filter(lambda char: char in character_table, shop_item_name)) + shop_item_name = rom_safe_text(shop_item_name) do_line_break = sum(character_table[char] for char in f"{shop_item_name} {location.price} Rupees") > NORMAL_LINE_WIDTH - description_text = '\x08\x05\x41%s%s%d Rupees\x01\x05\x42%s\x05\x40\x01Special deal! ONE LEFT!\x09\x0A\x02' % (shop_item_name, '\x01' if do_line_break else ' ', location.price, world.multiworld.get_player_name(location.item.player)) + description_text = '\x08\x05\x41%s%s%d Rupees\x01\x05\x42%s\x05\x40\x01Special deal! ONE LEFT!\x09\x0A\x02' % (shop_item_name, '\x01' if do_line_break else ' ', location.price, rom_safe_text(world.multiworld.get_player_name(location.item.player))) else: description_text = '\x08\x05\x41%s %d Rupees\x01\x05\x40Special deal! ONE LEFT!\x01Get it while it lasts!\x09\x0A\x02' % (shop_item_name, location.price) purchase_text = '\x08%s %d Rupees\x09\x01\x01\x1B\x05\x42Buy\x01Don\'t buy\x05\x40\x02' % (shop_item_name, location.price) diff --git a/worlds/oot/TextBox.py b/worlds/oot/TextBox.py index 4ea99757d8..a9db479962 100644 --- a/worlds/oot/TextBox.py +++ b/worlds/oot/TextBox.py @@ -367,3 +367,10 @@ def test_support_long_words(): print('"Support Long Words" test failed: Got ' + result + ', wanted ' + expected) else: print('"Support Long Words" test passed!') + + +# AP additions + +rom_safe_lambda = lambda c: c if c in character_table else '?' +def rom_safe_text(text): + return ''.join(map(rom_safe_lambda, text)) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index cae67e1e65..c967dfe182 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -118,7 +118,7 @@ class OOTWorld(World): data_version = 3 - required_client_version = (0, 3, 7) + required_client_version = (0, 4, 0) item_name_groups = { # internal groups From 599cd2c82e369085f8693ba284257440dee1e7a0 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 16 Apr 2023 01:57:52 +0200 Subject: [PATCH 053/489] Launcher: add Discord links and generate yamls (#1716) --- Launcher.py | 26 ++++++--- Options.py | 59 +++++++++++++++++++++ WebHostLib/options.py | 35 +----------- {WebHostLib/templates => data}/options.yaml | 0 setup.py | 8 +-- test/webhost/TestFileGeneration.py | 2 +- 6 files changed, 83 insertions(+), 47 deletions(-) rename {WebHostLib/templates => data}/options.yaml (100%) diff --git a/Launcher.py b/Launcher.py index be40987e32..2dfc466aaa 100644 --- a/Launcher.py +++ b/Launcher.py @@ -14,10 +14,12 @@ import itertools import shlex import subprocess import sys +import webbrowser from os.path import isfile from shutil import which from typing import Sequence, Union, Optional +import Utils from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier if __name__ == "__main__": @@ -38,7 +40,6 @@ def open_host_yaml(): exe = which("open") subprocess.Popen([exe, file]) else: - import webbrowser webbrowser.open(file) @@ -58,23 +59,36 @@ def open_patch(): launch([*get_exe(component), file], component.cli) +def generate_yamls(): + from Options import generate_yaml_templates + + target = Utils.user_path("Players", "Templates") + generate_yaml_templates(target, False) + open_folder(target) + + def browse_files(): - file = user_path() + open_folder(user_path()) + + +def open_folder(folder_path): if is_linux: exe = which('xdg-open') or which('gnome-open') or which('kde-open') - subprocess.Popen([exe, file]) + subprocess.Popen([exe, folder_path]) elif is_macos: exe = which("open") - subprocess.Popen([exe, file]) + subprocess.Popen([exe, folder_path]) else: - import webbrowser - webbrowser.open(file) + webbrowser.open(folder_path) components.extend([ # Functions Component('Open host.yaml', func=open_host_yaml), Component('Open Patch', func=open_patch), + Component('Generate Template Settings', func=generate_yamls), + Component('Discord Server', func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")), + Component('18+ Discord Server', func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")), Component('Browse Files', func=browse_files), ]) diff --git a/Options.py b/Options.py index 6fe70ad95f..5e638a463a 100644 --- a/Options.py +++ b/Options.py @@ -13,6 +13,7 @@ from Utils import get_fuzzy_results if typing.TYPE_CHECKING: from BaseClasses import PlandoOptions from worlds.AutoWorld import World + import pathlib class AssembleOptions(abc.ABCMeta): @@ -1022,6 +1023,64 @@ per_game_common_options = { "item_links": ItemLinks } + +def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True): + import os + + import yaml + from jinja2 import Template + + from worlds import AutoWorldRegister + from Utils import local_path, __version__ + + full_path: str + + os.makedirs(target_folder, exist_ok=True) + + # clean out old + for file in os.listdir(target_folder): + full_path = os.path.join(target_folder, file) + if os.path.isfile(full_path) and full_path.endswith(".yaml"): + os.unlink(full_path) + + def dictify_range(option: typing.Union[Range, SpecialRange]): + data = {option.default: 50} + for sub_option in ["random", "random-low", "random-high"]: + if sub_option != option.default: + data[sub_option] = 0 + + notes = {} + for name, number in getattr(option, "special_range_names", {}).items(): + notes[name] = f"equivalent to {number}" + if number in data: + data[name] = data[number] + del data[number] + else: + data[name] = 0 + + return data, notes + + for game_name, world in AutoWorldRegister.world_types.items(): + if not world.hidden or generate_hidden: + all_options: typing.Dict[str, AssembleOptions] = { + **per_game_common_options, + **world.option_definitions + } + + with open(local_path("data", "options.yaml")) as f: + file_data = f.read() + res = Template(file_data).render( + options=all_options, + __version__=__version__, game=game_name, yaml_dump=yaml.dump, + dictify_range=dictify_range, + ) + + del file_data + + with open(os.path.join(target_folder, game_name + ".yaml"), "w", encoding="utf-8-sig") as f: + f.write(res) + + if __name__ == "__main__": from worlds.alttp.Options import Logic diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 9b6fc90402..bf8fa0d419 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -17,29 +17,8 @@ handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hin def create(): target_folder = local_path("WebHostLib", "static", "generated") yaml_folder = os.path.join(target_folder, "configs") - os.makedirs(yaml_folder, exist_ok=True) - for file in os.listdir(yaml_folder): - full_path: str = os.path.join(yaml_folder, file) - if os.path.isfile(full_path): - os.unlink(full_path) - - def dictify_range(option: typing.Union[Options.Range, Options.SpecialRange]): - data = {option.default: 50} - for sub_option in ["random", "random-low", "random-high"]: - if sub_option != option.default: - data[sub_option] = 0 - - notes = {} - for name, number in getattr(option, "special_range_names", {}).items(): - notes[name] = f"equivalent to {number}" - if number in data: - data[name] = data[number] - del data[number] - else: - data[name] = 0 - - return data, notes + Options.generate_yaml_templates(yaml_folder) def get_html_doc(option_type: type(Options.Option)) -> str: if not option_type.__doc__: @@ -61,18 +40,6 @@ def create(): **Options.per_game_common_options, **world.option_definitions } - with open(local_path("WebHostLib", "templates", "options.yaml")) as f: - file_data = f.read() - res = Template(file_data).render( - options=all_options, - __version__=__version__, game=game_name, yaml_dump=yaml.dump, - dictify_range=dictify_range, - ) - - del file_data - - with open(os.path.join(target_folder, "configs", game_name + ".yaml"), "w", encoding="utf-8") as f: - f.write(res) # Generate JSON files for player-settings pages player_settings = { diff --git a/WebHostLib/templates/options.yaml b/data/options.yaml similarity index 100% rename from WebHostLib/templates/options.yaml rename to data/options.yaml diff --git a/setup.py b/setup.py index 4b0c39545c..94a242f0f0 100644 --- a/setup.py +++ b/setup.py @@ -309,16 +309,12 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): dirs_exist_ok=True) os.makedirs(self.buildfolder / "Players" / "Templates", exist_ok=True) - from WebHostLib.options import create - create() + from Options import generate_yaml_templates from worlds.AutoWorld import AutoWorldRegister assert not apworlds - set(AutoWorldRegister.world_types), "Unknown world designated for .apworld" folders_to_remove: typing.List[str] = [] + generate_yaml_templates(self.buildfolder / "Players" / "Templates", False) for worldname, worldtype in AutoWorldRegister.world_types.items(): - if not worldtype.hidden: - file_name = worldname+".yaml" - shutil.copyfile(os.path.join("WebHostLib", "static", "generated", "configs", file_name), - self.buildfolder / "Players" / "Templates" / file_name) if worldname in apworlds: file_name = os.path.split(os.path.dirname(worldtype.__file__))[1] world_directory = self.libfolder / "worlds" / file_name diff --git a/test/webhost/TestFileGeneration.py b/test/webhost/TestFileGeneration.py index 2954946db3..6010202c41 100644 --- a/test/webhost/TestFileGeneration.py +++ b/test/webhost/TestFileGeneration.py @@ -25,7 +25,7 @@ class TestFileGeneration(unittest.TestCase): for file in os.scandir(target): if file.is_file() and file.name.endswith(".yaml"): with self.subTest(file=file.name): - with open(file) as f: + with open(file, encoding="utf-8-sig") as f: for value in roll_options({file.name: f.read()})[0].values(): self.assertTrue(value is True, f"Default Options for template {file.name} cannot be run.") From 8a7806282514e0a756fb96475288e1368d508873 Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Sat, 15 Apr 2023 19:59:01 -0400 Subject: [PATCH 054/489] KH2: fixed lucky emblem required>lucky emblem amount (#1718) --- worlds/kh2/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index 30ace74ee1..08ab9eabce 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -314,8 +314,7 @@ class KH2World(World): luckyemblemamount = max(self.luckyemblemamount, self.luckyemblemrequired) self.multiworld.LuckyEmblemsAmount[self.player].value = luckyemblemamount - self.item_quantity_dict[ItemName.LuckyEmblem] = item_dictionary_table[ - ItemName.LuckyEmblem].quantity + self.luckyemblemamount + self.item_quantity_dict[ItemName.LuckyEmblem] = self.multiworld.LuckyEmblemsAmount[self.player].value # give this proof to unlock the final door once the player has the amount of lucky emblem required self.item_quantity_dict[ItemName.ProofofNonexistence] = 0 From acd3cb45bff490c0d28217669d4745c5ffdcc7c5 Mon Sep 17 00:00:00 2001 From: axe-y <58866768+axe-y@users.noreply.github.com> Date: Sat, 15 Apr 2023 23:04:47 -0400 Subject: [PATCH 055/489] DLCQuest: Fix documentation error (#1720) --- worlds/dlcquest/Options.py | 9 +++++++-- worlds/dlcquest/docs/en_DLCQuest.md | 14 +++++++------- worlds/dlcquest/docs/setup_en.md | 5 +++-- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/worlds/dlcquest/Options.py b/worlds/dlcquest/Options.py index c89c938b5b..9a805d3799 100644 --- a/worlds/dlcquest/Options.py +++ b/worlds/dlcquest/Options.py @@ -54,10 +54,15 @@ class CoinSanityRange(SpecialRange): range_start = 1 range_end = 100 default = 20 + special_range_names = { + "low": 5, + "normal": 20, + "high": 50, + } class EndingChoice(Choice): - """This is for the ending type of the basic game""" + """Which ending is considered completion for the basic campaign""" internal_name = "ending_choice" display_name = "Ending Choice" option_any = 0 @@ -66,7 +71,7 @@ class EndingChoice(Choice): class Campaign(Choice): - """Whitch game you wana play to end""" + """Which campaign you want to play""" internal_name = "campaign" display_name = "Campaign" option_basic = 0 diff --git a/worlds/dlcquest/docs/en_DLCQuest.md b/worlds/dlcquest/docs/en_DLCQuest.md index 333b1e8ed9..eaccc8ff0a 100644 --- a/worlds/dlcquest/docs/en_DLCQuest.md +++ b/worlds/dlcquest/docs/en_DLCQuest.md @@ -21,16 +21,16 @@ They can also choose to do both campaigns. Location checks in DLC Quest always include: - DLC Purchases from the shopkeeper - Awardment-related objectives - - Killing Sheep in DLC Quest - - Specific Awardment objectives in Live Freemium or Die + - Killing Sheep in DLC Quest + - Specific Awardment objectives in Live Freemium or Die There also are a number of location checks that are optional, and individual players choose to include them or not in their shuffling: - Items that your character can obtain in various ways - - Swords - - Gun - - Box of Various Supplies - - Humble Indie Bindle - - Pickaxe + - Swords + - Gun + - Box of Various Supplies + - Humble Indie Bindle + - Pickaxe - Coinsanity: Coins, either individually or as custom-sized bundles diff --git a/worlds/dlcquest/docs/setup_en.md b/worlds/dlcquest/docs/setup_en.md index 8111c654df..4a53740e07 100644 --- a/worlds/dlcquest/docs/setup_en.md +++ b/worlds/dlcquest/docs/setup_en.md @@ -1,4 +1,4 @@ -# Stardew Valley Randomizer Setup Guide +# DLCQuest Randomizer Setup Guide ## Required Software @@ -32,6 +32,7 @@ You can customize your settings by visiting the [DLC Quest Player Settings Page] - Run "DLCQuestipelagoInstaller.exe" + ![image](https://i.imgur.com/2sPhMgs.png) - The installer should describe what it is doing each step of the way, and will ask for your input when necessary. - It will allow you to choose where to install your modded game, and offer a default location @@ -44,7 +45,7 @@ You can customize your settings by visiting the [DLC Quest Player Settings Page] - Run BepInEx.NET.Framework.Launcher.exe. If you opted for a desktop shortcut, you will find it with an icon and a more recognizable name. -- ![image](https://i.imgur.com/ZUiFrhf.png) +![image](https://i.imgur.com/ZUiFrhf.png) - Your game should launch alongside a modloader console, which will contain important debugging information if you run into problems. From 50d9ab041a8311543df4acbd1c38082b892791db Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Sun, 16 Apr 2023 02:53:42 -0600 Subject: [PATCH 056/489] Blasphemous: Fix logic for Laudes (#1724) --- worlds/blasphemous/Rules.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py index ee99ecde63..22953ed4a1 100644 --- a/worlds/blasphemous/Rules.py +++ b/worlds/blasphemous/Rules.py @@ -299,7 +299,9 @@ class BlasphemousLogic(LogicMixin): return self.has_group("masks", player, 3) def _blasphemous_laudes_gate(self, player): - return self.has_all({"Petrified Bell", "Blood Perpetuated in Sand", "Three Gnarled Tongues", "Key of the Secular", "Key of the Scribe", "Verses Spun from Gold"}, player) + return self.has_all({"Petrified Bell", "Blood Perpetuated in Sand", "Three Gnarled Tongues", \ + "Key of the Secular", "Key of the Scribe"}, player) and \ + self.has("Verses Spun from Gold", player, 4) # Ten Piedad, Tres Angustias, Our Lady of the Charred Visage def _blasphemous_wound_boss_easy(self, player): From ea03c901527be69231fb86b2fc3fa269bd595805 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Sun, 16 Apr 2023 05:22:33 -0400 Subject: [PATCH 057/489] Stardew Valley: Fix a logic bug and a documentation typo (#1722) Typo in the documentation Logic error for help wanted quests --- worlds/stardew_valley/data/bundle_data.py | 12 ++- worlds/stardew_valley/docs/setup_en.md | 2 +- worlds/stardew_valley/rules.py | 17 +-- worlds/stardew_valley/test/TestBundles.py | 32 ++++-- worlds/stardew_valley/test/TestData.py | 8 +- worlds/stardew_valley/test/TestGeneration.py | 107 ++++++++++--------- 6 files changed, 103 insertions(+), 75 deletions(-) diff --git a/worlds/stardew_valley/data/bundle_data.py b/worlds/stardew_valley/data/bundle_data.py index d82a632af5..a424cfb80b 100644 --- a/worlds/stardew_valley/data/bundle_data.py +++ b/worlds/stardew_valley/data/bundle_data.py @@ -26,6 +26,16 @@ class BundleItem: def as_quality(self, quality: int): return BundleItem.item_bundle(self.item.name, self.item.item_id, self.amount, quality) + def as_gold_quality(self): + return self.as_quality(2) + + def as_quality_crop(self): + amount = 5 + difficult_crops = ["Sweet Gem Berry", "Ancient Fruit"] + if self.item.name in difficult_crops: + amount = 1 + return self.as_gold_quality().as_amount(amount) + def __repr__(self): return f"{self.amount} {quality_dict[self.quality]} {self.item.name}" @@ -281,7 +291,7 @@ summer_crops_items = [blueberry, corn, hops, hot_pepper, melon, poppy, fall_crops_items = [corn, sunflower, wheat, amaranth, bok_choy, cranberries, eggplant, fairy_rose, grape, pumpkin, yam, sweet_gem_berry] all_crops_items = sorted({*spring_crop_items, *summer_crops_items, *fall_crops_items}) -quality_crops_items = [item.as_quality(2).as_amount(5) for item in all_crops_items] +quality_crops_items = [item.as_quality_crop() for item in all_crops_items] # TODO void_egg, dinosaur_egg, ostrich_egg, golden_egg animal_product_items = [egg, large_egg, brown_egg, large_brown_egg, wool, milk, large_milk, goat_milk, large_goat_milk, truffle, duck_feather, duck_egg, rabbit_foot] diff --git a/worlds/stardew_valley/docs/setup_en.md b/worlds/stardew_valley/docs/setup_en.md index 37290d69d7..722c2db01f 100644 --- a/worlds/stardew_valley/docs/setup_en.md +++ b/worlds/stardew_valley/docs/setup_en.md @@ -4,7 +4,7 @@ - Stardew Valley on PC (Recommended: [Steam version](https://store.steampowered.com/app/413150/Stardew_Valley/)) - SMAPI ([Mod loader for Stardew Valley](https://smapi.io/)) -- [StardewArchipelago Mod Release 2.x.x](https://github.com/agilbert1412/StardewArchipelago/releases) +- [StardewArchipelago Mod Release 3.x.x](https://github.com/agilbert1412/StardewArchipelago/releases) - It is important to use a mod release of version 3.x.x to play seeds that have been generated here. Later releases can only be used with later releases of the world generator, that are not hosted on archipelago.gg yet. ## Optional Software diff --git a/worlds/stardew_valley/rules.py b/worlds/stardew_valley/rules.py index cbefe64810..ad48dc0706 100644 --- a/worlds/stardew_valley/rules.py +++ b/worlds/stardew_valley/rules.py @@ -124,21 +124,22 @@ def set_rules(multi_world: MultiWorld, player: int, world_options: StardewOption # Help Wanted Quests desired_number_help_wanted: int = world_options[options.HelpWantedLocations] // 7 - for i in range(1, desired_number_help_wanted + 1): + for i in range(0, desired_number_help_wanted): prefix = "Help Wanted:" delivery = "Item Delivery" - rule = logic.received("Month End", i - 1) + rule = logic.received("Month End", i) fishing_rule = rule & logic.can_fish() slay_rule = rule & logic.has_any_weapon() - for j in range(i, i + 4): - MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} {delivery} {j}", player), - rule.simplify()) + item_delivery_index = (i * 4) + 1 + for j in range(item_delivery_index, item_delivery_index + 4): + location_name = f"{prefix} {delivery} {j}" + MultiWorldRules.set_rule(multi_world.get_location(location_name, player), rule.simplify()) - MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Gathering {i}", player), + MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Gathering {i+1}", player), rule.simplify()) - MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Fishing {i}", player), + MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Fishing {i+1}", player), fishing_rule.simplify()) - MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Slay Monsters {i}", player), + MultiWorldRules.set_rule(multi_world.get_location(f"{prefix} Slay Monsters {i+1}", player), slay_rule.simplify()) set_fishsanity_rules(all_location_names, logic, multi_world, player) diff --git a/worlds/stardew_valley/test/TestBundles.py b/worlds/stardew_valley/test/TestBundles.py index 1e75bd9bb0..a13829eb67 100644 --- a/worlds/stardew_valley/test/TestBundles.py +++ b/worlds/stardew_valley/test/TestBundles.py @@ -1,16 +1,30 @@ import unittest -from ..data.bundle_data import all_bundle_items +from ..data.bundle_data import all_bundle_items, quality_crops_items class TestBundles(unittest.TestCase): def test_all_bundle_items_have_3_parts(self): for bundle_item in all_bundle_items: - name = bundle_item.item.name - assert len(name) > 0 - id = bundle_item.item.item_id - assert (id > 0 or id == -1) - amount = bundle_item.amount - assert amount > 0 - quality = bundle_item.quality - assert quality >= 0 + with self.subTest(bundle_item.item.name): + self.assertGreater(len(bundle_item.item.name), 0) + id = bundle_item.item.item_id + self.assertGreaterEqual(id, -1) + self.assertNotEqual(id, 0) + self.assertGreater(bundle_item.amount, 0) + self.assertGreaterEqual(bundle_item.quality, 0) + + def test_quality_crops_have_correct_amounts(self): + for bundle_item in quality_crops_items: + with self.subTest(bundle_item.item.name): + name = bundle_item.item.name + if name == "Sweet Gem Berry" or name == "Ancient Fruit": + self.assertEqual(bundle_item.amount, 1) + else: + self.assertEqual(bundle_item.amount, 5) + + def test_quality_crops_have_correct_quality(self): + for bundle_item in quality_crops_items: + with self.subTest(bundle_item.item.name): + self.assertEqual(bundle_item.quality, 2) + diff --git a/worlds/stardew_valley/test/TestData.py b/worlds/stardew_valley/test/TestData.py index c08cef0e74..a77dc17319 100644 --- a/worlds/stardew_valley/test/TestData.py +++ b/worlds/stardew_valley/test/TestData.py @@ -9,12 +9,12 @@ class TestCsvIntegrity(unittest.TestCase): items = load_item_csv() for item in items: - assert item.code_without_offset is not None, \ - "Some item do not have an id. Run the script `update_data.py` to generate them." + self.assertIsNotNone(item.code_without_offset, "Some item do not have an id." + " Run the script `update_data.py` to generate them.") def test_locations_integrity(self): locations = load_location_csv() for location in locations: - assert location.code_without_offset is not None, \ - "Some location do not have an id. Run the script `update_data.py` to generate them." + self.assertIsNotNone(location.code_without_offset, "Some location do not have an id." + " Run the script `update_data.py` to generate them.") diff --git a/worlds/stardew_valley/test/TestGeneration.py b/worlds/stardew_valley/test/TestGeneration.py index 9bf4fbcb09..16c216b240 100644 --- a/worlds/stardew_valley/test/TestGeneration.py +++ b/worlds/stardew_valley/test/TestGeneration.py @@ -17,54 +17,57 @@ class TestBaseItemGeneration(SVTestBase): if item.classification is classification} for item in all_classified_items: - assert item in self.multiworld.itempool + self.assertIn(item, self.multiworld.itempool) def test_creates_as_many_item_as_non_event_locations(self): non_event_locations = [location for location in self.multiworld.get_locations(self.player) if not location.event] - assert len(non_event_locations), len(self.multiworld.itempool) + self.assertEqual(len(non_event_locations), len(self.multiworld.itempool)) class TestGivenProgressiveBackpack(SVTestBase): options = {options.BackpackProgression.internal_name: options.BackpackProgression.option_progressive} def test_when_generate_world_then_two_progressive_backpack_are_added(self): - assert self.multiworld.itempool.count(self.world.create_item("Progressive Backpack")) == 2 + self.assertEqual(self.multiworld.itempool.count(self.world.create_item("Progressive Backpack")), 2) def test_when_generate_world_then_backpack_locations_are_added(self): created_locations = {location.name for location in self.multiworld.get_locations(1)} - assert all(location.name in created_locations for location in locations.locations_by_tag[LocationTags.BACKPACK]) + backpacks_exist = [location.name in created_locations + for location in locations.locations_by_tag[LocationTags.BACKPACK]] + all_exist = all(backpacks_exist) + self.assertTrue(all_exist) class TestRemixedMineRewards(SVTestBase): def test_when_generate_world_then_one_reward_is_added_per_chest(self): # assert self.world.create_item("Rusty Sword") in self.multiworld.itempool - assert any(self.world.create_item(item) in self.multiworld.itempool - for item in items_by_group[Group.MINES_FLOOR_10]) - assert any(self.world.create_item(item) in self.multiworld.itempool - for item in items_by_group[Group.MINES_FLOOR_20]) - assert self.world.create_item("Slingshot") in self.multiworld.itempool - assert any(self.world.create_item(item) in self.multiworld.itempool - for item in items_by_group[Group.MINES_FLOOR_50]) - assert any(self.world.create_item(item) in self.multiworld.itempool - for item in items_by_group[Group.MINES_FLOOR_60]) - assert self.world.create_item("Master Slingshot") in self.multiworld.itempool - assert any(self.world.create_item(item) in self.multiworld.itempool - for item in items_by_group[Group.MINES_FLOOR_80]) - assert any(self.world.create_item(item) in self.multiworld.itempool - for item in items_by_group[Group.MINES_FLOOR_90]) - assert self.world.create_item("Stardrop") in self.multiworld.itempool - assert any(self.world.create_item(item) in self.multiworld.itempool - for item in items_by_group[Group.MINES_FLOOR_110]) - assert self.world.create_item("Skull Key") in self.multiworld.itempool + self.assertTrue(any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_10])) + self.assertTrue(any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_20])) + self.assertIn(self.world.create_item("Slingshot"), self.multiworld.itempool) + self.assertTrue(any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_50])) + self.assertTrue(any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_60])) + self.assertIn(self.world.create_item("Master Slingshot"), self.multiworld.itempool) + self.assertTrue(any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_80])) + self.assertTrue(any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_90])) + self.assertIn(self.world.create_item("Stardrop"), self.multiworld.itempool) + self.assertTrue(any(self.world.create_item(item) in self.multiworld.itempool + for item in items_by_group[Group.MINES_FLOOR_110])) + self.assertIn(self.world.create_item("Skull Key"), self.multiworld.itempool) - # This test as 1 over 90,000 changes to fail... Sorry in advance + # This test has a 1/90,000 chance to fail... Sorry in advance def test_when_generate_world_then_rewards_are_not_all_vanilla(self): - assert not all(self.world.create_item(item) in self.multiworld.itempool + self.assertFalse(all(self.world.create_item(item) in self.multiworld.itempool for item in ["Leather Boots", "Steel Smallsword", "Tundra Boots", "Crystal Dagger", "Firewalker Boots", - "Obsidian Edge", "Space Boots"]) + "Obsidian Edge", "Space Boots"])) class TestProgressiveElevator(SVTestBase): @@ -81,11 +84,11 @@ class TestProgressiveElevator(SVTestBase): self.collect([self.get_item_by_name("Combat Level")] * 4) self.collect(self.get_item_by_name("Adventurer's Guild")) - assert not self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state) + self.assertFalse(self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state)) self.collect(self.get_item_by_name("Progressive Mine Elevator")) - assert self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state) + self.assertTrue(self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state)) def test_given_access_to_floor_115_when_find_another_pickaxe_and_sword_then_has_access_to_floor_120(self): self.collect([self.get_item_by_name("Progressive Pickaxe")] * 2) @@ -94,14 +97,14 @@ class TestProgressiveElevator(SVTestBase): self.collect([self.get_item_by_name("Combat Level")] * 4) self.collect(self.get_item_by_name("Adventurer's Guild")) - assert not self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state) + self.assertFalse(self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state)) self.collect(self.get_item_by_name("Progressive Pickaxe")) self.collect(self.multiworld.create_item("Steel Falchion", self.player)) self.collect(self.get_item_by_name("Combat Level")) self.collect(self.get_item_by_name("Combat Level")) - assert self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state) + self.assertTrue(self.multiworld.get_region("The Mines - Floor 120", self.player).can_reach(self.multiworld.state)) class TestLocationGeneration(SVTestBase): @@ -109,7 +112,7 @@ class TestLocationGeneration(SVTestBase): def test_all_location_created_are_in_location_table(self): for location in self.multiworld.get_locations(self.player): if not location.event: - assert location.name in location_table + self.assertIn(location.name, location_table) class TestLocationAndItemCount(SVTestBase): @@ -130,7 +133,7 @@ class TestLocationAndItemCount(SVTestBase): } def test_minimal_location_maximal_items_still_valid(self): - assert len(self.multiworld.get_locations()) >= len(self.multiworld.get_items()) + self.assertGreaterEqual(len(self.multiworld.get_locations()), len(self.multiworld.get_items())) class TestFriendsanityNone(SVTestBase): @@ -140,11 +143,11 @@ class TestFriendsanityNone(SVTestBase): def test_no_friendsanity_items(self): for item in self.multiworld.get_items(): - assert not item.name.endswith(": 1 <3") + self.assertFalse(item.name.endswith(": 1 <3")) def test_no_friendsanity_locations(self): for location in self.multiworld.get_locations(): - assert not location.name.startswith("Friendsanity") + self.assertFalse(location.name.startswith("Friendsanity")) class TestFriendsanityBachelors(SVTestBase): @@ -159,7 +162,7 @@ class TestFriendsanityBachelors(SVTestBase): for item in self.multiworld.get_items(): if item.name.endswith(suffix): villager_name = item.name[:item.name.index(suffix)] - assert villager_name in self.bachelors + self.assertIn(villager_name, self.bachelors) def test_friendsanity_only_bachelor_locations(self): prefix = "Friendsanity: " @@ -171,8 +174,8 @@ class TestFriendsanityBachelors(SVTestBase): parts = name_trimmed.split(" ") name = parts[0] hearts = parts[1] - assert name in self.bachelors - assert int(hearts) <= 8 + self.assertIn(name, self.bachelors) + self.assertLessEqual(int(hearts), 8) class TestFriendsanityStartingNpcs(SVTestBase): @@ -186,7 +189,7 @@ class TestFriendsanityStartingNpcs(SVTestBase): for item in self.multiworld.get_items(): if item.name.endswith(suffix): villager_name = item.name[:item.name.index(suffix)] - assert villager_name not in self.excluded_npcs + self.assertNotIn(villager_name, self.excluded_npcs) def test_friendsanity_only_starting_npcs_locations(self): prefix = "Friendsanity: " @@ -198,14 +201,14 @@ class TestFriendsanityStartingNpcs(SVTestBase): parts = name_trimmed.split(" ") name = parts[0] hearts = parts[1] - assert name not in self.excluded_npcs - assert name in all_villagers_by_name or name == "Pet" + self.assertNotIn(name, self.excluded_npcs) + self.assertTrue(name in all_villagers_by_name or name == "Pet") if name == "Pet": - assert int(hearts) <= 5 + self.assertLessEqual(int(hearts), 5) elif all_villagers_by_name[name].bachelor: - assert int(hearts) <= 8 + self.assertLessEqual(int(hearts), 8) else: - assert int(hearts) <= 10 + self.assertLessEqual(int(hearts), 10) class TestFriendsanityAllNpcs(SVTestBase): @@ -218,7 +221,7 @@ class TestFriendsanityAllNpcs(SVTestBase): for item in self.multiworld.get_items(): if item.name.endswith(suffix): villager_name = item.name[:item.name.index(suffix)] - assert villager_name in all_villagers_by_name or villager_name == "Pet" + self.assertTrue(villager_name in all_villagers_by_name or villager_name == "Pet") def test_friendsanity_all_locations(self): prefix = "Friendsanity: " @@ -230,13 +233,13 @@ class TestFriendsanityAllNpcs(SVTestBase): parts = name_trimmed.split(" ") name = parts[0] hearts = parts[1] - assert name in all_villagers_by_name or name == "Pet" + self.assertTrue(name in all_villagers_by_name or name == "Pet") if name == "Pet": - assert int(hearts) <= 5 + self.assertLessEqual(int(hearts), 5) elif all_villagers_by_name[name].bachelor: - assert int(hearts) <= 8 + self.assertLessEqual(int(hearts), 8) else: - assert int(hearts) <= 10 + self.assertLessEqual(int(hearts), 10) class TestFriendsanityAllNpcsWithMarriage(SVTestBase): @@ -249,7 +252,7 @@ class TestFriendsanityAllNpcsWithMarriage(SVTestBase): for item in self.multiworld.get_items(): if item.name.endswith(suffix): villager_name = item.name[:item.name.index(suffix)] - assert villager_name in all_villagers_by_name or villager_name == "Pet" + self.assertTrue(villager_name in all_villagers_by_name or villager_name == "Pet") def test_friendsanity_all_with_marriage_locations(self): prefix = "Friendsanity: " @@ -261,10 +264,10 @@ class TestFriendsanityAllNpcsWithMarriage(SVTestBase): parts = name_trimmed.split(" ") name = parts[0] hearts = parts[1] - assert name in all_villagers_by_name or name == "Pet" + self.assertTrue(name in all_villagers_by_name or name == "Pet") if name == "Pet": - assert int(hearts) <= 5 + self.assertLessEqual(int(hearts), 5) elif all_villagers_by_name[name].bachelor: - assert int(hearts) <= 14 + self.assertLessEqual(int(hearts), 14) else: - assert int(hearts) <= 10 + self.assertLessEqual(int(hearts), 10) From f395a6d1840ba69c61df8e642de0aaef463c1b5f Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 16 Apr 2023 12:59:53 +0200 Subject: [PATCH 058/489] Docs: has_all and has_any (#1725) Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> --- BaseClasses.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BaseClasses.py b/BaseClasses.py index 29b2c3f687..35761bc238 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -741,9 +741,11 @@ class CollectionState(): return self.prog_items[item, player] >= count def has_all(self, items: Set[str], player: int) -> bool: + """Returns True if each item name of items is in state at least once.""" return all(self.prog_items[item, player] for item in items) def has_any(self, items: Set[str], player: int) -> bool: + """Returns True if at least one item name of items is in state at least once.""" return any(self.prog_items[item, player] for item in items) def count(self, item: str, player: int) -> int: From f6758524d55fc4266b93a9b2f9ebe2c6bb635919 Mon Sep 17 00:00:00 2001 From: axe-y <58866768+axe-y@users.noreply.github.com> Date: Sun, 16 Apr 2023 20:38:48 -0400 Subject: [PATCH 059/489] DLCQuest: fix loader bug (#1729) --- worlds/dlcquest/data/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 worlds/dlcquest/data/__init__.py diff --git a/worlds/dlcquest/data/__init__.py b/worlds/dlcquest/data/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From cb634fa8d42301aa5ef43996b1c8415a8a0989d3 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sun, 16 Apr 2023 23:46:19 -0400 Subject: [PATCH 060/489] SM: failing generation fixes (#1726) - fixed wrong condition in Collect to assign lastAP - fixed possible infinite loop in generating output when many SM worlds are present - fixed new VARIA code that changed a list used for every SM worlds and would throw if many SM worlds uses Aea rando and not AreaLayout --- worlds/sm/__init__.py | 49 ++++++++++--------- worlds/sm/variaRandomizer/graph/graph.py | 5 +- worlds/sm/variaRandomizer/graph/location.py | 7 ++- .../sm/variaRandomizer/logic/smboolmanager.py | 7 ++- worlds/sm/variaRandomizer/rom/rompatcher.py | 7 +-- worlds/sm/variaRandomizer/utils/objectives.py | 2 +- 6 files changed, 43 insertions(+), 34 deletions(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 24d676034b..3ce405acb2 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -22,8 +22,9 @@ import Utils from .variaRandomizer.logic.smboolmanager import SMBoolManager from .variaRandomizer.graph.vanilla.graph_locations import locationsDict from .variaRandomizer.graph.graph_utils import getAccessPoint -from .variaRandomizer.rando.ItemLocContainer import ItemLocation +from .variaRandomizer.rando.ItemLocContainer import ItemLocation, ItemLocContainer from .variaRandomizer.rando.Items import ItemManager +from .variaRandomizer.rando.RandoServices import ComebackCheckType from .variaRandomizer.utils.parameters import * from .variaRandomizer.utils.utils import openFile from .variaRandomizer.logic.logic import Logic @@ -169,9 +170,13 @@ class SMWorld(World): elif item.Category == 'Nothing': isAdvancement = False + classification = ItemClassification.progression if isAdvancement else ItemClassification.filler itemClass = ItemManager.Items[item.Type].Class - smitem = SMItem(item.Name, ItemClassification.progression if isAdvancement else ItemClassification.filler, - item.Type, None if itemClass == 'Boss' else self.item_name_to_id[item.Name], player=self.player) + smitem = SMItem(item.Name, + classification, + item.Type, + None if itemClass == 'Boss' else self.item_name_to_id[item.Name], + player=self.player) if itemClass == 'Boss': self.locked_items[item.Name] = smitem elif item.Category == 'Nothing': @@ -645,10 +650,10 @@ class SMWorld(World): def collect(self, state: CollectionState, item: Item) -> bool: state.smbm[self.player].addItem(item.type) - if (item.location != None and item.location.player == self.player): - for entrance in self.multiworld.get_region(item.location.parent_region.name, self.player).entrances: + if item.location != None: + for entrance in self.multiworld.get_region(item.location.parent_region.name, item.location.player).entrances: if (entrance.parent_region.can_reach(state)): - state.smbm[self.player].lastAP = entrance.parent_region.name + state.smbm[item.location.player].lastAP = entrance.parent_region.name break return super(SMWorld, self).collect(state, item) @@ -797,29 +802,25 @@ class SMLocation(Location): def can_reach(self, state: CollectionState) -> bool: # self.access_rule computes faster on average, so placing it first for faster abort assert self.parent_region, "Can't reach location without region" - return self.access_rule(state) and self.parent_region.can_reach(state) and self.can_comeback(state) + return self.access_rule(state) and \ + self.parent_region.can_reach(state) and \ + self.can_comeback(state, self.item) - def can_comeback(self, state: CollectionState): - # some specific early/late game checks - if self.name == 'Bomb' or self.name == 'Mother Brain': - return True - + def can_comeback(self, state: CollectionState, item): randoExec = state.multiworld.worlds[self.player].variaRando.randoExec + randoService = randoExec.setup.services + + comebackCheck = ComebackCheckType.JustComeback n = 2 if GraphUtils.isStandardStart(randoExec.graphSettings.startAP) else 3 # is early game if (len([loc for loc in state.locations_checked if loc.player == self.player]) <= n): - return True - - for key in locationsDict[self.name].AccessFrom.keys(): - smbm = state.smbm[self.player] - if (randoExec.areaGraph.canAccess( smbm, - smbm.lastAP, - key, - smbm.maxDiff, - None)): - return True - return False - + comebackCheck = ComebackCheckType.NoCheck + container = ItemLocContainer(state.smbm[self.player], [], []) + return randoService.fullComebackCheck( container, + state.smbm[self.player].lastAP, + ItemManager.Items[item.type] if item is not None and item.player == self.player else None, + locationsDict[self.name], + comebackCheck) class SMItem(Item): game = "Super Metroid" diff --git a/worlds/sm/variaRandomizer/graph/graph.py b/worlds/sm/variaRandomizer/graph/graph.py index d6a3a09f94..36a061a224 100644 --- a/worlds/sm/variaRandomizer/graph/graph.py +++ b/worlds/sm/variaRandomizer/graph/graph.py @@ -362,7 +362,8 @@ class AccessGraph(object): # test access from an access point to another, given an optional item def canAccess(self, smbm, srcAccessPointName, destAccessPointName, maxDiff, item=None): - if item is not None: + addAndRemoveItem = item is not None and (smbm.isCountItem(item) or not smbm.haveItem(item)) + if addAndRemoveItem: smbm.addItem(item) #print("canAccess: item: {}, src: {}, dest: {}".format(item, srcAccessPointName, destAccessPointName)) destAccessPoint = self.accessPoints[destAccessPointName] @@ -371,7 +372,7 @@ class AccessGraph(object): can = destAccessPoint in availAccessPoints # if not can: # self.log.debug("canAccess KO: avail = {}".format([ap.Name for ap in availAccessPoints.keys()])) - if item is not None: + if addAndRemoveItem: smbm.removeItem(item) #print("canAccess: {}".format(can)) return can diff --git a/worlds/sm/variaRandomizer/graph/location.py b/worlds/sm/variaRandomizer/graph/location.py index 5831a4cc4c..466a050bcc 100644 --- a/worlds/sm/variaRandomizer/graph/location.py +++ b/worlds/sm/variaRandomizer/graph/location.py @@ -59,9 +59,12 @@ class Location: def evalPostAvailable(self, smbm): if self.difficulty.bool == True and self.PostAvailable is not None: - smbm.addItem(self.itemName) + addAndRemoveItem = smbm.isCountItem(self.itemName) or not smbm.haveItem(self.itemName) + if addAndRemoveItem: + smbm.addItem(self.itemName) postAvailable = self.PostAvailable(smbm) - smbm.removeItem(self.itemName) + if addAndRemoveItem: + smbm.removeItem(self.itemName) self.difficulty = self.difficulty & postAvailable if self.locDifficulty is not None: diff --git a/worlds/sm/variaRandomizer/logic/smboolmanager.py b/worlds/sm/variaRandomizer/logic/smboolmanager.py index bb31584714..351e2835f3 100644 --- a/worlds/sm/variaRandomizer/logic/smboolmanager.py +++ b/worlds/sm/variaRandomizer/logic/smboolmanager.py @@ -91,9 +91,12 @@ class SMBoolManager(object): return itemsDict def withItem(self, item, func): - self.addItem(item) + addAndRemoveItem = self.isCountItem(item) or not self.haveItem(item) + if addAndRemoveItem: + self.addItem(item) ret = func(self) - self.removeItem(item) + if addAndRemoveItem: + self.removeItem(item) return ret def resetItems(self): diff --git a/worlds/sm/variaRandomizer/rom/rompatcher.py b/worlds/sm/variaRandomizer/rom/rompatcher.py index fedb89e854..5e821da1fc 100644 --- a/worlds/sm/variaRandomizer/rom/rompatcher.py +++ b/worlds/sm/variaRandomizer/rom/rompatcher.py @@ -279,11 +279,12 @@ class RomPatcher: # apply area patches if self.settings["area"] == True: + areaPatches = list(RomPatcher.IPSPatches['Area']) if not self.settings["areaLayout"]: for p in ['area_rando_layout.ips', 'Sponge_Bath_Blinking_Door', 'east_ocean.ips', 'aqueduct_bomb_blocks.ips']: - RomPatcher.IPSPatches['Area'].remove(p) - RomPatcher.IPSPatches['Area'].append('area_rando_layout_base.ips') - for patchName in RomPatcher.IPSPatches['Area']: + areaPatches.remove(p) + areaPatches.append('area_rando_layout_base.ips') + for patchName in areaPatches: self.applyIPSPatch(patchName) else: self.applyIPSPatch('area_ids_alt.ips') diff --git a/worlds/sm/variaRandomizer/utils/objectives.py b/worlds/sm/variaRandomizer/utils/objectives.py index 5f585b28d9..8c886674fd 100644 --- a/worlds/sm/variaRandomizer/utils/objectives.py +++ b/worlds/sm/variaRandomizer/utils/objectives.py @@ -740,7 +740,7 @@ class Objectives(object): if c not in char2tile: continue romFile.writeWord(0x3800 + char2tile[c]) - + Synonyms.alreadyUsed = [] # write goal completed positions y in sprites OAM baseY = 0x40 addr = Addresses.getOne('objectivesSpritesOAM') From be74a4a71adaa92179170023f77eb21abdb2c0b7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 17 Apr 2023 12:45:44 +0200 Subject: [PATCH 061/489] LttP: update xxtea to 3.0.0 --- worlds/alttp/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/requirements.txt b/worlds/alttp/requirements.txt index bc35559363..f9b39c5df5 100644 --- a/worlds/alttp/requirements.txt +++ b/worlds/alttp/requirements.txt @@ -1,2 +1,2 @@ maseya-z3pr>=1.0.0rc1 -xxtea>=2.0.0.post0 \ No newline at end of file +xxtea>=3.0.0 \ No newline at end of file From 1a7bc4ffd4fe59d8e07a74b44acafe7c06d948e8 Mon Sep 17 00:00:00 2001 From: Trevor L <80716066+TRPG0@users.noreply.github.com> Date: Mon, 17 Apr 2023 18:03:06 -0600 Subject: [PATCH 062/489] Blasphemous: Fix logic for Laudes (for real this time) (#1727) --- worlds/blasphemous/Rules.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/worlds/blasphemous/Rules.py b/worlds/blasphemous/Rules.py index 22953ed4a1..ba01e4056f 100644 --- a/worlds/blasphemous/Rules.py +++ b/worlds/blasphemous/Rules.py @@ -299,9 +299,7 @@ class BlasphemousLogic(LogicMixin): return self.has_group("masks", player, 3) def _blasphemous_laudes_gate(self, player): - return self.has_all({"Petrified Bell", "Blood Perpetuated in Sand", "Three Gnarled Tongues", \ - "Key of the Secular", "Key of the Scribe"}, player) and \ - self.has("Verses Spun from Gold", player, 4) + return self.has("Verses Spun from Gold", player, 4) # Ten Piedad, Tres Angustias, Our Lady of the Charred Visage def _blasphemous_wound_boss_easy(self, player): @@ -481,8 +479,7 @@ def rules(blasphemousworld): lambda state: state._blasphemous_bridge_access(player)) set_rule(world.get_location("BotTC: Inside giant statue", player), lambda state: state._blasphemous_bridge_access(player) and \ - state._blasphemous_laudes_gate(player) and \ - state._blasphemous_1_mask(player)) + state._blasphemous_laudes_gate(player)) # Brotherhood of the Silent Sorrow set_rule(world.get_location("BotSS: Starting room Child of Moonlight", player), @@ -629,8 +626,7 @@ def rules(blasphemousworld): # Hall of the Dawning set_rule(world.get_location("HotD: Laudes, the First of the Amanecidas", player), lambda state: state._blasphemous_bridge_access(player) and \ - state._blasphemous_1_mask(player) and \ - state._blasphemous_laudes_gate(player)) + state._blasphemous_laudes_gate(player)) # Jondo set_rule(world.get_location("Jondo: Upper east chest", player), @@ -945,8 +941,7 @@ def rules(blasphemousworld): lambda state: state._blasphemous_ex_bridge_access(player)) set_rule(world.get_location("BotTC: Inside giant statue", player), lambda state: state._blasphemous_ex_bridge_access(player) and \ - state._blasphemous_laudes_gate(player) and \ - state._blasphemous_1_mask(player)) + state._blasphemous_laudes_gate(player)) set_rule(world.get_location("BotSS: Esdras' final gift", player), lambda state: state._blasphemous_blood_relic(player) and \ state._blasphemous_scapular(player) and \ @@ -995,8 +990,7 @@ def rules(blasphemousworld): state._blasphemous_ranged(player))))) set_rule(world.get_location("HotD: Laudes, the First of the Amanecidas", player), lambda state: state._blasphemous_ex_bridge_access(player) and \ - state._blasphemous_1_mask(player) and \ - state._blasphemous_laudes_gate(player)) + state._blasphemous_laudes_gate(player)) set_rule(world.get_location("LotNW: Elevator Child of Moonlight", player), lambda state: state._blasphemous_blood_relic(player) and \ (state._blasphemous_cherub_22_23_31_32(player) and \ From 7559adbb14042a829a5b7eb3a206c65c25f0a6f0 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Tue, 18 Apr 2023 21:17:28 -0500 Subject: [PATCH 063/489] LTTP: fix bad parens in logging --- worlds/alttp/Client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index 6d0f9f4d10..71a0cf3600 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -332,7 +332,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool: location = ctx.location_names[location_id] snes_logger.info( f'New Check: {location} ' + - f'({len(ctx.checked_locations + 1 if ctx.checked_locations else ctx.locations_checked)}/' + + f'({len(ctx.checked_locations) + 1 if ctx.checked_locations else len(ctx.locations_checked)}/' + f'{len(ctx.missing_locations) + len(ctx.checked_locations)})') try: From 7a9d4272be786ef51047d60d0c7238ebef065cd7 Mon Sep 17 00:00:00 2001 From: axe-y <58866768+axe-y@users.noreply.github.com> Date: Wed, 19 Apr 2023 17:14:46 -0400 Subject: [PATCH 064/489] Generation bug fix (#1740) --- worlds/dlcquest/Regions.py | 2 +- worlds/dlcquest/Rules.py | 18 +++++++++--------- worlds/dlcquest/__init__.py | 9 +++++++++ 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/worlds/dlcquest/Regions.py b/worlds/dlcquest/Regions.py index 5553cb4842..8135a1c362 100644 --- a/worlds/dlcquest/Regions.py +++ b/worlds/dlcquest/Regions.py @@ -131,7 +131,7 @@ def create_regions(world: MultiWorld, player: int, World_Options: Options.DLCQue Entrance(player, "Forest Double Jump", Regforest)] Regforest.locations += [DLCQuestLocation(player, loc_name, location_table[loc_name], Regforest) for loc_name in Locforest_name] - add_coin_dlcquest(Regforest, 169, player) + add_coin_dlcquest(Regforest, 171, player) world.regions.append(Regforest) Regforestdoublejump = Region("The Forest whit double Jump", player, world) diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py index ac9cd23d53..c57976e79e 100644 --- a/worlds/dlcquest/Rules.py +++ b/worlds/dlcquest/Rules.py @@ -15,13 +15,14 @@ def set_rules(world, player, World_Options: Options.DLCQuestOptions): def has_enough_coin(player: int, coin: int): def has_coin(state, player: int, coins: int): coin_possessed = 0 - for i in [4, 7, 9, 10, 46, 50, 60, 76, 89, 100, 169, 203]: + for i in [4, 7, 9, 10, 46, 50, 60, 76, 89, 100, 171, 203]: name_coin = f"{i} coins" if state.has(name_coin, player): coin_possessed += i return coin_possessed >= coins + return lambda state: has_coin(state, player, coin) def has_enough_coin_freemium(player: int, coin: int): @@ -104,9 +105,8 @@ def set_rules(world, player, World_Options: Options.DLCQuestOptions): number_of_bundle = math.floor(825 / World_Options[Options.CoinSanityRange]) for i in range(number_of_bundle): - item_coin = "DLC Quest: number Coin" - item_coin_loc = re.sub("number", str(World_Options[Options.CoinSanityRange] * (i + 1)), item_coin) - set_rule(world.get_location(item_coin_loc, player), + item_coin = f"DLC Quest: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin" + set_rule(world.get_location(item_coin, player), has_enough_coin(player, World_Options[Options.CoinSanityRange] * (i + 1))) if 825 % World_Options[Options.CoinSanityRange] != 0: set_rule(world.get_location("DLC Quest: 825 Coin", player), @@ -195,7 +195,6 @@ def set_rules(world, player, World_Options: Options.DLCQuestOptions): set_rule(world.get_location("Finish the Fight Pack", player), has_enough_coin(player, 5)) - if World_Options[Options.EndingChoice] == Options.EndingChoice.option_any: set_rule(world.get_location("Winning Basic", player), lambda state: state.has("Finish the Fight Pack", player)) @@ -210,8 +209,6 @@ def set_rules(world, player, World_Options: Options.DLCQuestOptions): lambda state: state.has("Wall Jump Pack", player)) set_rule(world.get_entrance("Harmless Plants", player), lambda state: state.has("Harmless Plants Pack", player)) - set_rule(world.get_entrance("Pickaxe Hard Cave", player), - lambda state: state.has("Pickaxe", player)) set_rule(world.get_entrance("Name Change Entrance", player), lambda state: state.has("Name Change Pack", player)) set_rule(world.get_entrance("Cut Content Entrance", player), @@ -226,12 +223,17 @@ def set_rules(world, player, World_Options: Options.DLCQuestOptions): lambda state: state.has("Death of Comedy Pack", player)) set_rule(world.get_location("Story is Important", player), lambda state: state.has("DLC NPC Pack", player)) + set_rule(world.get_entrance("Pickaxe Hard Cave", player), + lambda state: state.has("Pickaxe", player)) if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_disabled: set_rule(world.get_entrance("Vines", player), lambda state: state.has("Incredibly Important Pack", player)) set_rule(world.get_entrance("Behind Rocks", player), lambda state: state.can_reach("Cut Content", 'region', player)) + set_rule(world.get_entrance("Pickaxe Hard Cave", player), + lambda state: state.can_reach("Cut Content", 'region', player) and state.has("Name Change Pack", + player)) if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: set_rule(world.get_entrance("Vines", player), @@ -357,8 +359,6 @@ def set_rules(world, player, World_Options: Options.DLCQuestOptions): set_rule(world.get_location("Remove Ads Pack", player), has_enough_coin_freemium(player, 25)) - - if World_Options[Options.Campaign] == Options.Campaign.option_basic: world.completion_condition[player] = lambda state: state.has("Victory Basic", player) diff --git a/worlds/dlcquest/__init__.py b/worlds/dlcquest/__init__.py index c30e4a8f45..9569d0efcc 100644 --- a/worlds/dlcquest/__init__.py +++ b/worlds/dlcquest/__init__.py @@ -6,6 +6,7 @@ from .Locations import location_table, DLCQuestLocation from .Options import DLCQuest_options, DLCQuestOptions, fetch_options from .Rules import set_rules from .Regions import create_regions +from . import Options client_version = 0 @@ -49,6 +50,7 @@ class DLCqworld(World): return DLCQuestItem(event, True, None, self.player) def create_items(self): + self.precollect_coinsanity() locations_count = len([location for location in self.multiworld.get_locations(self.player) if not location.event]) @@ -59,11 +61,18 @@ class DLCqworld(World): created_items = create_items(self, self.options, locations_count + len(items_to_exclude), self.multiworld.random) self.multiworld.itempool += created_items + self.multiworld.early_items[self.player]["Movement Pack"] = 1 for item in items_to_exclude: if item in self.multiworld.itempool: self.multiworld.itempool.remove(item) + def precollect_coinsanity(self): + if self.options[Options.Campaign] == Options.Campaign.option_basic: + if self.options[Options.CoinSanity] == Options.CoinSanity.option_coin and self.options[Options.CoinSanityRange] >= 5: + self.multiworld.push_precollected(self.create_item("Movement Pack")) + + def create_item(self, item: Union[str, ItemData]) -> DLCQuestItem: if isinstance(item, str): item = item_table[item] From 664bbd86bbaa0d07fe754e3097f82ba559bb1815 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Wed, 19 Apr 2023 17:15:22 -0400 Subject: [PATCH 065/489] Stardew Valley: Fix Mistake and Formatting in settings page (#1737) --- worlds/stardew_valley/options.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index d8daed8fb6..445c2a2e1b 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -25,12 +25,9 @@ class Goal(Choice): Community Center: The world will be completed once you complete the Community Center. Grandpa's Evaluation: The world will be completed once 4 candles are lit at Grandpa's Shrine. Bottom of the Mines: The world will be completed once you reach level 120 in the mineshaft. - Cryptic Note: The world will be completed once you complete the quest "Cryptic Note" where Mr Qi asks you to - reach floor 100 in the Skull Cavern. - Master Angler: The world will be completed once you have caught every fish in the game. Pairs well with - Fishsanity. - Complete Collection: The world will be completed once you have completed the museum by donating every possible - item. Pairs well with Museumsanity. + Cryptic Note: The world will be completed once you complete the quest "Cryptic Note" where Mr Qi asks you to reach floor 100 in the Skull Cavern. + Master Angler: The world will be completed once you have caught every fish in the game. Pairs well with Fishsanity. + Complete Collection: The world will be completed once you have completed the museum by donating every possible item. Pairs well with Museumsanity. Full House: The world will be completed once you get married and have two kids. Pairs well with Friendsanity. """ internal_name = "goal" @@ -163,10 +160,9 @@ class SeasonRandomization(Choice): class SeedShuffle(Choice): """Should seeds be randomized? - Pierre now sells a random amount of seasonal seeds and Joja sells them without season requirements, but only in - huge packs. + Pierre now sells a random amount of seasonal seeds and Joja sells them without season requirements, but only in huge packs. Disabled: All the seeds will be unlocked from the start. - Randomized: The seeds will be unlocked as Archipelago items + Shuffled: The seeds will be unlocked as Archipelago items """ internal_name = "seed_shuffle" display_name = "Seed Shuffle" @@ -462,6 +458,7 @@ class GiftTax(SpecialRange): stardew_valley_option_classes = [ + Goal, StartingMoney, ResourcePackMultiplier, BundleRandomization, @@ -480,7 +477,6 @@ stardew_valley_option_classes = [ Museumsanity, Friendsanity, NumberOfPlayerBuffs, - Goal, MultipleDaySleepEnabled, MultipleDaySleepCost, ExperienceMultiplier, From 0ca3c5e6a29d6a00729f8fd7087a144d0d6383cd Mon Sep 17 00:00:00 2001 From: kindasneaki Date: Wed, 19 Apr 2023 15:16:13 -0600 Subject: [PATCH 066/489] [kivy] change the sizing for macOS (#1732) --- kvui.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/kvui.py b/kvui.py index 262f3684d9..77b96b896f 100644 --- a/kvui.py +++ b/kvui.py @@ -25,6 +25,7 @@ from kivy.base import ExceptionHandler, ExceptionManager from kivy.clock import Clock from kivy.factory import Factory from kivy.properties import BooleanProperty, ObjectProperty +from kivy.metrics import dp from kivy.uix.widget import Widget from kivy.uix.button import Button from kivy.uix.gridlayout import GridLayout @@ -343,18 +344,18 @@ class GameManager(App): self.grid = MainLayout() self.grid.cols = 1 - self.connect_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=30) + self.connect_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30)) # top part server_label = ServerLabel() self.connect_layout.add_widget(server_label) self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:", size_hint_y=None, - height=30, multiline=False, write_tab=False) + height=dp(30), multiline=False, write_tab=False) def connect_bar_validate(sender): if not self.ctx.server: self.connect_button_action(sender) self.server_connect_bar.bind(on_text_validate=connect_bar_validate) self.connect_layout.add_widget(self.server_connect_bar) - self.server_connect_button = Button(text="Connect", size=(100, 30), size_hint_y=None, size_hint_x=None) + self.server_connect_button = Button(text="Connect", size=(dp(100), dp(30)), size_hint_y=None, size_hint_x=None) self.server_connect_button.bind(on_press=self.connect_button_action) self.connect_layout.add_widget(self.server_connect_button) self.grid.add_widget(self.connect_layout) @@ -387,11 +388,11 @@ class GameManager(App): self.tabs.tab_height = 0 # bottom part - bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=30) - info_button = Button(height=30, text="Command:", size_hint_x=None) + bottom_layout = BoxLayout(orientation="horizontal", size_hint_y=None, height=dp(30)) + info_button = Button(size=(dp(100), dp(30)), text="Command:", size_hint_x=None) info_button.bind(on_release=self.command_button_action) bottom_layout.add_widget(info_button) - self.textinput = TextInput(size_hint_y=None, height=30, multiline=False, write_tab=False) + self.textinput = TextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False) self.textinput.bind(on_text_validate=self.on_message) self.textinput.text_validate_unfocus = False bottom_layout.add_widget(self.textinput) From 722757e18a4d46a739940dec2e01bfcf26c0aa37 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Wed, 19 Apr 2023 21:17:16 -0500 Subject: [PATCH 067/489] Clique: Better, less-obtuse, docs for Clique. (#1719) --- worlds/clique/docs/en_Clique.md | 8 +++++++- worlds/clique/docs/guide_en.md | 22 +++++++++++++++++++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/worlds/clique/docs/en_Clique.md b/worlds/clique/docs/en_Clique.md index afcf48c5d0..b63ec44dcd 100644 --- a/worlds/clique/docs/en_Clique.md +++ b/worlds/clique/docs/en_Clique.md @@ -2,7 +2,13 @@ ## What is this game? -Yes. +~~Clique is a psychological survival horror game where a player must survive the temptation to press red buttons.~~ + +Clique is a joke game developed for Archipelago in March 2023 to showcase how easy it can be to develop a world for +Archipelago. The objective of the game is to press the big red button. If a player is playing on `hard_mode`, they must +wait for someone else in the multiworld to "activate" their button before they can press it. + +Clique can be played on any HTML5-capable browser. ## Where is the settings page? diff --git a/worlds/clique/docs/guide_en.md b/worlds/clique/docs/guide_en.md index a1e07093a6..b2af48a51b 100644 --- a/worlds/clique/docs/guide_en.md +++ b/worlds/clique/docs/guide_en.md @@ -1,6 +1,22 @@ # Clique Start Guide -Go to the [Clique Game](http://clique.pharware.com/) and enter the hostname:ip address, -then your slot name. +After rolling your seed, go to the [Clique Game](http://clique.pharware.com/) site and enter the server details, your +slot name, and a room password if one is required. Then click "Connect". -Enjoy. \ No newline at end of file +If you're playing on "easy mode", just click the button and receive "Satisfaction". + +If you're playing on "hard mode", you may need to wait for activation before you can complete your objective. Luckily, +Clique runs in all the major browsers that support HTML5, so you can load Clique on your phone and be productive while +you wait! + +If you need some ideas for what to do while waiting for button activation, give the following a try: + +- Clean your room. +- Wash the dishes. +- Get some food from a non-descript fast food restaurant. +- Do the daily Wordle. +- ~~Sell your soul to Phar.~~ +- Do your school work. + + +~~If you run into any issues with this game, definitely do not contact Phar#4444 on discord. *wink* *wink*~~ From 4dc934729da1e34a7c68480da9337093d66cf761 Mon Sep 17 00:00:00 2001 From: Adam Heinermann Date: Wed, 19 Apr 2023 20:21:56 -0700 Subject: [PATCH 068/489] Noita: implement new game (#1676) * Noita: implement new game (#1676) --------- Co-authored-by: DaftBrit <87314354+DaftBrit@users.noreply.github.com> Co-authored-by: l.kelsall@b4rn.org.uk Co-authored-by: Fabian Dill Co-authored-by: Scipio Wright Co-authored-by: Scipio Wright Co-authored-by: Zach Parks --- README.md | 1 + worlds/noita/Events.py | 42 +++++++ worlds/noita/Items.py | 156 +++++++++++++++++++++++++ worlds/noita/Locations.py | 214 ++++++++++++++++++++++++++++++++++ worlds/noita/Options.py | 89 ++++++++++++++ worlds/noita/Regions.py | 145 +++++++++++++++++++++++ worlds/noita/Rules.py | 153 ++++++++++++++++++++++++ worlds/noita/__init__.py | 55 +++++++++ worlds/noita/docs/en_Noita.md | 63 ++++++++++ worlds/noita/docs/setup_en.md | 44 +++++++ 10 files changed, 962 insertions(+) create mode 100644 worlds/noita/Events.py create mode 100644 worlds/noita/Items.py create mode 100644 worlds/noita/Locations.py create mode 100644 worlds/noita/Options.py create mode 100644 worlds/noita/Regions.py create mode 100644 worlds/noita/Rules.py create mode 100644 worlds/noita/__init__.py create mode 100644 worlds/noita/docs/en_Noita.md create mode 100644 worlds/noita/docs/setup_en.md diff --git a/README.md b/README.md index e5419072a9..654cd6d600 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,7 @@ Currently, the following games are supported: * Clique * Adventure * DLC Quest +* Noita For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/worlds/noita/Events.py b/worlds/noita/Events.py new file mode 100644 index 0000000000..e759d38c6c --- /dev/null +++ b/worlds/noita/Events.py @@ -0,0 +1,42 @@ +from typing import Dict + +from BaseClasses import Item, ItemClassification, Location, MultiWorld, Region +from . import Items, Locations + + +def create_event(player: int, name: str) -> Item: + return Items.NoitaItem(name, ItemClassification.progression, None, player) + + +def create_location(player: int, name: str, region: Region) -> Location: + return Locations.NoitaLocation(player, name, None, region) + + +def create_locked_location_event(multiworld: MultiWorld, player: int, region_name: str, item: str) -> Location: + region = multiworld.get_region(region_name, player) + + new_location = create_location(player, item, region) + new_location.place_locked_item(create_event(player, item)) + + region.locations.append(new_location) + return new_location + + +def create_all_events(multiworld: MultiWorld, player: int) -> None: + for region, event in event_locks.items(): + create_locked_location_event(multiworld, player, region, event) + + multiworld.completion_condition[player] = lambda state: state.has("Victory", player) + + +# Maps region names to event names +event_locks: Dict[str, str] = { + "The Work": "Victory", + "Mines": "Portal to Holy Mountain 1", + "Coal Pits": "Portal to Holy Mountain 2", + "Snowy Depths": "Portal to Holy Mountain 3", + "Hiisi Base": "Portal to Holy Mountain 4", + "Underground Jungle": "Portal to Holy Mountain 5", + "The Vault": "Portal to Holy Mountain 6", + "Temple of the Art": "Portal to Holy Mountain 7", +} diff --git a/worlds/noita/Items.py b/worlds/noita/Items.py new file mode 100644 index 0000000000..6499e94517 --- /dev/null +++ b/worlds/noita/Items.py @@ -0,0 +1,156 @@ +import itertools +from collections import Counter +from typing import Dict, List, NamedTuple, Optional, Set + +from BaseClasses import Item, ItemClassification, MultiWorld +from .Options import BossesAsChecks, VictoryCondition + + +class ItemData(NamedTuple): + code: Optional[int] + group: str + classification: ItemClassification = ItemClassification.progression + required_num: int = 0 + + +class NoitaItem(Item): + game: str = "Noita" + + +def create_item(player: int, name: str) -> Item: + item_data = item_table[name] + return NoitaItem(name, item_data.classification, item_data.code, player) + + +def create_fixed_item_pool() -> List[str]: + required_items: Dict[str, int] = {name: data.required_num for name, data in item_table.items()} + return list(Counter(required_items).elements()) + + +def create_orb_items(victory_condition: VictoryCondition) -> List[str]: + orb_count = 0 + if victory_condition == VictoryCondition.option_pure_ending: + orb_count = 11 + elif victory_condition == VictoryCondition.option_peaceful_ending: + orb_count = 33 + return ["Orb" for _ in range(orb_count)] + + +def create_spatial_awareness_item(bosses_as_checks: BossesAsChecks) -> List[str]: + return ["Spatial Awareness Perk"] if bosses_as_checks.value >= BossesAsChecks.option_all_bosses else [] + + +def create_kantele(victory_condition: VictoryCondition) -> List[str]: + return ["Kantele"] if victory_condition.value >= VictoryCondition.option_pure_ending else [] + + +def create_random_items(multiworld: MultiWorld, player: int, random_count: int) -> List[str]: + filler_pool = filler_weights.copy() + if multiworld.bad_effects[player].value == 0: + del filler_pool["Trap"] + + return multiworld.random.choices( + population=list(filler_pool.keys()), + weights=list(filler_pool.values()), + k=random_count + ) + + +def create_all_items(multiworld: MultiWorld, player: int) -> None: + sum_locations = len(multiworld.get_unfilled_locations(player)) + + itempool = ( + create_fixed_item_pool() + + create_orb_items(multiworld.victory_condition[player]) + + create_spatial_awareness_item(multiworld.bosses_as_checks[player]) + + create_kantele(multiworld.victory_condition[player]) + ) + + random_count = sum_locations - len(itempool) + itempool += create_random_items(multiworld, player, random_count) + + multiworld.itempool += [create_item(player, name) for name in itempool] + + +# 110000 - 110032 +item_table: Dict[str, ItemData] = { + "Trap": ItemData(110000, "Traps", ItemClassification.trap), + "Extra Max HP": ItemData(110001, "Pickups", ItemClassification.useful), + "Spell Refresher": ItemData(110002, "Pickups", ItemClassification.filler), + "Potion": ItemData(110003, "Items", ItemClassification.filler), + "Gold (200)": ItemData(110004, "Gold", ItemClassification.filler), + "Gold (1000)": ItemData(110005, "Gold", ItemClassification.filler), + "Wand (Tier 1)": ItemData(110006, "Wands", ItemClassification.useful), + "Wand (Tier 2)": ItemData(110007, "Wands", ItemClassification.useful), + "Wand (Tier 3)": ItemData(110008, "Wands", ItemClassification.useful), + "Wand (Tier 4)": ItemData(110009, "Wands", ItemClassification.useful), + "Wand (Tier 5)": ItemData(110010, "Wands", ItemClassification.useful), + "Wand (Tier 6)": ItemData(110011, "Wands", ItemClassification.useful), + "Kantele": ItemData(110012, "Wands", ItemClassification.useful), + "Fire Immunity Perk": ItemData(110013, "Perks", ItemClassification.progression, 1), + "Toxic Immunity Perk": ItemData(110014, "Perks", ItemClassification.progression, 1), + "Explosion Immunity Perk": ItemData(110015, "Perks", ItemClassification.progression, 1), + "Melee Immunity Perk": ItemData(110016, "Perks", ItemClassification.progression, 1), + "Electricity Immunity Perk": ItemData(110017, "Perks", ItemClassification.progression, 1), + "Tinker with Wands Everywhere Perk": ItemData(110018, "Perks", ItemClassification.progression, 1), + "All-Seeing Eye Perk": ItemData(110019, "Perks", ItemClassification.progression, 1), + "Spatial Awareness Perk": ItemData(110020, "Perks", ItemClassification.progression), + "Extra Life Perk": ItemData(110021, "Repeatable Perks", ItemClassification.useful), + "Orb": ItemData(110022, "Orbs", ItemClassification.progression_skip_balancing), + "Random Potion": ItemData(110023, "Items", ItemClassification.filler), + "Secret Potion": ItemData(110024, "Items", ItemClassification.filler), + "Powder Pouch": ItemData(110025, "Items", ItemClassification.filler), + "Chaos Die": ItemData(110026, "Items", ItemClassification.filler), + "Greed Die": ItemData(110027, "Items", ItemClassification.filler), + "Kammi": ItemData(110028, "Items", ItemClassification.filler), + "Refreshing Gourd": ItemData(110029, "Items", ItemClassification.filler), + "Sädekivi": ItemData(110030, "Items", ItemClassification.filler), + "Broken Wand": ItemData(110031, "Items", ItemClassification.filler), + +} + +filler_weights: Dict[str, int] = { + "Trap": 15, + "Extra Max HP": 25, + "Spell Refresher": 20, + "Potion": 40, + "Gold (200)": 15, + "Gold (1000)": 6, + "Wand (Tier 1)": 10, + "Wand (Tier 2)": 8, + "Wand (Tier 3)": 7, + "Wand (Tier 4)": 6, + "Wand (Tier 5)": 5, + "Wand (Tier 6)": 4, + "Extra Life Perk": 10, + "Random Potion": 9, + "Secret Potion": 10, + "Powder Pouch": 10, + "Chaos Die": 4, + "Greed Die": 4, + "Kammi": 4, + "Refreshing Gourd": 4, + "Sädekivi": 3, + "Broken Wand": 10, +} + + +# These helper functions make the comprehensions below more readable +def get_item_group(item_name: str) -> str: + return item_table[item_name].group + + +def item_is_filler(item_name: str) -> bool: + return item_table[item_name].classification == ItemClassification.filler + + +def item_is_perk(item_name: str) -> bool: + return item_table[item_name].group == "Perks" + + +filler_items: List[str] = list(filter(item_is_filler, item_table.keys())) +item_name_to_id: Dict[str, int] = {name: data.code for name, data in item_table.items()} + +item_name_groups: Dict[str, Set[str]] = { + group: set(item_names) for group, item_names in itertools.groupby(item_table, get_item_group) +} diff --git a/worlds/noita/Locations.py b/worlds/noita/Locations.py new file mode 100644 index 0000000000..bbfae1c943 --- /dev/null +++ b/worlds/noita/Locations.py @@ -0,0 +1,214 @@ +# Locations are specific points that you would obtain an item at. +from enum import IntEnum +from typing import Dict, NamedTuple, Optional + +from BaseClasses import Location + + +class NoitaLocation(Location): + game: str = "Noita" + + +class LocationData(NamedTuple): + id: int + flag: int = 0 + ltype: Optional[str] = "" + + +class LocationFlag(IntEnum): + none = 0 + main_path = 1 + side_path = 2 + main_world = 3 + parallel_worlds = 4 + + +# Mapping of items in each region. +# Only the first Hidden Chest and Pedestal are mapped here, the others are created in Regions. +# ltype key: "chest" = Hidden Chests, "pedestal" = Pedestals, "boss" = Boss, "orb" = Orb. +# 110000-110649 +location_region_mapping: Dict[str, Dict[str, LocationData]] = { + "Coal Pits Holy Mountain": { + "Coal Pits Holy Mountain Shop Item 1": LocationData(110000), + "Coal Pits Holy Mountain Shop Item 2": LocationData(110001), + "Coal Pits Holy Mountain Shop Item 3": LocationData(110002), + "Coal Pits Holy Mountain Shop Item 4": LocationData(110003), + "Coal Pits Holy Mountain Shop Item 5": LocationData(110004), + "Coal Pits Holy Mountain Spell Refresh": LocationData(110005), + }, + "Snowy Depths Holy Mountain": { + "Snowy Depths Holy Mountain Shop Item 1": LocationData(110006), + "Snowy Depths Holy Mountain Shop Item 2": LocationData(110007), + "Snowy Depths Holy Mountain Shop Item 3": LocationData(110008), + "Snowy Depths Holy Mountain Shop Item 4": LocationData(110009), + "Snowy Depths Holy Mountain Shop Item 5": LocationData(110010), + "Snowy Depths Holy Mountain Spell Refresh": LocationData(110011), + }, + "Hiisi Base Holy Mountain": { + "Hiisi Base Holy Mountain Shop Item 1": LocationData(110012), + "Hiisi Base Holy Mountain Shop Item 2": LocationData(110013), + "Hiisi Base Holy Mountain Shop Item 3": LocationData(110014), + "Hiisi Base Holy Mountain Shop Item 4": LocationData(110015), + "Hiisi Base Holy Mountain Shop Item 5": LocationData(110016), + "Hiisi Base Holy Mountain Spell Refresh": LocationData(110017), + }, + "Underground Jungle Holy Mountain": { + "Underground Jungle Holy Mountain Shop Item 1": LocationData(110018), + "Underground Jungle Holy Mountain Shop Item 2": LocationData(110019), + "Underground Jungle Holy Mountain Shop Item 3": LocationData(110020), + "Underground Jungle Holy Mountain Shop Item 4": LocationData(110021), + "Underground Jungle Holy Mountain Shop Item 5": LocationData(110022), + "Underground Jungle Holy Mountain Spell Refresh": LocationData(110023), + }, + "Vault Holy Mountain": { + "Vault Holy Mountain Shop Item 1": LocationData(110024), + "Vault Holy Mountain Shop Item 2": LocationData(110025), + "Vault Holy Mountain Shop Item 3": LocationData(110026), + "Vault Holy Mountain Shop Item 4": LocationData(110027), + "Vault Holy Mountain Shop Item 5": LocationData(110028), + "Vault Holy Mountain Spell Refresh": LocationData(110029), + }, + "Temple of the Art Holy Mountain": { + "Temple of the Art Holy Mountain Shop Item 1": LocationData(110030), + "Temple of the Art Holy Mountain Shop Item 2": LocationData(110031), + "Temple of the Art Holy Mountain Shop Item 3": LocationData(110032), + "Temple of the Art Holy Mountain Shop Item 4": LocationData(110033), + "Temple of the Art Holy Mountain Shop Item 5": LocationData(110034), + "Temple of the Art Holy Mountain Spell Refresh": LocationData(110035), + }, + "Laboratory Holy Mountain": { + "Laboratory Holy Mountain Shop Item 1": LocationData(110036), + "Laboratory Holy Mountain Shop Item 2": LocationData(110037), + "Laboratory Holy Mountain Shop Item 3": LocationData(110038), + "Laboratory Holy Mountain Shop Item 4": LocationData(110039), + "Laboratory Holy Mountain Shop Item 5": LocationData(110040), + "Laboratory Holy Mountain Spell Refresh": LocationData(110041), + }, + "Secret Shop": { + "Secret Shop Item 1": LocationData(110042), + "Secret Shop Item 2": LocationData(110043), + "Secret Shop Item 3": LocationData(110044), + "Secret Shop Item 4": LocationData(110045), + }, + "Floating Island": { + "Floating Island Orb": LocationData(110658, LocationFlag.main_path, "orb"), + }, + "Pyramid": { + "Kolmisilmän Koipi": LocationData(110649, LocationFlag.main_world, "boss"), + "Pyramid Orb": LocationData(110659, LocationFlag.main_world, "orb"), + "Sandcave Orb": LocationData(110662, LocationFlag.main_world, "orb"), + }, + "Overgrown Cavern": { + "Overgrown Cavern Chest": LocationData(110526, LocationFlag.main_world, "chest"), + "Overgrown Cavern Pedestal": LocationData(110546, LocationFlag.main_world, "pedestal"), + }, + "Lake": { + "Syväolento": LocationData(110651, LocationFlag.main_world, "boss"), + }, + "Frozen Vault": { + "Frozen Vault Orb": LocationData(110660, LocationFlag.main_world, "orb"), + "Frozen Vault Chest": LocationData(110566, LocationFlag.main_world, "chest"), + "Frozen Vault Pedestal": LocationData(110586, LocationFlag.main_world, "pedestal"), + }, + "Mines": { + "Mines Chest": LocationData(110046, LocationFlag.main_path, "chest"), + "Mines Pedestal": LocationData(110066, LocationFlag.main_path, "pedestal"), + }, + # Collapsed Mines is a very small area, combining it with the Mines. Leaving this here in case we change our minds. + # "Collapsed Mines": { + # "Collapsed Mines Chest": LocationData(110086, LocationFlag.main_path, "chest"), + # "Collapsed Mines Pedestal": LocationData(110106, LocationFlag.main_path, "pedestal"), + # }, + "Ancient Laboratory": { + "Ylialkemisti": LocationData(110656, LocationFlag.side_path, "boss"), + }, + "Abyss Orb Room": { + "Sauvojen Tuntija": LocationData(110650, LocationFlag.side_path, "boss"), + "Abyss Orb": LocationData(110665, LocationFlag.main_path, "orb"), + }, + "Below Lava Lake": { + "Lava Lake Orb": LocationData(110661, LocationFlag.side_path, "orb"), + }, + "Coal Pits": { + "Coal Pits Chest": LocationData(110126, LocationFlag.main_path, "chest"), + "Coal Pits Pedestal": LocationData(110146, LocationFlag.main_path, "pedestal"), + }, + "Fungal Caverns": { + "Fungal Caverns Chest": LocationData(110166, LocationFlag.side_path, "chest"), + "Fungal Caverns Pedestal": LocationData(110186, LocationFlag.side_path, "pedestal"), + }, + "Snowy Depths": { + "Snowy Depths Chest": LocationData(110206, LocationFlag.main_path, "chest"), + "Snowy Depths Pedestal": LocationData(110226, LocationFlag.main_path, "pedestal"), + }, + "Magical Temple": { + "Magical Temple Orb": LocationData(110663, LocationFlag.side_path, "orb"), + }, + "Hiisi Base": { + "Hiisi Base Chest": LocationData(110246, LocationFlag.main_path, "chest"), + "Hiisi Base Pedestal": LocationData(110266, LocationFlag.main_path, "pedestal"), + }, + "Underground Jungle": { + "Suomuhauki": LocationData(110648, LocationFlag.main_path, "boss"), + "Underground Jungle Chest": LocationData(110286, LocationFlag.main_path, "chest"), + "Underground Jungle Pedestal": LocationData(110306, LocationFlag.main_path, "pedestal"), + }, + "Lukki Lair": { + "Lukki Lair Orb": LocationData(110664, LocationFlag.side_path, "orb"), + "Lukki Lair Chest": LocationData(110326, LocationFlag.side_path, "chest"), + "Lukki Lair Pedestal": LocationData(110346, LocationFlag.side_path, "pedestal"), + }, + "The Vault": { + "The Vault Chest": LocationData(110366, LocationFlag.main_path, "chest"), + "The Vault Pedestal": LocationData(110386, LocationFlag.main_path, "pedestal"), + }, + "Temple of the Art": { + "Gate Guardian": LocationData(110652, LocationFlag.main_path, "boss"), + "Temple of the Art Chest": LocationData(110406, LocationFlag.main_path, "chest"), + "Temple of the Art Pedestal": LocationData(110426, LocationFlag.main_path, "pedestal"), + }, + "The Tower": { + "The Tower Chest": LocationData(110606, LocationFlag.main_world, "chest"), + "The Tower Pedestal": LocationData(110626, LocationFlag.main_world, "pedestal"), + }, + "Wizard's Den": { + "Mestarien Mestari": LocationData(110655, LocationFlag.main_world, "boss"), + "Wizard's Den Orb": LocationData(110668, LocationFlag.main_world, "orb"), + "Wizards' Den Chest": LocationData(110446, LocationFlag.main_world, "chest"), + "Wizards' Den Pedestal": LocationData(110466, LocationFlag.main_world, "pedestal"), + }, + "Powerplant": { + "Kolmisilmän silmä": LocationData(110657, LocationFlag.main_world, "boss"), + "Power Plant Chest": LocationData(110486, LocationFlag.main_world, "chest"), + "Power Plant Pedestal": LocationData(110506, LocationFlag.main_world, "pedestal"), + }, + "Snow Chasm": { + "Unohdettu": LocationData(110653, LocationFlag.main_world, "boss"), + "Snow Chasm Orb": LocationData(110667, LocationFlag.main_world, "orb"), + }, + "Deep Underground": { + "Limatoukka": LocationData(110647, LocationFlag.main_world, "boss"), + }, + "The Laboratory": { + "Kolmisilmä": LocationData(110646, LocationFlag.main_path, "boss"), + }, + "Friend Cave": { + "Toveri": LocationData(110654, LocationFlag.main_world, "boss"), + }, + "The Work (Hell)": { + "The Work (Hell) Orb": LocationData(110666, LocationFlag.main_world, "orb"), + }, +} + + +# Iterating the hidden chest and pedestal locations here to avoid clutter above +def generate_location_entries(locname: str, locinfo: LocationData) -> Dict[str, int]: + if locinfo.ltype in ["chest", "pedestal"]: + return {f"{locname} {i + 1}": locinfo.id + i for i in range(20)} + return {locname: locinfo.id} + + +location_name_to_id: Dict[str, int] = {} +for location_group in location_region_mapping.values(): + for locname, locinfo in location_group.items(): + location_name_to_id.update(generate_location_entries(locname, locinfo)) diff --git a/worlds/noita/Options.py b/worlds/noita/Options.py new file mode 100644 index 0000000000..270814f41a --- /dev/null +++ b/worlds/noita/Options.py @@ -0,0 +1,89 @@ +from typing import Dict +from Options import Choice, DeathLink, DefaultOnToggle, Option, Range + + +class PathOption(Choice): + """Choose where you would like Hidden Chest and Pedestal checks to be placed. + Main Path includes the main 7 biomes you typically go through to get to the final boss. + Side Path includes the Lukki Lair and Fungal Caverns. 9 biomes total. + Main World includes the full world (excluding parallel worlds). 14 biomes total. + Note: The Collapsed Mines have been combined into the Mines as the biome is tiny.""" + display_name = "Path Option" + option_main_path = 1 + option_side_path = 2 + option_main_world = 3 + default = 1 + + +class HiddenChests(Range): + """Number of hidden chest checks added to the applicable biomes.""" + display_name = "Hidden Chests per Biome" + range_start = 0 + range_end = 20 + default = 3 + + +class PedestalChecks(Range): + """Number of checks that will spawn on pedestals in the applicable biomes.""" + display_name = "Pedestal Checks per Biome" + range_start = 0 + range_end = 20 + default = 6 + + +class Traps(DefaultOnToggle): + """Whether negative effects on the Noita world are added to the item pool.""" + display_name = "Traps" + + +class OrbsAsChecks(Choice): + """Decides whether finding the orbs that naturally spawn in the world count as checks. + The Main Path option includes only the Floating Island and Abyss Orb Room orbs. + The Side Path option includes the Main Path, Magical Temple, Lukki Lair, and Lava Lake orbs. + The Main World option includes all 11 orbs.""" + display_name = "Orbs as Location Checks" + option_no_orbs = 0 + option_main_path = 1 + option_side_path = 2 + option_main_world = 3 + default = 0 + + +class BossesAsChecks(Choice): + """Makes bosses count as location checks. The boss only needs to die, you do not need the kill credit. + The Main Path option includes Gate Guardian, Suomuhauki, and Kolmisilmä. + The Side Path option includes the Main Path bosses, Sauvojen Tuntija, and Ylialkemisti. + The All Bosses option includes all 12 bosses.""" + display_name = "Bosses as Location Checks" + option_no_bosses = 0 + option_main_path = 1 + option_side_path = 2 + option_all_bosses = 3 + default = 0 + + +# Note: the Sampo is an item that is picked up to trigger the boss fight at the normal ending location. +# The sampo is required for every ending (having orbs and bringing the sampo to a different spot changes the ending). +class VictoryCondition(Choice): + """Greed is to get to the bottom, beat the boss, and win the game. + Pure is to get the 11 orbs in the main world, grab the sampo, and bring it to the mountain altar. + Peaceful is to get all 33 orbs in main + parallel, grab the sampo, and bring it to the mountain altar. + Orbs will be added to the randomizer pool according to what victory condition you chose. + The base game orbs will not count towards these victory conditions.""" + display_name = "Victory Condition" + option_greed_ending = 0 + option_pure_ending = 1 + option_peaceful_ending = 2 + default = 0 + + +noita_options: Dict[str, type(Option)] = { + "death_link": DeathLink, + "bad_effects": Traps, + "victory_condition": VictoryCondition, + "path_option": PathOption, + "hidden_chests": HiddenChests, + "pedestal_checks": PedestalChecks, + "orbs_as_checks": OrbsAsChecks, + "bosses_as_checks": BossesAsChecks, +} diff --git a/worlds/noita/Regions.py b/worlds/noita/Regions.py new file mode 100644 index 0000000000..c6dac74d13 --- /dev/null +++ b/worlds/noita/Regions.py @@ -0,0 +1,145 @@ +# Regions are areas in your game that you travel to. +from typing import Dict, Set + +from BaseClasses import Entrance, MultiWorld, Region +from . import Locations + + +def add_location(player: int, loc_name: str, id: int, region: Region) -> None: + location = Locations.NoitaLocation(player, loc_name, id, region) + region.locations.append(location) + + +def add_locations(multiworld: MultiWorld, player: int, region: Region) -> None: + locations = Locations.location_region_mapping.get(region.name, {}) + for location_name, location_data in locations.items(): + location_type = location_data.ltype + flag = location_data.flag + + opt_orbs = multiworld.orbs_as_checks[player].value + opt_bosses = multiworld.bosses_as_checks[player].value + opt_paths = multiworld.path_option[player].value + opt_num_chests = multiworld.hidden_chests[player].value + opt_num_pedestals = multiworld.pedestal_checks[player].value + + is_orb_allowed = location_type == "orb" and flag <= opt_orbs + is_boss_allowed = location_type == "boss" and flag <= opt_bosses + if flag == Locations.LocationFlag.none or is_orb_allowed or is_boss_allowed: + add_location(player, location_name, location_data.id, region) + elif location_type == "chest" and flag <= opt_paths: + for i in range(opt_num_chests): + add_location(player, f"{location_name} {i+1}", location_data.id + i, region) + elif location_type == "pedestal" and flag <= opt_paths: + for i in range(opt_num_pedestals): + add_location(player, f"{location_name} {i+1}", location_data.id + i, region) + + +# Creates a new Region with the locations found in `location_region_mapping` and adds them to the world. +def create_region(multiworld: MultiWorld, player: int, region_name: str) -> Region: + new_region = Region(region_name, player, multiworld) + add_locations(multiworld, player, new_region) + return new_region + + +def create_regions(multiworld: MultiWorld, player: int) -> Dict[str, Region]: + return {name: create_region(multiworld, player, name) for name in noita_regions} + + +# An "Entrance" is really just a connection between two regions +def create_entrance(player: int, source: str, destination: str, regions: Dict[str, Region]): + entrance = Entrance(player, f"From {source} To {destination}", regions[source]) + entrance.connect(regions[destination]) + return entrance + + +# Creates connections based on our access mapping in `noita_connections`. +def create_connections(player: int, regions: Dict[str, Region]) -> None: + for source, destinations in noita_connections.items(): + new_entrances = [create_entrance(player, source, destination, regions) for destination in destinations] + regions[source].exits = new_entrances + + +# Creates all regions and connections. Called from NoitaWorld. +def create_all_regions_and_connections(multiworld: MultiWorld, player: int) -> None: + created_regions = create_regions(multiworld, player) + create_connections(player, created_regions) + + multiworld.regions += created_regions.values() + + +# Oh, what a tangled web we weave +# Notes to create artificial spheres: +# - Shaft is excluded to disconnect Mines from the Snowy Depths +# - Lukki Lair is disconnected from The Vault +# - Overgrown Cavern is connected to the Underground Jungle instead of the Desert due to similar difficulty +# - Powerplant is disconnected from the Sandcave due to difficulty and sphere creation +# - Snow Chasm is disconnected from the Snowy Wasteland +# - Pyramid is connected to the Hiisi Base instead of the Desert due to similar difficulty +# - Frozen Vault is connected to the Vault instead of the Snowy Wasteland due to similar difficulty +noita_connections: Dict[str, Set[str]] = { + "Menu": {"Forest"}, + "Forest": {"Mines", "Floating Island", "Desert", "Snowy Wasteland"}, + "Snowy Wasteland": {"Lake", "Forest"}, + "Frozen Vault": {"The Vault"}, + "Lake": {"Snowy Wasteland", "Desert"}, + "Desert": {"Lake", "Forest"}, + "Floating Island": {"Forest"}, + "Pyramid": {"Hiisi Base"}, + "Overgrown Cavern": {"Sandcave", "Undeground Jungle"}, + "Sandcave": {"Overgrown Cavern"}, + + ### + "Mines": {"Collapsed Mines", "Coal Pits Holy Mountain", "Lava Lake", "Forest"}, + "Collapsed Mines": {"Mines", "Dark Cave"}, + "Lava Lake": {"Mines", "Abyss Orb Room", "Below Lava Lake"}, + "Abyss Orb Room": {"Lava Lake"}, + "Below Lava Lake": {"Lava Lake"}, + "Dark Cave": {"Ancient Laboratory", "Collapsed Mines"}, + "Ancient Laboratory": {"Dark Cave"}, + + ### + "Coal Pits Holy Mountain": {"Coal Pits"}, + "Coal Pits": {"Coal Pits Holy Mountain", "Fungal Caverns", "Snowy Depths Holy Mountain"}, + "Fungal Caverns": {"Coal Pits"}, + + ### + "Snowy Depths Holy Mountain": {"Snowy Depths"}, + "Snowy Depths": {"Snowy Depths Holy Mountain", "Hiisi Base Holy Mountain", "Magical Temple"}, + "Magical Temple": {"Snowy Depths"}, + + ### + "Hiisi Base Holy Mountain": {"Hiisi Base"}, + "Hiisi Base": {"Hiisi Base Holy Mountain", "Secret Shop", "Pyramid", "Underground Jungle Holy Mountain"}, + "Secret Shop": {"Hiisi Base"}, + + ### + "Underground Jungle Holy Mountain": {"Underground Jungle"}, + "Underground Jungle": {"Underground Jungle Holy Mountain", "Dragoncave", "Overgrown Cavern", "Vault Holy Mountain", + "Lukki Lair"}, + "Dragoncave": {"Underground Jungle"}, + "Lukki Lair": {"Underground Jungle", "Snow Chasm", "Frozen Vault"}, + "Snow Chasm": {}, + + ### + "Vault Holy Mountain": {"The Vault"}, + "The Vault": {"Vault Holy Mountain", "Frozen Vault", "Temple of the Art Holy Mountain"}, + + ### + "Temple of the Art Holy Mountain": {"Temple of the Art"}, + "Temple of the Art": {"Temple of the Art Holy Mountain", "Laboratory Holy Mountain", "The Tower", + "Wizard's Den"}, + "Wizard's Den": {"Temple of the Art", "Powerplant"}, + "Powerplant": {"Wizard's Den", "Deep Underground"}, + "The Tower": {"Forest"}, + "Deep Underground": {}, + + ### + "Laboratory Holy Mountain": {"The Laboratory"}, + "The Laboratory": {"Laboratory Holy Mountain", "The Work", "Friend Cave", "The Work (Hell)"}, + "Friend Cave": {}, + "The Work": {}, + "The Work (Hell)": {}, + ### +} + +noita_regions: Set[str] = set(noita_connections.keys()).union(*noita_connections.values()) diff --git a/worlds/noita/Rules.py b/worlds/noita/Rules.py new file mode 100644 index 0000000000..e0e4b16baa --- /dev/null +++ b/worlds/noita/Rules.py @@ -0,0 +1,153 @@ +from typing import List, NamedTuple, Set + +from BaseClasses import CollectionState, MultiWorld +from . import Items, Locations +from .Options import BossesAsChecks, VictoryCondition +from worlds.generic import Rules as GenericRules + + +class EntranceLock(NamedTuple): + source: str + destination: str + event: str + items_needed: int + + +entrance_locks: List[EntranceLock] = [ + EntranceLock("Mines", "Coal Pits Holy Mountain", "Portal to Holy Mountain 1", 1), + EntranceLock("Coal Pits", "Snowy Depths Holy Mountain", "Portal to Holy Mountain 2", 2), + EntranceLock("Snowy Depths", "Hiisi Base Holy Mountain", "Portal to Holy Mountain 3", 3), + EntranceLock("Hiisi Base", "Underground Jungle Holy Mountain", "Portal to Holy Mountain 4", 4), + EntranceLock("Underground Jungle", "Vault Holy Mountain", "Portal to Holy Mountain 5", 5), + EntranceLock("The Vault", "Temple of the Art Holy Mountain", "Portal to Holy Mountain 6", 6), + EntranceLock("Temple of the Art", "Laboratory Holy Mountain", "Portal to Holy Mountain 7", 7), +] + + +holy_mountain_regions: List[str] = [ + "Coal Pits Holy Mountain", + "Snowy Depths Holy Mountain", + "Hiisi Base Holy Mountain", + "Underground Jungle Holy Mountain", + "Vault Holy Mountain", + "Temple of the Art Holy Mountain", + "Laboratory Holy Mountain", +] + + +wand_tiers: List[str] = [ + "Wand (Tier 1)", # Coal Pits + "Wand (Tier 2)", # Snowy Depths + "Wand (Tier 3)", # Hiisi Base + "Wand (Tier 4)", # Underground Jungle + "Wand (Tier 5)", # The Vault + "Wand (Tier 6)", # Temple of the Art +] + + +items_hidden_from_shops: Set[str] = {"Gold (200)", "Gold (1000)", "Potion", "Random Potion", "Secret Potion", + "Chaos Die", "Greed Die", "Kammi", "Refreshing Gourd", "Sädekivi", "Broken Wand", + "Powder Pouch"} + + +perk_list: List[str] = list(filter(Items.item_is_perk, Items.item_table.keys())) + + +# ---------------- +# Helper Functions +# ---------------- + + +def has_perk_count(state: CollectionState, player: int, amount: int) -> bool: + return sum(state.item_count(perk, player) for perk in perk_list) >= amount + + +def has_orb_count(state: CollectionState, player: int, amount: int) -> bool: + return state.item_count("Orb", player) >= amount + + +def forbid_items_at_location(multiworld: MultiWorld, location_name: str, items: Set[str], player: int): + location = multiworld.get_location(location_name, player) + GenericRules.forbid_items_for_player(location, items, player) + + +# ---------------- +# Rule Functions +# ---------------- + + +# Prevent gold and potions from appearing as purchasable items in shops (because physics will destroy them) +def ban_items_from_shops(multiworld: MultiWorld, player: int) -> None: + for location_name in Locations.location_name_to_id.keys(): + if "Shop Item" in location_name: + forbid_items_at_location(multiworld, location_name, items_hidden_from_shops, player) + + +# Prevent high tier wands from appearing in early Holy Mountain shops +def ban_early_high_tier_wands(multiworld: MultiWorld, player: int) -> None: + for i, region_name in enumerate(holy_mountain_regions): + wands_to_forbid = wand_tiers[i+1:] + + locations_in_region = Locations.location_region_mapping[region_name].keys() + for location_name in locations_in_region: + forbid_items_at_location(multiworld, location_name, wands_to_forbid, player) + + # Prevent high tier wands from appearing in the Secret shop + wands_to_forbid = wand_tiers[3:] + locations_in_region = Locations.location_region_mapping["Secret Shop"].keys() + for location_name in locations_in_region: + forbid_items_at_location(multiworld, location_name, wands_to_forbid, player) + + +def lock_holy_mountains_into_spheres(multiworld: MultiWorld, player: int) -> None: + for lock in entrance_locks: + location = multiworld.get_entrance(f"From {lock.source} To {lock.destination}", player) + GenericRules.set_rule(location, lambda state, evt=lock.event: state.has(evt, player)) + + +def holy_mountain_unlock_conditions(multiworld: MultiWorld, player: int) -> None: + victory_condition = multiworld.victory_condition[player].value + for lock in entrance_locks: + location = multiworld.get_location(lock.event, player) + + if victory_condition == VictoryCondition.option_greed_ending: + location.access_rule = lambda state, items_needed=lock.items_needed: ( + has_perk_count(state, player, items_needed//2) + ) + elif victory_condition == VictoryCondition.option_pure_ending: + location.access_rule = lambda state, items_needed=lock.items_needed: ( + has_perk_count(state, player, items_needed//2) and + has_orb_count(state, player, items_needed) + ) + elif victory_condition == VictoryCondition.option_peaceful_ending: + location.access_rule = lambda state, items_needed=lock.items_needed: ( + has_perk_count(state, player, items_needed//2) and + has_orb_count(state, player, items_needed * 3) + ) + + +def victory_unlock_conditions(multiworld: MultiWorld, player: int) -> None: + victory_condition = multiworld.victory_condition[player].value + victory_location = multiworld.get_location("Victory", player) + + if victory_condition == VictoryCondition.option_pure_ending: + victory_location.access_rule = lambda state: has_orb_count(state, player, 11) + elif victory_condition == VictoryCondition.option_peaceful_ending: + victory_location.access_rule = lambda state: has_orb_count(state, player, 33) + + +# ---------------- +# Main Function +# ---------------- + + +def create_all_rules(multiworld: MultiWorld, player: int) -> None: + ban_items_from_shops(multiworld, player) + ban_early_high_tier_wands(multiworld, player) + lock_holy_mountains_into_spheres(multiworld, player) + holy_mountain_unlock_conditions(multiworld, player) + victory_unlock_conditions(multiworld, player) + + # Prevent the Map perk (used to find Toveri) from being on Toveri (boss) + if multiworld.bosses_as_checks[player].value >= BossesAsChecks.option_all_bosses: + forbid_items_at_location(multiworld, "Toveri", {"Spatial Awareness Perk"}, player) diff --git a/worlds/noita/__init__.py b/worlds/noita/__init__.py new file mode 100644 index 0000000000..253c8e9df7 --- /dev/null +++ b/worlds/noita/__init__.py @@ -0,0 +1,55 @@ +from BaseClasses import Item, Tutorial +from worlds.AutoWorld import WebWorld, World +from . import Events, Items, Locations, Options, Regions, Rules + + +class NoitaWeb(WebWorld): + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide to setting up the Noita integration for Archipelago multiworld games.", + "English", + "setup_en.md", + "setup/en", + ["Heinermann", "ScipioWright", "DaftBrit"] + )] + theme = "partyTime" + bug_report_page = "https://github.com/DaftBrit/NoitaArchipelago/issues" + + +# Keeping World slim so that it's easier to comprehend +class NoitaWorld(World): + """ + Noita is a magical action roguelite set in a world where every pixel is physically simulated. Fight, explore, melt, + burn, freeze, and evaporate your way through the procedurally generated world using wands you've created yourself. + """ + + game = "Noita" + option_definitions = Options.noita_options + + item_name_to_id = Items.item_name_to_id + location_name_to_id = Locations.location_name_to_id + + item_name_groups = Items.item_name_groups + data_version = 1 + + web = NoitaWeb() + + # 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} + + def create_regions(self) -> None: + Regions.create_all_regions_and_connections(self.multiworld, self.player) + Events.create_all_events(self.multiworld, self.player) + + def create_item(self, name: str) -> Item: + return Items.create_item(self.player, name) + + def create_items(self) -> None: + Items.create_all_items(self.multiworld, self.player) + + def set_rules(self) -> None: + Rules.create_all_rules(self.multiworld, self.player) + + def get_filler_item_name(self) -> str: + return self.multiworld.random.choice(Items.filler_items) diff --git a/worlds/noita/docs/en_Noita.md b/worlds/noita/docs/en_Noita.md new file mode 100644 index 0000000000..ac58791718 --- /dev/null +++ b/worlds/noita/docs/en_Noita.md @@ -0,0 +1,63 @@ +# Noita + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Noita is a procedurally generated action roguelite. During runs in Noita you will find potions, wands, spells, perks, +pickups, and chests. Shop items, chests/hearts hidden in the environment, and pedestal items will be replaced with +location checks. Orbs and boss drops will optionally give location checks as well, if they are enabled in the settings. +Noita items that can be found in other players' games include specific perks, orbs (optional), wands, +hearts (Extra Max Health), gold, potions, and other items. If traps are enabled, some randomized negative effects can +affect your game when found. + +## What is the goal of Noita? + +The vanilla goal of Noita is to progress through each level and beat the final boss, taking the Sampo +(gear shaped object) through the portal, and interacting with the altar at the end. There are other endings as well +which require you to gather a certain number of orbs and bring the sampo to an alternate altar. +The Archipelago implementation maintains the same goals. While creating your YAML, you will choose what your goal will +be. While the sampo's location is not randomized, orbs are added to the randomizer pool based on the number of orbs +required for your goal. + +Starting a fresh run after death will re-deliver *some* previously delivered items. The standard wand, potion, and perk +pool are unaffected by the multiworld item pools. This will not present an issue with progression, and will make +progression easier as the multiworld progresses. + +## What Noita items can appear in other players' worlds? + +Positive rewards can be: + +* `Gold (200 or 1000)` +* `Extra Max HP` +* `Spell Refresher` +* `Random Wand (Tier 1 - 6)` +* `Potion` +* `Orb` +* `Immunity Perk` +* `Extra Life` +* `Other Helpful Perks` +* `Miscellaneous Other Items` + +Traps consist of all "Bad" and "Awful" events from Noita's native stream integration. Examples include: + +* `Slow Player` +* `Trailing Lava` +* `Worm Rain` +* `Spawning black holes` + +### How many items are there? + +The number of items is dependent on the settings you choose. Please check the information boxes next to the settings +when setting up your YAML for more information. + +## What does another world's item look like in Noita? + +Other players' items will look like the Archipelago logo. + +## Is Archipelago compatible with other Noita mods? + +Yes, most other Noita mods *should* work. However, they have not been tested. diff --git a/worlds/noita/docs/setup_en.md b/worlds/noita/docs/setup_en.md new file mode 100644 index 0000000000..29e151b5c0 --- /dev/null +++ b/worlds/noita/docs/setup_en.md @@ -0,0 +1,44 @@ +# Noita Setup Guide + +## Installation + +### Game + +Go through the standard installation process for [Noita](https://noitagame.com/) on any of its supported platforms. + +### Install Archipelago Mod + +Download the Archipelago mod zip from the GitHub page: + +[Archipelago Mod Download](https://github.com/DaftBrit/NoitaArchipelago/releases/latest) + +Firstly, go to your Noita installation directory. + +* **On Steam:** Find **Noita** in your Steam library. Right click, select *Manage* → *Browse local files*. +* **On GOG Galaxy:** Find **Noita** in your Installed Games library. Right click, select *Manage installation* → +*Show folder*. + +Here you should see your game files and a folder called `mods`. Create a folder called `archipelago` and place all files +from within the zip folder directly into the `archipelago` folder. After starting Noita, select the *Mods* menu. Here +you should see the *Archipelago* mod listed. + +In order to enable the mod you will first need to toggle **Unsafe mods** from *Disabled* to *Allowed*. This is required, +as some external libraries are used by the mod in order to communicate with the Archipelago server. Once that is done, +you can now enable the *Archipelago* mod (it should have an `[x]` next to it). + +### Configure Archipelago Mod + +In the Options menu, select Mod Settings. Under the Archipelago drop down, you will see the options for *Server*, +*Port*, and *Slot*, where you can fill in the relevant information. + +Once you start a new run in Noita, you should see "Connected to Archipelago server" in the bottom left of the screen. If +you do not see this message, ensure that the mod is enabled and installed per the instructions above. + +## Configuring your YAML File +### 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. + +### 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 +generate a YAML using a graphical interface. From e1f17fadfcaaf40025ecc36c74765b4973e70ded Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 17 Apr 2023 02:35:54 +0200 Subject: [PATCH 069/489] Launcher: add icons where present and add headers to table --- Launcher.py | 53 ++++++++++++++++++++++++----------- data/discord-mark-blue.png | Bin 0 -> 10386 bytes worlds/LauncherComponents.py | 3 +- 3 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 data/discord-mark-blue.png diff --git a/Launcher.py b/Launcher.py index 2dfc466aaa..a9c9a3181a 100644 --- a/Launcher.py +++ b/Launcher.py @@ -20,7 +20,7 @@ from shutil import which from typing import Sequence, Union, Optional import Utils -from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier +from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths if __name__ == "__main__": import ModuleUpdate @@ -84,12 +84,12 @@ def open_folder(folder_path): components.extend([ # Functions - Component('Open host.yaml', func=open_host_yaml), - Component('Open Patch', func=open_patch), - Component('Generate Template Settings', func=generate_yamls), - Component('Discord Server', func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")), - Component('18+ Discord Server', func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")), - Component('Browse Files', func=browse_files), + Component("Open host.yaml", func=open_host_yaml), + Component("Open Patch", func=open_patch), + Component("Generate Template Settings", func=generate_yamls), + Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2")), + Component("18+ Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4")), + Component("Browse Files", func=browse_files), ]) @@ -146,6 +146,8 @@ def launch(exe, in_terminal=False): def run_gui(): from kvui import App, ContainerLayout, GridLayout, Button, Label + from kivy.uix.image import Image + from kivy.uix.relativelayout import RelativeLayout class Launcher(App): base_title: str = "Archipelago Launcher" @@ -167,24 +169,43 @@ def run_gui(): self.container = ContainerLayout() self.grid = GridLayout(cols=2) self.container.add_widget(self.grid) - + self.grid.add_widget(Label(text="General")) + self.grid.add_widget(Label(text="Clients")) button_layout = self.grid # make buttons fill the window + + def build_button(component: Component): + """ + Builds a button widget for a given component. + + Args: + component (Component): The component associated with the button. + + Returns: + None. The button is added to the parent grid layout. + + """ + button = Button(text=component.display_name) + button.component = component + button.bind(on_release=self.component_action) + if component.icon != "icon": + image = Image(source=icon_paths[component.icon], size=(38, 38), size_hint=(None, 1), pos=(5, 0)) + box_layout = RelativeLayout() + box_layout.add_widget(button) + box_layout.add_widget(image) + button_layout.add_widget(box_layout) + else: + button_layout.add_widget(button) + for (tool, client) in itertools.zip_longest(itertools.chain( self._tools.items(), self._funcs.items(), self._adjusters.items()), self._clients.items()): # column 1 if tool: - button = Button(text=tool[0]) - button.component = tool[1] - button.bind(on_release=self.component_action) - button_layout.add_widget(button) + build_button(tool[1]) else: button_layout.add_widget(Label()) # column 2 if client: - button = Button(text=client[0]) - button.component = client[1] - button.bind(on_press=self.component_action) - button_layout.add_widget(button) + build_button(client[1]) else: button_layout.add_widget(Label()) diff --git a/data/discord-mark-blue.png b/data/discord-mark-blue.png new file mode 100644 index 0000000000000000000000000000000000000000..e9dc50d7feed9b4e50265cdf12a52a8e3f0b2b59 GIT binary patch literal 10386 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 pJEDEVN3uV1;JzL55R7~;d?82dqB~}Sz(q literal 0 HcmV?d00001 diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 7bf3ea291e..3a2e319e8b 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -102,5 +102,6 @@ components: List[Component] = [ icon_paths = { 'icon': local_path('data', 'icon.ico' if is_windows else 'icon.png'), - 'mcicon': local_path('data', 'mcicon.ico') + 'mcicon': local_path('data', 'mcicon.ico'), + 'discord': local_path('data', 'discord-mark-blue.png'), } From 4ef7e4352180c04ec235a147479f1de3713a600c Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Thu, 20 Apr 2023 03:08:59 -0400 Subject: [PATCH 070/489] KH2: client bug fixes (#1742) Use item index instead of location and player to determine if the player should get the item. Fixed getting stat increases on the title screen breaking stuff. Changed local locations list from a list organized by world-id to a set. Fixed the inventory slots to be the actual back of inventory. Fixed recaching when not closing the client but switching slots . Fixed getting a ability faster than the game so it dupes. Removed verify location since it was never used. --- KH2Client.py | 231 +++++++++++++++++++++------------------------------ 1 file changed, 96 insertions(+), 135 deletions(-) diff --git a/KH2Client.py b/KH2Client.py index 5223d8a111..7373537264 100644 --- a/KH2Client.py +++ b/KH2Client.py @@ -53,79 +53,8 @@ class KH2Context(CommonContext): self.collectible_override_flags_address = 0 self.collectible_offsets = {} self.sending = [] - # flag for if the player has gotten their starting inventory from the server - self.hasStartingInvo = False # list used to keep track of locations+items player has. Used for disoneccting - self.kh2seedsave = {"checked_locations": {"0": []}, - "starting_inventory": self.hasStartingInvo, - - # Character: [back of invo, front of invo] - "SoraInvo": [0x25CC, 0x2546], - "DonaldInvo": [0x2678, 0x2658], - "GoofyInvo": [0x278E, 0x276C], - "AmountInvo": { - "ServerItems": { - "Ability": {}, - "Amount": {}, - "Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, "Aerial Dodge": 0, - "Glide": 0}, - "Bitmask": [], - "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, - "Equipment": [], - "Magic": {}, - "StatIncrease": {}, - "Boost": {}, - }, - "LocalItems": { - "Ability": {}, - "Amount": {}, - "Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, - "Aerial Dodge": 0, "Glide": 0}, - "Bitmask": [], - "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, - "Equipment": [], - "Magic": {}, - "StatIncrease": {}, - "Boost": {}, - }}, - # 1,3,255 are in this list in case the player gets locations in those "worlds" and I need to still have them checked - "worldIdChecks": { - "1": [], # world of darkness (story cutscenes) - "2": [], - "3": [], # destiny island doesn't have checks to ima put tt checks here - "4": [], - "5": [], - "6": [], - "7": [], - "8": [], - "9": [], - "10": [], - "11": [], - # atlantica isn't a supported world. if you go in atlantica it will check dc - "12": [], - "13": [], - "14": [], - "15": [], - # world map, but you only go to the world map while on the way to goa so checking hb - "16": [], - "17": [], - "18": [], - "255": [], # starting screen - }, - "Levels": { - "SoraLevel": 0, - "ValorLevel": 0, - "WisdomLevel": 0, - "LimitLevel": 0, - "MasterLevel": 0, - "FinalLevel": 0, - }, - "SoldEquipment": [], - "SoldBoosts": {"Power Boost": 0, - "Magic Boost": 0, - "Defense Boost": 0, - "AP Boost": 0} - } + self.kh2seedsave = None self.slotDataProgressionNames = {} self.kh2seedname = None self.kh2slotdata = None @@ -202,14 +131,13 @@ class KH2Context(CommonContext): self.boost_set = set(CheckDupingItems["Boosts"]) self.stat_increase_set = set(CheckDupingItems["Stat Increases"]) - self.AbilityQuantityDict = {item: self.item_name_to_data[item].quantity for item in self.all_abilities} # Growth:[level 1,level 4,slot] - self.growth_values_dict = {"High Jump": [0x05E, 0x061, 0x25CE], - "Quick Run": [0x62, 0x65, 0x25D0], - "Dodge Roll": [0x234, 0x237, 0x25D2], - "Aerial Dodge": [0x066, 0x069, 0x25D4], - "Glide": [0x6A, 0x6D, 0x25D6]} + self.growth_values_dict = {"High Jump": [0x05E, 0x061, 0x25DA], + "Quick Run": [0x62, 0x65, 0x25DC], + "Dodge Roll": [0x234, 0x237, 0x25DE], + "Aerial Dodge": [0x066, 0x069, 0x25E0], + "Glide": [0x6A, 0x6D, 0x25E2]} self.boost_to_anchor_dict = { "Power Boost": 0x24F9, "Magic Boost": 0x24FA, @@ -269,19 +197,66 @@ class KH2Context(CommonContext): if not os.path.exists(self.game_communication_path): os.makedirs(self.game_communication_path) if not os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"): + self.kh2seedsave = {"itemIndex": -1, + # back of soras invo is 0x25E2. Growth should be moved there + # Character: [back of invo, front of invo] + "SoraInvo": [0x25D8, 0x2546], + "DonaldInvo": [0x26F4, 0x2658], + "GoofyInvo": [0x280A, 0x276C], + "AmountInvo": { + "ServerItems": { + "Ability": {}, + "Amount": {}, + "Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, + "Aerial Dodge": 0, + "Glide": 0}, + "Bitmask": [], + "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, + "Equipment": [], + "Magic": {}, + "StatIncrease": {}, + "Boost": {}, + }, + "LocalItems": { + "Ability": {}, + "Amount": {}, + "Growth": {"High Jump": 0, "Quick Run": 0, "Dodge Roll": 0, + "Aerial Dodge": 0, "Glide": 0}, + "Bitmask": [], + "Weapon": {"Sora": [], "Donald": [], "Goofy": []}, + "Equipment": [], + "Magic": {}, + "StatIncrease": {}, + "Boost": {}, + }}, + # 1,3,255 are in this list in case the player gets locations in those "worlds" and I need to still have them checked + "LocationsChecked": [], + "Levels": { + "SoraLevel": 0, + "ValorLevel": 0, + "WisdomLevel": 0, + "LimitLevel": 0, + "MasterLevel": 0, + "FinalLevel": 0, + }, + "SoldEquipment": [], + "SoldBoosts": {"Power Boost": 0, + "Magic Boost": 0, + "Defense Boost": 0, + "AP Boost": 0} + } with open(os.path.join(self.game_communication_path, f"kh2save{self.kh2seedname}{self.auth}.json"), 'wt') as f: pass + self.locations_checked = set() elif os.path.exists(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json"): with open(self.game_communication_path + f"\kh2save{self.kh2seedname}{self.auth}.json", 'r') as f: self.kh2seedsave = json.load(f) + self.locations_checked = set(self.kh2seedsave["LocationsChecked"]) + self.serverconneced = True if cmd in {"Connected"}: - for player in args['players']: - if str(player.slot) not in self.kh2seedsave["checked_locations"]: - self.kh2seedsave["checked_locations"].update({str(player.slot): []}) self.kh2slotdata = args['slot_data'] - self.serverconneced = True self.kh2LocalItems = {int(location): item for location, item in self.kh2slotdata["LocalItems"].items()} try: self.kh2 = pymem.Pymem(process_name="KINGDOM HEARTS II FINAL MIX") @@ -296,21 +271,10 @@ class KH2Context(CommonContext): if cmd in {"ReceivedItems"}: start_index = args["index"] - if start_index != len(self.items_received): + if start_index > self.kh2seedsave["itemIndex"]: + self.kh2seedsave["itemIndex"] = start_index for item in args['items']: - # starting invo from server - if item.location in {-2}: - if not self.kh2seedsave["starting_inventory"]: - asyncio.create_task(self.give_item(item.item)) - # if location is not already given or is !getitem - elif item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \ - or item.location in {-1}: - asyncio.create_task(self.give_item(item.item)) - if item.location not in self.kh2seedsave["checked_locations"][str(item.player)] \ - and item.location not in {-1, -2}: - self.kh2seedsave["checked_locations"][str(item.player)].append(item.location) - if not self.kh2seedsave["starting_inventory"]: - self.kh2seedsave["starting_inventory"] = True + asyncio.create_task(self.give_item(item.item)) if cmd in {"RoomUpdate"}: if "checked_locations" in args: @@ -326,12 +290,13 @@ class KH2Context(CommonContext): if currentworldint in self.worldid: curworldid = self.worldid[currentworldint] for location, data in curworldid.items(): - if location not in self.locations_checked \ + locationId = kh2_loc_name_to_id[location] + if locationId not in self.locations_checked \ and (int.from_bytes( self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big") & 0x1 << data.bitIndex) > 0: - self.locations_checked.add(location) - self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + + self.sending = self.sending + [(int(locationId))] except Exception as e: logger.info("Line 285") if self.kh2connected: @@ -344,12 +309,12 @@ class KH2Context(CommonContext): for location, data in SoraLevels.items(): currentLevel = int.from_bytes( self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), "big") - if location not in self.locations_checked \ + locationId = kh2_loc_name_to_id[location] + if locationId not in self.locations_checked \ and currentLevel >= data.bitIndex: if self.kh2seedsave["Levels"]["SoraLevel"] < currentLevel: self.kh2seedsave["Levels"]["SoraLevel"] = currentLevel - self.locations_checked.add(location) - self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + self.sending = self.sending + [(int(locationId))] formDict = { 0: ["ValorLevel", ValorLevels], 1: ["WisdomLevel", WisdomLevels], 2: ["LimitLevel", LimitLevels], 3: ["MasterLevel", MasterLevels], 4: ["FinalLevel", FinalLevels]} @@ -357,12 +322,12 @@ class KH2Context(CommonContext): for location, data in formDict[i][1].items(): formlevel = int.from_bytes( self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big") - if location not in self.locations_checked \ + locationId = kh2_loc_name_to_id[location] + if locationId not in self.locations_checked \ and formlevel >= data.bitIndex: if formlevel > self.kh2seedsave["Levels"][formDict[i][0]]: self.kh2seedsave["Levels"][formDict[i][0]] = formlevel - self.locations_checked.add(location) - self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + self.sending = self.sending + [(int(locationId))] except Exception as e: logger.info("Line 312") if self.kh2connected: @@ -373,18 +338,21 @@ class KH2Context(CommonContext): async def checkSlots(self): try: for location, data in weaponSlots.items(): - if location not in self.locations_checked: + locationId = kh2_loc_name_to_id[location] + if locationId not in self.locations_checked: if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big") > 0: - self.locations_checked.add(location) - self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + + self.sending = self.sending + [(int(locationId))] for location, data in formSlots.items(): - if location not in self.locations_checked: + locationId = kh2_loc_name_to_id[location] + if locationId not in self.locations_checked: if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + data.addrObtained, 1), "big") & 0x1 << data.bitIndex > 0: - self.locations_checked.add(location) - self.sending = self.sending + [(int(kh2_loc_name_to_id[location]))] + #self.locations_checked + self.sending = self.sending + [(int(locationId))] + except Exception as e: if self.kh2connected: logger.info("Line 333") @@ -394,8 +362,7 @@ class KH2Context(CommonContext): async def verifyChests(self): try: - currentworld = str(int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + 0x0714DB8, 1), "big")) - for location in self.kh2seedsave["worldIdChecks"][currentworld]: + for location in self.locations_checked: locationName = self.lookup_id_to_Location[location] if locationName in self.chest_set: if locationName in self.location_name_to_worlddata.keys(): @@ -428,24 +395,6 @@ class KH2Context(CommonContext): self.kh2.write_bytes(self.kh2.base_address + self.Save + anchor, (self.kh2seedsave["Levels"][leveltype]).to_bytes(1, 'big'), 1) - def verifyLocation(self, location): - locationData = self.location_name_to_worlddata[location] - locationName = self.lookup_id_to_Location[location] - isChecked = True - - if locationName not in levels_locations: - if (int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1), - "big") & 0x1 << locationData.bitIndex) == 0: - isChecked = False - elif locationName in SoraLevels: - if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + 0x24FF, 1), - "big") < locationData.bitIndex: - isChecked = False - elif int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + locationData.addrObtained, 1), - "big") < locationData.bitIndex: - isChecked = False - return isChecked - async def give_item(self, item, ItemType="ServerItems"): try: itemname = self.lookup_id_to_item[item] @@ -680,6 +629,17 @@ class KH2Context(CommonContext): ability = current & 0x0FFF if ability | 0x8000 != (0x8000 + itemData.memaddr): self.kh2.write_short(self.kh2.base_address + self.Save + slot, itemData.memaddr) + # removes the duped ability if client gave faster than the game. + for charInvo in {"SoraInvo", "DonaldInvo", "GoofyInvo"}: + if self.kh2.read_short(self.kh2.base_address + self.Save + self.kh2seedsave[charInvo][1]) != 0 and\ + self.kh2seedsave[charInvo][1]+2 < self.kh2seedsave[charInvo][0]: + self.kh2.write_short(self.kh2.base_address + self.Save + self.kh2seedsave[charInvo][1], 0) + # remove the dummy level 1 growths if they are in these invo slots. + for inventorySlot in {0x25CE, 0x25D0, 0x25D2, 0x25D4, 0x25D6, 0x25D8}: + current = self.kh2.read_short(self.kh2.base_address + self.Save + inventorySlot) + ability = current & 0x0FFF + if 0x05E <= ability <= 0x06D: + self.kh2.write_short(self.kh2.base_address + self.Save + inventorySlot, 0) for itemName in self.master_growth: growthLevel = self.kh2seedsave["AmountInvo"]["ServerItems"]["Growth"][itemName] \ @@ -753,10 +713,11 @@ class KH2Context(CommonContext): if itemName in server_stat: amountOfItems += self.kh2seedsave["AmountInvo"]["ServerItems"]["StatIncrease"][itemName] + # 0x130293 is Crit_1's location id for touching the computer if int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Save + itemData.memaddr, 1), "big") != amountOfItems \ and int.from_bytes(self.kh2.read_bytes(self.kh2.base_address + self.Slot1 + 0x1B2, 1), - "big") >= 5: + "big") >= 5 and 0x130293 in self.locations_checked: self.kh2.write_bytes(self.kh2.base_address + self.Save + itemData.memaddr, amountOfItems.to_bytes(1, 'big'), 1) @@ -859,9 +820,9 @@ async def kh2_watcher(ctx: KH2Context): location_ids = [] location_ids = [location for location in message[0]["locations"] if location not in location_ids] for location in location_ids: - currentWorld = int.from_bytes(ctx.kh2.read_bytes(ctx.kh2.base_address + 0x0714DB8, 1), "big") - if location not in ctx.kh2seedsave["worldIdChecks"][str(currentWorld)]: - ctx.kh2seedsave["worldIdChecks"][str(currentWorld)].append(location) + if location not in ctx.locations_checked: + ctx.locations_checked.add(location) + ctx.kh2seedsave["LocationsChecked"].append(location) if location in ctx.kh2LocalItems: item = ctx.kh2slotdata["LocalItems"][str(location)] await asyncio.create_task(ctx.give_item(item, "LocalItems")) From a38a2903d5343d68a02b521a8e0d752788797754 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Thu, 20 Apr 2023 03:10:21 -0400 Subject: [PATCH 071/489] SM: comeback fix4 (#1741) --- worlds/sm/Options.py | 1 + worlds/sm/__init__.py | 29 ++++++++++++++++++++---- worlds/sm/variaRandomizer/utils/utils.py | 2 ++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index 02599841d3..173e70c6bd 100644 --- a/worlds/sm/Options.py +++ b/worlds/sm/Options.py @@ -127,6 +127,7 @@ class AreaRandomization(Choice): option_off = 0 option_light = 1 option_full = 2 + alias_true = 2 default = 0 class AreaLayout(Toggle): diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 3ce405acb2..f67e5c06ee 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -8,6 +8,7 @@ import base64 from typing import Any, Dict, Iterable, List, Set, TextIO, TypedDict from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, CollectionState, Tutorial +from Fill import fill_restrictive from worlds.AutoWorld import World, AutoLogicRegister, WebWorld logger = logging.getLogger("Super Metroid") @@ -149,6 +150,7 @@ class SMWorld(World): pool = [] self.locked_items = {} self.NothingPool = [] + self.prefilled_locked_items = [] weaponCount = [0, 0, 0] for item in itemPool: isAdvancement = True @@ -170,13 +172,21 @@ class SMWorld(World): elif item.Category == 'Nothing': isAdvancement = False - classification = ItemClassification.progression if isAdvancement else ItemClassification.filler itemClass = ItemManager.Items[item.Type].Class smitem = SMItem(item.Name, - classification, + ItemClassification.progression if isAdvancement else ItemClassification.filler, item.Type, None if itemClass == 'Boss' else self.item_name_to_id[item.Name], player=self.player) + + beamItems = ['Spazer', 'Ice', 'Wave' ,'Plasma'] + self.ammoItems = ['Missile', 'Super', 'PowerBomb'] + if self.multiworld.doors_colors_rando[self.player].value != 0: + if item.Type in beamItems: + self.multiworld.local_items[self.player].value.add(item.Name) + elif item.Type in self.ammoItems and isAdvancement: + self.prefilled_locked_items.append(smitem) + if itemClass == 'Boss': self.locked_items[item.Name] = smitem elif item.Category == 'Nothing': @@ -205,11 +215,22 @@ class SMWorld(World): def set_rules(self): set_rules(self.multiworld, self.player) - def create_regions(self): create_locations(self, self.player) create_regions(self, self.multiworld, self.player) + def pre_fill(self): + from Fill import fill_restrictive + if len(self.prefilled_locked_items) > 0: + locations = [loc for loc in self.locations.values() if loc.item is None] + self.multiworld.random.shuffle(locations) + all_state = self.multiworld.get_all_state(False) + for item in self.ammoItems: + while (all_state.has(item.name, self.player, 1)): + all_state.remove(item) + + fill_restrictive(self.multiworld, all_state, locations, self.prefilled_locked_items, True, True) + def getWordArray(self, w: int) -> List[int]: """ little-endian convert a 16-bit number to an array of numbers <= 255 each """ return [w & 0x00FF, (w & 0xFF00) >> 8] @@ -813,7 +834,7 @@ class SMLocation(Location): comebackCheck = ComebackCheckType.JustComeback n = 2 if GraphUtils.isStandardStart(randoExec.graphSettings.startAP) else 3 # is early game - if (len([loc for loc in state.locations_checked if loc.player == self.player]) <= n): + if (len([loc for loc in state.locations_checked if loc.player == self.player]) <= n or randoExec.graphSettings.startAP == state.smbm[self.player].lastAP): comebackCheck = ComebackCheckType.NoCheck container = ItemLocContainer(state.smbm[self.player], [], []) return randoService.fullComebackCheck( container, diff --git a/worlds/sm/variaRandomizer/utils/utils.py b/worlds/sm/variaRandomizer/utils/utils.py index 08a8e0379b..094a3a9cae 100644 --- a/worlds/sm/variaRandomizer/utils/utils.py +++ b/worlds/sm/variaRandomizer/utils/utils.py @@ -373,6 +373,8 @@ def loadRandoPreset(world, player, args): args.gravityBehaviour = defaultMultiValues["gravityBehaviour"][world.gravity_behaviour[player].value] args.nerfedCharge = world.nerfed_charge[player].value args.area = world.area_randomization[player].current_key + if (args.area == "true"): + args.area = "full" if args.area != "off": args.areaLayoutBase = not world.area_layout[player].value args.escapeRando = world.escape_rando[player].value From c8fb46a5e6ddd73c19af9393374129b9c55ffd8a Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 20 Apr 2023 02:11:15 -0500 Subject: [PATCH 072/489] The Messenger: Throw error for invalid names and replace `_` with ` ` (#1728) * Replace '_' with ' ' and throw error for other invalid names Co-authored-by: el-u <109771707+el-u@users.noreply.github.com> --- worlds/messenger/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/worlds/messenger/__init__.py b/worlds/messenger/__init__.py index 5d304edd1d..919bdba66f 100644 --- a/worlds/messenger/__init__.py +++ b/worlds/messenger/__init__.py @@ -1,7 +1,7 @@ import logging from typing import Dict, Any, Optional, List -from BaseClasses import Tutorial, ItemClassification +from BaseClasses import Tutorial, ItemClassification, MultiWorld from worlds.AutoWorld import World, WebWorld from .Constants import NOTES, PHOBEKINS, ALL_ITEMS, ALWAYS_LOCATIONS, SEALS, BOSS_LOCATIONS from .Options import messenger_options, NotesNeeded, Goal, PowerSeals, Logic @@ -66,6 +66,13 @@ class MessengerWorld(World): total_seals: int = 0 required_seals: int = 0 + + @classmethod + def stage_assert_generate(cls, multiworld: MultiWorld) -> None: + for player in multiworld.get_game_players(cls.game): + player_name = multiworld.player_name[player] = multiworld.get_player_name(player).replace("_", " ") + if not all(c.isalnum() or c in "- " for c in player_name): + raise ValueError(f"Player name {player_name} is not alpha-numeric.") def generate_early(self) -> None: if self.multiworld.goal[self.player] == Goal.option_power_seal_hunt: From bf5c1cbbbf118c82344e700c137229a684663b62 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 17 Apr 2023 13:19:27 +0200 Subject: [PATCH 073/489] WebHost: add spoiler level field to generate form --- WebHostLib/api/generate.py | 5 ++--- WebHostLib/check.py | 5 +++-- WebHostLib/generate.py | 35 +++++++++++++++++++----------- WebHostLib/templates/generate.html | 22 +++++++++++++++++++ 4 files changed, 49 insertions(+), 18 deletions(-) diff --git a/WebHostLib/api/generate.py b/WebHostLib/api/generate.py index 1d9e6fd9c1..04a93f2cf4 100644 --- a/WebHostLib/api/generate.py +++ b/WebHostLib/api/generate.py @@ -48,9 +48,8 @@ def generate_api(): if len(options) > app.config["MAX_ROLL"]: return {"text": "Max size of multiworld exceeded", "detail": app.config["MAX_ROLL"]}, 409 - meta = get_meta(meta_options_source) - meta["race"] = race - results, gen_options = roll_options(options, meta["plando_options"]) + meta = get_meta(meta_options_source, race) + results, gen_options = roll_options(options, set(meta["plando_options"])) if any(type(result) == str for result in results.values()): return {"text": str(results), "detail": results}, 400 diff --git a/WebHostLib/check.py b/WebHostLib/check.py index b7f215da02..3d71fe7798 100644 --- a/WebHostLib/check.py +++ b/WebHostLib/check.py @@ -52,11 +52,12 @@ def get_yaml_data(file) -> Union[Dict[str, str], str, Markup]: if any(file.filename.endswith(".archipelago") for file in infolist): return Markup("Error: Your .zip file contains an .archipelago file. " - 'Did you mean to host a game?') + 'Did you mean to host a game?') for file in infolist: if file.filename.endswith(banned_zip_contents): - return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted." + return "Uploaded data contained a rom file, which is likely to contain copyrighted material. " \ + "Your file was deleted." elif file.filename.endswith((".yaml", ".json", ".yml", ".txt")): options[file.filename] = zfile.open(file, "r").read() else: diff --git a/WebHostLib/generate.py b/WebHostLib/generate.py index cbacd5153f..ea408fb275 100644 --- a/WebHostLib/generate.py +++ b/WebHostLib/generate.py @@ -6,7 +6,7 @@ import tempfile import zipfile import concurrent.futures from collections import Counter -from typing import Dict, Optional, Any +from typing import Dict, Optional, Any, Union, List from flask import request, flash, redirect, url_for, session, render_template from pony.orm import commit, db_session @@ -22,7 +22,7 @@ from .models import Generation, STATE_ERROR, STATE_QUEUED, Seed, UUID from .upload import upload_zip_to_db -def get_meta(options_source: dict) -> dict: +def get_meta(options_source: dict, race: bool = False) -> Dict[str, Union[List[str], Dict[str, Any]]]: plando_options = { options_source.get("plando_bosses", ""), options_source.get("plando_items", ""), @@ -39,7 +39,21 @@ def get_meta(options_source: dict) -> dict: "item_cheat": bool(int(options_source.get("item_cheat", 1))), "server_password": options_source.get("server_password", None), } - return {"server_options": server_options, "plando_options": list(plando_options)} + generator_options = { + "spoiler": int(options_source.get("spoiler", 0)), + "race": race + } + + if race: + server_options["item_cheat"] = False + server_options["remaining_mode"] = "disabled" + generator_options["spoiler"] = 0 + + return { + "server_options": server_options, + "plando_options": list(plando_options), + "generator_options": generator_options, + } @app.route('/generate', methods=['GET', 'POST']) @@ -55,13 +69,8 @@ def generate(race=False): if isinstance(options, str): flash(options) else: - meta = get_meta(request.form) - meta["race"] = race - results, gen_options = roll_options(options, meta["plando_options"]) - - if race: - meta["server_options"]["item_cheat"] = False - meta["server_options"]["remaining_mode"] = "disabled" + meta = get_meta(request.form, race) + results, gen_options = roll_options(options, set(meta["plando_options"])) if any(type(result) == str for result in results.values()): return render_template("checkResult.html", results=results) @@ -97,7 +106,7 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non meta: Dict[str, Any] = {} meta.setdefault("server_options", {}).setdefault("hint_cost", 10) - race = meta.setdefault("race", False) + race = meta["generator_options"].setdefault("race", False) def task(): target = tempfile.TemporaryDirectory() @@ -114,13 +123,13 @@ def gen_game(gen_options: dict, meta: Optional[Dict[str, Any]] = None, owner=Non erargs = parse_arguments(['--multi', str(playercount)]) erargs.seed = seed erargs.name = {x: "" for x in range(1, playercount + 1)} # only so it can be overwritten in mystery - erargs.spoiler = 0 if race else 3 + erargs.spoiler = meta["generator_options"]["spoiler"] erargs.race = race erargs.outputname = seedname erargs.outputpath = target.name erargs.teams = 1 erargs.plando_options = PlandoOptions.from_set(meta.setdefault("plando_options", - {"bosses", "items", "connections", "texts"})) + {"bosses", "items", "connections", "texts"})) name_counter = Counter() for player, (playerfile, settings) in enumerate(gen_options.items(), 1): diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html index b5fb83252e..dd25a90804 100644 --- a/WebHostLib/templates/generate.html +++ b/WebHostLib/templates/generate.html @@ -119,6 +119,28 @@ + + + + + + + + From 0515acc8feb513df4c0f0a0ccce191a76bbd0b82 Mon Sep 17 00:00:00 2001 From: zig-for Date: Thu, 20 Apr 2023 00:12:53 -0700 Subject: [PATCH 074/489] LADX: no pre fill (#1673) * no pre fill * redo trade logic --- worlds/ladx/Items.py | 50 ++++++++++++++++++++--------------------- worlds/ladx/__init__.py | 28 ++++++----------------- 2 files changed, 31 insertions(+), 47 deletions(-) diff --git a/worlds/ladx/Items.py b/worlds/ladx/Items.py index b8be03d67d..e36a589c2c 100644 --- a/worlds/ladx/Items.py +++ b/worlds/ladx/Items.py @@ -8,8 +8,7 @@ class ItemData(typing.NamedTuple): item_name: str ladxr_id: str classification: ItemClassification - mark_only_first_progression: bool = False - created_for_players = set() + @property def item_id(self): return CHEST_ITEMS[self.ladxr_id] @@ -33,6 +32,12 @@ class DungeonItemData(ItemData): s = self.ladxr_id[:-1] return DungeonItemType.__dict__[s] +class TradeItemData(ItemData): + vanilla_location = None + def __new__(cls, item_name, ladxr_id, classification, vanilla_location): + self = super(ItemData, cls).__new__(cls, (item_name, ladxr_id, classification)) + self.vanilla_location = vanilla_location + return self class LinksAwakeningItem(Item): game: str = Common.LINKS_AWAKENING @@ -40,14 +45,7 @@ class LinksAwakeningItem(Item): classification = item_data.classification if callable(classification): classification = classification(world, player) - # this doesn't work lol - MARK_FIRST_ITEM = False - if MARK_FIRST_ITEM: - if item_data.mark_only_first_progression: - if player in item_data.created_for_players: - classification = ItemClassification.filler - else: - item_data.created_for_players.add(player) + super().__init__(item_data.item_name, classification, Common.BASE_ID + item_data.item_id, player) self.item_data = item_data @@ -184,8 +182,8 @@ links_awakening_items = [ ItemData(ItemName.OCARINA, "OCARINA", ItemClassification.progression), ItemData(ItemName.FEATHER, "FEATHER", ItemClassification.progression), ItemData(ItemName.SHOVEL, "SHOVEL", ItemClassification.progression), - ItemData(ItemName.MAGIC_POWDER, "MAGIC_POWDER", ItemClassification.progression, True), - ItemData(ItemName.BOMB, "BOMB", ItemClassification.progression, True), + ItemData(ItemName.MAGIC_POWDER, "MAGIC_POWDER", ItemClassification.progression), + ItemData(ItemName.BOMB, "BOMB", ItemClassification.progression), ItemData(ItemName.SWORD, "SWORD", ItemClassification.progression), ItemData(ItemName.FLIPPERS, "FLIPPERS", ItemClassification.progression), ItemData(ItemName.MAGNIFYING_LENS, "MAGNIFYING_LENS", ItemClassification.progression), @@ -279,20 +277,20 @@ links_awakening_items = [ DungeonItemData(ItemName.INSTRUMENT6, "INSTRUMENT6", ItemClassification.progression), DungeonItemData(ItemName.INSTRUMENT7, "INSTRUMENT7", ItemClassification.progression), DungeonItemData(ItemName.INSTRUMENT8, "INSTRUMENT8", ItemClassification.progression), - ItemData(ItemName.TRADING_ITEM_YOSHI_DOLL, "TRADING_ITEM_YOSHI_DOLL", trade_item_prog), - ItemData(ItemName.TRADING_ITEM_RIBBON, "TRADING_ITEM_RIBBON", trade_item_prog), - ItemData(ItemName.TRADING_ITEM_DOG_FOOD, "TRADING_ITEM_DOG_FOOD", trade_item_prog), - ItemData(ItemName.TRADING_ITEM_BANANAS, "TRADING_ITEM_BANANAS", trade_item_prog), - ItemData(ItemName.TRADING_ITEM_STICK, "TRADING_ITEM_STICK", trade_item_prog), - ItemData(ItemName.TRADING_ITEM_HONEYCOMB, "TRADING_ITEM_HONEYCOMB", trade_item_prog), - ItemData(ItemName.TRADING_ITEM_PINEAPPLE, "TRADING_ITEM_PINEAPPLE", trade_item_prog), - ItemData(ItemName.TRADING_ITEM_HIBISCUS, "TRADING_ITEM_HIBISCUS", trade_item_prog), - ItemData(ItemName.TRADING_ITEM_LETTER, "TRADING_ITEM_LETTER", trade_item_prog), - ItemData(ItemName.TRADING_ITEM_BROOM, "TRADING_ITEM_BROOM", trade_item_prog), - ItemData(ItemName.TRADING_ITEM_FISHING_HOOK, "TRADING_ITEM_FISHING_HOOK", trade_item_prog), - ItemData(ItemName.TRADING_ITEM_NECKLACE, "TRADING_ITEM_NECKLACE", trade_item_prog), - ItemData(ItemName.TRADING_ITEM_SCALE, "TRADING_ITEM_SCALE", trade_item_prog), - ItemData(ItemName.TRADING_ITEM_MAGNIFYING_GLASS, "TRADING_ITEM_MAGNIFYING_GLASS", trade_item_prog) + TradeItemData(ItemName.TRADING_ITEM_YOSHI_DOLL, "TRADING_ITEM_YOSHI_DOLL", trade_item_prog, "Trendy Game (Mabe Village)"), + TradeItemData(ItemName.TRADING_ITEM_RIBBON, "TRADING_ITEM_RIBBON", trade_item_prog, "Papahl's Wife (Mabe Village)"), + TradeItemData(ItemName.TRADING_ITEM_DOG_FOOD, "TRADING_ITEM_DOG_FOOD", trade_item_prog, "YipYip (Mabe Village)"), + TradeItemData(ItemName.TRADING_ITEM_BANANAS, "TRADING_ITEM_BANANAS", trade_item_prog, "Banana Sale (Toronbo Shores)"), + TradeItemData(ItemName.TRADING_ITEM_STICK, "TRADING_ITEM_STICK", trade_item_prog, "Kiki (Ukuku Prairie)"), + TradeItemData(ItemName.TRADING_ITEM_HONEYCOMB, "TRADING_ITEM_HONEYCOMB", trade_item_prog, "Honeycomb (Ukuku Prairie)"), + TradeItemData(ItemName.TRADING_ITEM_PINEAPPLE, "TRADING_ITEM_PINEAPPLE", trade_item_prog, "Bear Cook (Animal Village)"), + TradeItemData(ItemName.TRADING_ITEM_HIBISCUS, "TRADING_ITEM_HIBISCUS", trade_item_prog, "Papahl (Tal Tal Heights)"), + TradeItemData(ItemName.TRADING_ITEM_LETTER, "TRADING_ITEM_LETTER", trade_item_prog, "Goat (Animal Village)"), + TradeItemData(ItemName.TRADING_ITEM_BROOM, "TRADING_ITEM_BROOM", trade_item_prog, "MrWrite (Goponga Swamp)"), + TradeItemData(ItemName.TRADING_ITEM_FISHING_HOOK, "TRADING_ITEM_FISHING_HOOK", trade_item_prog, "Grandma (Animal Village)"), + TradeItemData(ItemName.TRADING_ITEM_NECKLACE, "TRADING_ITEM_NECKLACE", trade_item_prog, "Fisher (Martha's Bay)"), + TradeItemData(ItemName.TRADING_ITEM_SCALE, "TRADING_ITEM_SCALE", trade_item_prog, "Mermaid (Martha's Bay)"), + TradeItemData(ItemName.TRADING_ITEM_MAGNIFYING_GLASS, "TRADING_ITEM_MAGNIFYING_GLASS", trade_item_prog, "Mermaid Statue (Martha's Bay)") ] ladxr_item_to_la_item_name = { diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index d64b950e38..ddb73f6b3c 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -9,12 +9,11 @@ from Fill import fill_restrictive from worlds.AutoWorld import WebWorld, World from .Common import * -from .Items import (DungeonItemData, DungeonItemType, LinksAwakeningItem, +from .Items import (DungeonItemData, DungeonItemType, LinksAwakeningItem, TradeItemData, ladxr_item_to_la_item_name, links_awakening_items, links_awakening_items_by_name) from .LADXR import generator from .LADXR.itempool import ItemPool as LADXRItemPool -from .LADXR.locations.tradeSequence import TradeSequenceItem from .LADXR.logic import Logic as LAXDRLogic from .LADXR.main import get_parser from .LADXR.settings import Settings as LADXRSettings @@ -23,7 +22,8 @@ from .LADXR.locations.instrument import Instrument from .LADXR.locations.constants import CHEST_ITEMS from .Locations import (LinksAwakeningLocation, LinksAwakeningRegion, create_regions_from_ladxr, get_locations_to_id) -from .Options import links_awakening_options +from .Options import links_awakening_options, DungeonItemShuffle + from .Rom import LADXDeltaPatch DEVELOPER_MODE = False @@ -140,12 +140,10 @@ class LinksAwakeningWorld(World): def create_items(self) -> None: exclude = [item.name for item in self.multiworld.precollected_items[self.player]] - self.trade_items = [] - dungeon_item_types = { } - from .Options import DungeonItemShuffle + self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ] self.prefill_own_dungeons = [] # For any and different world, set item rule instead @@ -183,8 +181,9 @@ class LinksAwakeningWorld(World): else: item = self.create_item(item_name) - if not self.multiworld.tradequest[self.player] and ladx_item_name.startswith("TRADING_"): - self.trade_items.append(item) + if not self.multiworld.tradequest[self.player] and isinstance(item.item_data, TradeItemData): + location = self.multiworld.get_location(item.item_data.vanilla_location, self.player) + location.place_locked_item(item) continue if isinstance(item.item_data, DungeonItemData): if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT: @@ -218,7 +217,6 @@ class LinksAwakeningWorld(World): else: self.multiworld.itempool.append(item) - def pre_fill(self): self.multi_key = self.generate_multi_key() dungeon_locations = [] @@ -261,18 +259,6 @@ class LinksAwakeningWorld(World): # Properly fill locations within dungeon location.dungeon = r.dungeon_index - # Tell the filler that if we're placing a dungeon item, restrict it to the dungeon the item associates with - # This will need changed once keysanity is implemented - #orig_rule = location.item_rule - #location.item_rule = lambda item, orig_rule=orig_rule: \ - # (not isinstance(item, DungeonItemData) or item.dungeon_index == location.dungeon) and orig_rule(item) - - for location in r.locations: - # If tradequests are disabled, place trade items directly in their proper location - if not self.multiworld.tradequest[self.player] and isinstance(location, LinksAwakeningLocation) and isinstance(location.ladxr_item, TradeSequenceItem): - item = next(i for i in self.trade_items if i.item_data.ladxr_id == location.ladxr_item.default_item) - location.place_locked_item(item) - for dungeon_index in range(0, 9): locs = dungeon_locations_by_dungeon[dungeon_index] locs = [loc for loc in locs if not loc.item] From a6ea3e1953bfbc8f8a9f828a0dd4e3747dd4c72d Mon Sep 17 00:00:00 2001 From: Ziktofel Date: Thu, 20 Apr 2023 09:13:34 +0200 Subject: [PATCH 075/489] Sc2wol logic update (#1715) Fixes some issues with typos and oversights logic generation in SC2 Rebalancing Adds Vultures to Advanced tactics as train killers Drops Firebats from basic unit pool and moves Goliaths from advanced tactics to standard --- worlds/sc2wol/Items.py | 3 +-- worlds/sc2wol/LogicMixin.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py index e17a7cbc3b..ea495adf79 100644 --- a/worlds/sc2wol/Items.py +++ b/worlds/sc2wol/Items.py @@ -152,14 +152,13 @@ item_table = { basic_units = { 'Marine', 'Marauder', - 'Firebat', + 'Goliath', 'Hellion', 'Vulture' } advanced_basic_units = basic_units.union({ 'Reaper', - 'Goliath', 'Diamondback', 'Viking' }) diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py index c803835f63..8c7182e0e8 100644 --- a/worlds/sc2wol/LogicMixin.py +++ b/worlds/sc2wol/LogicMixin.py @@ -30,7 +30,7 @@ class SC2WoLLogic(LogicMixin): defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player))) if self.has_any({'Marine', 'Marauder'}, player) and self.has('Bunker', player): defense_score += 3 - if self.has_all({'Siege Tank', 'Maelstrom Rounds'}, player): + if self.has_all({'Siege Tank', 'Maelstrom Rounds (Siege Tank)'}, player): defense_score += 2 if zerg_enemy: defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if self.has(item, player))) @@ -51,8 +51,15 @@ class SC2WoLLogic(LogicMixin): self.has('Siege Tank', player) and self._sc2wol_has_competent_anti_air(multiworld, player) def _sc2wol_has_train_killers(self, multiworld: MultiWorld, player: int) -> bool: - return (self.has_any({'Siege Tank', 'Diamondback', 'Marauder'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0 - and self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)) + return ( + self.has_any({'Siege Tank', 'Diamondback', 'Marauder'}, player) + or get_option_value(multiworld, player, 'required_tactics') > 0 + and ( + self.has_all({'Reaper', "G-4 Clusterbomb"}, player) + or self.has_all({'Spectre', 'Psionic Lash'}, player) + or self.has('Vulture', player) + ) + ) def _sc2wol_able_to_rescue(self, multiworld: MultiWorld, player: int) -> bool: return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) or get_option_value(multiworld, player, 'required_tactics') > 0 From a7a17a5a4d361ea6188f1ff1b5fcdcbfe2dc6fad Mon Sep 17 00:00:00 2001 From: zig-for Date: Thu, 20 Apr 2023 00:23:04 -0700 Subject: [PATCH 076/489] Fix OOT? (#1721) --- data/lua/connector_oot.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/data/lua/connector_oot.lua b/data/lua/connector_oot.lua index a5248e5ba4..d4e277032b 100644 --- a/data/lua/connector_oot.lua +++ b/data/lua/connector_oot.lua @@ -1,9 +1,9 @@ local socket = require("socket") local json = require('json') local math = require('math') -require('common.lua') +require('common') -local last_modified_date = '2022-4-9' -- Should be the last modified date +local last_modified_date = '2022-4-15' -- Should be the last modified date local script_version = 3 -------------------------------------------------- @@ -1895,4 +1895,4 @@ function main() end end -main() \ No newline at end of file +main() From 5e84f91d2ff67847f1adffb62fd99d6e378b19b1 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Fri, 21 Apr 2023 20:57:31 -0400 Subject: [PATCH 077/489] SM: comeback fix5 (#1746) --- worlds/sm/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index f67e5c06ee..d1804d9209 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -671,7 +671,7 @@ class SMWorld(World): def collect(self, state: CollectionState, item: Item) -> bool: state.smbm[self.player].addItem(item.type) - if item.location != None: + if item.location != None and item.location.game == self.game: for entrance in self.multiworld.get_region(item.location.parent_region.name, item.location.player).entrances: if (entrance.parent_region.can_reach(state)): state.smbm[item.location.player].lastAP = entrance.parent_region.name From ab5cb7adad770ee3cba7564764898e03341833e5 Mon Sep 17 00:00:00 2001 From: agilbert1412 Date: Sun, 23 Apr 2023 15:59:46 -0400 Subject: [PATCH 078/489] Stardew Valley - Add alias for renamed Fishsanity option (#1758) --- worlds/stardew_valley/options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/stardew_valley/options.py b/worlds/stardew_valley/options.py index 445c2a2e1b..e365dd6735 100644 --- a/worlds/stardew_valley/options.py +++ b/worlds/stardew_valley/options.py @@ -292,6 +292,7 @@ class Fishsanity(Choice): option_legendaries = 1 option_special = 2 option_randomized = 3 + alias_random_selection = option_randomized option_all = 4 From 67c307657267a619f3a74f3e64618717f963cd1a Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sun, 23 Apr 2023 16:16:01 -0400 Subject: [PATCH 079/489] SM: comeback fix6 and some refactor (#1756) refactored and cleaned a bit SMWorld class for best practices: - moved content of Regions.py and Rules.py in SMWorld - moved appropiate code to their dedicated World core functions - moved some Entrances being created in generate_basic to create_regions more comeback check fixes: - fixed setting progression door openers items local if doors_colors_rando is used - enable comeback check only for filling stage as later stages (progression balancing, accessibility and spoiler playthrough) are prone to fail with it --- worlds/sm/Regions.py | 42 ----- worlds/sm/Rules.py | 38 ----- worlds/sm/__init__.py | 366 ++++++++++++++++++++++++------------------ 3 files changed, 209 insertions(+), 237 deletions(-) delete mode 100644 worlds/sm/Regions.py delete mode 100644 worlds/sm/Rules.py diff --git a/worlds/sm/Regions.py b/worlds/sm/Regions.py deleted file mode 100644 index ee6af4082d..0000000000 --- a/worlds/sm/Regions.py +++ /dev/null @@ -1,42 +0,0 @@ -def create_regions(self, world, player: int): - from . import create_region - from BaseClasses import Entrance - from .variaRandomizer.logic.logic import Logic - from .variaRandomizer.graph.vanilla.graph_locations import locationsDict - - regions = [] - for accessPoint in Logic.accessPoints: - if not accessPoint.Escape: - regions.append(create_region(self, - world, - player, - accessPoint.Name, - None, - [accessPoint.Name + "->" + key for key in accessPoint.intraTransitions.keys()])) - - world.regions += regions - - # create a region for each location and link each to what the location has access - # we make them one way so that the filler (and spoiler log) doesnt try to use those region as intermediary path - # this is required in AP because a location cant have multiple parent regions - locationRegions = [] - for locationName, value in locationsDict.items(): - locationRegions.append(create_region( self, - world, - player, - locationName, - [locationName])) - for key in value.AccessFrom.keys(): - currentRegion =world.get_region(key, player) - currentRegion.exits.append(Entrance(player, key + "->"+ locationName, currentRegion)) - - world.regions += locationRegions - #create entrances - regionConcat = regions + locationRegions - for region in regionConcat: - for exit in region.exits: - exit.connect(world.get_region(exit.name[exit.name.find("->") + 2:], player)) - - world.regions += [ - create_region(self, world, player, 'Menu', None, ['StartAP']) - ] diff --git a/worlds/sm/Rules.py b/worlds/sm/Rules.py deleted file mode 100644 index 15706987ff..0000000000 --- a/worlds/sm/Rules.py +++ /dev/null @@ -1,38 +0,0 @@ -from worlds.generic.Rules import set_rule, add_rule - -from .variaRandomizer.graph.vanilla.graph_locations import locationsDict -from .variaRandomizer.logic.logic import Logic - -def evalSMBool(smbool, maxDiff): - return smbool.bool == True and smbool.difficulty <= maxDiff - -def add_accessFrom_rule(location, player, accessFrom): - add_rule(location, lambda state: any((state.can_reach(accessName, player=player) and evalSMBool(rule(state.smbm[player]), state.smbm[player].maxDiff)) for accessName, rule in accessFrom.items())) - -def add_postAvailable_rule(location, player, func): - add_rule(location, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - -def set_available_rule(location, player, func): - set_rule(location, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - -def set_entrance_rule(entrance, player, func): - set_rule(entrance, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - -def add_entrance_rule(entrance, player, func): - add_rule(entrance, lambda state: evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - -def set_rules(world, player): - world.completion_condition[player] = lambda state: state.has('Mother Brain', player) - - for key, value in locationsDict.items(): - location = world.get_location(key, player) - set_available_rule(location, player, value.Available) - if value.AccessFrom is not None: - add_accessFrom_rule(location, player, value.AccessFrom) - if value.PostAvailable is not None: - add_postAvailable_rule(location, player, value.PostAvailable) - - for accessPoint in Logic.accessPoints: - if not accessPoint.Escape: - for key, value1 in accessPoint.intraTransitions.items(): - set_entrance_rule(world.get_entrance(accessPoint.Name + "->" + key, player), player, value1) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index d1804d9209..ef7e50ba58 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -10,11 +10,10 @@ from typing import Any, Dict, Iterable, List, Set, TextIO, TypedDict from BaseClasses import Region, Entrance, Location, MultiWorld, Item, ItemClassification, CollectionState, Tutorial from Fill import fill_restrictive from worlds.AutoWorld import World, AutoLogicRegister, WebWorld +from worlds.generic.Rules import set_rule, add_rule, add_item_rule logger = logging.getLogger("Super Metroid") -from .Regions import create_regions -from .Rules import set_rules, add_entrance_rule from .Options import sm_options from .Client import SMSNIClient from .Rom import get_base_rom_path, SM_ROM_MAX_PLAYERID, SM_ROM_PLAYERDATA_COUNT, SMDeltaPatch, get_sm_symbols @@ -106,6 +105,7 @@ class SMWorld(World): def __init__(self, world: MultiWorld, player: int): self.rom_name_available_event = threading.Event() self.locations = {} + self.need_comeback_check = True super().__init__(world, player) @classmethod @@ -134,7 +134,7 @@ class SMWorld(World): self.multiworld.accessibility[self.player] = self.multiworld.accessibility[self.player].from_text("minimal") logger.warning(f"accessibility forced to 'minimal' for player {self.multiworld.get_player_name(self.player)} because of 'fun' settings") - def generate_basic(self): + def create_items(self): itemPool = self.variaRando.container.itemPool self.startItems = [variaItem for item in self.multiworld.precollected_items[self.player] for variaItem in ItemManager.Items.values() if variaItem.Name == item.name] if self.multiworld.start_inventory_removes_from_pool[self.player]: @@ -150,7 +150,6 @@ class SMWorld(World): pool = [] self.locked_items = {} self.NothingPool = [] - self.prefilled_locked_items = [] weaponCount = [0, 0, 0] for item in itemPool: isAdvancement = True @@ -180,12 +179,9 @@ class SMWorld(World): player=self.player) beamItems = ['Spazer', 'Ice', 'Wave' ,'Plasma'] - self.ammoItems = ['Missile', 'Super', 'PowerBomb'] if self.multiworld.doors_colors_rando[self.player].value != 0: if item.Type in beamItems: self.multiworld.local_items[self.player].value.add(item.Name) - elif item.Type in self.ammoItems and isAdvancement: - self.prefilled_locked_items.append(smitem) if itemClass == 'Boss': self.locked_items[item.Name] = smitem @@ -199,9 +195,96 @@ class SMWorld(World): for (location, item) in self.locked_items.items(): self.multiworld.get_location(location, self.player).place_locked_item(item) self.multiworld.get_location(location, self.player).address = None + + def evalSMBool(self, smbool, maxDiff): + return smbool.bool == True and smbool.difficulty <= maxDiff + + def add_entrance_rule(self, entrance, player, func): + add_rule(entrance, lambda state: self.evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) - startAP = self.multiworld.get_entrance('StartAP', self.player) - startAP.connect(self.multiworld.get_region(self.variaRando.args.startLocation, self.player)) + def set_rules(self): + def add_accessFrom_rule(location, player, accessFrom): + add_rule(location, lambda state: any((state.can_reach(accessName, player=player) and self.evalSMBool(rule(state.smbm[player]), state.smbm[player].maxDiff)) for accessName, rule in accessFrom.items())) + + def add_postAvailable_rule(location, player, func): + add_rule(location, lambda state: self.evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) + + def set_available_rule(location, player, func): + set_rule(location, lambda state: self.evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) + + def set_entrance_rule(entrance, player, func): + set_rule(entrance, lambda state: self.evalSMBool(func(state.smbm[player]), state.smbm[player].maxDiff)) + + self.multiworld.completion_condition[self.player] = lambda state: state.has('Mother Brain', self.player) + + ammoItems = ['Missile', 'Super', 'PowerBomb'] + for key, value in locationsDict.items(): + location = self.multiworld.get_location(key, self.player) + set_available_rule(location, self.player, value.Available) + if value.AccessFrom is not None: + add_accessFrom_rule(location, self.player, value.AccessFrom) + if value.PostAvailable is not None: + add_postAvailable_rule(location, self.player, value.PostAvailable) + + if self.multiworld.doors_colors_rando[self.player].value != 0: + add_item_rule(location, lambda item: item.type not in ammoItems or + (item.type in ammoItems and \ + (not item.advancement or (item.advancement and item.player == self.player)))) + + for accessPoint in Logic.accessPoints: + if not accessPoint.Escape: + for key, value1 in accessPoint.intraTransitions.items(): + set_entrance_rule(self.multiworld.get_entrance(accessPoint.Name + "->" + key, self.player), self.player, value1) + + def create_region(self, world: MultiWorld, player: int, name: str, locations=None, exits=None): + ret = Region(name, player, world) + if locations: + for loc in locations: + location = self.locations[loc] + location.parent_region = ret + ret.locations.append(location) + if exits: + for exit in exits: + ret.exits.append(Entrance(player, exit, ret)) + return ret + + def create_regions(self): + # create locations + for name in locationsDict: + self.locations[name] = SMLocation(self.player, name, self.location_name_to_id.get(name, None)) + + # create regions + regions = [] + for accessPoint in Logic.accessPoints: + if not accessPoint.Escape: + regions.append(self.create_region( self.multiworld, + self.player, + accessPoint.Name, + None, + [accessPoint.Name + "->" + key for key in accessPoint.intraTransitions.keys()])) + + self.multiworld.regions += regions + + # create a region for each location and link each to what the location has access + # we make them one way so that the filler (and spoiler log) doesnt try to use those region as intermediary path + # this is required in AP because a location cant have multiple parent regions + locationRegions = [] + for locationName, value in locationsDict.items(): + locationRegions.append(self.create_region( self.multiworld, + self.player, + locationName, + [locationName])) + for key in value.AccessFrom.keys(): + currentRegion = self.multiworld.get_region(key, self.player) + currentRegion.exits.append(Entrance(self.player, key + "->"+ locationName, currentRegion)) + + self.multiworld.regions += locationRegions + + #create entrances + regionConcat = regions + locationRegions + for region in regionConcat: + for exit in region.exits: + exit.connect(self.multiworld.get_region(exit.name[exit.name.find("->") + 2:], self.player)) for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions: src_region = self.multiworld.get_region(src.Name, self.player) @@ -210,26 +293,125 @@ class SMWorld(World): src_region.exits.append(Entrance(self.player, src.Name + "->" + dest.Name, src_region)) srcDestEntrance = self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player) srcDestEntrance.connect(dest_region) - add_entrance_rule(self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player), self.player, getAccessPoint(src.Name).traverse) + self.add_entrance_rule(self.multiworld.get_entrance(src.Name + "->" + dest.Name, self.player), self.player, getAccessPoint(src.Name).traverse) - def set_rules(self): - set_rules(self.multiworld, self.player) + self.multiworld.regions += [ + self.create_region(self.multiworld, self.player, 'Menu', None, ['StartAP']) + ] - def create_regions(self): - create_locations(self, self.player) - create_regions(self, self.multiworld, self.player) + startAP = self.multiworld.get_entrance('StartAP', self.player) + startAP.connect(self.multiworld.get_region(self.variaRando.args.startLocation, self.player)) + def collect(self, state: CollectionState, item: Item) -> bool: + state.smbm[self.player].addItem(item.type) + if item.location != None and item.location.game == self.game: + for entrance in self.multiworld.get_region(item.location.parent_region.name, item.location.player).entrances: + if (entrance.parent_region.can_reach(state)): + state.smbm[item.location.player].lastAP = entrance.parent_region.name + break + return super(SMWorld, self).collect(state, item) + + def remove(self, state: CollectionState, item: Item) -> bool: + state.smbm[self.player].removeItem(item.type) + return super(SMWorld, self).remove(state, item) + + def create_item(self, name: str) -> Item: + item = next(x for x in ItemManager.Items.values() if x.Name == name) + return SMItem(item.Name, ItemClassification.progression if item.Class != 'Minor' else ItemClassification.filler, item.Type, self.item_name_to_id[item.Name], + player=self.player) + + def get_filler_item_name(self) -> str: + if self.multiworld.random.randint(0, 100) < self.multiworld.minor_qty[self.player].value: + power_bombs = self.multiworld.power_bomb_qty[self.player].value + missiles = self.multiworld.missile_qty[self.player].value + super_missiles = self.multiworld.super_qty[self.player].value + roll = self.multiworld.random.randint(1, power_bombs + missiles + super_missiles) + if roll <= power_bombs: + return "Power Bomb" + elif roll <= power_bombs + missiles: + return "Missile" + else: + return "Super Missile" + else: + return "Nothing" + def pre_fill(self): - from Fill import fill_restrictive - if len(self.prefilled_locked_items) > 0: - locations = [loc for loc in self.locations.values() if loc.item is None] - self.multiworld.random.shuffle(locations) - all_state = self.multiworld.get_all_state(False) - for item in self.ammoItems: - while (all_state.has(item.name, self.player, 1)): - all_state.remove(item) + if len(self.NothingPool) > 0: + nonChozoLoc = [] + chozoLoc = [] - fill_restrictive(self.multiworld, all_state, locations, self.prefilled_locked_items, True, True) + for loc in self.locations.values(): + if loc.item is None: + if locationsDict[loc.name].isChozo(): + chozoLoc.append(loc) + else: + nonChozoLoc.append(loc) + + self.multiworld.random.shuffle(nonChozoLoc) + self.multiworld.random.shuffle(chozoLoc) + missingCount = len(self.NothingPool) - len(nonChozoLoc) + locations = nonChozoLoc + if (missingCount > 0): + locations += chozoLoc[:missingCount] + locations = locations[:len(self.NothingPool)] + for item, loc in zip(self.NothingPool, locations): + loc.place_locked_item(item) + loc.address = loc.item.code = None + + def post_fill(self): + self.itemLocs = [ + ItemLocation(ItemManager.Items[itemLoc.item.type + if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else + 'ArchipelagoItem'], + locationsDict[itemLoc.name], itemLoc.item.player, True) + for itemLoc in self.multiworld.get_locations(self.player) + ] + self.progItemLocs = [ + ItemLocation(ItemManager.Items[itemLoc.item.type + if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else + 'ArchipelagoItem'], + locationsDict[itemLoc.name], itemLoc.item.player, True) + for itemLoc in self.multiworld.get_locations(self.player) if itemLoc.item.advancement + ] + for itemLoc in self.itemLocs: + if itemLoc.Item.Class == "Boss": + itemLoc.Item.Class = "Minor" + for itemLoc in self.progItemLocs: + if itemLoc.Item.Class == "Boss": + itemLoc.Item.Class = "Minor" + + localItemLocs = [il for il in self.itemLocs if il.player == self.player] + localprogItemLocs = [il for il in self.progItemLocs if il.player == self.player] + + escapeTrigger = (localItemLocs, localprogItemLocs, 'Full') if self.variaRando.randoExec.randoSettings.restrictions["EscapeTrigger"] else None + escapeOk = self.variaRando.randoExec.graphBuilder.escapeGraph(self.variaRando.container, self.variaRando.randoExec.areaGraph, self.variaRando.randoExec.randoSettings.maxDiff, escapeTrigger) + assert escapeOk, "Could not find a solution for escape" + + self.variaRando.doors = GraphUtils.getDoorConnections(self.variaRando.randoExec.areaGraph, + self.variaRando.args.area, self.variaRando.args.bosses, + self.variaRando.args.escapeRando) + + self.variaRando.randoExec.postProcessItemLocs(self.itemLocs, self.variaRando.args.hideItems) + + self.need_comeback_check = False + + @classmethod + def stage_post_fill(cls, world): + new_state = CollectionState(world) + progitempool = [] + for item in world.itempool: + if item.game == "Super Metroid" and item.advancement: + progitempool.append(item) + + for item in progitempool: + new_state.collect(item, True) + + bossesLoc = ['Draygon', 'Kraid', 'Ridley', 'Phantoon', 'Mother Brain'] + for player in world.get_game_players("Super Metroid"): + for bossLoc in bossesLoc: + if not world.get_location(bossLoc, player).can_reach(new_state): + world.state.smbm[player].onlyBossLeft = True + break def getWordArray(self, w: int) -> List[int]: """ little-endian convert a 16-bit number to an array of numbers <= 255 each """ @@ -669,115 +851,6 @@ class SMWorld(World): return slot_data - def collect(self, state: CollectionState, item: Item) -> bool: - state.smbm[self.player].addItem(item.type) - if item.location != None and item.location.game == self.game: - for entrance in self.multiworld.get_region(item.location.parent_region.name, item.location.player).entrances: - if (entrance.parent_region.can_reach(state)): - state.smbm[item.location.player].lastAP = entrance.parent_region.name - break - return super(SMWorld, self).collect(state, item) - - def remove(self, state: CollectionState, item: Item) -> bool: - state.smbm[self.player].removeItem(item.type) - return super(SMWorld, self).remove(state, item) - - def create_item(self, name: str) -> Item: - item = next(x for x in ItemManager.Items.values() if x.Name == name) - return SMItem(item.Name, ItemClassification.progression if item.Class != 'Minor' else ItemClassification.filler, item.Type, self.item_name_to_id[item.Name], - player=self.player) - - def get_filler_item_name(self) -> str: - if self.multiworld.random.randint(0, 100) < self.multiworld.minor_qty[self.player].value: - power_bombs = self.multiworld.power_bomb_qty[self.player].value - missiles = self.multiworld.missile_qty[self.player].value - super_missiles = self.multiworld.super_qty[self.player].value - roll = self.multiworld.random.randint(1, power_bombs + missiles + super_missiles) - if roll <= power_bombs: - return "Power Bomb" - elif roll <= power_bombs + missiles: - return "Missile" - else: - return "Super Missile" - else: - return "Nothing" - - def pre_fill(self): - if len(self.NothingPool) > 0: - nonChozoLoc = [] - chozoLoc = [] - - for loc in self.locations.values(): - if loc.item is None: - if locationsDict[loc.name].isChozo(): - chozoLoc.append(loc) - else: - nonChozoLoc.append(loc) - - self.multiworld.random.shuffle(nonChozoLoc) - self.multiworld.random.shuffle(chozoLoc) - missingCount = len(self.NothingPool) - len(nonChozoLoc) - locations = nonChozoLoc - if (missingCount > 0): - locations += chozoLoc[:missingCount] - locations = locations[:len(self.NothingPool)] - for item, loc in zip(self.NothingPool, locations): - loc.place_locked_item(item) - loc.address = loc.item.code = None - - def post_fill(self): - self.itemLocs = [ - ItemLocation(ItemManager.Items[itemLoc.item.type - if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else - 'ArchipelagoItem'], - locationsDict[itemLoc.name], itemLoc.item.player, True) - for itemLoc in self.multiworld.get_locations(self.player) - ] - self.progItemLocs = [ - ItemLocation(ItemManager.Items[itemLoc.item.type - if isinstance(itemLoc.item, SMItem) and itemLoc.item.type in ItemManager.Items else - 'ArchipelagoItem'], - locationsDict[itemLoc.name], itemLoc.item.player, True) - for itemLoc in self.multiworld.get_locations(self.player) if itemLoc.item.advancement - ] - for itemLoc in self.itemLocs: - if itemLoc.Item.Class == "Boss": - itemLoc.Item.Class = "Minor" - for itemLoc in self.progItemLocs: - if itemLoc.Item.Class == "Boss": - itemLoc.Item.Class = "Minor" - - localItemLocs = [il for il in self.itemLocs if il.player == self.player] - localprogItemLocs = [il for il in self.progItemLocs if il.player == self.player] - - escapeTrigger = (localItemLocs, localprogItemLocs, 'Full') if self.variaRando.randoExec.randoSettings.restrictions["EscapeTrigger"] else None - escapeOk = self.variaRando.randoExec.graphBuilder.escapeGraph(self.variaRando.container, self.variaRando.randoExec.areaGraph, self.variaRando.randoExec.randoSettings.maxDiff, escapeTrigger) - assert escapeOk, "Could not find a solution for escape" - - self.variaRando.doors = GraphUtils.getDoorConnections(self.variaRando.randoExec.areaGraph, - self.variaRando.args.area, self.variaRando.args.bosses, - self.variaRando.args.escapeRando) - - self.variaRando.randoExec.postProcessItemLocs(self.itemLocs, self.variaRando.args.hideItems) - - @classmethod - def stage_post_fill(cls, world): - new_state = CollectionState(world) - progitempool = [] - for item in world.itempool: - if item.game == "Super Metroid" and item.advancement: - progitempool.append(item) - - for item in progitempool: - new_state.collect(item, True) - - bossesLoc = ['Draygon', 'Kraid', 'Ridley', 'Phantoon', 'Mother Brain'] - for player in world.get_game_players("Super Metroid"): - for bossLoc in bossesLoc: - if not world.get_location(bossLoc, player).can_reach(new_state): - world.state.smbm[player].onlyBossLeft = True - break - def write_spoiler(self, spoiler_handle: TextIO): if self.multiworld.area_randomization[self.player].value != 0: spoiler_handle.write('\n\nArea Transitions:\n\n') @@ -793,39 +866,18 @@ class SMWorld(World): '<=>', dest.Name) for src, dest in self.variaRando.randoExec.areaGraph.InterAreaTransitions if src.Boss])) -def create_locations(self, player: int): - for name in locationsDict: - self.locations[name] = SMLocation(player, name, self.location_name_to_id.get(name, None)) - - -def create_region(self, world: MultiWorld, player: int, name: str, locations=None, exits=None): - ret = Region(name, player, world) - if locations: - for loc in locations: - location = self.locations[loc] - location.parent_region = ret - ret.locations.append(location) - if exits: - for exit in exits: - ret.exits.append(Entrance(player, exit, ret)) - return ret - - class SMLocation(Location): game: str = "Super Metroid" def __init__(self, player: int, name: str, address=None, parent=None): super(SMLocation, self).__init__(player, name, address, parent) - def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool: - return self.always_allow(state, item) or (self.item_rule(item) and (not check_access or self.can_reach(state))) - def can_reach(self, state: CollectionState) -> bool: # self.access_rule computes faster on average, so placing it first for faster abort assert self.parent_region, "Can't reach location without region" - return self.access_rule(state) and \ - self.parent_region.can_reach(state) and \ - self.can_comeback(state, self.item) + return super(SMLocation, self).can_reach(state) and \ + (not state.multiworld.worlds[self.player].need_comeback_check or \ + self.can_comeback(state, self.item)) def can_comeback(self, state: CollectionState, item): randoExec = state.multiworld.worlds[self.player].variaRando.randoExec From 62a265cc31cc1820f70f5c311d70928999f2a8d5 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Sun, 23 Apr 2023 16:17:03 -0400 Subject: [PATCH 080/489] =?UTF-8?q?Pok=C3=A9mon=20R/B:=20logic=20and=20loc?= =?UTF-8?q?ation=20name=20fixes=20(#1752)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Corrects incorrect name listed for location in rules.py leading to logic rules failing to apply. Swaps location names for incorrectly-named trainersanity checks in Viridian Gym --- worlds/pokemon_rb/__init__.py | 2 +- worlds/pokemon_rb/locations.py | 4 ++-- worlds/pokemon_rb/rules.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/pokemon_rb/__init__.py b/worlds/pokemon_rb/__init__.py index b223568ff0..753d35c735 100644 --- a/worlds/pokemon_rb/__init__.py +++ b/worlds/pokemon_rb/__init__.py @@ -40,7 +40,7 @@ class PokemonRedBlueWorld(World): game = "Pokemon Red and Blue" option_definitions = pokemon_rb_options - data_version = 7 + data_version = 8 required_client_version = (0, 3, 9) topology_present = False diff --git a/worlds/pokemon_rb/locations.py b/worlds/pokemon_rb/locations.py index a1b64e12e5..f942662c8e 100644 --- a/worlds/pokemon_rb/locations.py +++ b/worlds/pokemon_rb/locations.py @@ -708,8 +708,8 @@ location_data = [ LocationData("Viridian Gym", "Cooltrainer M 2", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_0_ITEM"], EventFlag(446), inclusion=trainersanity), LocationData("Viridian Gym", "Blackbelt 2", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_1_ITEM"], EventFlag(445), inclusion=trainersanity), LocationData("Viridian Gym", "Tamer 2", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_2_ITEM"], EventFlag(440), inclusion=trainersanity), - LocationData("Viridian Gym", "Cooltrainer M 3", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_5_ITEM"], EventFlag(437), inclusion=trainersanity), - LocationData("Viridian Gym", "Blackbelt 3", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_4_ITEM"], EventFlag(438), inclusion=trainersanity), + LocationData("Viridian Gym", "Blackbelt 3", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_5_ITEM"], EventFlag(437), inclusion=trainersanity), + LocationData("Viridian Gym", "Cooltrainer M 3", None, rom_addresses["Trainersanity_EVENT_BEAT_VIRIDIAN_GYM_TRAINER_4_ITEM"], EventFlag(438), inclusion=trainersanity), LocationData("Victory Road 1F", "Cooltrainer F", None, rom_addresses["Trainersanity_EVENT_BEAT_VICTORY_ROAD_1_TRAINER_0_ITEM"], EventFlag(15), inclusion=trainersanity), LocationData("Victory Road 1F", "Cooltrainer M", None, rom_addresses["Trainersanity_EVENT_BEAT_VICTORY_ROAD_1_TRAINER_1_ITEM"], EventFlag(14), inclusion=trainersanity), LocationData("Victory Road 2F", "Blackbelt", None, rom_addresses["Trainersanity_EVENT_BEAT_VICTORY_ROAD_2_TRAINER_0_ITEM"], EventFlag(162), inclusion=trainersanity), diff --git a/worlds/pokemon_rb/rules.py b/worlds/pokemon_rb/rules.py index e9e64e218e..33fe248a8c 100644 --- a/worlds/pokemon_rb/rules.py +++ b/worlds/pokemon_rb/rules.py @@ -146,7 +146,7 @@ def set_rules(world, player): "Silph Co 11F - Rocket 2 (Card Key)": lambda state: state.has("Card Key", player), "Silph Co 9F - Rocket 2 (Card Key)": lambda state: state.has("Card Key", player), "Silph Co 3F - Scientist (Card Key)": lambda state: state.has("Card Key", player), - "Route 10 North - Pokemaniac": lambda state: state.pokemon_rb_can_surf(player), + "Route 10 - Pokemaniac": lambda state: state.pokemon_rb_can_surf(player), "Rocket Hideout B1F - Rocket 5 (Lift Key)": lambda state: state.has("Lift Key", player), "Rocket Hideout B4F - Rocket 2 (Lift Key)": lambda state: state.has("Lift Key", player), "Rocket Hideout B4F - Rocket 3 (Lift Key)": lambda state: state.has("Lift Key", player), From 06a25a903e803933af4bc23112185e6ce524b25b Mon Sep 17 00:00:00 2001 From: JaredWeakStrike <96694163+JaredWeakStrike@users.noreply.github.com> Date: Sun, 23 Apr 2023 16:20:43 -0400 Subject: [PATCH 081/489] KH2: New Unit Test and better keyblade fill (#1744) __init__: - Added exception for if the player has too many excluded abilities on keyblades. - Fixed Action Abilities only on keyblades from breaking. - Added proper support for ability quantity's instead of 1 of the ability - Moved filling the localitems slot data to init instead of generate_output so I could easily unit test it TestSlotData: - Checks if the "localItems" part of slot data is filled. This is used for keeping track of local items and making sure nothing dupes --- worlds/kh2/OpenKH.py | 8 ++------ worlds/kh2/__init__.py | 35 +++++++++++++++++++++++++-------- worlds/kh2/test/TestSlotData.py | 21 ++++++++++++++++++++ 3 files changed, 50 insertions(+), 14 deletions(-) create mode 100644 worlds/kh2/test/TestSlotData.py diff --git a/worlds/kh2/OpenKH.py b/worlds/kh2/OpenKH.py index eb1a846e42..c3334dbb99 100644 --- a/worlds/kh2/OpenKH.py +++ b/worlds/kh2/OpenKH.py @@ -6,8 +6,7 @@ import Utils import zipfile from .Items import item_dictionary_table, CheckDupingItems -from .Locations import all_locations, SoraLevels, exclusion_table, AllWeaponSlot -from .Names import LocationName +from .Locations import all_locations, SoraLevels, exclusion_table from .XPValues import lvlStats, formExp, soraExp from worlds.Files import APContainer @@ -83,7 +82,7 @@ def patch_kh2(self, output_directory): elif self.multiworld.LevelDepth[self.player] == "level_99": levelsetting.extend(exclusion_table["Level99"]) - elif self.multiworld.LevelDepth[self.player] in ["level_50_sanity", "level_99_sanity"]: + elif self.multiworld.LevelDepth[self.player] != "level_1": levelsetting.extend(exclusion_table["Level50Sanity"]) if self.multiworld.LevelDepth[self.player] == "level_99_sanity": @@ -96,9 +95,6 @@ def patch_kh2(self, output_directory): data = all_locations[location.name] if location.item.player == self.player: itemcode = item_dictionary_table[location.item.name].kh2id - if location.item.name in slotDataDuping and \ - location.name not in AllWeaponSlot: - self.LocalItems[location.address] = item_dictionary_table[location.item.name].code else: itemcode = 90 # castle map diff --git a/worlds/kh2/__init__.py b/worlds/kh2/__init__.py index 08ab9eabce..23075a2084 100644 --- a/worlds/kh2/__init__.py +++ b/worlds/kh2/__init__.py @@ -2,7 +2,7 @@ from BaseClasses import Tutorial, ItemClassification import logging from .Items import * -from .Locations import all_locations, setup_locations, exclusion_table +from .Locations import all_locations, setup_locations, exclusion_table, AllWeaponSlot from .Names import ItemName, LocationName from .OpenKH import patch_kh2 from .Options import KH2_Options @@ -62,8 +62,22 @@ class KH2World(World): self.growth_list = list() for x in range(4): self.growth_list.extend(Movement_Table.keys()) + self.slotDataDuping = set() + self.localItems = dict() def fill_slot_data(self) -> dict: + for values in CheckDupingItems.values(): + if isinstance(values, set): + self.slotDataDuping = self.slotDataDuping.union(values) + else: + for inner_values in values.values(): + self.slotDataDuping = self.slotDataDuping.union(inner_values) + self.LocalItems = {location.address: item_dictionary_table[location.item.name].code + for location in self.multiworld.get_filled_locations(self.player) + if location.item.player == self.player + and location.item.name in self.slotDataDuping + and location.name not in AllWeaponSlot} + return {"hitlist": self.hitlist, "LocalItems": self.LocalItems, "Goal": self.multiworld.Goal[self.player].value, @@ -132,7 +146,7 @@ class KH2World(World): # Creating filler for unfilled locations itempool += [self.create_filler() - for _ in range(self.totalLocations-len(itempool))] + for _ in range(self.totalLocations - len(itempool))] self.multiworld.itempool += itempool def generate_early(self) -> None: @@ -245,13 +259,15 @@ class KH2World(World): ItemName.FinishingPlus: 1}} elif self.multiworld.KeybladeAbilities[self.player] == "action": - self.sora_keyblade_ability_pool = {item: data for item, data in self.item_quantity_dict.items() if item in ActionAbility_Table} + self.sora_keyblade_ability_pool = {item: data for item, data in self.item_quantity_dict.items() if + item in ActionAbility_Table} # there are too little action abilities so 2 random support abilities are placed for _ in range(3): - randomSupportAbility = self.multiworld.per_slot_randoms[self.player].choice(list(SupportAbility_Table.keys())) + randomSupportAbility = self.multiworld.per_slot_randoms[self.player].choice( + list(SupportAbility_Table.keys())) while randomSupportAbility in self.sora_keyblade_ability_pool: randomSupportAbility = self.multiworld.per_slot_randoms[self.player].choice( - list(SupportAbility_Table.keys())) + list(SupportAbility_Table.keys())) self.sora_keyblade_ability_pool[randomSupportAbility] = 1 else: # both action and support on keyblades. @@ -259,7 +275,8 @@ class KH2World(World): self.sora_keyblade_ability_pool = { **{item: data for item, data in self.item_quantity_dict.items() if item in SupportAbility_Table}, **{item: data for item, data in self.item_quantity_dict.items() if item in ActionAbility_Table}, - **{ItemName.NegativeCombo: 1, ItemName.AirComboPlus: 1, ItemName.ComboPlus: 1, ItemName.FinishingPlus: 1}} + **{ItemName.NegativeCombo: 1, ItemName.AirComboPlus: 1, ItemName.ComboPlus: 1, + ItemName.FinishingPlus: 1}} for ability in self.multiworld.BlacklistKeyblade[self.player].value: if ability in self.sora_keyblade_ability_pool: @@ -267,7 +284,8 @@ class KH2World(World): # magic number for amount of keyblades if sum(self.sora_keyblade_ability_pool.values()) < 28: - raise Exception(f"{self.multiworld.get_file_safe_player_name(self.player)} has too little Keyblade Abilities in the Keyblade Pool") + raise Exception( + f"{self.multiworld.get_file_safe_player_name(self.player)} has too little Keyblade Abilities in the Keyblade Pool") self.valid_abilities = list(self.sora_keyblade_ability_pool.keys()) # Kingdom Key cannot have No Experience so plandoed here instead of checking 26 times if its kingdom key @@ -379,4 +397,5 @@ class KH2World(World): self.totalLocations -= 76 def get_filler_item_name(self) -> str: - return self.multiworld.random.choice([ItemName.PowerBoost, ItemName.MagicBoost, ItemName.DefenseBoost, ItemName.APBoost]) + return self.multiworld.random.choice( + [ItemName.PowerBoost, ItemName.MagicBoost, ItemName.DefenseBoost, ItemName.APBoost]) diff --git a/worlds/kh2/test/TestSlotData.py b/worlds/kh2/test/TestSlotData.py new file mode 100644 index 0000000000..656cd48d5a --- /dev/null +++ b/worlds/kh2/test/TestSlotData.py @@ -0,0 +1,21 @@ +import unittest + +from test.general import setup_solo_multiworld +from . import KH2TestBase +from .. import KH2World, all_locations, item_dictionary_table, CheckDupingItems, AllWeaponSlot, KH2Item +from ..Names import ItemName +from ... import AutoWorldRegister +from ...AutoWorld import call_all + + +class TestLocalItems(KH2TestBase): + + def testSlotData(self): + gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill") + multiworld = setup_solo_multiworld(KH2World, gen_steps) + for location in multiworld.get_locations(): + if location.item is None: + location.place_locked_item(multiworld.worlds[1].create_item(ItemName.NoExperience)) + call_all(multiworld, "fill_slot_data") + slotdata = multiworld.worlds[1].fill_slot_data() + assert len(slotdata["LocalItems"]) > 0, f"{slotdata['LocalItems']} is empty" From 58aea7ca5841fc0d61943d48f359a428a7360929 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 23 Apr 2023 22:21:28 +0200 Subject: [PATCH 082/489] Multiserver: cleaner exit (#1743) --- MultiServer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 59c2975e12..3d5053bbe5 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -1865,7 +1865,7 @@ class ServerCommandProcessor(CommonCommandProcessor): def _cmd_exit(self) -> bool: """Shutdown the server""" - async_start(self.ctx.server.ws_server._close()) + self.ctx.server.ws_server.close() if self.ctx.shutdown_task: self.ctx.shutdown_task.cancel() self.ctx.exit_event.set() @@ -2206,7 +2206,7 @@ async def auto_shutdown(ctx, to_cancel=None): await asyncio.sleep(ctx.auto_shutdown) while not ctx.exit_event.is_set(): if not ctx.client_activity_timers.values(): - async_start(ctx.server.ws_server._close()) + ctx.server.ws_server.close() ctx.exit_event.set() if to_cancel: for task in to_cancel: @@ -2217,7 +2217,7 @@ async def auto_shutdown(ctx, to_cancel=None): delta = datetime.datetime.now(datetime.timezone.utc) - newest_activity seconds = ctx.auto_shutdown - delta.total_seconds() if seconds < 0: - async_start(ctx.server.ws_server._close()) + ctx.server.ws_server.close() ctx.exit_event.set() if to_cancel: for task in to_cancel: From b950af09a6368a9923ab0caf77b96ea4e6deac84 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 24 Apr 2023 01:58:26 +0200 Subject: [PATCH 083/489] Factorio: remove tech_tree_layout_prerequisites from core --- BaseClasses.py | 1 - worlds/factorio/Mod.py | 2 +- worlds/factorio/Shapes.py | 2 +- worlds/factorio/__init__.py | 2 ++ 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 35761bc238..68407ee083 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -135,7 +135,6 @@ class MultiWorld(): def set_player_attr(attr, val): self.__dict__.setdefault(attr, {})[player] = val - set_player_attr('tech_tree_layout_prerequisites', {}) set_player_attr('_region_cache', {}) set_player_attr('shuffle', "vanilla") set_player_attr('logic', "noglitches") diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 4f1f3fd9d0..270e7dacf0 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -120,7 +120,7 @@ def generate_mod(world: "Factorio", output_directory: str): "mod_name": mod_name, "allowed_science_packs": multiworld.max_science_pack[player].get_allowed_packs(), "custom_technologies": multiworld.worlds[player].custom_technologies, - "tech_tree_layout_prerequisites": multiworld.tech_tree_layout_prerequisites[player], + "tech_tree_layout_prerequisites": world.tech_tree_layout_prerequisites, "slot_name": multiworld.player_name[player], "seed_name": multiworld.seed_name, "slot_player": player, "starting_items": multiworld.starting_items[player], "recipes": recipes, diff --git a/worlds/factorio/Shapes.py b/worlds/factorio/Shapes.py index 84bcb06cab..d40871f7fa 100644 --- a/worlds/factorio/Shapes.py +++ b/worlds/factorio/Shapes.py @@ -247,5 +247,5 @@ def get_shapes(factorio_world: "Factorio") -> Dict["FactorioScienceLocation", Se else: raise NotImplementedError(f"Layout {layout} is not implemented.") - world.tech_tree_layout_prerequisites[player] = prerequisites + factorio_world.tech_tree_layout_prerequisites = prerequisites return prerequisites diff --git a/worlds/factorio/__init__.py b/worlds/factorio/__init__.py index 269ec45566..10dda905e1 100644 --- a/worlds/factorio/__init__.py +++ b/worlds/factorio/__init__.py @@ -69,6 +69,7 @@ class Factorio(World): required_client_version = (0, 4, 0) ordered_science_packs: typing.List[str] = MaxSciencePack.get_ordered_science_packs() + tech_tree_layout_prerequisites: typing.Dict[FactorioScienceLocation, typing.Set[FactorioScienceLocation]] tech_mix: int = 0 skip_silo: bool = False science_locations: typing.List[FactorioScienceLocation] @@ -78,6 +79,7 @@ class Factorio(World): self.advancement_technologies = set() self.custom_recipes = {} self.science_locations = [] + self.tech_tree_layout_prerequisites = {} generate_output = generate_mod From dcc628f878c0074a2d7780153179c28513b8c7c1 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 23 Apr 2023 22:12:55 +0200 Subject: [PATCH 084/489] Core: correct typing info for item_in_locations Core: rename item_in_locations to item_name_in_location_names Core: add actual item_name_in_locations --- worlds/alttp/Rules.py | 12 ++++++------ worlds/generic/Rules.py | 14 +++++++++++--- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/worlds/alttp/Rules.py b/worlds/alttp/Rules.py index e6c5f15a2f..09c63aca01 100644 --- a/worlds/alttp/Rules.py +++ b/worlds/alttp/Rules.py @@ -4,7 +4,7 @@ from typing import Iterator, Set from BaseClasses import Entrance, MultiWorld from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item, - item_in_locations, location_item_name, set_rule, allow_self_locking_items) + item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items) from . import OverworldGlitchRules from .Bosses import GanonDefeatRule @@ -305,7 +305,7 @@ def global_rules(world, player): set_rule(world.get_location('Ice Palace - Big Chest', player), lambda state: state.has('Big Key (Ice Palace)', player)) set_rule(world.get_entrance('Ice Palace (Kholdstare)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player) and state.has('Big Key (Ice Palace)', player) and (state._lttp_has_key('Small Key (Ice Palace)', player, 2) or (state.has('Cane of Somaria', player) and state._lttp_has_key('Small Key (Ice Palace)', player, 1)))) set_rule(world.get_entrance('Ice Palace (East)', player), lambda state: (state.has('Hookshot', player) or ( - item_in_locations(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player))) + item_name_in_location_names(state, 'Big Key (Ice Palace)', player, [('Ice Palace - Spike Room', player), ('Ice Palace - Big Key Chest', player), ('Ice Palace - Map Chest', player)]) and state._lttp_has_key('Small Key (Ice Palace)', player))) and (state.multiworld.can_take_damage[player] or state.has('Hookshot', player) or state.has('Cape', player) or state.has('Cane of Byrna', player))) set_rule(world.get_entrance('Ice Palace (East Top)', player), lambda state: can_lift_rocks(state, player) and state.has('Hammer', player)) set_rule(world.get_entrance('Misery Mire Entrance Gap', player), lambda state: (state.has('Pegasus Boots', player) or state.has('Hookshot', player)) and (has_sword(state, player) or state.has('Fire Rod', player) or state.has('Ice Rod', player) or state.has('Hammer', player) or state.has('Cane of Somaria', player) or can_shoot_arrows(state, player))) # need to defeat wizzrobes, bombs don't work ... @@ -381,17 +381,17 @@ def global_rules(world, player): #The actual requirements for these rooms to avoid key-lock set_rule(world.get_location('Ganons Tower - Firesnake Room', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) or (( - item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_in_locations(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 2))) + item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) or item_name_in_location_names(state, 'Small Key (Ganons Tower)', player, [('Ganons Tower - Firesnake Room', player)])) and state._lttp_has_key('Small Key (Ganons Tower)', player, 2))) for location in randomizer_room_chests: set_rule(world.get_location(location, player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or ( - item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))) + item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(randomizer_room_chests, [player] * len(randomizer_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3))) # Once again it is possible to need more than 3 keys... set_rule(world.get_entrance('Ganons Tower (Tile Room) Key Door', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 3) and state.has('Fire Rod', player)) # Actual requirements for location in compass_room_chests: set_rule(world.get_location(location, player), lambda state: state.has('Fire Rod', player) and (state._lttp_has_key('Small Key (Ganons Tower)', player, 4) or ( - item_in_locations(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))) + item_name_in_location_names(state, 'Big Key (Ganons Tower)', player, zip(compass_room_chests, [player] * len(compass_room_chests))) and state._lttp_has_key('Small Key (Ganons Tower)', player, 3)))) set_rule(world.get_location('Ganons Tower - Big Chest', player), lambda state: state.has('Big Key (Ganons Tower)', player)) @@ -919,7 +919,7 @@ def set_trock_key_rules(world, player): else: # Middle to front requires 2 keys if the back is locked, otherwise 4 set_rule(world.get_entrance('Turtle Rock (Chain Chomp Room) (South)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 2) - if item_in_locations(state, 'Big Key (Turtle Rock)', player, front_locked_locations) + if item_name_in_location_names(state, 'Big Key (Turtle Rock)', player, front_locked_locations) else state._lttp_has_key('Small Key (Turtle Rock)', player, 4)) # Front to middle requires 2 keys (if the middle is accessible then these doors can be avoided, otherwise no keys can be wasted) diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index fb783edb67..520ad22525 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -140,14 +140,22 @@ def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str location.item_rule = lambda item: rule(item) or old_rule(item) -def item_in_locations(state: "BaseClasses.CollectionState", item: str, player: int, - locations: typing.Sequence["BaseClasses.Location"]) -> bool: - for location in locations: +def item_name_in_location_names(state: "BaseClasses.CollectionState", item: str, player: int, + location_name_player_pairs: typing.Sequence[typing.Tuple[str, int]]) -> bool: + for location in location_name_player_pairs: if location_item_name(state, location[0], location[1]) == (item, player): return True return False +def item_name_in_locations(item: str, player: int, + locations: typing.Sequence["BaseClasses.Location"]) -> bool: + for location in locations: + if location.item and location.item.name == item and location.item.player == player: + return True + return False + + def location_item_name(state: "BaseClasses.CollectionState", location: str, player: int) -> \ typing.Optional[typing.Tuple[str, int]]: location = state.multiworld.get_location(location, player) From c0cf35edda630777fbba9e12d4a723c3fc22d5d1 Mon Sep 17 00:00:00 2001 From: Bicoloursnake <60069210+Bicoloursnake@users.noreply.github.com> Date: Mon, 24 Apr 2023 18:53:33 -0400 Subject: [PATCH 085/489] StarCraft 2 macOS documentation (#1747) * Adding SC2 macOS instructions A few hours ago, I tested whether the client would run successfully on macOS (Send/Receive items, load maps, download maps, etc.). After the successful testing, I thought adding some documentation would be nice for those who want to play Archipelago on a macOS system. * Don't need sudo Turns out you don't need sudo to do the download_data, oops. * Removed Extraneous Parantheses --- worlds/sc2wol/docs/setup_en.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/worlds/sc2wol/docs/setup_en.md b/worlds/sc2wol/docs/setup_en.md index 267c8430aa..13c7cb91e3 100644 --- a/worlds/sc2wol/docs/setup_en.md +++ b/worlds/sc2wol/docs/setup_en.md @@ -16,6 +16,7 @@ to obtain a config file for StarCraft 2. - Linux users should also follow the instructions found at the bottom of this page (["Running in Linux"](#running-in-linux)). 2. Run ArchipelagoStarcraft2Client.exe. + - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only. 3. Type the command `/download_data`. This will automatically install the Maps and Data files from the third link above. ## Where do I get a config file (aka "YAML") for this game? @@ -34,6 +35,7 @@ Check out [Creating a YAML](https://archipelago.gg/tutorial/Archipelago/setup/en ## How do I join a MultiWorld game? 1. Run ArchipelagoStarcraft2Client.exe. + - macOS users should instead follow the instructions found at ["Running in macOS"](#running-in-macos) for this step only. 2. Type `/connect [server ip]`. 3. Type your slot name and the server's password when prompted. 4. Once connected, switch to the 'StarCraft 2 Launcher' tab in the client. There, you can see every mission. By default, @@ -45,6 +47,10 @@ First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel for help. Please include a specific description of what's going wrong and attach your log file to your message. +## Running in macOS + +To run StarCraft 2 through Archipelago in macOS, you will need to run the client via source as seen here: [macOS Guide](https://archipelago.gg/tutorial/Archipelago/mac/en). Note: when running the client, you will need to run the command `python3 Starcraft2Client.py`. This is done to make sure that `/download_data` works correctly. + ## Running in Linux To run StarCraft 2 through Archipelago in Linux, you will need to install the game using Wine, then run the Linux build From 173513c9f4c957240eb30d1686ba5be014dfdf4d Mon Sep 17 00:00:00 2001 From: axe-y <58866768+axe-y@users.noreply.github.com> Date: Tue, 25 Apr 2023 03:06:58 -0400 Subject: [PATCH 086/489] DLCQuest: Generation bug fix (#1757) * Fix documentation error * init_creation somehow this fix bug * item_shuffle_fix also a count as been corrected * Fix_early_generation * Update __init__.py * Update __init__.py fix version specific bug * fix rule set for final boss and did some reformation (thanks kaito) * Update Rules.py the sword trio can now be in itself if before or actually themself * Core: correct typing info for item_in_locations Core: rename item_in_locations to item_name_in_location_names Core: add actual item_name_in_locations * item_shuffle_fix also a count as been corrected * Fix_early_generation * fix rule set for final boss and did some reformation (thanks kaito) * Update Rules.py the sword trio can now be in itself if before or actually themself * Fix the missing [] and switch to the good function * - Cleanup and Black Sliver's suggestions --------- Co-authored-by: Fabian Dill Co-authored-by: Alex Gilbert --- worlds/dlcquest/Rules.py | 685 ++++++++++++++++++++++----------------- 1 file changed, 382 insertions(+), 303 deletions(-) diff --git a/worlds/dlcquest/Rules.py b/worlds/dlcquest/Rules.py index c57976e79e..b571bdd3eb 100644 --- a/worlds/dlcquest/Rules.py +++ b/worlds/dlcquest/Rules.py @@ -1,7 +1,7 @@ import math import re from .Locations import DLCQuestLocation -from ..generic.Rules import add_rule, set_rule +from ..generic.Rules import add_rule, set_rule, item_name_in_locations from .Items import DLCQuestItem from BaseClasses import ItemClassification from . import Options @@ -22,7 +22,6 @@ def set_rules(world, player, World_Options: Options.DLCQuestOptions): return coin_possessed >= coins - return lambda state: has_coin(state, player, coin) def has_enough_coin_freemium(player: int, coin: int): @@ -37,334 +36,414 @@ def set_rules(world, player, World_Options: Options.DLCQuestOptions): return lambda state: has_coin(state, player, coin) - if World_Options[Options.Campaign] == Options.Campaign.option_basic or World_Options[ - Options.Campaign] == Options.Campaign.option_both: - set_rule(world.get_entrance("Moving", player), - lambda state: state.has("Movement Pack", player)) - set_rule(world.get_entrance("Cloud", player), - lambda state: state.has("Psychological Warfare Pack", player)) - set_rule(world.get_entrance("Forest Entrance", player), - lambda state: state.has("Map Pack", player)) - set_rule(world.get_entrance("Forest True Double Jump", player), - lambda state: state.has("Double Jump Pack", player)) + set_basic_rules(World_Options, has_enough_coin, player, world) + set_lfod_rules(World_Options, has_enough_coin_freemium, player, world) + set_completion_condition(World_Options, player, world) - if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_disabled: - set_rule(world.get_entrance("Behind Ogre", player), - lambda state: state.has("Gun Pack", player)) - if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required: - set_rule(world.get_entrance("Tree", player), - lambda state: state.has("Time is Money Pack", player)) - set_rule(world.get_entrance("Cave Tree", player), - lambda state: state.has("Time is Money Pack", player)) - set_rule(world.get_location("Shepherd Sheep", player), - lambda state: state.has("Time is Money Pack", player)) - set_rule(world.get_location("North West Ceiling Sheep", player), - lambda state: state.has("Time is Money Pack", player)) - set_rule(world.get_location("North West Alcove Sheep", player), - lambda state: state.has("Time is Money Pack", player)) - set_rule(world.get_location("West Cave Sheep", player), - lambda state: state.has("Time is Money Pack", player)) +def set_basic_rules(World_Options, has_enough_coin, player, world): + if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die: + return + set_basic_entrance_rules(player, world) + set_basic_self_obtained_items_rules(World_Options, player, world) + set_basic_shuffled_items_rules(World_Options, player, world) + set_double_jump_glitchless_rules(World_Options, player, world) + set_easy_double_jump_glitch_rules(World_Options, player, world) + self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world) + set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world) + self_basic_win_condition(World_Options, player, world) - if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: - set_rule(world.get_entrance("Behind Ogre", player), - lambda state: state.has("Gun", player)) - set_rule(world.get_entrance("Tree", player), - lambda state: state.has("Sword", player) or state.has("Gun", player)) - set_rule(world.get_entrance("Cave Tree", player), - lambda state: state.has("Sword", player) or state.has("Gun", player)) - set_rule(world.get_entrance("True Double Jump", player), - lambda state: state.has("Double Jump Pack", player)) - set_rule(world.get_location("Shepherd Sheep", player), - lambda state: state.has("Sword", player) or state.has("Gun", player)) - set_rule(world.get_location("North West Ceiling Sheep", player), - lambda state: state.has("Sword", player) or state.has("Gun", player)) - set_rule(world.get_location("North West Alcove Sheep", player), - lambda state: state.has("Sword", player) or state.has("Gun", player)) - set_rule(world.get_location("West Cave Sheep", player), - lambda state: state.has("Sword", player) or state.has("Gun", player)) - if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required: - set_rule(world.get_location("Sword", player), - lambda state: state.has("Time is Money Pack", player)) +def set_basic_entrance_rules(player, world): + set_rule(world.get_entrance("Moving", player), + lambda state: state.has("Movement Pack", player)) + set_rule(world.get_entrance("Cloud", player), + lambda state: state.has("Psychological Warfare Pack", player)) + set_rule(world.get_entrance("Forest Entrance", player), + lambda state: state.has("Map Pack", player)) + set_rule(world.get_entrance("Forest True Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) - if World_Options[Options.FalseDoubleJump] == Options.FalseDoubleJump.option_none: - set_rule(world.get_entrance("Cloud Double Jump", player), - lambda state: state.has("Double Jump Pack", player)) - set_rule(world.get_entrance("Forest Double Jump", player), - lambda state: state.has("Double Jump Pack", player)) - if World_Options[Options.FalseDoubleJump] == Options.FalseDoubleJump.option_none or World_Options[ - Options.FalseDoubleJump] == Options.FalseDoubleJump.option_simple: - set_rule(world.get_entrance("Behind Tree Double Jump", player), - lambda state: state.has("Double Jump Pack", player)) - set_rule(world.get_entrance("Cave Roof", player), - lambda state: state.has("Double Jump Pack", player)) +def set_basic_self_obtained_items_rules(World_Options, player, world): + if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_disabled: + return + set_rule(world.get_entrance("Behind Ogre", player), + lambda state: state.has("Gun Pack", player)) - if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: - number_of_bundle = math.floor(825 / World_Options[Options.CoinSanityRange]) - for i in range(number_of_bundle): + if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required: + set_rule(world.get_entrance("Tree", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_entrance("Cave Tree", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_location("Shepherd Sheep", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_location("North West Ceiling Sheep", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_location("North West Alcove Sheep", player), + lambda state: state.has("Time is Money Pack", player)) + set_rule(world.get_location("West Cave Sheep", player), + lambda state: state.has("Time is Money Pack", player)) - item_coin = f"DLC Quest: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin" - set_rule(world.get_location(item_coin, player), - has_enough_coin(player, World_Options[Options.CoinSanityRange] * (i + 1))) - if 825 % World_Options[Options.CoinSanityRange] != 0: - set_rule(world.get_location("DLC Quest: 825 Coin", player), - has_enough_coin(player, 825)) - set_rule(world.get_location("Movement Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(4 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Animation Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Audio Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Pause Menu Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Time is Money Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(20 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Double Jump Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(100 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Pet Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Sexy Outfits Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Top Hat Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Map Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(140 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Gun Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(75 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("The Zombie Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Night Map Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(75 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Psychological Warfare Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(50 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Armor for your Horse Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(250 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Finish the Fight Pack", player), - lambda state: state.has("DLC Quest: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) +def set_basic_shuffled_items_rules(World_Options, player, world): + if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_shuffled: + return + set_rule(world.get_entrance("Behind Ogre", player), + lambda state: state.has("Gun", player)) + set_rule(world.get_entrance("Tree", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_entrance("Cave Tree", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_entrance("True Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) + set_rule(world.get_location("Shepherd Sheep", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_location("North West Ceiling Sheep", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_location("North West Alcove Sheep", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) + set_rule(world.get_location("West Cave Sheep", player), + lambda state: state.has("Sword", player) or state.has("Gun", player)) - if World_Options[Options.CoinSanity] == Options.CoinSanity.option_none: - set_rule(world.get_location("Movement Pack", player), - has_enough_coin(player, 4)) - set_rule(world.get_location("Animation Pack", player), - has_enough_coin(player, 5)) - set_rule(world.get_location("Audio Pack", player), - has_enough_coin(player, 5)) - set_rule(world.get_location("Pause Menu Pack", player), - has_enough_coin(player, 5)) - set_rule(world.get_location("Time is Money Pack", player), - has_enough_coin(player, 20)) - set_rule(world.get_location("Double Jump Pack", player), - has_enough_coin(player, 100)) - set_rule(world.get_location("Pet Pack", player), - has_enough_coin(player, 5)) - set_rule(world.get_location("Sexy Outfits Pack", player), - has_enough_coin(player, 5)) - set_rule(world.get_location("Top Hat Pack", player), - has_enough_coin(player, 5)) - set_rule(world.get_location("Map Pack", player), - has_enough_coin(player, 140)) - set_rule(world.get_location("Gun Pack", player), - has_enough_coin(player, 75)) - set_rule(world.get_location("The Zombie Pack", player), - has_enough_coin(player, 5)) - set_rule(world.get_location("Night Map Pack", player), - has_enough_coin(player, 75)) - set_rule(world.get_location("Psychological Warfare Pack", player), - has_enough_coin(player, 50)) - set_rule(world.get_location("Armor for your Horse Pack", player), - has_enough_coin(player, 250)) - set_rule(world.get_location("Finish the Fight Pack", player), - has_enough_coin(player, 5)) + if World_Options[Options.TimeIsMoney] == Options.TimeIsMoney.option_required: + set_rule(world.get_location("Sword", player), + lambda state: state.has("Time is Money Pack", player)) - if World_Options[Options.EndingChoice] == Options.EndingChoice.option_any: - set_rule(world.get_location("Winning Basic", player), - lambda state: state.has("Finish the Fight Pack", player)) - if World_Options[Options.EndingChoice] == Options.EndingChoice.option_true: - set_rule(world.get_location("Winning Basic", player), - lambda state: state.has("Armor for your Horse Pack", player) and state.has("Finish the Fight Pack", - player)) - if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die or World_Options[ - Options.Campaign] == Options.Campaign.option_both: - set_rule(world.get_entrance("Wall Jump Entrance", player), - lambda state: state.has("Wall Jump Pack", player)) - set_rule(world.get_entrance("Harmless Plants", player), - lambda state: state.has("Harmless Plants Pack", player)) - set_rule(world.get_entrance("Name Change Entrance", player), - lambda state: state.has("Name Change Pack", player)) - set_rule(world.get_entrance("Cut Content Entrance", player), - lambda state: state.has("Cut Content Pack", player)) - set_rule(world.get_entrance("Blizzard", player), - lambda state: state.has("Season Pass", player)) - set_rule(world.get_entrance("Boss Door", player), - lambda state: state.has("Big Sword Pack", player) and state.has("Really Big Sword Pack", - player) and state.has( - "Unfathomable Sword Pack", player)) - set_rule(world.get_location("I Get That Reference!", player), - lambda state: state.has("Death of Comedy Pack", player)) - set_rule(world.get_location("Story is Important", player), - lambda state: state.has("DLC NPC Pack", player)) - set_rule(world.get_entrance("Pickaxe Hard Cave", player), - lambda state: state.has("Pickaxe", player)) +def set_double_jump_glitchless_rules(World_Options, player, world): + if World_Options[Options.FalseDoubleJump] != Options.FalseDoubleJump.option_none: + return + set_rule(world.get_entrance("Cloud Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) + set_rule(world.get_entrance("Forest Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) - if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_disabled: - set_rule(world.get_entrance("Vines", player), - lambda state: state.has("Incredibly Important Pack", player)) - set_rule(world.get_entrance("Behind Rocks", player), - lambda state: state.can_reach("Cut Content", 'region', player)) - set_rule(world.get_entrance("Pickaxe Hard Cave", player), - lambda state: state.can_reach("Cut Content", 'region', player) and state.has("Name Change Pack", - player)) - if World_Options[Options.ItemShuffle] == Options.ItemShuffle.option_shuffled: - set_rule(world.get_entrance("Vines", player), - lambda state: state.has("Wooden Sword", player) or state.has("Pickaxe", player)) - set_rule(world.get_entrance("Behind Rocks", player), - lambda state: state.has("Pickaxe", player)) +def set_easy_double_jump_glitch_rules(World_Options, player, world): + if World_Options[Options.FalseDoubleJump] == Options.FalseDoubleJump.option_all: + return + set_rule(world.get_entrance("Behind Tree Double Jump", player), + lambda state: state.has("Double Jump Pack", player)) + set_rule(world.get_entrance("Cave Roof", player), + lambda state: state.has("Double Jump Pack", player)) - set_rule(world.get_location("Wooden Sword", player), - lambda state: state.has("Incredibly Important Pack", player)) - set_rule(world.get_location("Pickaxe", player), - lambda state: state.has("Humble Indie Bindle", player)) - set_rule(world.get_location("Humble Indie Bindle", player), - lambda state: state.has("Box of Various Supplies", player) and state.can_reach("Cut Content", - 'region', player)) - set_rule(world.get_location("Box of Various Supplies", player), - lambda state: state.can_reach("Cut Content", 'region', player)) - if World_Options[Options.CoinSanity] == Options.CoinSanity.option_coin: - number_of_bundle = math.floor(889 / World_Options[Options.CoinSanityRange]) - for i in range(number_of_bundle): +def self_basic_coinsanity_funded_purchase_rules(World_Options, has_enough_coin, player, world): + if World_Options[Options.CoinSanity] != Options.CoinSanity.option_coin: + return + number_of_bundle = math.floor(825 / World_Options[Options.CoinSanityRange]) + for i in range(number_of_bundle): - item_coin_freemium = "Live Freemium or Die: number Coin" - item_coin_loc_freemium = re.sub("number", str(World_Options[Options.CoinSanityRange] * (i + 1)), - item_coin_freemium) - set_rule(world.get_location(item_coin_loc_freemium, player), - has_enough_coin_freemium(player, World_Options[Options.CoinSanityRange] * (i + 1))) - if 889 % World_Options[Options.CoinSanityRange] != 0: - set_rule(world.get_location("Live Freemium or Die: 889 Coin", player), - has_enough_coin_freemium(player, 889)) + item_coin = f"DLC Quest: {World_Options[Options.CoinSanityRange] * (i + 1)} Coin" + set_rule(world.get_location(item_coin, player), + has_enough_coin(player, World_Options[Options.CoinSanityRange] * (i + 1))) + if 825 % World_Options[Options.CoinSanityRange] != 0: + set_rule(world.get_location("DLC Quest: 825 Coin", player), + has_enough_coin(player, 825)) - set_rule(world.get_entrance("Boss Door", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(889 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Movement Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(4 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Animation Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Audio Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Pause Menu Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Time is Money Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(20 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Double Jump Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(100 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Pet Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Sexy Outfits Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Top Hat Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Map Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(140 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Gun Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(75 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("The Zombie Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Night Map Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(75 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Psychological Warfare Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(50 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Armor for your Horse Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(250 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Finish the Fight Pack", player), + lambda state: state.has("DLC Quest: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Particles Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Day One Patch Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Checkpoint Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Incredibly Important Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(15 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Wall Jump Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(35 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Health Bar Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Parallax Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(5 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Harmless Plants Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(130 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Death of Comedy Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(15 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Canadian Dialog Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(10 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("DLC NPC Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(15 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Cut Content Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(40 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Name Change Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(150 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Season Pass", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(199 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("High Definition Next Gen Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(20 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Increased HP Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(10 / World_Options[Options.CoinSanityRange]))) - set_rule(world.get_location("Remove Ads Pack", player), - lambda state: state.has("Live Freemium or Die: Coin Bundle", player, - math.ceil(25 / World_Options[Options.CoinSanityRange]))) - if World_Options[Options.CoinSanity] == Options.CoinSanity.option_none: - set_rule(world.get_entrance("Boss Door", player), +def set_basic_self_funded_purchase_rules(World_Options, has_enough_coin, player, world): + if World_Options[Options.CoinSanity] != Options.CoinSanity.option_none: + return + set_rule(world.get_location("Movement Pack", player), + has_enough_coin(player, 4)) + set_rule(world.get_location("Animation Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Audio Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Pause Menu Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Time is Money Pack", player), + has_enough_coin(player, 20)) + set_rule(world.get_location("Double Jump Pack", player), + has_enough_coin(player, 100)) + set_rule(world.get_location("Pet Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Sexy Outfits Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Top Hat Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Map Pack", player), + has_enough_coin(player, 140)) + set_rule(world.get_location("Gun Pack", player), + has_enough_coin(player, 75)) + set_rule(world.get_location("The Zombie Pack", player), + has_enough_coin(player, 5)) + set_rule(world.get_location("Night Map Pack", player), + has_enough_coin(player, 75)) + set_rule(world.get_location("Psychological Warfare Pack", player), + has_enough_coin(player, 50)) + set_rule(world.get_location("Armor for your Horse Pack", player), + has_enough_coin(player, 250)) + set_rule(world.get_location("Finish the Fight Pack", player), + has_enough_coin(player, 5)) + + +def self_basic_win_condition(World_Options, player, world): + if World_Options[Options.EndingChoice] == Options.EndingChoice.option_any: + set_rule(world.get_location("Winning Basic", player), + lambda state: state.has("Finish the Fight Pack", player)) + if World_Options[Options.EndingChoice] == Options.EndingChoice.option_true: + set_rule(world.get_location("Winning Basic", player), + lambda state: state.has("Armor for your Horse Pack", player) and state.has("Finish the Fight Pack", + player)) + + +def set_lfod_rules(World_Options, has_enough_coin_freemium, player, world): + if World_Options[Options.Campaign] == Options.Campaign.option_basic: + return + set_lfod_entrance_rules(player, world) + set_boss_door_requirements_rules(player, world) + set_lfod_self_obtained_items_rules(World_Options, player, world) + set_lfod_shuffled_items_rules(World_Options, player, world) + self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world) + set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world) + + +def set_lfod_entrance_rules(player, world): + set_rule(world.get_entrance("Wall Jump Entrance", player), + lambda state: state.has("Wall Jump Pack", player)) + set_rule(world.get_entrance("Harmless Plants", player), + lambda state: state.has("Harmless Plants Pack", player)) + set_rule(world.get_entrance("Name Change Entrance", player), + lambda state: state.has("Name Change Pack", player)) + set_rule(world.get_entrance("Cut Content Entrance", player), + lambda state: state.has("Cut Content Pack", player)) + set_rule(world.get_entrance("Blizzard", player), + lambda state: state.has("Season Pass", player)) + set_rule(world.get_location("I Get That Reference!", player), + lambda state: state.has("Death of Comedy Pack", player)) + set_rule(world.get_location("Story is Important", player), + lambda state: state.has("DLC NPC Pack", player)) + set_rule(world.get_entrance("Pickaxe Hard Cave", player), + lambda state: state.has("Pickaxe", player)) + + +def set_boss_door_requirements_rules(player, world): + sword_1 = "Big Sword Pack" + sword_2 = "Really Big Sword Pack" + sword_3 = "Unfathomable Sword Pack" + + big_sword_location = world.get_location(sword_1, player) + really_big_sword_location = world.get_location(sword_2, player) + unfathomable_sword_location = world.get_location(sword_3, player) + + big_sword_valid_locations = [big_sword_location] + really_big_sword_valid_locations = [big_sword_location, really_big_sword_location] + unfathomable_sword_valid_locations = [big_sword_location, really_big_sword_location, unfathomable_sword_location] + + big_sword_during_boss_fight = item_name_in_locations(sword_1, player, big_sword_valid_locations) + really_big_sword_during_boss_fight = item_name_in_locations(sword_2, player, really_big_sword_valid_locations) + unfathomable_sword_during_boss_fight = item_name_in_locations(sword_3, player, unfathomable_sword_valid_locations) + + # For each sword, either already have received it, or be guaranteed to get it during the fight at a valid stage. + # Otherwise, a player can get soft locked. + has_3_swords = lambda state: ((state.has(sword_1, player) or big_sword_during_boss_fight) and + (state.has(sword_2, player) or really_big_sword_during_boss_fight) and + (state.has(sword_3, player) or unfathomable_sword_during_boss_fight)) + set_rule(world.get_entrance("Boss Door", player), has_3_swords) + + +def set_lfod_self_obtained_items_rules(World_Options, player, world): + if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_disabled: + return + set_rule(world.get_entrance("Vines", player), + lambda state: state.has("Incredibly Important Pack", player)) + set_rule(world.get_entrance("Behind Rocks", player), + lambda state: state.can_reach("Cut Content", 'region', player)) + set_rule(world.get_entrance("Pickaxe Hard Cave", player), + lambda state: state.can_reach("Cut Content", 'region', player) and + state.has("Name Change Pack", player)) + + +def set_lfod_shuffled_items_rules(World_Options, player, world): + if World_Options[Options.ItemShuffle] != Options.ItemShuffle.option_shuffled: + return + set_rule(world.get_entrance("Vines", player), + lambda state: state.has("Wooden Sword", player) or state.has("Pickaxe", player)) + set_rule(world.get_entrance("Behind Rocks", player), + lambda state: state.has("Pickaxe", player)) + + set_rule(world.get_location("Wooden Sword", player), + lambda state: state.has("Incredibly Important Pack", player)) + set_rule(world.get_location("Pickaxe", player), + lambda state: state.has("Humble Indie Bindle", player)) + set_rule(world.get_location("Humble Indie Bindle", player), + lambda state: state.has("Box of Various Supplies", player) and + state.can_reach("Cut Content", 'region', player)) + set_rule(world.get_location("Box of Various Supplies", player), + lambda state: state.can_reach("Cut Content", 'region', player)) + + +def self_lfod_coinsanity_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world): + if World_Options[Options.CoinSanity] != Options.CoinSanity.option_coin: + return + number_of_bundle = math.floor(889 / World_Options[Options.CoinSanityRange]) + for i in range(number_of_bundle): + + item_coin_freemium = "Live Freemium or Die: number Coin" + item_coin_loc_freemium = re.sub("number", str(World_Options[Options.CoinSanityRange] * (i + 1)), + item_coin_freemium) + set_rule(world.get_location(item_coin_loc_freemium, player), + has_enough_coin_freemium(player, World_Options[Options.CoinSanityRange] * (i + 1))) + if 889 % World_Options[Options.CoinSanityRange] != 0: + set_rule(world.get_location("Live Freemium or Die: 889 Coin", player), has_enough_coin_freemium(player, 889)) - set_rule(world.get_location("Particles Pack", player), - has_enough_coin_freemium(player, 5)) - set_rule(world.get_location("Day One Patch Pack", player), - has_enough_coin_freemium(player, 5)) - set_rule(world.get_location("Checkpoint Pack", player), - has_enough_coin_freemium(player, 5)) - set_rule(world.get_location("Incredibly Important Pack", player), - has_enough_coin_freemium(player, 15)) - set_rule(world.get_location("Wall Jump Pack", player), - has_enough_coin_freemium(player, 35)) - set_rule(world.get_location("Health Bar Pack", player), - has_enough_coin_freemium(player, 5)) - set_rule(world.get_location("Parallax Pack", player), - has_enough_coin_freemium(player, 5)) - set_rule(world.get_location("Harmless Plants Pack", player), - has_enough_coin_freemium(player, 130)) - set_rule(world.get_location("Death of Comedy Pack", player), - has_enough_coin_freemium(player, 15)) - set_rule(world.get_location("Canadian Dialog Pack", player), - has_enough_coin_freemium(player, 10)) - set_rule(world.get_location("DLC NPC Pack", player), - has_enough_coin_freemium(player, 15)) - set_rule(world.get_location("Cut Content Pack", player), - has_enough_coin_freemium(player, 40)) - set_rule(world.get_location("Name Change Pack", player), - has_enough_coin_freemium(player, 150)) - set_rule(world.get_location("Season Pass", player), - has_enough_coin_freemium(player, 199)) - set_rule(world.get_location("High Definition Next Gen Pack", player), - has_enough_coin_freemium(player, 20)) - set_rule(world.get_location("Increased HP Pack", player), - has_enough_coin_freemium(player, 10)) - set_rule(world.get_location("Remove Ads Pack", player), - has_enough_coin_freemium(player, 25)) + add_rule(world.get_entrance("Boss Door", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(889 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Particles Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Day One Patch Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Checkpoint Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Incredibly Important Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(15 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Wall Jump Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(35 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Health Bar Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Parallax Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(5 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Harmless Plants Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(130 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Death of Comedy Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(15 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Canadian Dialog Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(10 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("DLC NPC Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(15 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Cut Content Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(40 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Name Change Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(150 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Season Pass", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(199 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("High Definition Next Gen Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(20 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Increased HP Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(10 / World_Options[Options.CoinSanityRange]))) + set_rule(world.get_location("Remove Ads Pack", player), + lambda state: state.has("Live Freemium or Die: Coin Bundle", player, + math.ceil(25 / World_Options[Options.CoinSanityRange]))) + + +def set_lfod_self_funded_purchase_rules(World_Options, has_enough_coin_freemium, player, world): + if World_Options[Options.CoinSanity] != Options.CoinSanity.option_none: + return + add_rule(world.get_entrance("Boss Door", player), + has_enough_coin_freemium(player, 889)) + + set_rule(world.get_location("Particles Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Day One Patch Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Checkpoint Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Incredibly Important Pack", player), + has_enough_coin_freemium(player, 15)) + set_rule(world.get_location("Wall Jump Pack", player), + has_enough_coin_freemium(player, 35)) + set_rule(world.get_location("Health Bar Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Parallax Pack", player), + has_enough_coin_freemium(player, 5)) + set_rule(world.get_location("Harmless Plants Pack", player), + has_enough_coin_freemium(player, 130)) + set_rule(world.get_location("Death of Comedy Pack", player), + has_enough_coin_freemium(player, 15)) + set_rule(world.get_location("Canadian Dialog Pack", player), + has_enough_coin_freemium(player, 10)) + set_rule(world.get_location("DLC NPC Pack", player), + has_enough_coin_freemium(player, 15)) + set_rule(world.get_location("Cut Content Pack", player), + has_enough_coin_freemium(player, 40)) + set_rule(world.get_location("Name Change Pack", player), + has_enough_coin_freemium(player, 150)) + set_rule(world.get_location("Season Pass", player), + has_enough_coin_freemium(player, 199)) + set_rule(world.get_location("High Definition Next Gen Pack", player), + has_enough_coin_freemium(player, 20)) + set_rule(world.get_location("Increased HP Pack", player), + has_enough_coin_freemium(player, 10)) + set_rule(world.get_location("Remove Ads Pack", player), + has_enough_coin_freemium(player, 25)) + + +def set_completion_condition(World_Options, player, world): if World_Options[Options.Campaign] == Options.Campaign.option_basic: world.completion_condition[player] = lambda state: state.has("Victory Basic", player) - if World_Options[Options.Campaign] == Options.Campaign.option_live_freemium_or_die: world.completion_condition[player] = lambda state: state.has("Victory Freemium", player) - if World_Options[Options.Campaign] == Options.Campaign.option_both: world.completion_condition[player] = lambda state: state.has("Victory Basic", player) and state.has( "Victory Freemium", player) From 22ed7ff9c3efca8c8cbc9040d9ecf328cce300b7 Mon Sep 17 00:00:00 2001 From: Doug Hoskisson Date: Tue, 25 Apr 2023 22:24:47 -0700 Subject: [PATCH 087/489] Zillion: fix empty 1st Sphere (#1770) There was a low probability that the Zillion 1st sphere could be empty. caused this test failure: https://github.com/ArchipelagoMW/Archipelago/actions/runs/4791795268/jobs/8522615992 --- worlds/zillion/__init__.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/worlds/zillion/__init__.py b/worlds/zillion/__init__.py index 241cb452a9..e52e730072 100644 --- a/worlds/zillion/__init__.py +++ b/worlds/zillion/__init__.py @@ -147,6 +147,16 @@ class ZillionWorld(World): self.my_locations = [] self.zz_system.randomizer.place_canister_gun_reqs() + # low probability that place_canister_gun_reqs() results in empty 1st sphere + # testing code to force low probability event: + # for zz_room_name in ["r01c2", "r02c0", "r02c7", "r03c5"]: + # for zz_loc in self.zz_system.randomizer.regions[zz_room_name].locations: + # zz_loc.req.gun = 2 + if len(self.zz_system.randomizer.get_locations(Req(gun=1, jump=1))) == 0: + self.logger.info("Zillion avoided rare empty 1st sphere.") + for zz_loc in self.zz_system.randomizer.regions["r03c5"].locations: + zz_loc.req.gun = 1 + assert len(self.zz_system.randomizer.get_locations(Req(gun=1, jump=1))) != 0 start = self.zz_system.randomizer.regions['start'] From bb56f7b400a2d734b3e75a9838212d0a8effadf4 Mon Sep 17 00:00:00 2001 From: TheBigSalarius <60804015+TheBigSalarius@users.noreply.github.com> Date: Wed, 26 Apr 2023 04:47:25 -0400 Subject: [PATCH 088/489] FF1: Added URange fix for Bizhawk 2.9 support URange wasn't moved to common.lua (and no longer exists in connector_ff1.lua) when the lua files were changed for Bizhawk 2.9 socket change. --- data/lua/common.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/data/lua/common.lua b/data/lua/common.lua index 4df2ab8470..93caf9bdb4 100644 --- a/data/lua/common.lua +++ b/data/lua/common.lua @@ -31,6 +31,7 @@ local untestedBizhawkMessage = "Warning: this version of bizhawk is newer than w u8 = memory.read_u8 wU8 = memory.write_u8 u16 = memory.read_u16_le +uRange = memory.readbyterange function getMaxMessageLength() local denominator = 12 @@ -99,4 +100,4 @@ function checkBizhawkVersion() print(untestedBizhawkMessage) end return true -end \ No newline at end of file +end From 4c3eaf2996aea6120676daaf9a2c2aa5ad7cad8b Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 26 Apr 2023 10:48:08 +0200 Subject: [PATCH 089/489] LttP: fix that collect can bypass requirements for ganon ped goal (#1771) LttP: more pep8 --- Utils.py | 2 +- worlds/alttp/Bosses.py | 1 + worlds/alttp/Client.py | 57 ++++++++++++++------------- worlds/alttp/Dungeons.py | 10 +++-- worlds/alttp/EntranceShuffle.py | 6 ++- worlds/alttp/InvertedRegions.py | 5 ++- worlds/alttp/ItemPool.py | 15 +++---- worlds/alttp/Items.py | 1 + worlds/alttp/OverworldGlitchRules.py | 6 ++- worlds/alttp/Rom.py | 22 +++++------ worlds/alttp/Shops.py | 10 +++-- worlds/alttp/StateHelpers.py | 25 ++++++++++++ worlds/alttp/SubClasses.py | 1 + worlds/alttp/UnderworldGlitchRules.py | 7 ++-- worlds/alttp/__init__.py | 5 +-- 15 files changed, 107 insertions(+), 66 deletions(-) diff --git a/Utils.py b/Utils.py index 60b3904ff6..8a9478e49c 100644 --- a/Utils.py +++ b/Utils.py @@ -39,7 +39,7 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.4.0" +__version__ = "0.4.1" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") diff --git a/worlds/alttp/Bosses.py b/worlds/alttp/Bosses.py index 51615ddc45..8c6dcabc87 100644 --- a/worlds/alttp/Bosses.py +++ b/worlds/alttp/Bosses.py @@ -6,6 +6,7 @@ from Fill import FillError from .Options import LTTPBosses as Bosses from .StateHelpers import can_shoot_arrows, can_extend_magic, can_get_good_bee, has_sword, has_beam_sword, has_melee_weapon, has_fire_source + def BossFactory(boss: str, player: int) -> Optional[Boss]: if boss in boss_table: enemizer_name, defeat_rule = boss_table[boss] diff --git a/worlds/alttp/Client.py b/worlds/alttp/Client.py index 71a0cf3600..f81222e268 100644 --- a/worlds/alttp/Client.py +++ b/worlds/alttp/Client.py @@ -10,7 +10,7 @@ import Utils from NetUtils import ClientStatus, color from worlds.AutoSNIClient import SNIClient -from worlds.alttp import Shops, Regions +from . import Shops, Regions from .Rom import ROM_PLAYER_LIMIT snes_logger = logging.getLogger("SNES") @@ -270,17 +270,20 @@ location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10), 'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40), 'Ganons Tower - Validation Chest': (0x4d, 0x10)} -boss_locations = {Regions.lookup_name_to_id[name] for name in {'Eastern Palace - Boss', - 'Desert Palace - Boss', - 'Tower of Hera - Boss', - 'Palace of Darkness - Boss', - 'Swamp Palace - Boss', - 'Skull Woods - Boss', - "Thieves' Town - Boss", - 'Ice Palace - Boss', - 'Misery Mire - Boss', - 'Turtle Rock - Boss', - 'Sahasrahla'}} +collect_ignore_locations = {Regions.lookup_name_to_id[name] for name in { + 'Eastern Palace - Boss', + 'Desert Palace - Boss', + 'Tower of Hera - Boss', + 'Palace of Darkness - Boss', + 'Swamp Palace - Boss', + 'Skull Woods - Boss', + "Thieves' Town - Boss", + 'Ice Palace - Boss', + 'Misery Mire - Boss', + 'Turtle Rock - Boss', + 'Sahasrahla', + 'Master Sword Pedestal', # can circumvent ganon pedestal's goal's pendant collection +}} location_table_uw_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_uw.items()} @@ -322,8 +325,15 @@ location_table_misc = {'Bottle Merchant': (0x3c9, 0x2), location_table_misc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_misc.items()} +def should_collect(ctx, location_id: int) -> bool: + return ctx.allow_collect and location_id not in collect_ignore_locations and location_id in ctx.checked_locations \ + and location_id not in ctx.locations_checked and location_id in ctx.locations_info \ + and ctx.locations_info[location_id].player != ctx.slot + + async def track_locations(ctx, roomid, roomdata) -> bool: from SNIClient import snes_read, snes_buffered_write, snes_flush_writes + location_id: int new_locations = [] def new_check(location_id): @@ -340,11 +350,10 @@ async def track_locations(ctx, roomid, roomdata) -> bool: shop_data_changed = False shop_data = list(shop_data) for cnt, b in enumerate(shop_data): - location = Shops.SHOP_ID_START + cnt - if int(b) and location not in ctx.locations_checked: - new_check(location) - if ctx.allow_collect and location in ctx.checked_locations and location not in ctx.locations_checked \ - and location in ctx.locations_info and ctx.locations_info[location].player != ctx.slot: + location_id = Shops.SHOP_ID_START + cnt + if int(b) and location_id not in ctx.locations_checked: + new_check(location_id) + if should_collect(ctx, location_id): if not int(b): shop_data[cnt] += 1 shop_data_changed = True @@ -371,9 +380,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool: uw_unchecked[location_id] = (roomid, mask) uw_begin = min(uw_begin, roomid) uw_end = max(uw_end, roomid + 1) - if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \ - and location_id not in ctx.locations_checked and location_id in ctx.locations_info \ - and ctx.locations_info[location_id].player != ctx.slot: + if should_collect(ctx, location_id): uw_begin = min(uw_begin, roomid) uw_end = max(uw_end, roomid + 1) uw_checked[location_id] = (roomid, mask) @@ -404,8 +411,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool: ow_unchecked[location_id] = screenid ow_begin = min(ow_begin, screenid) ow_end = max(ow_end, screenid + 1) - if ctx.allow_collect and location_id in ctx.checked_locations and location_id in ctx.locations_info \ - and ctx.locations_info[location_id].player != ctx.slot: + if should_collect(ctx, location_id): ow_checked[location_id] = screenid if ow_begin < ow_end: @@ -428,9 +434,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool: for location_id, mask in location_table_npc_id.items(): if npc_value & mask != 0 and location_id not in ctx.locations_checked: new_check(location_id) - if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \ - and location_id not in ctx.locations_checked and location_id in ctx.locations_info \ - and ctx.locations_info[location_id].player != ctx.slot: + if should_collect(ctx, location_id): npc_value |= mask npc_value_changed = True if npc_value_changed: @@ -446,8 +450,7 @@ async def track_locations(ctx, roomid, roomdata) -> bool: assert (0x3c6 <= offset <= 0x3c9) if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked: new_check(location_id) - if ctx.allow_collect and location_id in ctx.checked_locations and location_id not in ctx.locations_checked \ - and location_id in ctx.locations_info and ctx.locations_info[location_id].player != ctx.slot: + if should_collect(ctx, location_id): misc_data_changed = True misc_data[offset - 0x3c6] |= mask if misc_data_changed: diff --git a/worlds/alttp/Dungeons.py b/worlds/alttp/Dungeons.py index ec6862b9d0..a6a3bec9bf 100644 --- a/worlds/alttp/Dungeons.py +++ b/worlds/alttp/Dungeons.py @@ -1,15 +1,17 @@ import typing from BaseClasses import Dungeon -from worlds.alttp.Bosses import BossFactory from Fill import fill_restrictive -from worlds.alttp.Items import ItemFactory -from worlds.alttp.Regions import lookup_boss_drops -from worlds.alttp.Options import smallkey_shuffle + +from .Bosses import BossFactory +from .Items import ItemFactory +from .Regions import lookup_boss_drops +from .Options import smallkey_shuffle if typing.TYPE_CHECKING: from .SubClasses import ALttPLocation + def create_dungeons(world, player): def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items): dungeon = Dungeon(name, dungeon_regions, big_key, diff --git a/worlds/alttp/EntranceShuffle.py b/worlds/alttp/EntranceShuffle.py index e10f4d5445..b7fe688431 100644 --- a/worlds/alttp/EntranceShuffle.py +++ b/worlds/alttp/EntranceShuffle.py @@ -1,7 +1,9 @@ # ToDo: With shuffle_ganon option, prevent gtower from linking to an exit only location through a 2 entrance cave. from collections import defaultdict -from worlds.alttp.OverworldGlitchRules import overworld_glitch_connections -from worlds.alttp.UnderworldGlitchRules import underworld_glitch_connections + +from .OverworldGlitchRules import overworld_glitch_connections +from .UnderworldGlitchRules import underworld_glitch_connections + def link_entrances(world, player): connect_two_way(world, 'Links House', 'Links House Exit', player) # unshuffled. For now diff --git a/worlds/alttp/InvertedRegions.py b/worlds/alttp/InvertedRegions.py index 153dda4fc3..acec73bf33 100644 --- a/worlds/alttp/InvertedRegions.py +++ b/worlds/alttp/InvertedRegions.py @@ -1,6 +1,7 @@ import collections -from worlds.alttp.Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region -from worlds.alttp.SubClasses import LTTPRegionType + +from .Regions import create_lw_region, create_dw_region, create_cave_region, create_dungeon_region +from .SubClasses import LTTPRegionType def create_inverted_regions(world, player): diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index 7fd93ab93e..5761e5f099 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -2,14 +2,15 @@ from collections import namedtuple import logging from BaseClasses import ItemClassification -from worlds.alttp.SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType -from worlds.alttp.Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops, create_dynamic_shop_locations -from worlds.alttp.Bosses import place_bosses -from worlds.alttp.Dungeons import get_dungeon_item_pool_player -from worlds.alttp.EntranceShuffle import connect_entrance from Fill import FillError -from worlds.alttp.Items import ItemFactory, GetBeemizerItem -from worlds.alttp.Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle, LTTPBosses + +from .SubClasses import ALttPLocation, LTTPRegion, LTTPRegionType +from .Shops import TakeAny, total_shop_slots, set_up_shops, shuffle_shops, create_dynamic_shop_locations +from .Bosses import place_bosses +from .Dungeons import get_dungeon_item_pool_player +from .EntranceShuffle import connect_entrance +from .Items import ItemFactory, GetBeemizerItem +from .Options import smallkey_shuffle, compass_shuffle, bigkey_shuffle, map_shuffle, LTTPBosses from .StateHelpers import has_triforce_pieces, has_melee_weapon # This file sets the item pools for various modes. Timed modes and triforce hunt are enforced first, and then extra items are specified per mode to fill in the remaining space. diff --git a/worlds/alttp/Items.py b/worlds/alttp/Items.py index caa916ca1d..40634de8da 100644 --- a/worlds/alttp/Items.py +++ b/worlds/alttp/Items.py @@ -2,6 +2,7 @@ import typing from BaseClasses import ItemClassification as IC + def GetBeemizerItem(world, player: int, item): item_name = item if isinstance(item, str) else item.name diff --git a/worlds/alttp/OverworldGlitchRules.py b/worlds/alttp/OverworldGlitchRules.py index f6c3ec8d14..146fc2f0ca 100644 --- a/worlds/alttp/OverworldGlitchRules.py +++ b/worlds/alttp/OverworldGlitchRules.py @@ -6,18 +6,21 @@ from BaseClasses import Entrance from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw + def get_sword_required_superbunny_mirror_regions(): """ Cave regions that superbunny can get through - but only with a sword. """ yield 'Spiral Cave (Top)' + def get_boots_required_superbunny_mirror_regions(): """ Cave regions that superbunny can get through - but only with boots. """ yield 'Two Brothers House' + def get_boots_required_superbunny_mirror_locations(): """ Cave locations that superbunny can access - but only with boots. @@ -207,7 +210,6 @@ def get_mirror_offset_spots_lw(player): yield ('Death Mountain Offset Mirror (Houlihan Exit)', 'Death Mountain', 'Hyrule Castle Ledge', lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player) and state.has('Moon Pearl', player)) - def get_invalid_bunny_revival_dungeons(): """ Dungeon regions that can't be bunny revived from without superbunny state. @@ -300,6 +302,7 @@ def create_no_logic_connections(player, world, connections): parent.exits.append(connection) connection.connect(target) + def create_owg_connections(player, world, connections): for entrance, parent_region, target_region, *rule_override in connections: parent = world.get_region(parent_region, player) @@ -308,6 +311,7 @@ def create_owg_connections(player, world, connections): parent.exits.append(connection) connection.connect(target) + def set_owg_connection_rules(player, world, connections, default_rule): for entrance, _, _, *rule_override in connections: connection = world.get_entrance(entrance, player) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index e1cbb5c0cd..a9dba3277a 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -21,22 +21,22 @@ import bsdiff4 from typing import Optional, List from BaseClasses import CollectionState, Region, Location, MultiWorld -from worlds.alttp.Shops import ShopType, ShopPriceType -from worlds.alttp.Dungeons import dungeon_music_addresses -from worlds.alttp.Regions import location_table, old_location_address_to_new_location_address -from worlds.alttp.Text import MultiByteTextMapper, text_addresses, Credits, TextTable -from worlds.alttp.Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, \ +from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom + +from .Shops import ShopType, ShopPriceType +from .Dungeons import dungeon_music_addresses +from .Regions import old_location_address_to_new_location_address +from .Text import MultiByteTextMapper, text_addresses, Credits, TextTable +from .Text import Uncle_texts, Ganon1_texts, TavernMan_texts, Sahasrahla2_texts, Triforce_texts, \ Blind_texts, \ BombShop2_texts, junk_texts - -from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, \ +from .Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts, Blacksmiths_texts, \ DeathMountain_texts, \ LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \ SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names -from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom -from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items -from worlds.alttp.EntranceShuffle import door_addresses -from worlds.alttp.Options import smallkey_shuffle +from .Items import ItemFactory, item_table, item_name_groups, progression_items +from .EntranceShuffle import door_addresses +from .Options import smallkey_shuffle try: from maseya import z3pr diff --git a/worlds/alttp/Shops.py b/worlds/alttp/Shops.py index 8e183c87a3..b067634da0 100644 --- a/worlds/alttp/Shops.py +++ b/worlds/alttp/Shops.py @@ -3,12 +3,14 @@ from enum import unique, IntEnum from typing import List, Optional, Set, NamedTuple, Dict import logging -from worlds.alttp.SubClasses import ALttPLocation -from worlds.alttp.EntranceShuffle import door_addresses -from worlds.alttp.Items import item_name_groups, item_table, ItemFactory, trap_replaceable, GetBeemizerItem -from worlds.alttp.Options import smallkey_shuffle from Utils import int16_as_bytes +from .SubClasses import ALttPLocation +from .EntranceShuffle import door_addresses +from .Items import item_name_groups, item_table, ItemFactory, trap_replaceable, GetBeemizerItem +from .Options import smallkey_shuffle + + logger = logging.getLogger("Shops") diff --git a/worlds/alttp/StateHelpers.py b/worlds/alttp/StateHelpers.py index 33cea8fbfb..95e31e5ba3 100644 --- a/worlds/alttp/StateHelpers.py +++ b/worlds/alttp/StateHelpers.py @@ -1,50 +1,62 @@ from .SubClasses import LTTPRegion from BaseClasses import CollectionState + def is_not_bunny(state: CollectionState, region: LTTPRegion, player: int) -> bool: if state.has('Moon Pearl', player): return True return region.is_light_world if state.multiworld.mode[player] != 'inverted' else region.is_dark_world + def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bool: return is_not_bunny(state, region, player) and state.has('Pegasus Boots', player) + def can_buy_unlimited(state: CollectionState, item: str, player: int) -> bool: return any(shop.region.player == player and shop.has_unlimited(item) and shop.region.can_reach(state) for shop in state.multiworld.shops) + def can_buy(state: CollectionState, item: str, player: int) -> bool: return any(shop.region.player == player and shop.has(item) and shop.region.can_reach(state) for shop in state.multiworld.shops) + def can_shoot_arrows(state: CollectionState, player: int) -> bool: if state.multiworld.retro_bow[player]: return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_buy(state, 'Single Arrow', player) return state.has('Bow', player) or state.has('Silver Bow', player) + def has_triforce_pieces(state: CollectionState, player: int) -> bool: count = state.multiworld.treasure_hunt_count[player] return state.item_count('Triforce Piece', player) + state.item_count('Power Star', player) >= count + def has_crystals(state: CollectionState, count: int, player: int) -> bool: found = state.count_group("Crystals", player) return found >= count + def can_lift_rocks(state: CollectionState, player: int): return state.has('Power Glove', player) or state.has('Titans Mitts', player) + def can_lift_heavy_rocks(state: CollectionState, player: int) -> bool: return state.has('Titans Mitts', player) + def bottle_count(state: CollectionState, player: int) -> int: return min(state.multiworld.difficulty_requirements[player].progressive_bottle_limit, state.count_group("Bottles", player)) + def has_hearts(state: CollectionState, player: int, count: int) -> int: # Warning: This only considers items that are marked as advancement items return heart_count(state, player) >= count + def heart_count(state: CollectionState, player: int) -> int: # Warning: This only considers items that are marked as advancement items diff = state.multiworld.difficulty_requirements[player] @@ -53,6 +65,7 @@ def heart_count(state: CollectionState, player: int) -> int: + min(state.item_count('Piece of Heart', player), diff.heart_piece_limit) // 4 \ + 3 # starting hearts + def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16, fullrefill: bool = False): # This reflects the total magic Link has, not the total extra he has. basemagic = 8 @@ -69,6 +82,7 @@ def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16, basemagic = basemagic + basemagic * bottle_count(state, player) return basemagic >= smallmagic + def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) -> bool: return (has_melee_weapon(state, player) or state.has('Cane of Somaria', player) @@ -77,6 +91,7 @@ def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) or state.has('Fire Rod', player) or (state.has('Bombs (10)', player) and enemies < 6)) + def can_get_good_bee(state: CollectionState, player: int) -> bool: cave = state.multiworld.get_region('Good Bee Cave', player) return ( @@ -87,49 +102,59 @@ def can_get_good_bee(state: CollectionState, player: int) -> bool: is_not_bunny(state, cave, player) ) + def can_retrieve_tablet(state: CollectionState, player: int) -> bool: return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or (state.multiworld.swordless[player] and state.has("Hammer", player))) + def has_sword(state: CollectionState, player: int) -> bool: return state.has('Fighter Sword', player) \ or state.has('Master Sword', player) \ or state.has('Tempered Sword', player) \ or state.has('Golden Sword', player) + def has_beam_sword(state: CollectionState, player: int) -> bool: return state.has('Master Sword', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', player) + def has_melee_weapon(state: CollectionState, player: int) -> bool: return has_sword(state, player) or state.has('Hammer', player) + def has_fire_source(state: CollectionState, player: int) -> bool: return state.has('Fire Rod', player) or state.has('Lamp', player) + def can_melt_things(state: CollectionState, player: int) -> bool: return state.has('Fire Rod', player) or \ (state.has('Bombos', player) and (state.multiworld.swordless[player] or has_sword(state, player))) + def has_misery_mire_medallion(state: CollectionState, player: int) -> bool: return state.has(state.multiworld.required_medallions[player][0], player) def has_turtle_rock_medallion(state: CollectionState, player: int) -> bool: return state.has(state.multiworld.required_medallions[player][1], player) + def can_boots_clip_lw(state: CollectionState, player: int) -> bool: if state.multiworld.mode[player] == 'inverted': return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) return state.has('Pegasus Boots', player) + def can_boots_clip_dw(state: CollectionState, player: int) -> bool: if state.multiworld.mode[player] != 'inverted': return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player) return state.has('Pegasus Boots', player) + def can_get_glitched_speed_dw(state: CollectionState, player: int) -> bool: rules = [state.has('Pegasus Boots', player), any([state.has('Hookshot', player), has_sword(state, player)])] if state.multiworld.mode[player] != 'inverted': diff --git a/worlds/alttp/SubClasses.py b/worlds/alttp/SubClasses.py index 5fc2aa0ba3..e791b73e75 100644 --- a/worlds/alttp/SubClasses.py +++ b/worlds/alttp/SubClasses.py @@ -4,6 +4,7 @@ from enum import IntEnum from BaseClasses import Location, Item, ItemClassification, Region, MultiWorld + class ALttPLocation(Location): game: str = "A Link to the Past" crystal: bool diff --git a/worlds/alttp/UnderworldGlitchRules.py b/worlds/alttp/UnderworldGlitchRules.py index f3d78e365c..11a95bf7cd 100644 --- a/worlds/alttp/UnderworldGlitchRules.py +++ b/worlds/alttp/UnderworldGlitchRules.py @@ -1,12 +1,11 @@ - from BaseClasses import Entrance -from .SubClasses import LTTPRegion from worlds.generic.Rules import set_rule, add_rule from .StateHelpers import can_bomb_clip, has_sword, has_beam_sword, has_fire_source, can_melt_things, has_misery_mire_medallion + # We actually need the logic to properly "mark" these regions as Light or Dark world. -# Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules. -def underworld_glitch_connections(world, player): +# Therefore we need to make these connections during the normal link_entrances stage, rather than during set_rules. +def underworld_glitch_connections(world, player): specrock = world.get_region('Spectacle Rock Cave (Bottom)', player) mire = world.get_region('Misery Mire (West)', player) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index d1a44df12f..5ea936cc9d 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -3,7 +3,6 @@ import os import random import threading import typing -from collections import OrderedDict import Utils from BaseClasses import Item, CollectionState, Tutorial, MultiWorld @@ -122,7 +121,7 @@ class ALTTPWorld(World): dungeons on your quest to rescue the descendents of the seven wise men and defeat the evil Ganon! """ - game: str = "A Link to the Past" + game = "A Link to the Past" option_definitions = alttp_options topology_present = True item_name_groups = item_name_groups @@ -202,7 +201,7 @@ class ALTTPWorld(World): location_name_to_id = lookup_name_to_id data_version = 8 - required_client_version = (0, 3, 2) + required_client_version = (0, 4, 1) web = ALTTPWeb() pedestal_credit_texts: typing.Dict[int, str] = \ From 6c459066a761db1fb69b0759bd03b51af26afdcf Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 25 Apr 2023 13:26:52 +0200 Subject: [PATCH 090/489] Core: add generator_version to network protocol --- CommonClient.py | 15 +++++++++++---- MultiServer.py | 18 +++++++++--------- Utils.py | 3 +++ docs/network protocol.md | 3 ++- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 2e10f6d5c0..87fa59cbf2 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -157,6 +157,7 @@ class CommonContext: disconnected_intentionally: bool = False server: typing.Optional[Endpoint] = None server_version: Version = Version(0, 0, 0) + generator_version: Version = Version(0, 0, 0) current_energy_link_value: typing.Optional[int] = None # to display in UI, gets set by server last_death_link: float = time.time() # last send/received death link on AP layer @@ -260,6 +261,7 @@ class CommonContext: self.items_received = [] self.locations_info = {} self.server_version = Version(0, 0, 0) + self.generator_version = Version(0, 0, 0) self.server = None self.server_task = None self.hint_cost = None @@ -646,11 +648,16 @@ async def process_server_cmd(ctx: CommonContext, args: dict): logger.info('Room Information:') logger.info('--------------------------------') version = args["version"] - ctx.server_version = tuple(version) - version = ".".join(str(item) for item in version) + ctx.server_version = Version(*version) - logger.info(f'Server protocol version: {version}') - logger.info("Server protocol tags: " + ", ".join(args["tags"])) + if "generator_version" in args: + ctx.generator_version = Version(*args["generator_version"]) + logger.info(f'Server protocol version: {ctx.server_version.as_simple_string()}, ' + f'generator version: {ctx.generator_version.as_simple_string()}, ' + f'tags: {", ".join(args["tags"])}') + else: + logger.info(f'Server protocol version: {ctx.server_version.as_simple_string()}, ' + f'tags: {", ".join(args["tags"])}') if args['password']: logger.info('Password required') ctx.update_permissions(args.get("permissions", {})) diff --git a/MultiServer.py b/MultiServer.py index 3d5053bbe5..0537fd9b95 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -3,9 +3,6 @@ from __future__ import annotations import argparse import asyncio import copy -import functools -import logging -import zlib import collections import datetime import functools @@ -162,7 +159,7 @@ class Context: read_data: typing.Dict[str, object] stored_data_notification_clients: typing.Dict[str, typing.Set[Client]] slot_info: typing.Dict[int, NetworkSlot] - + generator_version = Version(0, 0, 0) checksums: typing.Dict[str, str] item_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown item (ID:{code})') item_name_groups: typing.Dict[str, typing.Dict[str, typing.Set[str]]] @@ -226,7 +223,7 @@ class Context: self.save_dirty = False self.tags = ['AP'] self.games: typing.Dict[int, str] = {} - self.minimum_client_versions: typing.Dict[int, Utils.Version] = {} + self.minimum_client_versions: typing.Dict[int, Version] = {} self.seed_name = "" self.groups = {} self.group_collected: typing.Dict[int, typing.Set[int]] = {} @@ -384,15 +381,17 @@ class Context: def _load(self, decoded_obj: dict, game_data_packages: typing.Dict[str, typing.Any], use_embedded_server_options: bool): + self.read_data = {} mdata_ver = decoded_obj["minimum_versions"]["server"] - if mdata_ver > Utils.version_tuple: + if mdata_ver > version_tuple: raise RuntimeError(f"Supplied Multidata (.archipelago) requires a server of at least version {mdata_ver}," - f"however this server is of version {Utils.version_tuple}") + f"however this server is of version {version_tuple}") + self.generator_version = Version(*decoded_obj["version"]) clients_ver = decoded_obj["minimum_versions"].get("clients", {}) self.minimum_client_versions = {} for player, version in clients_ver.items(): - self.minimum_client_versions[player] = max(Utils.Version(*version), min_client_version) + self.minimum_client_versions[player] = max(Version(*version), min_client_version) self.slot_info = decoded_obj["slot_info"] self.games = {slot: slot_info.game for slot, slot_info in self.slot_info.items()} @@ -758,7 +757,8 @@ async def on_client_connected(ctx: Context, client: Client): # tags are for additional features in the communication. # Name them by feature or fork, as you feel is appropriate. 'tags': ctx.tags, - 'version': Utils.version_tuple, + 'version': version_tuple, + 'generator_version': ctx.generator_version, 'permissions': get_permissions(ctx), 'hint_cost': ctx.hint_cost, 'location_check_points': ctx.location_check_points, diff --git a/Utils.py b/Utils.py index 8a9478e49c..46312dc3ff 100644 --- a/Utils.py +++ b/Utils.py @@ -38,6 +38,9 @@ class Version(typing.NamedTuple): minor: int build: int + def as_simple_string(self) -> str: + return ".".join(str(item) for item in self) + __version__ = "0.4.1" version_tuple = tuplize_version(__version__) diff --git a/docs/network protocol.md b/docs/network protocol.md index 052d62a531..48cf33183d 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -67,10 +67,11 @@ Sent to clients when they connect to an Archipelago server. | Name | Type | Notes | |-----------------------|-----------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which the server is running. | +| generator_version | [NetworkVersion](#NetworkVersion) | Object denoting the version of Archipelago which generated the multiworld. | | tags | list\[str\] | Denotes special features or capabilities that the sender is capable of. Example: `WebHost` | | password | bool | Denoted whether a password is required to join this room. | | permissions | dict\[str, [Permission](#Permission)\[int\]\] | Mapping of permission name to [Permission](#Permission), keys are: "release", "collect" and "remaining". | -| hint_cost | int | The percentage of total locations that need to be checked to receive a hint from the server. | +| hint_cost | int | The percentage of total locations that need to be checked to receive a hint from the server. | | location_check_points | int | The amount of hint points you receive per item/location check completed. | | games | list\[str\] | List of games present in this multiworld. | | datapackage_versions | dict\[str, int\] | Data versions of the individual games' data packages the server will send. Used to decide which games' caches are outdated. See [Data Package Contents](#Data-Package-Contents). **Deprecated. Use `datapackage_checksums` instead.** | From b704070de54666c483943792339f85cda986d716 Mon Sep 17 00:00:00 2001 From: zig-for Date: Wed, 26 Apr 2023 01:49:38 -0700 Subject: [PATCH 091/489] LADX: Fix palettes (#1767) --- worlds/ladx/LADXR/generator.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/worlds/ladx/LADXR/generator.py b/worlds/ladx/LADXR/generator.py index 90670c0258..c63d2b72c5 100644 --- a/worlds/ladx/LADXR/generator.py +++ b/worlds/ladx/LADXR/generator.py @@ -54,8 +54,10 @@ from .patches import multiworld as _ from .patches import tradeSequence as _ from . import hints -from .locations.keyLocation import KeyLocation from .patches import bank34 +from .patches.aesthetics import rgb_to_bin, bin_to_rgb + +from .locations.keyLocation import KeyLocation from ..Options import TrendyGame, Palette, MusicChangeCondition @@ -368,7 +370,6 @@ def generateRom(args, settings, ap_settings, auth, seed_name, logic, rnd=None, m if x > max: return max return x - from patches.aesthetics import rgb_to_bin, bin_to_rgb for address in range(start, end, 2): packed = (rom.banks[bank][address + 1] << 8) | rom.banks[bank][address] From 9d40471dee687183821e80e8b8b37af366ff8557 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 24 Apr 2023 15:49:19 +0200 Subject: [PATCH 092/489] Subnautica: add free samples option --- worlds/subnautica/Options.py | 9 ++++++++- worlds/subnautica/__init__.py | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index fa66026d7d..582e93eb0e 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,6 +1,6 @@ import typing -from Options import Choice, Range, DeathLink, DefaultOnToggle, StartInventoryPool +from Options import Choice, Range, DeathLink, Toggle, DefaultOnToggle, StartInventoryPool from .Creatures import all_creatures, Definitions @@ -35,6 +35,12 @@ class EarlySeaglide(DefaultOnToggle): display_name = "Early Seaglide" +class FreeSamples(Toggle): + """Get free items with your blueprints. + Items that can go into your inventory are awarded when you unlock their blueprint through Archipelago.""" + display_name = "Free Samples" + + class Goal(Choice): """Goal to complete. Launch: Leave the planet. @@ -100,6 +106,7 @@ class SubnauticaDeathLink(DeathLink): options = { "swim_rule": SwimRule, "early_seaglide": EarlySeaglide, + "free_samples": FreeSamples, "goal": Goal, "creature_scans": CreatureScans, "creature_scan_logic": AggressiveScanLogic, diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index bc1e4f696c..0807f1a77d 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -45,7 +45,7 @@ class SubnauticaWorld(World): option_definitions = Options.options data_version = 9 - required_client_version = (0, 3, 9) + required_client_version = (0, 4, 0) creatures_to_scan: List[str] @@ -129,6 +129,7 @@ class SubnauticaWorld(World): "vanilla_tech": vanilla_tech, "creatures_to_scan": self.creatures_to_scan, "death_link": self.multiworld.death_link[self.player].value, + "free_samples": self.multiworld.free_samples[self.player].value, } return slot_data From a7816d186ff404e2412d93874947de9140409a69 Mon Sep 17 00:00:00 2001 From: Abacys <45606585+Abacys@users.noreply.github.com> Date: Wed, 26 Apr 2023 07:43:23 -0400 Subject: [PATCH 093/489] Launcher: Correcting minor formatting error (#1768) Reformatting comment to comply with PEP format --- worlds/LauncherComponents.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index 3a2e319e8b..d9f04e601d 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -95,7 +95,7 @@ components: List[Component] = [ # Zillion Component('Zillion Client', 'ZillionClient', file_identifier=SuffixIdentifier('.apzl')), - #Kingdom Hearts 2 + # Kingdom Hearts 2 Component('KH2 Client', "KH2Client"), ] From 7bcf29941286c2206d1370e6d2eb88f6406e2352 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Wed, 26 Apr 2023 20:23:52 -0400 Subject: [PATCH 094/489] SM: missing foreign item filter fix (#1774) --- worlds/sm/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index ef7e50ba58..d60b0f51e6 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -227,7 +227,7 @@ class SMWorld(World): add_postAvailable_rule(location, self.player, value.PostAvailable) if self.multiworld.doors_colors_rando[self.player].value != 0: - add_item_rule(location, lambda item: item.type not in ammoItems or + add_item_rule(location, lambda item: item.game != self.game or item.type not in ammoItems or (item.type in ammoItems and \ (not item.advancement or (item.advancement and item.player == self.player)))) From b55174ccdf033894c39f81d32f125915a6cc7368 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 27 Apr 2023 02:33:49 -0500 Subject: [PATCH 095/489] Docs: document option alias in the options doc (#1755) * Docs: document option alias in the options doc * give an example of alias and move it under option creation. * use clearer example names --- docs/options api.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/options api.md b/docs/options api.md index a1407f2ceb..fdabd9facd 100644 --- a/docs/options api.md +++ b/docs/options api.md @@ -13,14 +13,20 @@ need to create: - A new option class with a docstring detailing what the option will do to your user. - A `display_name` to be displayed on the webhost. - A new entry in the `option_definitions` dict for your World. -By style and convention, the internal names should be snake_case. If the option supports having multiple sub_options -such as Choice options, these can be defined with `option_my_sub_option`, where the preceding `option_` is required and -stripped for users, so will show as `my_sub_option` in yaml files and if `auto_display_name` is True `My Sub Option` -on the webhost. All options support `random` as a generic option. `random` chooses from any of the available -values for that option, and is reserved by AP. You can set this as your default value but you cannot define your own -new `option_random`. +By style and convention, the internal names should be snake_case. ### Option Creation +- If the option supports having multiple sub_options, such as Choice options, these can be defined with +`option_value1`. Any attributes of the class with a preceding `option_` is added to the class's `options` lookup. The +`option_` is then stripped for users, so will show as `value1` in yaml files. If `auto_display_name` is True, it will +display as `Value1` on the webhost. +- An alternative name can be set for any specific option by setting an alias attribute +(i.e. `alias_value_1 = option_value1`) which will allow users to use either `value_1` or `value1` in their yaml +files, and both will resolve as `value1`. This should be used when changing options around, i.e. changing a Toggle to a +Choice, and defining `alias_true = option_full`. +- All options support `random` as a generic option. `random` chooses from any of the available values for that option, +and is reserved by AP. You can set this as your default value, but you cannot define your own `option_random`. + As an example, suppose we want an option that lets the user start their game with a sword in their inventory. Let's create our option class (with a docstring), give it a `display_name`, and add it to a dictionary that keeps track of our options: From 28c5e9ee65f36bcd8bf16eb6703a57254482ff7f Mon Sep 17 00:00:00 2001 From: zig-for Date: Thu, 27 Apr 2023 20:30:13 -0700 Subject: [PATCH 096/489] LADX: Rework dungeon item fill (#1763) --- worlds/ladx/__init__.py | 112 ++++++++++++++++++++++++++++++---------- 1 file changed, 85 insertions(+), 27 deletions(-) diff --git a/worlds/ladx/__init__.py b/worlds/ladx/__init__.py index ddb73f6b3c..b30919a82c 100644 --- a/worlds/ladx/__init__.py +++ b/worlds/ladx/__init__.py @@ -1,5 +1,6 @@ import binascii import bsdiff4 +import itertools import os import pkgutil import tempfile @@ -137,7 +138,7 @@ class LinksAwakeningWorld(World): def create_event(self, event: str): return Item(event, ItemClassification.progression, None, self.player) - def create_items(self) -> None: + def create_items(self) -> None: exclude = [item.name for item in self.multiworld.precollected_items[self.player]] dungeon_item_types = { @@ -146,6 +147,7 @@ class LinksAwakeningWorld(World): self.prefill_original_dungeon = [ [], [], [], [], [], [], [], [], [] ] self.prefill_own_dungeons = [] + self.pre_fill_items = [] # For any and different world, set item rule instead for option in ["maps", "compasses", "small_keys", "nightmare_keys", "stone_beaks"]: @@ -185,6 +187,7 @@ class LinksAwakeningWorld(World): location = self.multiworld.get_location(item.item_data.vanilla_location, self.player) location.place_locked_item(item) continue + if isinstance(item.item_data, DungeonItemData): if item.item_data.dungeon_item_type == DungeonItemType.INSTRUMENT: # Find instrument, lock @@ -210,8 +213,10 @@ class LinksAwakeningWorld(World): shuffle_type = dungeon_item_types[item_type] if shuffle_type == DungeonItemShuffle.option_original_dungeon: self.prefill_original_dungeon[item.item_data.dungeon_index - 1].append(item) + self.pre_fill_items.append(item) elif shuffle_type == DungeonItemShuffle.option_own_dungeons: self.prefill_own_dungeons.append(item) + self.pre_fill_items.append(item) else: self.multiworld.itempool.append(item) else: @@ -219,16 +224,12 @@ class LinksAwakeningWorld(World): self.multi_key = self.generate_multi_key() - dungeon_locations = [] - dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] - all_state = self.multiworld.get_all_state(use_cache=False) - # Add special case for trendy shop access trendy_region = self.multiworld.get_region("Trendy Shop", self.player) event_location = Location(self.player, "Can Play Trendy Game", parent=trendy_region) trendy_region.locations.insert(0, event_location) event_location.place_locked_item(self.create_event("Can Play Trendy Game")) - + # For now, special case first item FORCE_START_ITEM = True if FORCE_START_ITEM: @@ -241,41 +242,98 @@ class LinksAwakeningWorld(World): index = self.multiworld.random.choice(possible_start_items) start_item = self.multiworld.itempool.pop(index) start_loc.place_locked_item(start_item) + + self.dungeon_locations_by_dungeon = [[], [], [], [], [], [], [], [], []] for r in self.multiworld.get_regions(): if r.player != self.player: continue # Set aside dungeon locations if r.dungeon_index: - dungeon_locations += r.locations - dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations + self.dungeon_locations_by_dungeon[r.dungeon_index - 1] += r.locations for location in r.locations: - if location.name == "Pit Button Chest (Tail Cave)": - # Don't place dungeon items on pit button chest, to reduce chance of the filler blowing up - # TODO: no need for this if small key shuffle - dungeon_locations.remove(location) - dungeon_locations_by_dungeon[r.dungeon_index - 1].remove(location) + # Don't place dungeon items on pit button chest, to reduce chance of the filler blowing up + # TODO: no need for this if small key shuffle + if location.name == "Pit Button Chest (Tail Cave)" or location.item: + self.dungeon_locations_by_dungeon[r.dungeon_index - 1].remove(location) # Properly fill locations within dungeon location.dungeon = r.dungeon_index - for dungeon_index in range(0, 9): - locs = dungeon_locations_by_dungeon[dungeon_index] - locs = [loc for loc in locs if not loc.item] - self.multiworld.random.shuffle(locs) - self.multiworld.random.shuffle(self.prefill_original_dungeon[dungeon_index]) - fill_restrictive(self.multiworld, all_state, locs, self.prefill_original_dungeon[dungeon_index], lock=True) - assert not self.prefill_original_dungeon[dungeon_index] + def get_pre_fill_items(self): + return self.pre_fill_items - # Fill dungeon items first, to not torture the fill algo - dungeon_locations = [loc for loc in dungeon_locations if not loc.item] - # dungeon_items = sorted(self.prefill_own_dungeons, key=lambda item: item.item_data.dungeon_item_type) - self.multiworld.random.shuffle(self.prefill_own_dungeons) - self.multiworld.random.shuffle(dungeon_locations) - fill_restrictive(self.multiworld, all_state, dungeon_locations, self.prefill_own_dungeons, lock=True) + def pre_fill(self) -> None: + allowed_locations_by_item = {} + + + # Set up filter rules + + # The list of items we will pass to fill_restrictive, contains at first the items that go to all dungeons + all_dungeon_items_to_fill = list(self.prefill_own_dungeons) + # set containing the list of all possible dungeon locations for the player + all_dungeon_locs = set() + + # Do dungeon specific things + for dungeon_index in range(0, 9): + # set up allow-list for dungeon specific items + locs = set(self.dungeon_locations_by_dungeon[dungeon_index]) + for item in self.prefill_original_dungeon[dungeon_index]: + allowed_locations_by_item[item] = locs + + # put the items for this dungeon in the list to fill + all_dungeon_items_to_fill.extend(self.prefill_original_dungeon[dungeon_index]) + + # ...and gather the list of all dungeon locations + all_dungeon_locs |= locs + # ...also set the rules for the dungeon + for location in locs: + orig_rule = location.item_rule + # If an item is about to be placed on a dungeon location, it can go there iff + # 1. it fits the general rules for that location (probably 'return True' for most places) + # 2. Either + # 2a. it's not a restricted dungeon item + # 2b. it's a restricted dungeon item and this location is specified as allowed + location.item_rule = lambda item, location=location, orig_rule=orig_rule: \ + (item not in allowed_locations_by_item or location in allowed_locations_by_item[item]) and orig_rule(item) + + # Now set up the allow-list for any-dungeon items + for item in self.prefill_own_dungeons: + # They of course get to go in any spot + allowed_locations_by_item[item] = all_dungeon_locs + + # Get the list of locations and shuffle + all_dungeon_locs_to_fill = list(all_dungeon_locs) + self.multiworld.random.shuffle(all_dungeon_locs_to_fill) + + # Get the list of items and sort by priority + def priority(item): + # 0 - Nightmare dungeon-specific + # 1 - Key dungeon-specific + # 2 - Other dungeon-specific + # 3 - Nightmare any local dungeon + # 4 - Key any local dungeon + # 5 - Other any local dungeon + i = 2 + if "Nightmare" in item.name: + i = 0 + elif "Key" in item.name: + i = 1 + if allowed_locations_by_item[item] is all_dungeon_locs: + i += 3 + return i + all_dungeon_items_to_fill.sort(key=priority) + + # Set up state + all_state = self.multiworld.get_all_state(use_cache=False) + # Remove dungeon items we are about to put in from the state so that we don't double count + for item in all_dungeon_items_to_fill: + all_state.remove(item) + + # Finally, fill! + fill_restrictive(self.multiworld, all_state, all_dungeon_locs_to_fill, all_dungeon_items_to_fill, lock=True, single_player_placement=True, allow_partial=False) name_cache = {} - # Tries to associate an icon from another game with an icon we have def guess_icon_for_other_world(self, other): if not self.name_cache: From 42da24cb5e8f80b54613f2105cfb510a6a728570 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 21 Apr 2023 15:12:43 +0200 Subject: [PATCH 097/489] WebHost: offer room owner log download link --- WebHostLib/misc.py | 6 +++++- WebHostLib/templates/hostRoom.html | 19 ++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/WebHostLib/misc.py b/WebHostLib/misc.py index bf9f4e2fd7..6d3e82c00c 100644 --- a/WebHostLib/misc.py +++ b/WebHostLib/misc.py @@ -116,7 +116,11 @@ def display_log(room: UUID): if room is None: return abort(404) if room.owner == session["_id"]: - return Response(_read_log(os.path.join("logs", str(room.id) + ".txt")), mimetype="text/plain;charset=UTF-8") + file_path = os.path.join("logs", str(room.id) + ".txt") + if os.path.exists(file_path): + return Response(_read_log(file_path), mimetype="text/plain;charset=UTF-8") + return "Log File does not exist." + return "Access Denied", 403 diff --git a/WebHostLib/templates/hostRoom.html b/WebHostLib/templates/hostRoom.html index 6f02dc0944..ba15d64aca 100644 --- a/WebHostLib/templates/hostRoom.html +++ b/WebHostLib/templates/hostRoom.html @@ -32,13 +32,18 @@ {% endif %} {{ macros.list_patches_room(room) }} {% if room.owner == session["_id"] %} -

-
- - -
- +
+
+
+ + +
+
+ + Open Log File... + +
{% endblock %} {% block body %} {% include 'header/oceanHeader.html' %}

Currently Supported Games

+
+
+
+ + + +
+
{% for game_name in worlds | title_sorted %} {% set world = worlds[game_name] %} -

{{ game_name }}

-

+

+  {{ game_name }} +

+
  • Create New Room From e377068d1fcc1f7d09035ad5a65e66938de75273 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Mon, 2 Oct 2023 20:17:34 +0200 Subject: [PATCH 465/489] Core: more gitignore (#2249) gitignore versioned venvs, prof output, appimagetool and sni downloads --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index e374a12954..f4bcd35c32 100644 --- a/.gitignore +++ b/.gitignore @@ -33,11 +33,14 @@ setups build bundle/components.wxs dist +/prof/ README.html .vs/ EnemizerCLI/ /Players/ /SNI/ +/sni-*/ +/appimagetool* /host.yaml /options.yaml /config.yaml @@ -140,6 +143,7 @@ ipython_config.py .venv* env/ venv/ +/venv*/ ENV/ env.bak/ venv.bak/ From 24403eba1b167c1e3f45f57531a42867c301a76e Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Mon, 2 Oct 2023 11:52:00 -0700 Subject: [PATCH 466/489] Launcher: Allow opening patches for clients without an exe (#2176) * Launcher: Allow opening patches for clients without an exe * Launcher: Restore behavior for not showing patch suffixes for clients that aren't installed --- Launcher.py | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/Launcher.py b/Launcher.py index a1548d594c..9e184bf108 100644 --- a/Launcher.py +++ b/Launcher.py @@ -50,17 +50,22 @@ def open_host_yaml(): def open_patch(): suffixes = [] for c in components: - if isfile(get_exe(c)[-1]): - suffixes += c.file_identifier.suffixes if c.type == Type.CLIENT and \ - isinstance(c.file_identifier, SuffixIdentifier) else [] + if c.type == Type.CLIENT and \ + isinstance(c.file_identifier, SuffixIdentifier) and \ + (c.script_name is None or isfile(get_exe(c)[-1])): + suffixes += c.file_identifier.suffixes try: - filename = open_filename('Select patch', (('Patches', suffixes),)) + filename = open_filename("Select patch", (("Patches", suffixes),)) except Exception as e: - messagebox('Error', str(e), error=True) + messagebox("Error", str(e), error=True) else: file, component = identify(filename) if file and component: - launch([*get_exe(component), file], component.cli) + exe = get_exe(component) + if exe is None or not isfile(exe[-1]): + exe = get_exe("Launcher") + + launch([*exe, file], component.cli) def generate_yamls(): @@ -107,7 +112,7 @@ def identify(path: Union[None, str]): return None, None for component in components: if component.handles_file(path): - return path, component + return path, component elif path == component.display_name or path == component.script_name: return None, component return None, None @@ -117,25 +122,25 @@ def get_exe(component: Union[str, Component]) -> Optional[Sequence[str]]: if isinstance(component, str): name = component component = None - if name.startswith('Archipelago'): + if name.startswith("Archipelago"): name = name[11:] - if name.endswith('.exe'): + if name.endswith(".exe"): name = name[:-4] - if name.endswith('.py'): + if name.endswith(".py"): name = name[:-3] if not name: return None for c in components: - if c.script_name == name or c.frozen_name == f'Archipelago{name}': + if c.script_name == name or c.frozen_name == f"Archipelago{name}": component = c break if not component: return None if is_frozen(): - suffix = '.exe' if is_windows else '' - return [local_path(f'{component.frozen_name}{suffix}')] + suffix = ".exe" if is_windows else "" + return [local_path(f"{component.frozen_name}{suffix}")] if component.frozen_name else None else: - return [sys.executable, local_path(f'{component.script_name}.py')] + return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None def launch(exe, in_terminal=False): From bc11c9dfd444b24e988d46326752d0d55189cba5 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Mon, 2 Oct 2023 17:44:19 -0700 Subject: [PATCH 467/489] BizHawkClient: Add BizHawkClient (#1978) Adds a generic client that can communicate with BizHawk. Similar to SNIClient, but for arbitrary systems and doesn't have an intermediary application like SNI. --- BizHawkClient.py | 9 + data/lua/base64.lua | 119 ++++++ data/lua/connector_bizhawk_generic.lua | 564 +++++++++++++++++++++++++ inno_setup.iss | 4 + worlds/LauncherComponents.py | 3 + worlds/_bizhawk/__init__.py | 326 ++++++++++++++ worlds/_bizhawk/client.py | 87 ++++ worlds/_bizhawk/context.py | 188 +++++++++ 8 files changed, 1300 insertions(+) create mode 100644 BizHawkClient.py create mode 100644 data/lua/base64.lua create mode 100644 data/lua/connector_bizhawk_generic.lua create mode 100644 worlds/_bizhawk/__init__.py create mode 100644 worlds/_bizhawk/client.py create mode 100644 worlds/_bizhawk/context.py diff --git a/BizHawkClient.py b/BizHawkClient.py new file mode 100644 index 0000000000..86c8e5197e --- /dev/null +++ b/BizHawkClient.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +import ModuleUpdate +ModuleUpdate.update() + +from worlds._bizhawk.context import launch + +if __name__ == "__main__": + launch() diff --git a/data/lua/base64.lua b/data/lua/base64.lua new file mode 100644 index 0000000000..ebe8064353 --- /dev/null +++ b/data/lua/base64.lua @@ -0,0 +1,119 @@ +-- This file originates from this repository: https://github.com/iskolbin/lbase64 +-- It was modified to translate between base64 strings and lists of bytes instead of base64 strings and strings. + +local base64 = {} + +local extract = _G.bit32 and _G.bit32.extract -- Lua 5.2/Lua 5.3 in compatibility mode +if not extract then + if _G._VERSION == "Lua 5.4" then + extract = load[[return function( v, from, width ) + return ( v >> from ) & ((1 << width) - 1) + end]]() + elseif _G.bit then -- LuaJIT + local shl, shr, band = _G.bit.lshift, _G.bit.rshift, _G.bit.band + extract = function( v, from, width ) + return band( shr( v, from ), shl( 1, width ) - 1 ) + end + elseif _G._VERSION == "Lua 5.1" then + extract = function( v, from, width ) + local w = 0 + local flag = 2^from + for i = 0, width-1 do + local flag2 = flag + flag + if v % flag2 >= flag then + w = w + 2^i + end + flag = flag2 + end + return w + end + end +end + + +function base64.makeencoder( s62, s63, spad ) + local encoder = {} + for b64code, char in pairs{[0]='A','B','C','D','E','F','G','H','I','J', + 'K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y', + 'Z','a','b','c','d','e','f','g','h','i','j','k','l','m','n', + 'o','p','q','r','s','t','u','v','w','x','y','z','0','1','2', + '3','4','5','6','7','8','9',s62 or '+',s63 or'/',spad or'='} do + encoder[b64code] = char:byte() + end + return encoder +end + +function base64.makedecoder( s62, s63, spad ) + local decoder = {} + for b64code, charcode in pairs( base64.makeencoder( s62, s63, spad )) do + decoder[charcode] = b64code + end + return decoder +end + +local DEFAULT_ENCODER = base64.makeencoder() +local DEFAULT_DECODER = base64.makedecoder() + +local char, concat = string.char, table.concat + +function base64.encode( arr, encoder ) + encoder = encoder or DEFAULT_ENCODER + local t, k, n = {}, 1, #arr + local lastn = n % 3 + for i = 1, n-lastn, 3 do + local a, b, c = arr[i], arr[i + 1], arr[i + 2] + local v = a*0x10000 + b*0x100 + c + local s + s = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[extract(v,0,6)]) + t[k] = s + k = k + 1 + end + if lastn == 2 then + local a, b = arr[n-1], arr[n] + local v = a*0x10000 + b*0x100 + t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[extract(v,6,6)], encoder[64]) + elseif lastn == 1 then + local v = arr[n]*0x10000 + t[k] = char(encoder[extract(v,18,6)], encoder[extract(v,12,6)], encoder[64], encoder[64]) + end + return concat( t ) +end + +function base64.decode( b64, decoder ) + decoder = decoder or DEFAULT_DECODER + local pattern = '[^%w%+%/%=]' + if decoder then + local s62, s63 + for charcode, b64code in pairs( decoder ) do + if b64code == 62 then s62 = charcode + elseif b64code == 63 then s63 = charcode + end + end + pattern = ('[^%%w%%%s%%%s%%=]'):format( char(s62), char(s63) ) + end + b64 = b64:gsub( pattern, '' ) + local t, k = {}, 1 + local n = #b64 + local padding = b64:sub(-2) == '==' and 2 or b64:sub(-1) == '=' and 1 or 0 + for i = 1, padding > 0 and n-4 or n, 4 do + local a, b, c, d = b64:byte( i, i+3 ) + local s + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + decoder[d] + table.insert(t,extract(v,16,8)) + table.insert(t,extract(v,8,8)) + table.insert(t,extract(v,0,8)) + end + if padding == 1 then + local a, b, c = b64:byte( n-3, n-1 ) + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + decoder[c]*0x40 + table.insert(t,extract(v,16,8)) + table.insert(t,extract(v,8,8)) + elseif padding == 2 then + local a, b = b64:byte( n-3, n-2 ) + local v = decoder[a]*0x40000 + decoder[b]*0x1000 + table.insert(t,extract(v,16,8)) + end + return t +end + +return base64 diff --git a/data/lua/connector_bizhawk_generic.lua b/data/lua/connector_bizhawk_generic.lua new file mode 100644 index 0000000000..b0b06de447 --- /dev/null +++ b/data/lua/connector_bizhawk_generic.lua @@ -0,0 +1,564 @@ +--[[ +Copyright (c) 2023 Zunawe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +]] + +local SCRIPT_VERSION = 1 + +--[[ +This script expects to receive JSON and will send JSON back. A message should +be a list of 1 or more requests which will be executed in order. Each request +will have a corresponding response in the same order. + +Every individual request and response is a JSON object with at minimum one +field `type`. The value of `type` determines what other fields may exist. + +To get the script version, instead of JSON, send "VERSION" to get the script +version directly (e.g. "2"). + +#### Ex. 1 + +Request: `[{"type": "PING"}]` + +Response: `[{"type": "PONG"}]` + +--- + +#### Ex. 2 + +Request: `[{"type": "LOCK"}, {"type": "HASH"}]` + +Response: `[{"type": "LOCKED"}, {"type": "HASH_RESPONSE", "value": "F7D18982"}]` + +--- + +#### Ex. 3 + +Request: + +```json +[ + {"type": "GUARD", "address": 100, "expected_data": "aGVsbG8=", "domain": "System Bus"}, + {"type": "READ", "address": 500, "size": 4, "domain": "ROM"} +] +``` + +Response: + +```json +[ + {"type": "GUARD_RESPONSE", "address": 100, "value": true}, + {"type": "READ_RESPONSE", "value": "dGVzdA=="} +] +``` + +--- + +#### Ex. 4 + +Request: + +```json +[ + {"type": "GUARD", "address": 100, "expected_data": "aGVsbG8=", "domain": "System Bus"}, + {"type": "READ", "address": 500, "size": 4, "domain": "ROM"} +] +``` + +Response: + +```json +[ + {"type": "GUARD_RESPONSE", "address": 100, "value": false}, + {"type": "GUARD_RESPONSE", "address": 100, "value": false} +] +``` + +--- + +### Supported Request Types + +- `PING` + Does nothing; resets timeout. + + Expected Response Type: `PONG` + +- `SYSTEM` + Returns the system of the currently loaded ROM (N64, GBA, etc...). + + Expected Response Type: `SYSTEM_RESPONSE` + +- `PREFERRED_CORES` + Returns the user's default cores for systems with multiple cores. If the + current ROM's system has multiple cores, the one that is currently + running is very probably the preferred core. + + Expected Response Type: `PREFERRED_CORES_RESPONSE` + +- `HASH` + Returns the hash of the currently loaded ROM calculated by BizHawk. + + Expected Response Type: `HASH_RESPONSE` + +- `GUARD` + Checks a section of memory against `expected_data`. If the bytes starting + at `address` do not match `expected_data`, the response will have `value` + set to `false`, and all subsequent requests will not be executed and + receive the same `GUARD_RESPONSE`. + + Expected Response Type: `GUARD_RESPONSE` + + Additional Fields: + - `address` (`int`): The address of the memory to check + - `expected_data` (string): A base64 string of contiguous data + - `domain` (`string`): The name of the memory domain the address + corresponds to + +- `LOCK` + Halts emulation and blocks on incoming requests until an `UNLOCK` request + is received or the client times out. All requests processed while locked + will happen on the same frame. + + Expected Response Type: `LOCKED` + +- `UNLOCK` + Resumes emulation after the current list of requests is done being + executed. + + Expected Response Type: `UNLOCKED` + +- `READ` + Reads an array of bytes at the provided address. + + Expected Response Type: `READ_RESPONSE` + + Additional Fields: + - `address` (`int`): The address of the memory to read + - `size` (`int`): The number of bytes to read + - `domain` (`string`): The name of the memory domain the address + corresponds to + +- `WRITE` + Writes an array of bytes to the provided address. + + Expected Response Type: `WRITE_RESPONSE` + + Additional Fields: + - `address` (`int`): The address of the memory to write to + - `value` (`string`): A base64 string representing the data to write + - `domain` (`string`): The name of the memory domain the address + corresponds to + +- `DISPLAY_MESSAGE` + Adds a message to the message queue which will be displayed using + `gui.addmessage` according to the message interval. + + Expected Response Type: `DISPLAY_MESSAGE_RESPONSE` + + Additional Fields: + - `message` (`string`): The string to display + +- `SET_MESSAGE_INTERVAL` + Sets the minimum amount of time to wait between displaying messages. + Potentially useful if you add many messages quickly but want players + to be able to read each of them. + + Expected Response Type: `SET_MESSAGE_INTERVAL_RESPONSE` + + Additional Fields: + - `value` (`number`): The number of seconds to set the interval to + + +### Response Types + +- `PONG` + Acknowledges `PING`. + +- `SYSTEM_RESPONSE` + Contains the name of the system for currently running ROM. + + Additional Fields: + - `value` (`string`): The returned system name + +- `PREFERRED_CORES_RESPONSE` + Contains the user's preferred cores for systems with multiple supported + cores. Currently includes NES, SNES, GB, GBC, DGB, SGB, PCE, PCECD, and + SGX. + + Additional Fields: + - `value` (`{[string]: [string]}`): A dictionary map from system name to + core name + +- `HASH_RESPONSE` + Contains the hash of the currently loaded ROM calculated by BizHawk. + + Additional Fields: + - `value` (`string`): The returned hash + +- `GUARD_RESPONSE` + The result of an attempted `GUARD` request. + + Additional Fields: + - `value` (`boolean`): true if the memory was validated, false if not + - `address` (`int`): The address of the memory that was invalid (the same + address provided by the `GUARD`, not the address of the individual invalid + byte) + +- `LOCKED` + Acknowledges `LOCK`. + +- `UNLOCKED` + Acknowledges `UNLOCK`. + +- `READ_RESPONSE` + Contains the result of a `READ` request. + + Additional Fields: + - `value` (`string`): A base64 string representing the read data + +- `WRITE_RESPONSE` + Acknowledges `WRITE`. + +- `DISPLAY_MESSAGE_RESPONSE` + Acknowledges `DISPLAY_MESSAGE`. + +- `SET_MESSAGE_INTERVAL_RESPONSE` + Acknowledges `SET_MESSAGE_INTERVAL`. + +- `ERROR` + Signifies that something has gone wrong while processing a request. + + Additional Fields: + - `err` (`string`): A description of the problem +]] + +local base64 = require("base64") +local socket = require("socket") +local json = require("json") + +-- Set to log incoming requests +-- Will cause lag due to large console output +local DEBUG = false + +local SOCKET_PORT = 43055 + +local STATE_NOT_CONNECTED = 0 +local STATE_CONNECTED = 1 + +local server = nil +local client_socket = nil + +local current_state = STATE_NOT_CONNECTED + +local timeout_timer = 0 +local message_timer = 0 +local message_interval = 0 +local prev_time = 0 +local current_time = 0 + +local locked = false + +local rom_hash = nil + +local lua_major, lua_minor = _VERSION:match("Lua (%d+)%.(%d+)") +lua_major = tonumber(lua_major) +lua_minor = tonumber(lua_minor) + +if lua_major > 5 or (lua_major == 5 and lua_minor >= 3) then + require("lua_5_3_compat") +end + +local bizhawk_version = client.getversion() +local bizhawk_major, bizhawk_minor, bizhawk_patch = bizhawk_version:match("(%d+)%.(%d+)%.?(%d*)") +bizhawk_major = tonumber(bizhawk_major) +bizhawk_minor = tonumber(bizhawk_minor) +if bizhawk_patch == "" then + bizhawk_patch = 0 +else + bizhawk_patch = tonumber(bizhawk_patch) +end + +function queue_push (self, value) + self[self.right] = value + self.right = self.right + 1 +end + +function queue_is_empty (self) + return self.right == self.left +end + +function queue_shift (self) + value = self[self.left] + self[self.left] = nil + self.left = self.left + 1 + return value +end + +function new_queue () + local queue = {left = 1, right = 1} + return setmetatable(queue, {__index = {is_empty = queue_is_empty, push = queue_push, shift = queue_shift}}) +end + +local message_queue = new_queue() + +function lock () + locked = true + client_socket:settimeout(2) +end + +function unlock () + locked = false + client_socket:settimeout(0) +end + +function process_request (req) + local res = {} + + if req["type"] == "PING" then + res["type"] = "PONG" + + elseif req["type"] == "SYSTEM" then + res["type"] = "SYSTEM_RESPONSE" + res["value"] = emu.getsystemid() + + elseif req["type"] == "PREFERRED_CORES" then + local preferred_cores = client.getconfig().PreferredCores + res["type"] = "PREFERRED_CORES_RESPONSE" + res["value"] = {} + res["value"]["NES"] = preferred_cores.NES + res["value"]["SNES"] = preferred_cores.SNES + res["value"]["GB"] = preferred_cores.GB + res["value"]["GBC"] = preferred_cores.GBC + res["value"]["DGB"] = preferred_cores.DGB + res["value"]["SGB"] = preferred_cores.SGB + res["value"]["PCE"] = preferred_cores.PCE + res["value"]["PCECD"] = preferred_cores.PCECD + res["value"]["SGX"] = preferred_cores.SGX + + elseif req["type"] == "HASH" then + res["type"] = "HASH_RESPONSE" + res["value"] = rom_hash + + elseif req["type"] == "GUARD" then + res["type"] = "GUARD_RESPONSE" + local expected_data = base64.decode(req["expected_data"]) + + local actual_data = memory.read_bytes_as_array(req["address"], #expected_data, req["domain"]) + + local data_is_validated = true + for i, byte in ipairs(actual_data) do + if byte ~= expected_data[i] then + data_is_validated = false + break + end + end + + res["value"] = data_is_validated + res["address"] = req["address"] + + elseif req["type"] == "LOCK" then + res["type"] = "LOCKED" + lock() + + elseif req["type"] == "UNLOCK" then + res["type"] = "UNLOCKED" + unlock() + + elseif req["type"] == "READ" then + res["type"] = "READ_RESPONSE" + res["value"] = base64.encode(memory.read_bytes_as_array(req["address"], req["size"], req["domain"])) + + elseif req["type"] == "WRITE" then + res["type"] = "WRITE_RESPONSE" + memory.write_bytes_as_array(req["address"], base64.decode(req["value"]), req["domain"]) + + elseif req["type"] == "DISPLAY_MESSAGE" then + res["type"] = "DISPLAY_MESSAGE_RESPONSE" + message_queue:push(req["message"]) + + elseif req["type"] == "SET_MESSAGE_INTERVAL" then + res["type"] = "SET_MESSAGE_INTERVAL_RESPONSE" + message_interval = req["value"] + + else + res["type"] = "ERROR" + res["err"] = "Unknown command: "..req["type"] + end + + return res +end + +-- Receive data from AP client and send message back +function send_receive () + local message, err = client_socket:receive() + + -- Handle errors + if err == "closed" then + if current_state == STATE_CONNECTED then + print("Connection to client closed") + end + current_state = STATE_NOT_CONNECTED + return + elseif err == "timeout" then + unlock() + return + elseif err ~= nil then + print(err) + current_state = STATE_NOT_CONNECTED + unlock() + return + end + + -- Reset timeout timer + timeout_timer = 5 + + -- Process received data + if DEBUG then + print("Received Message ["..emu.framecount().."]: "..'"'..message..'"') + end + + if message == "VERSION" then + local result, err client_socket:send(tostring(SCRIPT_VERSION).."\n") + else + local res = {} + local data = json.decode(message) + local failed_guard_response = nil + for i, req in ipairs(data) do + if failed_guard_response ~= nil then + res[i] = failed_guard_response + else + -- An error is more likely to cause an NLua exception than to return an error here + local status, response = pcall(process_request, req) + if status then + res[i] = response + + -- If the GUARD validation failed, skip the remaining commands + if response["type"] == "GUARD_RESPONSE" and not response["value"] then + failed_guard_response = response + end + else + res[i] = {type = "ERROR", err = response} + end + end + end + + client_socket:send(json.encode(res).."\n") + end +end + +function main () + server, err = socket.bind("localhost", SOCKET_PORT) + if err ~= nil then + print(err) + return + end + + while true do + current_time = socket.socket.gettime() + timeout_timer = timeout_timer - (current_time - prev_time) + message_timer = message_timer - (current_time - prev_time) + prev_time = current_time + + if message_timer <= 0 and not message_queue:is_empty() then + gui.addmessage(message_queue:shift()) + message_timer = message_interval + end + + if current_state == STATE_NOT_CONNECTED then + if emu.framecount() % 60 == 0 then + server:settimeout(2) + local client, timeout = server:accept() + if timeout == nil then + print("Client connected") + current_state = STATE_CONNECTED + client_socket = client + client_socket:settimeout(0) + else + print("No client found. Trying again...") + end + end + else + repeat + send_receive() + until not locked + + if timeout_timer <= 0 then + print("Client timed out") + current_state = STATE_NOT_CONNECTED + end + end + + coroutine.yield() + end +end + +event.onexit(function () + print("\n-- Restarting Script --\n") + if server ~= nil then + server:close() + end +end) + +if bizhawk_major < 2 or (bizhawk_major == 2 and bizhawk_minor < 7) then + print("Must use BizHawk 2.7.0 or newer") +elseif bizhawk_major > 2 or (bizhawk_major == 2 and bizhawk_minor > 9) then + print("Warning: This version of BizHawk is newer than this script. If it doesn't work, consider downgrading to 2.9.") +else + if emu.getsystemid() == "NULL" then + print("No ROM is loaded. Please load a ROM.") + while emu.getsystemid() == "NULL" do + emu.frameadvance() + end + end + + rom_hash = gameinfo.getromhash() + + print("Waiting for client to connect. Emulation will freeze intermittently until a client is found.\n") + + local co = coroutine.create(main) + function tick () + local status, err = coroutine.resume(co) + + if not status then + print("\nERROR: "..err) + print("Consider reporting this crash.\n") + + if server ~= nil then + server:close() + end + + co = coroutine.create(main) + end + end + + -- Gambatte has a setting which can cause script execution to become + -- misaligned, so for GB and GBC we explicitly set the callback on + -- vblank instead. + -- https://github.com/TASEmulators/BizHawk/issues/3711 + if emu.getsystemid() == "GB" or emu.getsystemid() == "GBC" then + event.onmemoryexecute(tick, 0x40, "tick", "System Bus") + else + event.onframeend(tick) + end + + while true do + emu.frameadvance() + end +end diff --git a/inno_setup.iss b/inno_setup.iss index 147cd74dca..3c1bdc4571 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -74,6 +74,7 @@ Name: "client/sni/sm"; Description: "SNI Client - Super Metroid Patch Setup"; Name: "client/sni/dkc3"; Description: "SNI Client - Donkey Kong Country 3 Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/smw"; Description: "SNI Client - Super Mario World Patch Setup"; Types: full playing; Flags: disablenouninstallwarning Name: "client/sni/l2ac"; Description: "SNI Client - Lufia II Ancient Cave Patch Setup"; Types: full playing; Flags: disablenouninstallwarning +Name: "client/bizhawk"; Description: "BizHawk Client"; Types: full playing Name: "client/factorio"; Description: "Factorio"; Types: full playing Name: "client/kh2"; Description: "Kingdom Hearts 2"; Types: full playing Name: "client/minecraft"; Description: "Minecraft"; Types: full playing; ExtraDiskSpaceRequired: 226894278 @@ -122,6 +123,7 @@ Source: "{#source_path}\ArchipelagoServer.exe"; DestDir: "{app}"; Flags: ignorev Source: "{#source_path}\ArchipelagoFactorioClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/factorio Source: "{#source_path}\ArchipelagoTextClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/text Source: "{#source_path}\ArchipelagoSNIClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni +Source: "{#source_path}\ArchipelagoBizHawkClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/bizhawk Source: "{#source_path}\ArchipelagoLinksAwakeningClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/ladx Source: "{#source_path}\ArchipelagoLttPAdjuster.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/sni/lttp or generator/lttp Source: "{#source_path}\ArchipelagoMinecraftClient.exe"; DestDir: "{app}"; Flags: ignoreversion; Components: client/minecraft @@ -146,6 +148,7 @@ Name: "{group}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe" Name: "{group}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Components: server Name: "{group}\{#MyAppName} Text Client"; Filename: "{app}\ArchipelagoTextClient.exe"; Components: client/text Name: "{group}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Components: client/sni +Name: "{group}\{#MyAppName} BizHawk Client"; Filename: "{app}\ArchipelagoBizHawkClient.exe"; Components: client/bizhawk Name: "{group}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Components: client/factorio Name: "{group}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Components: client/minecraft Name: "{group}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Components: client/oot @@ -166,6 +169,7 @@ Name: "{commondesktop}\{#MyAppName} Folder"; Filename: "{app}"; Tasks: desktopic Name: "{commondesktop}\{#MyAppName} Launcher"; Filename: "{app}\ArchipelagoLauncher.exe"; Tasks: desktopicon Name: "{commondesktop}\{#MyAppName} Server"; Filename: "{app}\ArchipelagoServer"; Tasks: desktopicon; Components: server Name: "{commondesktop}\{#MyAppName} SNI Client"; Filename: "{app}\ArchipelagoSNIClient.exe"; Tasks: desktopicon; Components: client/sni +Name: "{commondesktop}\{#MyAppName} BizHawk Client"; Filename: "{app}\ArchipelagoBizHawkClient.exe"; Tasks: desktopicon; Components: client/bizhawk Name: "{commondesktop}\{#MyAppName} Factorio Client"; Filename: "{app}\ArchipelagoFactorioClient.exe"; Tasks: desktopicon; Components: client/factorio Name: "{commondesktop}\{#MyAppName} Minecraft Client"; Filename: "{app}\ArchipelagoMinecraftClient.exe"; Tasks: desktopicon; Components: client/minecraft Name: "{commondesktop}\{#MyAppName} Ocarina of Time Client"; Filename: "{app}\ArchipelagoOoTClient.exe"; Tasks: desktopicon; Components: client/oot diff --git a/worlds/LauncherComponents.py b/worlds/LauncherComponents.py index c3ae2b0495..2d445a77b8 100644 --- a/worlds/LauncherComponents.py +++ b/worlds/LauncherComponents.py @@ -89,6 +89,9 @@ components: List[Component] = [ Component('SNI Client', 'SNIClient', file_identifier=SuffixIdentifier('.apz3', '.apm3', '.apsoe', '.aplttp', '.apsm', '.apsmz3', '.apdkc3', '.apsmw', '.apl2ac')), + # BizHawk + Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, + file_identifier=SuffixIdentifier()), Component('Links Awakening DX Client', 'LinksAwakeningClient', file_identifier=SuffixIdentifier('.apladx')), Component('LttP Adjuster', 'LttPAdjuster'), diff --git a/worlds/_bizhawk/__init__.py b/worlds/_bizhawk/__init__.py new file mode 100644 index 0000000000..cdf227ec7b --- /dev/null +++ b/worlds/_bizhawk/__init__.py @@ -0,0 +1,326 @@ +""" +A module for interacting with BizHawk through `connector_bizhawk_generic.lua`. + +Any mention of `domain` in this module refers to the names BizHawk gives to memory domains in its own lua api. They are +naively passed to BizHawk without validation or modification. +""" + +import asyncio +import base64 +import enum +import json +import typing + + +BIZHAWK_SOCKET_PORT = 43055 +EXPECTED_SCRIPT_VERSION = 1 + + +class ConnectionStatus(enum.IntEnum): + NOT_CONNECTED = 1 + TENTATIVE = 2 + CONNECTED = 3 + + +class BizHawkContext: + streams: typing.Optional[typing.Tuple[asyncio.StreamReader, asyncio.StreamWriter]] + connection_status: ConnectionStatus + + def __init__(self) -> None: + self.streams = None + self.connection_status = ConnectionStatus.NOT_CONNECTED + + +class NotConnectedError(Exception): + """Raised when something tries to make a request to the connector script before a connection has been established""" + pass + + +class RequestFailedError(Exception): + """Raised when the connector script did not respond to a request""" + pass + + +class ConnectorError(Exception): + """Raised when the connector script encounters an error while processing a request""" + pass + + +class SyncError(Exception): + """Raised when the connector script responded with a mismatched response type""" + pass + + +async def connect(ctx: BizHawkContext) -> bool: + """Attempts to establish a connection with the connector script. Returns True if successful.""" + try: + ctx.streams = await asyncio.open_connection("localhost", BIZHAWK_SOCKET_PORT) + ctx.connection_status = ConnectionStatus.TENTATIVE + return True + except (TimeoutError, ConnectionRefusedError): + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + return False + + +def disconnect(ctx: BizHawkContext) -> None: + """Closes the connection to the connector script.""" + if ctx.streams is not None: + ctx.streams[1].close() + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + + +async def get_script_version(ctx: BizHawkContext) -> int: + if ctx.streams is None: + raise NotConnectedError("You tried to send a request before a connection to BizHawk was made") + + try: + reader, writer = ctx.streams + writer.write("VERSION".encode("ascii") + b"\n") + await asyncio.wait_for(writer.drain(), timeout=5) + + version = await asyncio.wait_for(reader.readline(), timeout=5) + + if version == b"": + writer.close() + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection closed") + + return int(version.decode("ascii")) + except asyncio.TimeoutError as exc: + writer.close() + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection timed out") from exc + except ConnectionResetError as exc: + writer.close() + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection reset") from exc + + +async def send_requests(ctx: BizHawkContext, req_list: typing.List[typing.Dict[str, typing.Any]]) -> typing.List[typing.Dict[str, typing.Any]]: + """Sends a list of requests to the BizHawk connector and returns their responses. + + It's likely you want to use the wrapper functions instead of this.""" + if ctx.streams is None: + raise NotConnectedError("You tried to send a request before a connection to BizHawk was made") + + try: + reader, writer = ctx.streams + writer.write(json.dumps(req_list).encode("utf-8") + b"\n") + await asyncio.wait_for(writer.drain(), timeout=5) + + res = await asyncio.wait_for(reader.readline(), timeout=5) + + if res == b"": + writer.close() + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection closed") + + if ctx.connection_status == ConnectionStatus.TENTATIVE: + ctx.connection_status = ConnectionStatus.CONNECTED + + ret = json.loads(res.decode("utf-8")) + for response in ret: + if response["type"] == "ERROR": + raise ConnectorError(response["err"]) + + return ret + except asyncio.TimeoutError as exc: + writer.close() + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection timed out") from exc + except ConnectionResetError as exc: + writer.close() + ctx.streams = None + ctx.connection_status = ConnectionStatus.NOT_CONNECTED + raise RequestFailedError("Connection reset") from exc + + +async def ping(ctx: BizHawkContext) -> None: + """Sends a PING request and receives a PONG response.""" + res = (await send_requests(ctx, [{"type": "PING"}]))[0] + + if res["type"] != "PONG": + raise SyncError(f"Expected response of type PONG but got {res['type']}") + + +async def get_hash(ctx: BizHawkContext) -> str: + """Gets the system name for the currently loaded ROM""" + res = (await send_requests(ctx, [{"type": "HASH"}]))[0] + + if res["type"] != "HASH_RESPONSE": + raise SyncError(f"Expected response of type HASH_RESPONSE but got {res['type']}") + + return res["value"] + + +async def get_system(ctx: BizHawkContext) -> str: + """Gets the system name for the currently loaded ROM""" + res = (await send_requests(ctx, [{"type": "SYSTEM"}]))[0] + + if res["type"] != "SYSTEM_RESPONSE": + raise SyncError(f"Expected response of type SYSTEM_RESPONSE but got {res['type']}") + + return res["value"] + + +async def get_cores(ctx: BizHawkContext) -> typing.Dict[str, str]: + """Gets the preferred cores for systems with multiple cores. Only systems with multiple available cores have + entries.""" + res = (await send_requests(ctx, [{"type": "PREFERRED_CORES"}]))[0] + + if res["type"] != "PREFERRED_CORES_RESPONSE": + raise SyncError(f"Expected response of type PREFERRED_CORES_RESPONSE but got {res['type']}") + + return res["value"] + + +async def lock(ctx: BizHawkContext) -> None: + """Locks BizHawk in anticipation of receiving more requests this frame. + + Consider using guarded reads and writes instead of locks if possible. + + While locked, emulation will halt and the connector will block on incoming requests until an `UNLOCK` request is + sent. Remember to unlock when you're done, or the emulator will appear to freeze. + + Sending multiple lock commands is the same as sending one.""" + res = (await send_requests(ctx, [{"type": "LOCK"}]))[0] + + if res["type"] != "LOCKED": + raise SyncError(f"Expected response of type LOCKED but got {res['type']}") + + +async def unlock(ctx: BizHawkContext) -> None: + """Unlocks BizHawk to allow it to resume emulation. See `lock` for more info. + + Sending multiple unlock commands is the same as sending one.""" + res = (await send_requests(ctx, [{"type": "UNLOCK"}]))[0] + + if res["type"] != "UNLOCKED": + raise SyncError(f"Expected response of type UNLOCKED but got {res['type']}") + + +async def display_message(ctx: BizHawkContext, message: str) -> None: + """Displays the provided message in BizHawk's message queue.""" + res = (await send_requests(ctx, [{"type": "DISPLAY_MESSAGE", "message": message}]))[0] + + if res["type"] != "DISPLAY_MESSAGE_RESPONSE": + raise SyncError(f"Expected response of type DISPLAY_MESSAGE_RESPONSE but got {res['type']}") + + +async def set_message_interval(ctx: BizHawkContext, value: float) -> None: + """Sets the minimum amount of time in seconds to wait between queued messages. The default value of 0 will allow one + new message to display per frame.""" + res = (await send_requests(ctx, [{"type": "SET_MESSAGE_INTERVAL", "value": value}]))[0] + + if res["type"] != "SET_MESSAGE_INTERVAL_RESPONSE": + raise SyncError(f"Expected response of type SET_MESSAGE_INTERVAL_RESPONSE but got {res['type']}") + + +async def guarded_read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]], + guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> typing.Optional[typing.List[bytes]]: + """Reads an array of bytes at 1 or more addresses if and only if every byte in guard_list matches its expected + value. + + Items in read_list should be organized (address, size, domain) where + - `address` is the address of the first byte of data + - `size` is the number of bytes to read + - `domain` is the name of the region of memory the address corresponds to + + Items in `guard_list` should be organized `(address, expected_data, domain)` where + - `address` is the address of the first byte of data + - `expected_data` is the bytes that the data starting at this address is expected to match + - `domain` is the name of the region of memory the address corresponds to + + Returns None if any item in guard_list failed to validate. Otherwise returns a list of bytes in the order they + were requested.""" + res = await send_requests(ctx, [{ + "type": "GUARD", + "address": address, + "expected_data": base64.b64encode(bytes(expected_data)).decode("ascii"), + "domain": domain + } for address, expected_data, domain in guard_list] + [{ + "type": "READ", + "address": address, + "size": size, + "domain": domain + } for address, size, domain in read_list]) + + ret: typing.List[bytes] = [] + for item in res: + if item["type"] == "GUARD_RESPONSE": + if not item["value"]: + return None + else: + if item["type"] != "READ_RESPONSE": + raise SyncError(f"Expected response of type READ_RESPONSE or GUARD_RESPONSE but got {res['type']}") + + ret.append(base64.b64decode(item["value"])) + + return ret + + +async def read(ctx: BizHawkContext, read_list: typing.List[typing.Tuple[int, int, str]]) -> typing.List[bytes]: + """Reads data at 1 or more addresses. + + Items in `read_list` should be organized `(address, size, domain)` where + - `address` is the address of the first byte of data + - `size` is the number of bytes to read + - `domain` is the name of the region of memory the address corresponds to + + Returns a list of bytes in the order they were requested.""" + return await guarded_read(ctx, read_list, []) + + +async def guarded_write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]], + guard_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> bool: + """Writes data to 1 or more addresses if and only if every byte in guard_list matches its expected value. + + Items in `write_list` should be organized `(address, value, domain)` where + - `address` is the address of the first byte of data + - `value` is a list of bytes to write, in order, starting at `address` + - `domain` is the name of the region of memory the address corresponds to + + Items in `guard_list` should be organized `(address, expected_data, domain)` where + - `address` is the address of the first byte of data + - `expected_data` is the bytes that the data starting at this address is expected to match + - `domain` is the name of the region of memory the address corresponds to + + Returns False if any item in guard_list failed to validate. Otherwise returns True.""" + res = await send_requests(ctx, [{ + "type": "GUARD", + "address": address, + "expected_data": base64.b64encode(bytes(expected_data)).decode("ascii"), + "domain": domain + } for address, expected_data, domain in guard_list] + [{ + "type": "WRITE", + "address": address, + "value": base64.b64encode(bytes(value)).decode("ascii"), + "domain": domain + } for address, value, domain in write_list]) + + for item in res: + if item["type"] == "GUARD_RESPONSE": + if not item["value"]: + return False + else: + if item["type"] != "WRITE_RESPONSE": + raise SyncError(f"Expected response of type WRITE_RESPONSE or GUARD_RESPONSE but got {res['type']}") + + return True + + +async def write(ctx: BizHawkContext, write_list: typing.List[typing.Tuple[int, typing.Iterable[int], str]]) -> None: + """Writes data to 1 or more addresses. + + Items in write_list should be organized `(address, value, domain)` where + - `address` is the address of the first byte of data + - `value` is a list of bytes to write, in order, starting at `address` + - `domain` is the name of the region of memory the address corresponds to""" + await guarded_write(ctx, write_list, []) diff --git a/worlds/_bizhawk/client.py b/worlds/_bizhawk/client.py new file mode 100644 index 0000000000..b614c083ba --- /dev/null +++ b/worlds/_bizhawk/client.py @@ -0,0 +1,87 @@ +""" +A module containing the BizHawkClient base class and metaclass +""" + + +from __future__ import annotations + +import abc +from typing import TYPE_CHECKING, Any, ClassVar, Dict, Optional, Tuple, Union + +from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components, launch_subprocess + +if TYPE_CHECKING: + from .context import BizHawkClientContext +else: + BizHawkClientContext = object + + +class AutoBizHawkClientRegister(abc.ABCMeta): + game_handlers: ClassVar[Dict[Tuple[str, ...], Dict[str, BizHawkClient]]] = {} + + def __new__(cls, name: str, bases: Tuple[type, ...], namespace: Dict[str, Any]) -> AutoBizHawkClientRegister: + new_class = super().__new__(cls, name, bases, namespace) + + if "system" in namespace: + systems = (namespace["system"],) if type(namespace["system"]) is str else tuple(sorted(namespace["system"])) + if systems not in AutoBizHawkClientRegister.game_handlers: + AutoBizHawkClientRegister.game_handlers[systems] = {} + + if "game" in namespace: + AutoBizHawkClientRegister.game_handlers[systems][namespace["game"]] = new_class() + + return new_class + + @staticmethod + async def get_handler(ctx: BizHawkClientContext, system: str) -> Optional[BizHawkClient]: + for systems, handlers in AutoBizHawkClientRegister.game_handlers.items(): + if system in systems: + for handler in handlers.values(): + if await handler.validate_rom(ctx): + return handler + + return None + + +class BizHawkClient(abc.ABC, metaclass=AutoBizHawkClientRegister): + system: ClassVar[Union[str, Tuple[str, ...]]] + """The system that the game this client is for runs on""" + + game: ClassVar[str] + """The game this client is for""" + + @abc.abstractmethod + async def validate_rom(self, ctx: BizHawkClientContext) -> bool: + """Should return whether the currently loaded ROM should be handled by this client. You might read the game name + from the ROM header, for example. This function will only be asked to validate ROMs from the system set by the + client class, so you do not need to check the system yourself. + + Once this function has determined that the ROM should be handled by this client, it should also modify `ctx` + as necessary (such as setting `ctx.game = self.game`, modifying `ctx.items_handling`, etc...).""" + ... + + async def set_auth(self, ctx: BizHawkClientContext) -> None: + """Should set ctx.auth in anticipation of sending a `Connected` packet. You may override this if you store slot + name in your patched ROM. If ctx.auth is not set after calling, the player will be prompted to enter their + username.""" + pass + + @abc.abstractmethod + async def game_watcher(self, ctx: BizHawkClientContext) -> None: + """Runs on a loop with the approximate interval `ctx.watcher_timeout`. The currently loaded ROM is guaranteed + to have passed your validator when this function is called, and the emulator is very likely to be connected.""" + ... + + def on_package(self, ctx: BizHawkClientContext, cmd: str, args: dict) -> None: + """For handling packages from the server. Called from `BizHawkClientContext.on_package`.""" + pass + + +def launch_client(*args) -> None: + from .context import launch + launch_subprocess(launch, name="BizHawkClient") + + +if not any(component.script_name == "BizHawkClient" for component in components): + components.append(Component("BizHawk Client", "BizHawkClient", component_type=Type.CLIENT, func=launch_client, + file_identifier=SuffixIdentifier())) diff --git a/worlds/_bizhawk/context.py b/worlds/_bizhawk/context.py new file mode 100644 index 0000000000..6e53b370af --- /dev/null +++ b/worlds/_bizhawk/context.py @@ -0,0 +1,188 @@ +""" +A module containing context and functions relevant to running the client. This module should only be imported for type +checking or launching the client, otherwise it will probably cause circular import issues. +""" + + +import asyncio +import traceback +from typing import Any, Dict, Optional + +from CommonClient import CommonContext, ClientCommandProcessor, get_base_parser, server_loop, logger, gui_enabled +import Patch +import Utils + +from . import BizHawkContext, ConnectionStatus, RequestFailedError, connect, disconnect, get_hash, get_script_version, \ + get_system, ping +from .client import BizHawkClient, AutoBizHawkClientRegister + + +EXPECTED_SCRIPT_VERSION = 1 + + +class BizHawkClientCommandProcessor(ClientCommandProcessor): + def _cmd_bh(self): + """Shows the current status of the client's connection to BizHawk""" + if isinstance(self.ctx, BizHawkClientContext): + if self.ctx.bizhawk_ctx.connection_status == ConnectionStatus.NOT_CONNECTED: + logger.info("BizHawk Connection Status: Not Connected") + elif self.ctx.bizhawk_ctx.connection_status == ConnectionStatus.TENTATIVE: + logger.info("BizHawk Connection Status: Tentatively Connected") + elif self.ctx.bizhawk_ctx.connection_status == ConnectionStatus.CONNECTED: + logger.info("BizHawk Connection Status: Connected") + + +class BizHawkClientContext(CommonContext): + command_processor = BizHawkClientCommandProcessor + client_handler: Optional[BizHawkClient] + slot_data: Optional[Dict[str, Any]] = None + rom_hash: Optional[str] = None + bizhawk_ctx: BizHawkContext + + watcher_timeout: float + """The maximum amount of time the game watcher loop will wait for an update from the server before executing""" + + def __init__(self, server_address: Optional[str], password: Optional[str]): + super().__init__(server_address, password) + self.client_handler = None + self.bizhawk_ctx = BizHawkContext() + self.watcher_timeout = 0.5 + + def run_gui(self): + from kvui import GameManager + + class BizHawkManager(GameManager): + base_title = "Archipelago BizHawk Client" + + self.ui = BizHawkManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + def on_package(self, cmd, args): + if cmd == "Connected": + self.slot_data = args.get("slot_data", None) + + if self.client_handler is not None: + self.client_handler.on_package(self, cmd, args) + + +async def _game_watcher(ctx: BizHawkClientContext): + showed_connecting_message = False + showed_connected_message = False + showed_no_handler_message = False + + while not ctx.exit_event.is_set(): + try: + await asyncio.wait_for(ctx.watcher_event.wait(), ctx.watcher_timeout) + except asyncio.TimeoutError: + pass + + ctx.watcher_event.clear() + + try: + if ctx.bizhawk_ctx.connection_status == ConnectionStatus.NOT_CONNECTED: + showed_connected_message = False + + if not showed_connecting_message: + logger.info("Waiting to connect to BizHawk...") + showed_connecting_message = True + + if not await connect(ctx.bizhawk_ctx): + continue + + showed_no_handler_message = False + + script_version = await get_script_version(ctx.bizhawk_ctx) + + if script_version != EXPECTED_SCRIPT_VERSION: + logger.info(f"Connector script is incompatible. Expected version {EXPECTED_SCRIPT_VERSION} but got {script_version}. Disconnecting.") + disconnect(ctx.bizhawk_ctx) + continue + + showed_connecting_message = False + + await ping(ctx.bizhawk_ctx) + + if not showed_connected_message: + showed_connected_message = True + logger.info("Connected to BizHawk") + + rom_hash = await get_hash(ctx.bizhawk_ctx) + if ctx.rom_hash is not None and ctx.rom_hash != rom_hash: + if ctx.server is not None: + logger.info(f"ROM changed. Disconnecting from server.") + await ctx.disconnect(True) + + ctx.auth = None + ctx.username = None + ctx.rom_hash = rom_hash + + if ctx.client_handler is None: + system = await get_system(ctx.bizhawk_ctx) + ctx.client_handler = await AutoBizHawkClientRegister.get_handler(ctx, system) + + if ctx.client_handler is None: + if not showed_no_handler_message: + logger.info("No handler was found for this game") + showed_no_handler_message = True + continue + else: + showed_no_handler_message = False + logger.info(f"Running handler for {ctx.client_handler.game}") + + except RequestFailedError as exc: + logger.info(f"Lost connection to BizHawk: {exc.args[0]}") + continue + + # Get slot name and send `Connect` + if ctx.server is not None and ctx.username is None: + await ctx.client_handler.set_auth(ctx) + + if ctx.auth is None: + await ctx.get_username() + + await ctx.send_connect() + + await ctx.client_handler.game_watcher(ctx) + + +async def _run_game(rom: str): + import webbrowser + webbrowser.open(rom) + + +async def _patch_and_run_game(patch_file: str): + metadata, output_file = Patch.create_rom_file(patch_file) + Utils.async_start(_run_game(output_file)) + + +def launch() -> None: + async def main(): + parser = get_base_parser() + parser.add_argument("patch_file", default="", type=str, nargs="?", help="Path to an Archipelago patch file") + args = parser.parse_args() + + ctx = BizHawkClientContext(args.connect, args.password) + ctx.server_task = asyncio.create_task(server_loop(ctx), name="ServerLoop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + if args.patch_file != "": + Utils.async_start(_patch_and_run_game(args.patch_file)) + + watcher_task = asyncio.create_task(_game_watcher(ctx), name="GameWatcher") + + try: + await watcher_task + except Exception as e: + logger.error("".join(traceback.format_exception(e))) + + await ctx.exit_event.wait() + await ctx.shutdown() + + Utils.init_logging("BizHawkClient", exception_logger="Client") + import colorama + colorama.init() + asyncio.run(main()) + colorama.deinit() From 6b48f9aac55b672fa17c91abacc58f3c72070a24 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 3 Oct 2023 02:52:58 +0200 Subject: [PATCH 468/489] WebHost: link to stats from the use statistics directly on landing (#2242) --- WebHostLib/static/styles/landing.css | 3 --- WebHostLib/templates/landing.html | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/WebHostLib/static/styles/landing.css b/WebHostLib/static/styles/landing.css index 202c43badd..96975553c1 100644 --- a/WebHostLib/static/styles/landing.css +++ b/WebHostLib/static/styles/landing.css @@ -235,9 +235,6 @@ html{ line-height: 30px; } -#landing .variable{ - color: #ffff00; -} .landing-deco{ position: absolute; diff --git a/WebHostLib/templates/landing.html b/WebHostLib/templates/landing.html index fd45b78cfb..b489ef18ac 100644 --- a/WebHostLib/templates/landing.html +++ b/WebHostLib/templates/landing.html @@ -49,9 +49,9 @@ our crazy idea into a reality.

    - {{ seeds }} + {{ seeds }} games were generated and - {{ rooms }} + {{ rooms }} were hosted in the last 7 days.

  • From cdbb2cf7b769eb6614d63bc2233e1ba01635f5dc Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 3 Oct 2023 09:47:22 +0200 Subject: [PATCH 469/489] Core: fix unittest world discovery (#2262) --- test/__init__.py | 3 ++- test/worlds/__init__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index 03716a10d7..37ebe3f627 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -13,5 +13,6 @@ ModuleUpdate.update_ran = True # don't upgrade import Utils -Utils.local_path.cached_path = pathlib.Path(__file__).parent.parent +file_path = pathlib.Path(__file__).parent.parent +Utils.local_path.cached_path = file_path Utils.user_path() # initialize cached_path diff --git a/test/worlds/__init__.py b/test/worlds/__init__.py index d1817cc674..cf396111bf 100644 --- a/test/worlds/__init__.py +++ b/test/worlds/__init__.py @@ -1,7 +1,7 @@ def load_tests(loader, standard_tests, pattern): import os import unittest - from ..TestBase import file_path + from .. import file_path from worlds.AutoWorld import AutoWorldRegister suite = unittest.TestSuite() From 78057476f309c3c012b5534b83a2d25c6b3b2a42 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Tue, 3 Oct 2023 05:19:09 -0500 Subject: [PATCH 470/489] Docs: python 3.11 works now (#2258) * Docs: python 3.11 works now * change to py 3.12 unsupported --- docs/running from source.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/running from source.md b/docs/running from source.md index c0f4bf5802..b7367308d8 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -8,7 +8,7 @@ use that version. These steps are for developers or platforms without compiled r What you'll need: * [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version - * **Python 3.11 does not work currently** + * **Python 3.12 is currently unsupported** * pip: included in downloads from python.org, separate in many Linux distributions * Matching C compiler * possibly optional, read operating system specific sections @@ -30,7 +30,7 @@ After this, you should be able to run the programs. Recommended steps * Download and install a "Windows installer (64-bit)" from the [Python download page](https://www.python.org/downloads) - * **Python 3.11 does not work currently** + * **Python 3.12 is currently unsupported** * **Optional**: Download and install Visual Studio Build Tools from [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/). From f6e92a18de0b5cccd8a5b439f951c24ebfff6489 Mon Sep 17 00:00:00 2001 From: Aaron Wagener Date: Wed, 4 Oct 2023 11:23:29 -0500 Subject: [PATCH 471/489] The Messenger: Fix items accessibility region rule (#2263) --- worlds/messenger/Rules.py | 3 ++- worlds/messenger/test/TestAccess.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/worlds/messenger/Rules.py b/worlds/messenger/Rules.py index c24f60fbaa..664bf5f6d7 100644 --- a/worlds/messenger/Rules.py +++ b/worlds/messenger/Rules.py @@ -263,5 +263,6 @@ def set_self_locking_items(multiworld: MultiWorld, player: int) -> None: allow_self_locking_items(multiworld.get_location("Elemental Skylands Seal - Water", player), "Currents Master") # add these locations when seals and shards aren't shuffled elif not multiworld.shuffle_shards[player]: - allow_self_locking_items(multiworld.get_region("Cloud Ruins Right", player), "Ruxxtin's Amulet") + for entrance in multiworld.get_region("Cloud Ruins", player).entrances: + entrance.access_rule = lambda state: state.has("Wingsuit", player) or state.has("Rope Dart", player) allow_self_locking_items(multiworld.get_region("Forlorn Temple", player), *PHOBEKINS) diff --git a/worlds/messenger/test/TestAccess.py b/worlds/messenger/test/TestAccess.py index 452ed1189f..e95a81c5c1 100644 --- a/worlds/messenger/test/TestAccess.py +++ b/worlds/messenger/test/TestAccess.py @@ -163,6 +163,8 @@ class ItemsAccessTest(MessengerTestBase): "Forlorn Temple - Demon King": PHOBEKINS } + self.multiworld.state = self.multiworld.get_all_state(True) + self.remove_by_name(location_lock_pairs.values()) for loc in location_lock_pairs: for item_name in location_lock_pairs[loc]: item = self.get_item_by_name(item_name) From 6c4a3959c36a47863ec2a7ef1d9fe51d51dc9733 Mon Sep 17 00:00:00 2001 From: Nicholas Saylor <79181893+nicholassaylor@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:52:34 -0400 Subject: [PATCH 472/489] Docs: Categorize Commands in Guide (#2213) * Update commands_en.md Commands re-ordered and put into categories Some commands were better documented / explained more clearly Other formatting changes * Status command moved to General category and elaboration on getitem command * "Multi-world" -> "Multiworld" * Moved game-specific local commands to game pages --- worlds/factorio/docs/en_Factorio.md | 6 ++ worlds/ff1/docs/en_Final Fantasy.md | 5 + worlds/generic/docs/commands_en.md | 160 +++++++++++++++------------- 3 files changed, 97 insertions(+), 74 deletions(-) diff --git a/worlds/factorio/docs/en_Factorio.md b/worlds/factorio/docs/en_Factorio.md index 61bceb3820..dbc33d05df 100644 --- a/worlds/factorio/docs/en_Factorio.md +++ b/worlds/factorio/docs/en_Factorio.md @@ -42,3 +42,9 @@ depositing excess energy and supplementing energy deficits, much like Accumulato Each placed EnergyLink Bridge provides 10 MW of throughput. The shared storage has unlimited capacity, but 25% of energy is lost during depositing. The amount of energy currently in the shared storage is displayed in the Archipelago client. It can also be queried by typing `/energy-link` in-game. + +## Unique Local Commands +The following commands are only available when using the FactorioClient to play Factorio with Archipelago. + +- `/factorio ` Sends the command argument to the Factorio server as a command. +- `/energy-link` Displays the amount of energy currently in shared storage for EnergyLink diff --git a/worlds/ff1/docs/en_Final Fantasy.md b/worlds/ff1/docs/en_Final Fantasy.md index 29d4d29f80..8962919743 100644 --- a/worlds/ff1/docs/en_Final Fantasy.md +++ b/worlds/ff1/docs/en_Final Fantasy.md @@ -24,3 +24,8 @@ All items can appear in other players worlds, including consumables, shards, wea All local and remote items appear the same. Final Fantasy will say that you received an item, then BOTH the client log and the emulator will display what was found external to the in-game text box. + +## Unique Local Commands +The following command is only available when using the FF1Client for the Final Fantasy Randomizer. + +- `/nes` Shows the current status of the NES connection. diff --git a/worlds/generic/docs/commands_en.md b/worlds/generic/docs/commands_en.md index e52ea20fd2..3e7c0bd4bd 100644 --- a/worlds/generic/docs/commands_en.md +++ b/worlds/generic/docs/commands_en.md @@ -1,96 +1,108 @@ -### Helpful Commands +# Helpful Commands Commands are split into two types: client commands and server commands. Client commands are commands which are executed by the client and do not affect the Archipelago remote session. Server commands are commands which are executed by the Archipelago server and affect the Archipelago session or otherwise provide feedback from the server. -In clients which have their own commands the commands are typically prepended by a forward slash:`/`. Remote commands -are always submitted to the server prepended with an exclamation point: `!`. +In clients which have their own commands the commands are typically prepended by a forward slash: `/`. -#### Local Commands +Server commands are always submitted to the server prepended with an exclamation point: `!`.
    -The following list is a list of client commands which may be available to you through your Archipelago client. You +# Server Commands + +Server commands may be executed by any client which allows for sending text chat to the Archipelago server. If your +client does not allow for sending chat then you may connect to your game slot with the TextClient which comes with the +Archipelago installation. In order to execute the command you need to merely send a text message with the command, +including the exclamation point. + +### General +- `!help` Returns a listing of available commands. +- `!license` Returns the software licensing information. +- `!options` Returns the current server options, including password in plaintext. +- `!players` Returns info about the currently connected and non-connected players. +- `!status` Returns information about the connection status and check completion numbers for all players in the current room.
    (Optionally mention a Tag name and get information on who has that Tag. For example: !status DeathLink) + + +### Utilities +- `!countdown ` Starts a countdown using the given seconds value. Useful for synchronizing starts. + Defaults to 10 seconds if no argument is provided. +- `!alias ` Sets your alias, which allows you to use commands with the alias rather than your provided name. +- `!admin ` Executes a command as if you typed it into the server console. Remote administration must be + enabled. + +### Information +- `!remaining` Lists the items remaining in your game, but not where they are or who they go to. +- `!missing` Lists the location checks you are missing from the server's perspective. +- `!checked` Lists all the location checks you've done from the server's perspective. + +### Hints +- `!hint` Lists all hints relevant to your world, the number of points you have for hints, and how much a hint costs. +- `!hint ` Tells you the game world and location your item is in, uses points earned from completing locations. +- `!hint_location ` Tells you what item is in a specific location, uses points earned from completing locations. + +### Collect/Release +- `!collect` Grants you all the remaining items for your world by collecting them from all games. Typically used after +goal completion. +- `!release` Releases all items contained in your world to other worlds. Typically, done automatically by the sever, but +can be configured to allow/require manual usage of this command. + +### Cheats +- `!getitem ` Cheats an item to the currently connected slot, if it is enabled in the server. + + +## Host only (on Archipelago.gg or in your server console) + +### General +- `/help` Returns a list of commands available in the console. +- `/license` Returns the software licensing information. +- `/options` Lists the server's current options, including password in plaintext. +- `/players` List currently connected players. +- `/save` Saves the state of the current multiworld. Note that the server auto-saves on a minute basis. +- `/exit` Shutdown the server + +### Utilities +- `/countdown ` Starts a countdown sent to all players via text chat. Defaults to 10 seconds if no + argument is provided. +- `/option