From 0ac67bfe7617ab0aabf2540d43a5a6c12dc5c5e5 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Fri, 19 Aug 2022 09:02:39 -0400 Subject: [PATCH 01/91] Smz3 early sword fix (#939) --- worlds/smz3/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 732a8b5548..15ac85c7c3 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -590,8 +590,6 @@ class SMZ3World(World): # /* Check Swords option and place as needed */ if self.smz3World.Config.SwordLocation == SwordLocation.Uncle: self.FillItemAtLocation(self.progression, TotalSMZ3Item.ItemType.ProgressiveSword, self.smz3World.GetLocation("Link's Uncle")) - elif self.smz3World.Config.SwordLocation == SwordLocation.Early: - self.FrontFillItemInOwnWorld(self.progression, TotalSMZ3Item.ItemType.ProgressiveSword) # /* Check Morph option and place as needed */ if self.smz3World.Config.MorphLocation == MorphLocation.Original: @@ -599,6 +597,10 @@ class SMZ3World(World): elif self.smz3World.Config.MorphLocation == MorphLocation.Early: self.FrontFillItemInOwnWorld(self.progression, TotalSMZ3Item.ItemType.Morph) + # We do early Sword placement after Morph in case its Original location + if self.smz3World.Config.SwordLocation == SwordLocation.Early: + self.FrontFillItemInOwnWorld(self.progression, TotalSMZ3Item.ItemType.ProgressiveSword) + # /* We place a PB and Super in Sphere 1 to make sure the filler # * doesn't start locking items behind this when there are a # * high chance of the trash fill actually making them available */ From 89ab4aff9cd4aef8e23be76a4bbab99475fe67ee Mon Sep 17 00:00:00 2001 From: TheCondor07 Date: Fri, 19 Aug 2022 13:50:44 -0700 Subject: [PATCH 02/91] SC2: Logic changes and fixes, 6 new locations, 2 removed locations (#933) --- worlds/sc2wol/Items.py | 28 ++++---- worlds/sc2wol/Locations.py | 133 ++++++++++++++++++++++-------------- worlds/sc2wol/LogicMixin.py | 21 ++++-- worlds/sc2wol/Regions.py | 5 +- worlds/sc2wol/__init__.py | 2 +- 5 files changed, 116 insertions(+), 73 deletions(-) diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py index 4ecff7e15f..59b59bc137 100644 --- a/worlds/sc2wol/Items.py +++ b/worlds/sc2wol/Items.py @@ -49,27 +49,27 @@ item_table = { "Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0), "Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1), - "Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2), + "Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler), "Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3), "Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4), "Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5), - "Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6), + "Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6, classification=ItemClassification.filler), "Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7), "Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8), - "Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9), - "Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10), - "Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11), + "Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression), + "Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.progression), + "Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression), "Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler), "Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13), "Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14), "Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15), "U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16), - "G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.filler), + "G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression), "Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler), "Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1), - "Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2), - "Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3), + "Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler), + "Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler), "Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4), "Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5), "Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler), @@ -77,9 +77,9 @@ item_table = { "Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8), "Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9), "Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler), - "Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11), + "Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, classification=ItemClassification.filler), "Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler), - "Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13), + "Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler), "Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14), "Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15), "Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler), @@ -88,7 +88,7 @@ item_table = { "Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler), "Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20), "Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21), - "Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22), + "Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, classification=ItemClassification.progression), "Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23), "330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler), "Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler), @@ -97,12 +97,12 @@ item_table = { "Missile Turret": ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, classification=ItemClassification.progression), "Sensor Tower": ItemData(402 + SC2WOL_ITEM_ID_OFFSET, "Building", 2), - "War Pigs": ItemData(500 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 0), + "War Pigs": ItemData(500 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 0, classification=ItemClassification.progression), "Devil Dogs": ItemData(501 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 1, classification=ItemClassification.filler), "Hammer Securities": ItemData(502 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 2), - "Spartan Company": ItemData(503 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 3), + "Spartan Company": ItemData(503 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 3, classification=ItemClassification.progression), "Siege Breakers": ItemData(504 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 4), - "Hel's Angel": ItemData(505 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 5), + "Hel's Angel": ItemData(505 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 5, classification=ItemClassification.progression), "Dusk Wings": ItemData(506 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 6), "Jackson's Revenge": ItemData(507 + SC2WOL_ITEM_ID_OFFSET, "Mercenary", 7), diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index 92dfb033c0..3425dc7199 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -44,7 +44,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Zero Hour", "Beat Zero Hour", None, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400, - lambda state: state._sc2wol_has_mobile_anti_air(world, player)), + lambda state: state._sc2wol_has_anti_air(world, player)), LocationData("Evacuation", "Evacuation: First Chysalis", SC2WOL_LOC_ID_OFFSET + 401), LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402, lambda state: state._sc2wol_has_common_unit(world, player)), @@ -52,7 +52,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Evacuation", "Beat Evacuation", None, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500, lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501, @@ -63,19 +63,37 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Safe Haven", "Safe Haven: North Nexus", SC2WOL_LOC_ID_OFFSET + 601, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Safe Haven", "Safe Haven: East Nexus", SC2WOL_LOC_ID_OFFSET + 602, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Safe Haven", "Safe Haven: South Nexus", SC2WOL_LOC_ID_OFFSET + 603, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Safe Haven", "Beat Safe Haven", None, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), + LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703, + lambda state: state._sc2wol_has_common_unit(world, player) and + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Haven's Fall", "Beat Haven's Fall", None, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801), LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802), LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803, @@ -101,11 +119,7 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L state._sc2wol_has_anti_air(world, player) and state._sc2wol_has_heavy_defense(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000, - lambda state: state._sc2wol_has_air(world, player)), - LocationData("The Moebius Factor", "The Moebius Factor: 1st Data Core ", SC2WOL_LOC_ID_OFFSET + 1001, - lambda state: True), - LocationData("The Moebius Factor", "The Moebius Factor: 2nd Data Core", SC2WOL_LOC_ID_OFFSET + 1002, - lambda state: state._sc2wol_has_air(world, player)), + lambda state: state._sc2wol_has_air(world, player) and state._sc2wol_has_anti_air(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003, lambda state: state._sc2wol_able_to_rescue(world, player)), LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004, @@ -121,33 +135,46 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("The Moebius Factor", "Beat The Moebius Factor", None, lambda state: state._sc2wol_has_air(world, player)), LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101), - LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102, - lambda state: state._sc2wol_has_common_unit(world, player)), + LocationData("Supernova", "Supernova: North Relic", SC2WOL_LOC_ID_OFFSET + 1102), LocationData("Supernova", "Supernova: South Relic", SC2WOL_LOC_ID_OFFSET + 1103, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Supernova", "Beat Supernova", None, - lambda state: state._sc2wol_has_common_unit(world, player)), + lambda state: state._sc2wol_beats_protoss_deathball(world, player)), LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201), - LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202), + LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202, + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Maw of the Void", "Beat Maw of the Void", None, - lambda state: state.has('Battlecruiser', player) or state.has('Science Vessel', player) and - state._sc2wol_has_air(world, player)), + lambda state: state.has('Battlecruiser', player) or + state._sc2wol_has_air(world, player) and + state._sc2wol_has_competent_anti_air(world, player) and + state.has('Science Vessel', player)), LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301), @@ -157,17 +184,17 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Welcome to the Jungle", "Welcome to the Jungle: Close Relic", SC2WOL_LOC_ID_OFFSET + 1401), LocationData("Welcome to the Jungle", "Welcome to the Jungle: West Relic", SC2WOL_LOC_ID_OFFSET + 1402, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Welcome to the Jungle", "Beat Welcome to the Jungle", None, lambda state: state._sc2wol_has_common_unit(world, player) and - state._sc2wol_has_mobile_anti_air(world, player)), + state._sc2wol_has_competent_anti_air(world, player)), LocationData("Breakout", "Breakout: Victory", SC2WOL_LOC_ID_OFFSET + 1500), LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501), LocationData("Breakout", "Breakout: Siegetank Prison", SC2WOL_LOC_ID_OFFSET + 1502), @@ -180,7 +207,8 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605), LocationData("Ghost of a Chance", "Beat Ghost of a Chance", None), LocationData("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700, - lambda state: state._sc2wol_has_train_killers(world, player)), + lambda state: state._sc2wol_has_train_killers(world, player) and + state._sc2wol_has_anti_air(world, player)), LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701), LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702), LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703), @@ -198,20 +226,20 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Cutthroat", "Beat Cutthroat", None, lambda state: state._sc2wol_has_common_unit(world, player)), LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900, - lambda state: state._sc2wol_has_mobile_anti_air(world, player)), + lambda state: state._sc2wol_has_competent_anti_air(world, player)), LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901), LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902, - lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + lambda state: state._sc2wol_has_competent_anti_air(world, player) and state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Engine of Destruction", "Engine of Destruction: Lab Devourer", SC2WOL_LOC_ID_OFFSET + 1903), LocationData("Engine of Destruction", "Engine of Destruction: North Devourer", SC2WOL_LOC_ID_OFFSET + 1904, - lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + lambda state: state._sc2wol_has_competent_anti_air(world, player) and state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905, - lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + lambda state: state._sc2wol_has_competent_anti_air(world, player) and state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Engine of Destruction", "Beat Engine of Destruction", None, - lambda state: state._sc2wol_has_mobile_anti_air(world, player) and + lambda state: state._sc2wol_has_competent_anti_air(world, player) and state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)), LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000, lambda state: state._sc2wol_has_competent_comp(world, player)), @@ -224,13 +252,19 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004), LocationData("Media Blitz", "Beat Media Blitz", None, lambda state: state._sc2wol_has_competent_comp(world, player)), - LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100), + LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101), - LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102), - LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103), - LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104), - LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105), - LocationData("Piercing the Shroud", "Beat Piercing the Shroud", None), + LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), + LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), + LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), + LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)), + LocationData("Piercing the Shroud", "Beat Piercing the Shroud", None, + lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, 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), LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202), @@ -251,13 +285,12 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L lambda state: state._sc2wol_has_protoss_common_units(world, player)), LocationData("Echoes of the Future", "Beat Echoes of the Future", None, lambda state: state._sc2wol_has_protoss_medium_units(world, player)), - LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500, - lambda state: state._sc2wol_has_protoss_medium_units(world, player)), + LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500), LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501, lambda state: state._sc2wol_has_protoss_medium_units(world, player)), - LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502), - LocationData("In Utter Darkness", "Beat In Utter Darkness", None, - lambda state: state._sc2wol_has_protoss_medium_units(world, player)), + LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502, + lambda state: state._sc2wol_has_protoss_common_units(world, player)), + LocationData("In Utter Darkness", "Beat In Utter Darkness", None), LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600, lambda state: state._sc2wol_has_competent_comp(world, player)), LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601, diff --git a/worlds/sc2wol/LogicMixin.py b/worlds/sc2wol/LogicMixin.py index baf77dc677..7a08142672 100644 --- a/worlds/sc2wol/LogicMixin.py +++ b/worlds/sc2wol/LogicMixin.py @@ -10,16 +10,17 @@ class SC2WoLLogic(LogicMixin): return self.has_any({'Marine', 'Marauder'}, player) def _sc2wol_has_air(self, world: MultiWorld, player: int) -> bool: - return self.has_any({'Viking', 'Wraith', 'Medivac', 'Banshee', 'Hercules'}, player) + return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or \ + self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player) def _sc2wol_has_air_anti_air(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Viking', 'Wraith'}, player) - def _sc2wol_has_mobile_anti_air(self, world: MultiWorld, player: int) -> bool: + def _sc2wol_has_competent_anti_air(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Marine', 'Goliath'}, player) or self._sc2wol_has_air_anti_air(world, player) def _sc2wol_has_anti_air(self, world: MultiWorld, player: int) -> bool: - return self.has('Missile Turret', player) or self._sc2wol_has_mobile_anti_air(world, player) + return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser'}, player) or self._sc2wol_has_competent_anti_air(world, player) def _sc2wol_has_heavy_defense(self, world: MultiWorld, player: int) -> bool: return (self.has_any({'Siege Tank', 'Vulture'}, player) or @@ -28,13 +29,15 @@ class SC2WoLLogic(LogicMixin): def _sc2wol_has_competent_comp(self, world: MultiWorld, player: int) -> bool: return (self.has('Marine', player) or self.has('Marauder', player) and - self._sc2wol_has_mobile_anti_air(world, player)) and self.has_any({'Medivac', 'Medic'}, player) or \ - self.has('Thor', player) or self.has("Banshee", player) and self._sc2wol_has_mobile_anti_air(world, player) or \ - self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(world, player) + self._sc2wol_has_competent_anti_air(world, player)) and self.has_any({'Medivac', 'Medic'}, player) or \ + self.has('Thor', player) or self.has("Banshee", player) and self._sc2wol_has_competent_anti_air(world, player) or \ + self.has('Battlecruiser', player) and self._sc2wol_has_common_unit(world, player) or \ + self.has('Siege Tank', player) and self._sc2wol_has_competent_anti_air(world, player) def _sc2wol_has_train_killers(self, world: MultiWorld, player: int) -> bool: return (self.has_any({'Siege Tank', 'Diamondback'}, player) or - self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)) + self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player) + or self.has('Marauders', player)) def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool: return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) @@ -46,6 +49,10 @@ class SC2WoLLogic(LogicMixin): return self._sc2wol_has_protoss_common_units(world, player) and \ self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player) + def _sc2wol_beats_protoss_deathball(self, world: MultiWorld, player: int) -> bool: + return self.has_any({'Banshee', 'Battlecruiser'}, player) and self._sc2wol_has_competent_anti_air or \ + self._sc2wol_has_competent_comp(world, player) and self._sc2wol_has_air_anti_air(world, player) + def _sc2wol_cleared_missions(self, world: MultiWorld, player: int, mission_count: int) -> bool: return self.has_group("Missions", player, mission_count) diff --git a/worlds/sc2wol/Regions.py b/worlds/sc2wol/Regions.py index 3a00b60401..4e20752982 100644 --- a/worlds/sc2wol/Regions.py +++ b/worlds/sc2wol/Regions.py @@ -200,7 +200,10 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData connect(world, player, names, "Menu", missions[i]) else: connect(world, player, names, missions[connection], missions[i], - (lambda name: (lambda state: state.has(f"Beat {name}", player)))(missions[connection])) + (lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and + state._sc2wol_cleared_missions(world, player, + missions_req))) + (missions[connection], vanilla_shuffle_order[i].number)) connections.append(connection + 1) mission_req_table.update({missions[i]: MissionInfo( diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index 33522569d5..cf3175bd6e 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -33,7 +33,7 @@ class SC2WoLWorld(World): game = "Starcraft 2 Wings of Liberty" web = Starcraft2WoLWebWorld() - data_version = 2 + data_version = 3 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 a074d16297f906763bdbc9019f1bb0f8671907d3 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Sat, 20 Aug 2022 10:46:44 -0400 Subject: [PATCH 03/91] DKC3 v1.1.0 (#938) Features: * KONGsanity option (Collect all KONG letters in each level for a check) * Autosave option * Difficulty option * MERRY option * Handle collected/co-op locations Bugfixes: * Fixed Mekanos softlock * Prevent Brothers Bear giving extra Banana Birds * Fixed Banana Bird Mother check sending prematurely * Fix Logic bug with Krematoa level costs --- worlds/dkc3/Client.py | 52 ++++++--- worlds/dkc3/Locations.py | 53 +++++++++ worlds/dkc3/Names/LocationName.py | 47 +++++++- worlds/dkc3/Options.py | 48 +++++++- worlds/dkc3/Regions.py | 121 +++++++++++++++----- worlds/dkc3/Rom.py | 181 +++++++++++++++++++++++++++++- worlds/dkc3/__init__.py | 15 ++- 7 files changed, 463 insertions(+), 54 deletions(-) diff --git a/worlds/dkc3/Client.py b/worlds/dkc3/Client.py index 8ac20131f3..7ab82187b0 100644 --- a/worlds/dkc3/Client.py +++ b/worlds/dkc3/Client.py @@ -66,7 +66,7 @@ async def dkc3_game_watcher(ctx: Context): return new_checks = [] - from worlds.dkc3.Rom import location_rom_data, item_rom_data + from worlds.dkc3.Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map for loc_id, loc_data in location_rom_data.items(): if loc_id not in ctx.locations_checked: data = await snes_read(ctx, WRAM_START + loc_data[0], 1) @@ -186,22 +186,40 @@ async def dkc3_game_watcher(ctx: Context): # DKC3_TODO: This method of collect should work, however it does not unlock the next level correctly when previous is flagged # Handle Collected Locations - #for loc_id in ctx.checked_locations: - # if loc_id not in ctx.locations_checked: - # loc_data = location_rom_data[loc_id] - # data = await snes_read(ctx, WRAM_START + loc_data[0], 1) - # invert_bit = ((len(loc_data) >= 3) and loc_data[2]) - # if not invert_bit: - # masked_data = data[0] | (1 << loc_data[1]) - # print("Collected Location: ", hex(loc_data[0]), " | ", loc_data[1]) - # snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data])) - # await snes_flush_writes(ctx) - # else: - # masked_data = data[0] & ~(1 << loc_data[1]) - # print("Collected Inverted Location: ", hex(loc_data[0]), " | ", loc_data[1]) - # snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data])) - # await snes_flush_writes(ctx) - # ctx.locations_checked.add(loc_id) + for loc_id in ctx.checked_locations: + if loc_id not in ctx.locations_checked and loc_id not in boss_location_ids: + loc_data = location_rom_data[loc_id] + data = await snes_read(ctx, WRAM_START + loc_data[0], 1) + invert_bit = ((len(loc_data) >= 3) and loc_data[2]) + if not invert_bit: + masked_data = data[0] | (1 << loc_data[1]) + #print("Collected Location: ", hex(loc_data[0]), " | ", loc_data[1]) + snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data])) + + if (loc_data[1] == 1): + # Make the next levels accessible + level_id = loc_data[0] - 0x632 + levels_to_tiles = await snes_read(ctx, ROM_START + 0x3FF800, 0x60) + tiles_to_levels = await snes_read(ctx, ROM_START + 0x3FF860, 0x60) + tile_id = levels_to_tiles[level_id] if levels_to_tiles[level_id] != 0xFF else level_id + tile_id = tile_id + 0x632 + #print("Tile ID: ", hex(tile_id)) + if tile_id in level_unlock_map: + for next_level_address in level_unlock_map[tile_id]: + next_level_id = next_level_address - 0x632 + next_tile_id = tiles_to_levels[next_level_id] if tiles_to_levels[next_level_id] != 0xFF else next_level_id + next_tile_id = next_tile_id + 0x632 + #print("Next Level ID: ", hex(next_tile_id)) + next_data = await snes_read(ctx, WRAM_START + next_tile_id, 1) + snes_buffered_write(ctx, WRAM_START + next_tile_id, bytes([next_data[0] | 0x01])) + + await snes_flush_writes(ctx) + else: + masked_data = data[0] & ~(1 << loc_data[1]) + print("Collected Inverted Location: ", hex(loc_data[0]), " | ", loc_data[1]) + snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data])) + await snes_flush_writes(ctx) + ctx.locations_checked.add(loc_id) # Calculate Boomer Cost Text boomer_cost_text = await snes_read(ctx, WRAM_START + 0xAAFD, 2) diff --git a/worlds/dkc3/Locations.py b/worlds/dkc3/Locations.py index aa8acf729a..e8d5409b15 100644 --- a/worlds/dkc3/Locations.py +++ b/worlds/dkc3/Locations.py @@ -221,6 +221,55 @@ level_location_table = { LocationName.rocket_rush_dk: 0xDC30A0, } +kong_location_table = { + LocationName.lakeside_limbo_kong: 0xDC3100, + LocationName.doorstop_dash_kong: 0xDC3104, + LocationName.tidal_trouble_kong: 0xDC3108, + LocationName.skiddas_row_kong: 0xDC310C, + LocationName.murky_mill_kong: 0xDC3110, + + LocationName.barrel_shield_bust_up_kong: 0xDC3114, + LocationName.riverside_race_kong: 0xDC3118, + LocationName.squeals_on_wheels_kong: 0xDC311C, + LocationName.springin_spiders_kong: 0xDC3120, + LocationName.bobbing_barrel_brawl_kong: 0xDC3124, + + LocationName.bazzas_blockade_kong: 0xDC3128, + LocationName.rocket_barrel_ride_kong: 0xDC312C, + LocationName.kreeping_klasps_kong: 0xDC3130, + LocationName.tracker_barrel_trek_kong: 0xDC3134, + LocationName.fish_food_frenzy_kong: 0xDC3138, + + LocationName.fire_ball_frenzy_kong: 0xDC313C, + LocationName.demolition_drain_pipe_kong: 0xDC3140, + LocationName.ripsaw_rage_kong: 0xDC3144, + LocationName.blazing_bazookas_kong: 0xDC3148, + LocationName.low_g_labyrinth_kong: 0xDC314C, + + LocationName.krevice_kreepers_kong: 0xDC3150, + LocationName.tearaway_toboggan_kong: 0xDC3154, + LocationName.barrel_drop_bounce_kong: 0xDC3158, + LocationName.krack_shot_kroc_kong: 0xDC315C, + LocationName.lemguin_lunge_kong: 0xDC3160, + + LocationName.buzzer_barrage_kong: 0xDC3164, + LocationName.kong_fused_cliffs_kong: 0xDC3168, + LocationName.floodlit_fish_kong: 0xDC316C, + LocationName.pothole_panic_kong: 0xDC3170, + LocationName.ropey_rumpus_kong: 0xDC3174, + + LocationName.konveyor_rope_clash_kong: 0xDC3178, + LocationName.creepy_caverns_kong: 0xDC317C, + LocationName.lightning_lookout_kong: 0xDC3180, + LocationName.koindozer_klamber_kong: 0xDC3184, + LocationName.poisonous_pipeline_kong: 0xDC3188, + + LocationName.stampede_sprint_kong: 0xDC318C, + LocationName.criss_cross_cliffs_kong: 0xDC3191, + LocationName.tyrant_twin_tussle_kong: 0xDC3195, + LocationName.swoopy_salvo_kong: 0xDC319A, +} + boss_location_table = { LocationName.belchas_barn: 0xDC30A1, @@ -266,6 +315,7 @@ all_locations = { **boss_location_table, **secret_cave_location_table, **brothers_bear_location_table, + **kong_location_table, } location_table = {} @@ -277,6 +327,9 @@ def setup_locations(world, player: int): if False:#world.include_trade_sequence[player].value: location_table.update({**brothers_bear_location_table}) + if world.kongsanity[player].value: + location_table.update({**kong_location_table}) + return location_table diff --git a/worlds/dkc3/Names/LocationName.py b/worlds/dkc3/Names/LocationName.py index b3aca3b0f1..f79a25f143 100644 --- a/worlds/dkc3/Names/LocationName.py +++ b/worlds/dkc3/Names/LocationName.py @@ -1,197 +1,236 @@ # Level Definitions lakeside_limbo_flag = "Lakeside Limbo - Flag" +lakeside_limbo_kong = "Lakeside Limbo - KONG" lakeside_limbo_bonus_1 = "Lakeside Limbo - Bonus 1" lakeside_limbo_bonus_2 = "Lakeside Limbo - Bonus 2" lakeside_limbo_dk = "Lakeside Limbo - DK Coin" doorstop_dash_flag = "Doorstop Dash - Flag" +doorstop_dash_kong = "Doorstop Dash - KONG" doorstop_dash_bonus_1 = "Doorstop Dash - Bonus 1" doorstop_dash_bonus_2 = "Doorstop Dash - Bonus 2" doorstop_dash_dk = "Doorstop Dash - DK Coin" tidal_trouble_flag = "Tidal Trouble - Flag" +tidal_trouble_kong = "Tidal Trouble - KONG" tidal_trouble_bonus_1 = "Tidal Trouble - Bonus 1" tidal_trouble_bonus_2 = "Tidal Trouble - Bonus 2" tidal_trouble_dk = "Tidal Trouble - DK Coin" skiddas_row_flag = "Skidda's Row - Flag" +skiddas_row_kong = "Skidda's Row - KONG" skiddas_row_bonus_1 = "Skidda's Row - Bonus 1" skiddas_row_bonus_2 = "Skidda's Row - Bonus 2" skiddas_row_dk = "Skidda's Row - DK Coin" murky_mill_flag = "Murky Mill - Flag" +murky_mill_kong = "Murky Mill - KONG" murky_mill_bonus_1 = "Murky Mill - Bonus 1" murky_mill_bonus_2 = "Murky Mill - Bonus 2" murky_mill_dk = "Murky Mill - DK Coin" barrel_shield_bust_up_flag = "Barrel Shield Bust-Up - Flag" +barrel_shield_bust_up_kong = "Barrel Shield Bust-Up - KONG" barrel_shield_bust_up_bonus_1 = "Barrel Shield Bust-Up - Bonus 1" barrel_shield_bust_up_bonus_2 = "Barrel Shield Bust-Up - Bonus 2" barrel_shield_bust_up_dk = "Barrel Shield Bust-Up - DK Coin" riverside_race_flag = "Riverside Race - Flag" +riverside_race_kong = "Riverside Race - KONG" riverside_race_bonus_1 = "Riverside Race - Bonus 1" riverside_race_bonus_2 = "Riverside Race - Bonus 2" riverside_race_dk = "Riverside Race - DK Coin" squeals_on_wheels_flag = "Squeals On Wheels - Flag" +squeals_on_wheels_kong = "Squeals On Wheels - KONG" squeals_on_wheels_bonus_1 = "Squeals On Wheels - Bonus 1" squeals_on_wheels_bonus_2 = "Squeals On Wheels - Bonus 2" squeals_on_wheels_dk = "Squeals On Wheels - DK Coin" springin_spiders_flag = "Springin' Spiders - Flag" +springin_spiders_kong = "Springin' Spiders - KONG" springin_spiders_bonus_1 = "Springin' Spiders - Bonus 1" springin_spiders_bonus_2 = "Springin' Spiders - Bonus 2" springin_spiders_dk = "Springin' Spiders - DK Coin" bobbing_barrel_brawl_flag = "Bobbing Barrel Brawl - Flag" +bobbing_barrel_brawl_kong = "Bobbing Barrel Brawl - KONG" bobbing_barrel_brawl_bonus_1 = "Bobbing Barrel Brawl - Bonus 1" bobbing_barrel_brawl_bonus_2 = "Bobbing Barrel Brawl - Bonus 2" bobbing_barrel_brawl_dk = "Bobbing Barrel Brawl - DK Coin" bazzas_blockade_flag = "Bazza's Blockade - Flag" +bazzas_blockade_kong = "Bazza's Blockade - KONG" bazzas_blockade_bonus_1 = "Bazza's Blockade - Bonus 1" bazzas_blockade_bonus_2 = "Bazza's Blockade - Bonus 2" bazzas_blockade_dk = "Bazza's Blockade - DK Coin" rocket_barrel_ride_flag = "Rocket Barrel Ride - Flag" +rocket_barrel_ride_kong = "Rocket Barrel Ride - KONG" rocket_barrel_ride_bonus_1 = "Rocket Barrel Ride - Bonus 1" rocket_barrel_ride_bonus_2 = "Rocket Barrel Ride - Bonus 2" rocket_barrel_ride_dk = "Rocket Barrel Ride - DK Coin" kreeping_klasps_flag = "Kreeping Klasps - Flag" +kreeping_klasps_kong = "Kreeping Klasps - KONG" kreeping_klasps_bonus_1 = "Kreeping Klasps - Bonus 1" kreeping_klasps_bonus_2 = "Kreeping Klasps - Bonus 2" kreeping_klasps_dk = "Kreeping Klasps - DK Coin" tracker_barrel_trek_flag = "Tracker Barrel Trek - Flag" +tracker_barrel_trek_kong = "Tracker Barrel Trek - KONG" tracker_barrel_trek_bonus_1 = "Tracker Barrel Trek - Bonus 1" tracker_barrel_trek_bonus_2 = "Tracker Barrel Trek - Bonus 2" tracker_barrel_trek_dk = "Tracker Barrel Trek - DK Coin" fish_food_frenzy_flag = "Fish Food Frenzy - Flag" +fish_food_frenzy_kong = "Fish Food Frenzy - KONG" fish_food_frenzy_bonus_1 = "Fish Food Frenzy - Bonus 1" fish_food_frenzy_bonus_2 = "Fish Food Frenzy - Bonus 2" fish_food_frenzy_dk = "Fish Food Frenzy - DK Coin" fire_ball_frenzy_flag = "Fire-Ball Frenzy - Flag" +fire_ball_frenzy_kong = "Fire-Ball Frenzy - KONG" fire_ball_frenzy_bonus_1 = "Fire-Ball Frenzy - Bonus 1" fire_ball_frenzy_bonus_2 = "Fire-Ball Frenzy - Bonus 2" fire_ball_frenzy_dk = "Fire-Ball Frenzy - DK Coin" demolition_drain_pipe_flag = "Demolition Drain-Pipe - Flag" +demolition_drain_pipe_kong = "Demolition Drain-Pipe - KONG" demolition_drain_pipe_bonus_1 = "Demolition Drain-Pipe - Bonus 1" demolition_drain_pipe_bonus_2 = "Demolition Drain-Pipe - Bonus 2" demolition_drain_pipe_dk = "Demolition Drain-Pipe - DK Coin" ripsaw_rage_flag = "Ripsaw Rage - Flag" +ripsaw_rage_kong = "Ripsaw Rage - KONG" ripsaw_rage_bonus_1 = "Ripsaw Rage - Bonus 1" ripsaw_rage_bonus_2 = "Ripsaw Rage - Bonus 2" ripsaw_rage_dk = "Ripsaw Rage - DK Coin" -blazing_bazookas_flag = "Blazing Bazookas - Flag" -blazing_bazookas_bonus_1 = "Blazing Bazookas - Bonus 1" -blazing_bazookas_bonus_2 = "Blazing Bazookas - Bonus 2" -blazing_bazookas_dk = "Blazing Bazookas - DK Coin" +blazing_bazookas_flag = "Blazing Bazukas - Flag" +blazing_bazookas_kong = "Blazing Bazukas - KONG" +blazing_bazookas_bonus_1 = "Blazing Bazukas - Bonus 1" +blazing_bazookas_bonus_2 = "Blazing Bazukas - Bonus 2" +blazing_bazookas_dk = "Blazing Bazukas - DK Coin" low_g_labyrinth_flag = "Low-G Labyrinth - Flag" +low_g_labyrinth_kong = "Low-G Labyrinth - KONG" low_g_labyrinth_bonus_1 = "Low-G Labyrinth - Bonus 1" low_g_labyrinth_bonus_2 = "Low-G Labyrinth - Bonus 2" low_g_labyrinth_dk = "Low-G Labyrinth - DK Coin" krevice_kreepers_flag = "Krevice Kreepers - Flag" +krevice_kreepers_kong = "Krevice Kreepers - KONG" krevice_kreepers_bonus_1 = "Krevice Kreepers - Bonus 1" krevice_kreepers_bonus_2 = "Krevice Kreepers - Bonus 2" krevice_kreepers_dk = "Krevice Kreepers - DK Coin" tearaway_toboggan_flag = "Tearaway Toboggan - Flag" +tearaway_toboggan_kong = "Tearaway Toboggan - KONG" tearaway_toboggan_bonus_1 = "Tearaway Toboggan - Bonus 1" tearaway_toboggan_bonus_2 = "Tearaway Toboggan - Bonus 2" tearaway_toboggan_dk = "Tearaway Toboggan - DK Coin" barrel_drop_bounce_flag = "Barrel Drop Bounce - Flag" +barrel_drop_bounce_kong = "Barrel Drop Bounce - KONG" barrel_drop_bounce_bonus_1 = "Barrel Drop Bounce - Bonus 1" barrel_drop_bounce_bonus_2 = "Barrel Drop Bounce - Bonus 2" barrel_drop_bounce_dk = "Barrel Drop Bounce - DK Coin" krack_shot_kroc_flag = "Krack-Shot Kroc - Flag" +krack_shot_kroc_kong = "Krack-Shot Kroc - KONG" krack_shot_kroc_bonus_1 = "Krack-Shot Kroc - Bonus 1" krack_shot_kroc_bonus_2 = "Krack-Shot Kroc - Bonus 2" krack_shot_kroc_dk = "Krack-Shot Kroc - DK Coin" lemguin_lunge_flag = "Lemguin Lunge - Flag" +lemguin_lunge_kong = "Lemguin Lunge - KONG" lemguin_lunge_bonus_1 = "Lemguin Lunge - Bonus 1" lemguin_lunge_bonus_2 = "Lemguin Lunge - Bonus 2" lemguin_lunge_dk = "Lemguin Lunge - DK Coin" buzzer_barrage_flag = "Buzzer Barrage - Flag" +buzzer_barrage_kong = "Buzzer Barrage - KONG" buzzer_barrage_bonus_1 = "Buzzer Barrage - Bonus 1" buzzer_barrage_bonus_2 = "Buzzer Barrage - Bonus 2" buzzer_barrage_dk = "Buzzer Barrage - DK Coin" kong_fused_cliffs_flag = "Kong-Fused Cliffs - Flag" +kong_fused_cliffs_kong = "Kong-Fused Cliffs - KONG" kong_fused_cliffs_bonus_1 = "Kong-Fused Cliffs - Bonus 1" kong_fused_cliffs_bonus_2 = "Kong-Fused Cliffs - Bonus 2" kong_fused_cliffs_dk = "Kong-Fused Cliffs - DK Coin" floodlit_fish_flag = "Floodlit Fish - Flag" +floodlit_fish_kong = "Floodlit Fish - KONG" floodlit_fish_bonus_1 = "Floodlit Fish - Bonus 1" floodlit_fish_bonus_2 = "Floodlit Fish - Bonus 2" floodlit_fish_dk = "Floodlit Fish - DK Coin" pothole_panic_flag = "Pothole Panic - Flag" +pothole_panic_kong = "Pothole Panic - KONG" pothole_panic_bonus_1 = "Pothole Panic - Bonus 1" pothole_panic_bonus_2 = "Pothole Panic - Bonus 2" pothole_panic_dk = "Pothole Panic - DK Coin" ropey_rumpus_flag = "Ropey Rumpus - Flag" +ropey_rumpus_kong = "Ropey Rumpus - KONG" ropey_rumpus_bonus_1 = "Ropey Rumpus - Bonus 1" ropey_rumpus_bonus_2 = "Ropey Rumpus - Bonus 2" ropey_rumpus_dk = "Ropey Rumpus - DK Coin" konveyor_rope_clash_flag = "Konveyor Rope Klash - Flag" +konveyor_rope_clash_kong = "Konveyor Rope Klash - KONG" konveyor_rope_clash_bonus_1 = "Konveyor Rope Klash - Bonus 1" konveyor_rope_clash_bonus_2 = "Konveyor Rope Klash - Bonus 2" konveyor_rope_clash_dk = "Konveyor Rope Klash - DK Coin" creepy_caverns_flag = "Creepy Caverns - Flag" +creepy_caverns_kong = "Creepy Caverns - KONG" creepy_caverns_bonus_1 = "Creepy Caverns - Bonus 1" creepy_caverns_bonus_2 = "Creepy Caverns - Bonus 2" creepy_caverns_dk = "Creepy Caverns - DK Coin" lightning_lookout_flag = "Lightning Lookout - Flag" +lightning_lookout_kong = "Lightning Lookout - KONG" lightning_lookout_bonus_1 = "Lightning Lookout - Bonus 1" lightning_lookout_bonus_2 = "Lightning Lookout - Bonus 2" lightning_lookout_dk = "Lightning Lookout - DK Coin" koindozer_klamber_flag = "Koindozer Klamber - Flag" +koindozer_klamber_kong = "Koindozer Klamber - KONG" koindozer_klamber_bonus_1 = "Koindozer Klamber - Bonus 1" koindozer_klamber_bonus_2 = "Koindozer Klamber - Bonus 2" koindozer_klamber_dk = "Koindozer Klamber - DK Coin" poisonous_pipeline_flag = "Poisonous Pipeline - Flag" +poisonous_pipeline_kong = "Poisonous Pipeline - KONG" poisonous_pipeline_bonus_1 = "Poisonous Pipeline - Bonus 1" poisonous_pipeline_bonus_2 = "Poisonous Pipeline - Bonus 2" poisonous_pipeline_dk = "Poisonous Pipeline - DK Coin" stampede_sprint_flag = "Stampede Sprint - Flag" +stampede_sprint_kong = "Stampede Sprint - KONG" stampede_sprint_bonus_1 = "Stampede Sprint - Bonus 1" stampede_sprint_bonus_2 = "Stampede Sprint - Bonus 2" stampede_sprint_bonus_3 = "Stampede Sprint - Bonus 3" stampede_sprint_dk = "Stampede Sprint - DK Coin" criss_cross_cliffs_flag = "Criss Kross Cliffs - Flag" +criss_cross_cliffs_kong = "Criss Kross Cliffs - KONG" criss_cross_cliffs_bonus_1 = "Criss Kross Cliffs - Bonus 1" criss_cross_cliffs_bonus_2 = "Criss Kross Cliffs - Bonus 2" criss_cross_cliffs_dk = "Criss Kross Cliffs - DK Coin" tyrant_twin_tussle_flag = "Tyrant Twin Tussle - Flag" +tyrant_twin_tussle_kong = "Tyrant Twin Tussle - KONG" tyrant_twin_tussle_bonus_1 = "Tyrant Twin Tussle - Bonus 1" tyrant_twin_tussle_bonus_2 = "Tyrant Twin Tussle - Bonus 2" tyrant_twin_tussle_bonus_3 = "Tyrant Twin Tussle - Bonus 3" tyrant_twin_tussle_dk = "Tyrant Twin Tussle - DK Coin" swoopy_salvo_flag = "Swoopy Salvo - Flag" +swoopy_salvo_kong = "Swoopy Salvo - KONG" swoopy_salvo_bonus_1 = "Swoopy Salvo - Bonus 1" swoopy_salvo_bonus_2 = "Swoopy Salvo - Bonus 2" swoopy_salvo_bonus_3 = "Swoopy Salvo - Bonus 3" diff --git a/worlds/dkc3/Options.py b/worlds/dkc3/Options.py index 9e00014933..7c0f532cfc 100644 --- a/worlds/dkc3/Options.py +++ b/worlds/dkc3/Options.py @@ -6,7 +6,7 @@ from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle, O class Goal(Choice): """ Determines the goal of the seed - Knautilus: Reach the Knautilus and defeat Baron K. Roolenstein + Knautilus: Scuttle the Knautilus in Krematoa and defeat Baron K. Roolenstein Banana Bird Hunt: Find a certain number of Banana Birds and rescue their mother """ display_name = "Goal" @@ -75,6 +75,13 @@ class PercentageOfBananaBirds(Range): default = 100 +class KONGsanity(Toggle): + """ + Whether collecting all four KONG letters in each level grants a check + """ + display_name = "KONGsanity" + + class LevelShuffle(Toggle): """ Whether levels are shuffled @@ -82,6 +89,41 @@ class LevelShuffle(Toggle): display_name = "Level Shuffle" +class Difficulty(Choice): + """ + Which Difficulty Level to use + NORML: The Normal Difficulty + HARDR: Many DK Barrels are removed + TUFST: Most DK Barrels and all Midway Barrels are removed + """ + display_name = "Difficulty" + option_norml = 0 + option_hardr = 1 + option_tufst = 2 + default = 0 + + @classmethod + def get_option_name(cls, value) -> str: + if cls.auto_display_name: + return cls.name_lookup[value].upper() + else: + return cls.name_lookup[value] + + +class Autosave(DefaultOnToggle): + """ + Whether the game should autosave after each level + """ + display_name = "Autosave" + + +class MERRY(Toggle): + """ + Whether the Bonus Barrels will be Christmas-themed + """ + display_name = "MERRY" + + class MusicShuffle(Toggle): """ Whether music is shuffled @@ -125,7 +167,11 @@ dkc3_options: typing.Dict[str, type(Option)] = { "percentage_of_extra_bonus_coins": PercentageOfExtraBonusCoins, "number_of_banana_birds": NumberOfBananaBirds, "percentage_of_banana_birds": PercentageOfBananaBirds, + "kongsanity": KONGsanity, "level_shuffle": LevelShuffle, + "difficulty": Difficulty, + "autosave": Autosave, + "merry": MERRY, "music_shuffle": MusicShuffle, "kong_palette_swap": KongPaletteSwap, "starting_life_count": StartingLifeCount, diff --git a/worlds/dkc3/Regions.py b/worlds/dkc3/Regions.py index 501f1a0ea4..e33ff38c15 100644 --- a/worlds/dkc3/Regions.py +++ b/worlds/dkc3/Regions.py @@ -44,6 +44,8 @@ def create_regions(world, player: int, active_locations): LocationName.lakeside_limbo_bonus_2 : [0x657, 3], LocationName.lakeside_limbo_dk : [0x657, 5], } + if world.kongsanity[player]: + lakeside_limbo_region_locations[LocationName.lakeside_limbo_kong] = [] lakeside_limbo_region = create_region(world, player, active_locations, LocationName.lakeside_limbo_region, lakeside_limbo_region_locations, None) @@ -53,6 +55,8 @@ def create_regions(world, player: int, active_locations): LocationName.doorstop_dash_bonus_2 : [0x65A, 3], LocationName.doorstop_dash_dk : [0x65A, 5], } + if world.kongsanity[player]: + doorstop_dash_region_locations[LocationName.doorstop_dash_kong] = [] doorstop_dash_region = create_region(world, player, active_locations, LocationName.doorstop_dash_region, doorstop_dash_region_locations, None) @@ -62,6 +66,8 @@ def create_regions(world, player: int, active_locations): LocationName.tidal_trouble_bonus_2 : [0x659, 3], LocationName.tidal_trouble_dk : [0x659, 5], } + if world.kongsanity[player]: + tidal_trouble_region_locations[LocationName.tidal_trouble_kong] = [] tidal_trouble_region = create_region(world, player, active_locations, LocationName.tidal_trouble_region, tidal_trouble_region_locations, None) @@ -71,6 +77,8 @@ def create_regions(world, player: int, active_locations): LocationName.skiddas_row_bonus_2 : [0x65D, 3], LocationName.skiddas_row_dk : [0x65D, 5], } + if world.kongsanity[player]: + skiddas_row_region_locations[LocationName.skiddas_row_kong] = [] skiddas_row_region = create_region(world, player, active_locations, LocationName.skiddas_row_region, skiddas_row_region_locations, None) @@ -80,6 +88,8 @@ def create_regions(world, player: int, active_locations): LocationName.murky_mill_bonus_2 : [0x65C, 3], LocationName.murky_mill_dk : [0x65C, 5], } + if world.kongsanity[player]: + murky_mill_region_locations[LocationName.murky_mill_kong] = [] murky_mill_region = create_region(world, player, active_locations, LocationName.murky_mill_region, murky_mill_region_locations, None) @@ -89,6 +99,8 @@ def create_regions(world, player: int, active_locations): LocationName.barrel_shield_bust_up_bonus_2 : [0x662, 3], LocationName.barrel_shield_bust_up_dk : [0x662, 5], } + if world.kongsanity[player]: + barrel_shield_bust_up_region_locations[LocationName.barrel_shield_bust_up_kong] = [] barrel_shield_bust_up_region = create_region(world, player, active_locations, LocationName.barrel_shield_bust_up_region, barrel_shield_bust_up_region_locations, None) @@ -98,6 +110,8 @@ def create_regions(world, player: int, active_locations): LocationName.riverside_race_bonus_2 : [0x664, 3], LocationName.riverside_race_dk : [0x664, 5], } + if world.kongsanity[player]: + riverside_race_region_locations[LocationName.riverside_race_kong] = [] riverside_race_region = create_region(world, player, active_locations, LocationName.riverside_race_region, riverside_race_region_locations, None) @@ -107,6 +121,8 @@ def create_regions(world, player: int, active_locations): LocationName.squeals_on_wheels_bonus_2 : [0x65B, 3], LocationName.squeals_on_wheels_dk : [0x65B, 5], } + if world.kongsanity[player]: + squeals_on_wheels_region_locations[LocationName.squeals_on_wheels_kong] = [] squeals_on_wheels_region = create_region(world, player, active_locations, LocationName.squeals_on_wheels_region, squeals_on_wheels_region_locations, None) @@ -116,6 +132,8 @@ def create_regions(world, player: int, active_locations): LocationName.springin_spiders_bonus_2 : [0x661, 3], LocationName.springin_spiders_dk : [0x661, 5], } + if world.kongsanity[player]: + springin_spiders_region_locations[LocationName.springin_spiders_kong] = [] springin_spiders_region = create_region(world, player, active_locations, LocationName.springin_spiders_region, springin_spiders_region_locations, None) @@ -125,6 +143,8 @@ def create_regions(world, player: int, active_locations): LocationName.bobbing_barrel_brawl_bonus_2 : [0x666, 3], LocationName.bobbing_barrel_brawl_dk : [0x666, 5], } + if world.kongsanity[player]: + bobbing_barrel_brawl_region_locations[LocationName.bobbing_barrel_brawl_kong] = [] bobbing_barrel_brawl_region = create_region(world, player, active_locations, LocationName.bobbing_barrel_brawl_region, bobbing_barrel_brawl_region_locations, None) @@ -134,6 +154,8 @@ def create_regions(world, player: int, active_locations): LocationName.bazzas_blockade_bonus_2 : [0x667, 3], LocationName.bazzas_blockade_dk : [0x667, 5], } + if world.kongsanity[player]: + bazzas_blockade_region_locations[LocationName.bazzas_blockade_kong] = [] bazzas_blockade_region = create_region(world, player, active_locations, LocationName.bazzas_blockade_region, bazzas_blockade_region_locations, None) @@ -143,6 +165,8 @@ def create_regions(world, player: int, active_locations): LocationName.rocket_barrel_ride_bonus_2 : [0x66A, 3], LocationName.rocket_barrel_ride_dk : [0x66A, 5], } + if world.kongsanity[player]: + rocket_barrel_ride_region_locations[LocationName.rocket_barrel_ride_kong] = [] rocket_barrel_ride_region = create_region(world, player, active_locations, LocationName.rocket_barrel_ride_region, rocket_barrel_ride_region_locations, None) @@ -152,6 +176,8 @@ def create_regions(world, player: int, active_locations): LocationName.kreeping_klasps_bonus_2 : [0x658, 3], LocationName.kreeping_klasps_dk : [0x658, 5], } + if world.kongsanity[player]: + kreeping_klasps_region_locations[LocationName.kreeping_klasps_kong] = [] kreeping_klasps_region = create_region(world, player, active_locations, LocationName.kreeping_klasps_region, kreeping_klasps_region_locations, None) @@ -161,6 +187,8 @@ def create_regions(world, player: int, active_locations): LocationName.tracker_barrel_trek_bonus_2 : [0x66B, 3], LocationName.tracker_barrel_trek_dk : [0x66B, 5], } + if world.kongsanity[player]: + tracker_barrel_trek_region_locations[LocationName.tracker_barrel_trek_kong] = [] tracker_barrel_trek_region = create_region(world, player, active_locations, LocationName.tracker_barrel_trek_region, tracker_barrel_trek_region_locations, None) @@ -170,6 +198,8 @@ def create_regions(world, player: int, active_locations): LocationName.fish_food_frenzy_bonus_2 : [0x668, 3], LocationName.fish_food_frenzy_dk : [0x668, 5], } + if world.kongsanity[player]: + fish_food_frenzy_region_locations[LocationName.fish_food_frenzy_kong] = [] fish_food_frenzy_region = create_region(world, player, active_locations, LocationName.fish_food_frenzy_region, fish_food_frenzy_region_locations, None) @@ -179,6 +209,8 @@ def create_regions(world, player: int, active_locations): LocationName.fire_ball_frenzy_bonus_2 : [0x66D, 3], LocationName.fire_ball_frenzy_dk : [0x66D, 5], } + if world.kongsanity[player]: + fire_ball_frenzy_region_locations[LocationName.fire_ball_frenzy_kong] = [] fire_ball_frenzy_region = create_region(world, player, active_locations, LocationName.fire_ball_frenzy_region, fire_ball_frenzy_region_locations, None) @@ -188,6 +220,8 @@ def create_regions(world, player: int, active_locations): LocationName.demolition_drain_pipe_bonus_2 : [0x672, 3], LocationName.demolition_drain_pipe_dk : [0x672, 5], } + if world.kongsanity[player]: + demolition_drain_pipe_region_locations[LocationName.demolition_drain_pipe_kong] = [] demolition_drain_pipe_region = create_region(world, player, active_locations, LocationName.demolition_drain_pipe_region, demolition_drain_pipe_region_locations, None) @@ -197,6 +231,8 @@ def create_regions(world, player: int, active_locations): LocationName.ripsaw_rage_bonus_2 : [0x660, 3], LocationName.ripsaw_rage_dk : [0x660, 5], } + if world.kongsanity[player]: + ripsaw_rage_region_locations[LocationName.ripsaw_rage_kong] = [] ripsaw_rage_region = create_region(world, player, active_locations, LocationName.ripsaw_rage_region, ripsaw_rage_region_locations, None) @@ -206,6 +242,8 @@ def create_regions(world, player: int, active_locations): LocationName.blazing_bazookas_bonus_2 : [0x66E, 3], LocationName.blazing_bazookas_dk : [0x66E, 5], } + if world.kongsanity[player]: + blazing_bazookas_region_locations[LocationName.blazing_bazookas_kong] = [] blazing_bazookas_region = create_region(world, player, active_locations, LocationName.blazing_bazookas_region, blazing_bazookas_region_locations, None) @@ -215,6 +253,8 @@ def create_regions(world, player: int, active_locations): LocationName.low_g_labyrinth_bonus_2 : [0x670, 3], LocationName.low_g_labyrinth_dk : [0x670, 5], } + if world.kongsanity[player]: + low_g_labyrinth_region_locations[LocationName.low_g_labyrinth_kong] = [] low_g_labyrinth_region = create_region(world, player, active_locations, LocationName.low_g_labyrinth_region, low_g_labyrinth_region_locations, None) @@ -224,6 +264,8 @@ def create_regions(world, player: int, active_locations): LocationName.krevice_kreepers_bonus_2 : [0x673, 3], LocationName.krevice_kreepers_dk : [0x673, 5], } + if world.kongsanity[player]: + krevice_kreepers_region_locations[LocationName.krevice_kreepers_kong] = [] krevice_kreepers_region = create_region(world, player, active_locations, LocationName.krevice_kreepers_region, krevice_kreepers_region_locations, None) @@ -233,6 +275,8 @@ def create_regions(world, player: int, active_locations): LocationName.tearaway_toboggan_bonus_2 : [0x65F, 3], LocationName.tearaway_toboggan_dk : [0x65F, 5], } + if world.kongsanity[player]: + tearaway_toboggan_region_locations[LocationName.tearaway_toboggan_kong] = [] tearaway_toboggan_region = create_region(world, player, active_locations, LocationName.tearaway_toboggan_region, tearaway_toboggan_region_locations, None) @@ -242,6 +286,8 @@ def create_regions(world, player: int, active_locations): LocationName.barrel_drop_bounce_bonus_2 : [0x66C, 3], LocationName.barrel_drop_bounce_dk : [0x66C, 5], } + if world.kongsanity[player]: + barrel_drop_bounce_region_locations[LocationName.barrel_drop_bounce_kong] = [] barrel_drop_bounce_region = create_region(world, player, active_locations, LocationName.barrel_drop_bounce_region, barrel_drop_bounce_region_locations, None) @@ -251,6 +297,8 @@ def create_regions(world, player: int, active_locations): LocationName.krack_shot_kroc_bonus_2 : [0x66F, 3], LocationName.krack_shot_kroc_dk : [0x66F, 5], } + if world.kongsanity[player]: + krack_shot_kroc_region_locations[LocationName.krack_shot_kroc_kong] = [] krack_shot_kroc_region = create_region(world, player, active_locations, LocationName.krack_shot_kroc_region, krack_shot_kroc_region_locations, None) @@ -260,6 +308,8 @@ def create_regions(world, player: int, active_locations): LocationName.lemguin_lunge_bonus_2 : [0x65E, 3], LocationName.lemguin_lunge_dk : [0x65E, 5], } + if world.kongsanity[player]: + lemguin_lunge_region_locations[LocationName.lemguin_lunge_kong] = [] lemguin_lunge_region = create_region(world, player, active_locations, LocationName.lemguin_lunge_region, lemguin_lunge_region_locations, None) @@ -269,6 +319,8 @@ def create_regions(world, player: int, active_locations): LocationName.buzzer_barrage_bonus_2 : [0x676, 3], LocationName.buzzer_barrage_dk : [0x676, 5], } + if world.kongsanity[player]: + buzzer_barrage_region_locations[LocationName.buzzer_barrage_kong] = [] buzzer_barrage_region = create_region(world, player, active_locations, LocationName.buzzer_barrage_region, buzzer_barrage_region_locations, None) @@ -278,6 +330,8 @@ def create_regions(world, player: int, active_locations): LocationName.kong_fused_cliffs_bonus_2 : [0x674, 3], LocationName.kong_fused_cliffs_dk : [0x674, 5], } + if world.kongsanity[player]: + kong_fused_cliffs_region_locations[LocationName.kong_fused_cliffs_kong] = [] kong_fused_cliffs_region = create_region(world, player, active_locations, LocationName.kong_fused_cliffs_region, kong_fused_cliffs_region_locations, None) @@ -287,6 +341,8 @@ def create_regions(world, player: int, active_locations): LocationName.floodlit_fish_bonus_2 : [0x669, 3], LocationName.floodlit_fish_dk : [0x669, 5], } + if world.kongsanity[player]: + floodlit_fish_region_locations[LocationName.floodlit_fish_kong] = [] floodlit_fish_region = create_region(world, player, active_locations, LocationName.floodlit_fish_region, floodlit_fish_region_locations, None) @@ -296,6 +352,8 @@ def create_regions(world, player: int, active_locations): LocationName.pothole_panic_bonus_2 : [0x677, 3], LocationName.pothole_panic_dk : [0x677, 5], } + if world.kongsanity[player]: + pothole_panic_region_locations[LocationName.pothole_panic_kong] = [] pothole_panic_region = create_region(world, player, active_locations, LocationName.pothole_panic_region, pothole_panic_region_locations, None) @@ -305,6 +363,8 @@ def create_regions(world, player: int, active_locations): LocationName.ropey_rumpus_bonus_2 : [0x675, 3], LocationName.ropey_rumpus_dk : [0x675, 5], } + if world.kongsanity[player]: + ropey_rumpus_region_locations[LocationName.ropey_rumpus_kong] = [] ropey_rumpus_region = create_region(world, player, active_locations, LocationName.ropey_rumpus_region, ropey_rumpus_region_locations, None) @@ -314,6 +374,8 @@ def create_regions(world, player: int, active_locations): LocationName.konveyor_rope_clash_bonus_2 : [0x657, 3], LocationName.konveyor_rope_clash_dk : [0x657, 5], } + if world.kongsanity[player]: + konveyor_rope_clash_region_locations[LocationName.konveyor_rope_clash_kong] = [] konveyor_rope_clash_region = create_region(world, player, active_locations, LocationName.konveyor_rope_clash_region, konveyor_rope_clash_region_locations, None) @@ -323,6 +385,8 @@ def create_regions(world, player: int, active_locations): LocationName.creepy_caverns_bonus_2 : [0x678, 3], LocationName.creepy_caverns_dk : [0x678, 5], } + if world.kongsanity[player]: + creepy_caverns_region_locations[LocationName.creepy_caverns_kong] = [] creepy_caverns_region = create_region(world, player, active_locations, LocationName.creepy_caverns_region, creepy_caverns_region_locations, None) @@ -332,6 +396,8 @@ def create_regions(world, player: int, active_locations): LocationName.lightning_lookout_bonus_2 : [0x665, 3], LocationName.lightning_lookout_dk : [0x665, 5], } + if world.kongsanity[player]: + lightning_lookout_region_locations[LocationName.lightning_lookout_kong] = [] lightning_lookout_region = create_region(world, player, active_locations, LocationName.lightning_lookout_region, lightning_lookout_region_locations, None) @@ -341,6 +407,8 @@ def create_regions(world, player: int, active_locations): LocationName.koindozer_klamber_bonus_2 : [0x679, 3], LocationName.koindozer_klamber_dk : [0x679, 5], } + if world.kongsanity[player]: + koindozer_klamber_region_locations[LocationName.koindozer_klamber_kong] = [] koindozer_klamber_region = create_region(world, player, active_locations, LocationName.koindozer_klamber_region, koindozer_klamber_region_locations, None) @@ -350,6 +418,8 @@ def create_regions(world, player: int, active_locations): LocationName.poisonous_pipeline_bonus_2 : [0x671, 3], LocationName.poisonous_pipeline_dk : [0x671, 5], } + if world.kongsanity[player]: + poisonous_pipeline_region_locations[LocationName.poisonous_pipeline_kong] = [] poisonous_pipeline_region = create_region(world, player, active_locations, LocationName.poisonous_pipeline_region, poisonous_pipeline_region_locations, None) @@ -360,6 +430,8 @@ def create_regions(world, player: int, active_locations): LocationName.stampede_sprint_bonus_3 : [0x67B, 4], LocationName.stampede_sprint_dk : [0x67B, 5], } + if world.kongsanity[player]: + stampede_sprint_region_locations[LocationName.stampede_sprint_kong] = [] stampede_sprint_region = create_region(world, player, active_locations, LocationName.stampede_sprint_region, stampede_sprint_region_locations, None) @@ -369,6 +441,8 @@ def create_regions(world, player: int, active_locations): LocationName.criss_cross_cliffs_bonus_2 : [0x67C, 3], LocationName.criss_cross_cliffs_dk : [0x67C, 5], } + if world.kongsanity[player]: + criss_cross_cliffs_region_locations[LocationName.criss_cross_cliffs_kong] = [] criss_cross_cliffs_region = create_region(world, player, active_locations, LocationName.criss_cross_cliffs_region, criss_cross_cliffs_region_locations, None) @@ -379,6 +453,8 @@ def create_regions(world, player: int, active_locations): LocationName.tyrant_twin_tussle_bonus_3 : [0x67D, 4], LocationName.tyrant_twin_tussle_dk : [0x67D, 5], } + if world.kongsanity[player]: + tyrant_twin_tussle_region_locations[LocationName.tyrant_twin_tussle_kong] = [] tyrant_twin_tussle_region = create_region(world, player, active_locations, LocationName.tyrant_twin_tussle_region, tyrant_twin_tussle_region_locations, None) @@ -389,6 +465,8 @@ def create_regions(world, player: int, active_locations): LocationName.swoopy_salvo_bonus_3 : [0x663, 4], LocationName.swoopy_salvo_dk : [0x663, 5], } + if world.kongsanity[player]: + swoopy_salvo_region_locations[LocationName.swoopy_salvo_kong] = [] swoopy_salvo_region = create_region(world, player, active_locations, LocationName.swoopy_salvo_region, swoopy_salvo_region_locations, None) @@ -503,9 +581,7 @@ def create_regions(world, player: int, active_locations): sky_high_secret_region_locations = {} if False:#world.include_trade_sequence[player]: - sky_high_secret_region_locations.update({ - LocationName.sky_high_secret: [0x64B, 1], - }) + sky_high_secret_region_locations[LocationName.sky_high_secret] = [0x64B, 1] sky_high_secret_region = create_region(world, player, active_locations, LocationName.sky_high_secret_region, sky_high_secret_region_locations, None) @@ -517,9 +593,7 @@ def create_regions(world, player: int, active_locations): cifftop_cache_region_locations = {} if False:#world.include_trade_sequence[player]: - cifftop_cache_region_locations.update({ - LocationName.cifftop_cache: [0x64D, 1], - }) + cifftop_cache_region_locations[LocationName.cifftop_cache] = [0x64D, 1] cifftop_cache_region = create_region(world, player, active_locations, LocationName.cifftop_cache_region, cifftop_cache_region_locations, None) @@ -622,29 +696,19 @@ def create_regions(world, player: int, active_locations): LocationName.bazaars_general_store_2: [0x615, 3, True], }) - bramble_region_locations.update({ - LocationName.brambles_bungalow: [0x619, 2], - }) + bramble_region_locations[LocationName.brambles_bungalow] = [0x619, 2] #flower_spot_region_locations.update({ # LocationName.flower_spot: [0x615, 3, True], #}) - barter_region_locations.update({ - LocationName.barters_swap_shop: [0x61B, 3], - }) + barter_region_locations[LocationName.barters_swap_shop] = [0x61B, 3] - barnacle_region_locations.update({ - LocationName.barnacles_island: [0x61D, 2], - }) + barnacle_region_locations[LocationName.barnacles_island] = [0x61D, 2] - blue_region_locations.update({ - LocationName.blues_beach_hut: [0x621, 4], - }) + blue_region_locations[LocationName.blues_beach_hut] = [0x621, 4] - blizzard_region_locations.update({ - LocationName.blizzards_basecamp: [0x625, 4, True], - }) + blizzard_region_locations[LocationName.blizzards_basecamp] = [0x625, 4, True] bazaar_region = create_region(world, player, active_locations, LocationName.bazaar_region, bazaar_region_locations, None) @@ -817,7 +881,6 @@ def connect_regions(world, player, level_list): level_list[32], level_list[33], level_list[34], - LocationName.kastle_kaos_region, LocationName.sewer_stockpile_region, ] @@ -835,10 +898,16 @@ def connect_regions(world, player, level_list): for i in range(0, len(krematoa_levels)): connect(world, player, names, LocationName.krematoa_region, krematoa_levels[i], - lambda state: (state.has(ItemName.bonus_coin, player, world.krematoa_bonus_coin_cost[player].value * (i+1)))) - - connect(world, player, names, LocationName.krematoa_region, LocationName.knautilus_region, - lambda state: (state.has(ItemName.krematoa_cog, player, 5))) + lambda state, i=i: (state.has(ItemName.bonus_coin, player, world.krematoa_bonus_coin_cost[player].value * (i+1)))) + + if world.goal[player] == "knautilus": + connect(world, player, names, LocationName.kaos_kore_region, LocationName.knautilus_region) + connect(world, player, names, LocationName.krematoa_region, LocationName.kastle_kaos_region, + lambda state: (state.has(ItemName.krematoa_cog, player, 5))) + else: + connect(world, player, names, LocationName.kaos_kore_region, LocationName.kastle_kaos_region) + connect(world, player, names, LocationName.krematoa_region, LocationName.knautilus_region, + lambda state: (state.has(ItemName.krematoa_cog, player, 5))) def create_region(world: MultiWorld, player: int, active_locations, name: str, locations=None, exits=None): diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py index 7e83589ffa..90c4507e44 100644 --- a/worlds/dkc3/Rom.py +++ b/worlds/dkc3/Rom.py @@ -11,187 +11,270 @@ import os import math +level_unlock_map = { + 0x657: [0x65A], + 0x65A: [0x680, 0x639, 0x659], + 0x659: [0x65D], + 0x65D: [0x65C], + 0x65C: [0x688, 0x64F], + + 0x662: [0x681, 0x664], + 0x664: [0x65B], + 0x65B: [0x689, 0x661], + 0x661: [0x63A, 0x666], + 0x666: [0x650, 0x649], + + 0x667: [0x66A], + 0x66A: [0x682, 0x658], + 0x658: [0x68A, 0x66B], + 0x66B: [0x668], + 0x668: [0x651], + + 0x66D: [0x63C, 0x672], + 0x672: [0x68B, 0x660], + 0x660: [0x683, 0x66E], + 0x66E: [0x670], + 0x670: [0x652], + + 0x673: [0x684, 0x65F], + 0x65F: [0x66C], + 0x66C: [0x66F], + 0x66F: [0x65E], + 0x65E: [0x63D, 0x653, 0x68C, 0x64C], + + 0x676: [0x63E, 0x674, 0x685], + 0x674: [0x63F, 0x669], + 0x669: [0x677], + 0x677: [0x68D, 0x675], + 0x675: [0x654], + + 0x67A: [0x640, 0x678], + 0x678: [0x665], + 0x665: [0x686, 0x679], + 0x679: [0x68E, 0x671], + + 0x67B: [0x67C], + 0x67C: [0x67D], + 0x67D: [0x663], + 0x663: [0x67E], +} + location_rom_data = { 0xDC3000: [0x657, 1], # Lakeside Limbo 0xDC3001: [0x657, 2], 0xDC3002: [0x657, 3], 0xDC3003: [0x657, 5], + 0xDC3100: [0x657, 7], 0xDC3004: [0x65A, 1], # Doorstop Dash 0xDC3005: [0x65A, 2], 0xDC3006: [0x65A, 3], 0xDC3007: [0x65A, 5], + 0xDC3104: [0x65A, 7], 0xDC3008: [0x659, 1], # Tidal Trouble 0xDC3009: [0x659, 2], 0xDC300A: [0x659, 3], 0xDC300B: [0x659, 5], + 0xDC3108: [0x659, 7], 0xDC300C: [0x65D, 1], # Skidda's Row 0xDC300D: [0x65D, 2], 0xDC300E: [0x65D, 3], 0xDC300F: [0x65D, 5], + 0xDC310C: [0x65D, 7], 0xDC3010: [0x65C, 1], # Murky Mill 0xDC3011: [0x65C, 2], 0xDC3012: [0x65C, 3], 0xDC3013: [0x65C, 5], + 0xDC3110: [0x65C, 7], 0xDC3014: [0x662, 1], # Barrel Shield Bust-Up 0xDC3015: [0x662, 2], 0xDC3016: [0x662, 3], 0xDC3017: [0x662, 5], + 0xDC3114: [0x662, 7], 0xDC3018: [0x664, 1], # Riverside Race 0xDC3019: [0x664, 2], 0xDC301A: [0x664, 3], 0xDC301B: [0x664, 5], + 0xDC3118: [0x664, 7], 0xDC301C: [0x65B, 1], # Squeals on Wheels 0xDC301D: [0x65B, 2], 0xDC301E: [0x65B, 3], 0xDC301F: [0x65B, 5], + 0xDC311C: [0x65B, 7], 0xDC3020: [0x661, 1], # Springin' Spiders 0xDC3021: [0x661, 2], 0xDC3022: [0x661, 3], 0xDC3023: [0x661, 5], + 0xDC3120: [0x661, 7], 0xDC3024: [0x666, 1], # Bobbing Barrel Brawl 0xDC3025: [0x666, 2], 0xDC3026: [0x666, 3], 0xDC3027: [0x666, 5], + 0xDC3124: [0x666, 7], 0xDC3028: [0x667, 1], # Bazza's Blockade 0xDC3029: [0x667, 2], 0xDC302A: [0x667, 3], 0xDC302B: [0x667, 5], + 0xDC3128: [0x667, 7], 0xDC302C: [0x66A, 1], # Rocket Barrel Ride 0xDC302D: [0x66A, 2], 0xDC302E: [0x66A, 3], 0xDC302F: [0x66A, 5], + 0xDC312C: [0x66A, 7], 0xDC3030: [0x658, 1], # Kreeping Klasps 0xDC3031: [0x658, 2], 0xDC3032: [0x658, 3], 0xDC3033: [0x658, 5], + 0xDC3130: [0x658, 7], 0xDC3034: [0x66B, 1], # Tracker Barrel Trek 0xDC3035: [0x66B, 2], 0xDC3036: [0x66B, 3], 0xDC3037: [0x66B, 5], + 0xDC3134: [0x66B, 7], 0xDC3038: [0x668, 1], # Fish Food Frenzy 0xDC3039: [0x668, 2], 0xDC303A: [0x668, 3], 0xDC303B: [0x668, 5], + 0xDC3138: [0x668, 7], 0xDC303C: [0x66D, 1], # Fire-ball Frenzy 0xDC303D: [0x66D, 2], 0xDC303E: [0x66D, 3], 0xDC303F: [0x66D, 5], + 0xDC313C: [0x66D, 7], 0xDC3040: [0x672, 1], # Demolition Drainpipe 0xDC3041: [0x672, 2], 0xDC3042: [0x672, 3], 0xDC3043: [0x672, 5], + 0xDC3140: [0x672, 7], 0xDC3044: [0x660, 1], # Ripsaw Rage 0xDC3045: [0x660, 2], 0xDC3046: [0x660, 3], 0xDC3047: [0x660, 5], + 0xDC3144: [0x660, 7], 0xDC3048: [0x66E, 1], # Blazing Bazukas 0xDC3049: [0x66E, 2], 0xDC304A: [0x66E, 3], 0xDC304B: [0x66E, 5], + 0xDC3148: [0x66E, 7], 0xDC304C: [0x670, 1], # Low-G Labyrinth 0xDC304D: [0x670, 2], 0xDC304E: [0x670, 3], 0xDC304F: [0x670, 5], + 0xDC314C: [0x670, 7], 0xDC3050: [0x673, 1], # Krevice Kreepers 0xDC3051: [0x673, 2], 0xDC3052: [0x673, 3], 0xDC3053: [0x673, 5], + 0xDC3150: [0x673, 7], 0xDC3054: [0x65F, 1], # Tearaway Toboggan 0xDC3055: [0x65F, 2], 0xDC3056: [0x65F, 3], 0xDC3057: [0x65F, 5], + 0xDC3154: [0x65F, 7], 0xDC3058: [0x66C, 1], # Barrel Drop Bounce 0xDC3059: [0x66C, 2], 0xDC305A: [0x66C, 3], 0xDC305B: [0x66C, 5], + 0xDC3158: [0x66C, 7], 0xDC305C: [0x66F, 1], # Krack-Shot Kroc 0xDC305D: [0x66F, 2], 0xDC305E: [0x66F, 3], 0xDC305F: [0x66F, 5], + 0xDC315C: [0x66F, 7], 0xDC3060: [0x65E, 1], # Lemguin Lunge 0xDC3061: [0x65E, 2], 0xDC3062: [0x65E, 3], 0xDC3063: [0x65E, 5], + 0xDC3160: [0x65E, 7], 0xDC3064: [0x676, 1], # Buzzer Barrage 0xDC3065: [0x676, 2], 0xDC3066: [0x676, 3], 0xDC3067: [0x676, 5], + 0xDC3164: [0x676, 7], 0xDC3068: [0x674, 1], # Kong-Fused Cliffs 0xDC3069: [0x674, 2], 0xDC306A: [0x674, 3], 0xDC306B: [0x674, 5], + 0xDC3168: [0x674, 7], 0xDC306C: [0x669, 1], # Floodlit Fish 0xDC306D: [0x669, 2], 0xDC306E: [0x669, 3], 0xDC306F: [0x669, 5], + 0xDC316C: [0x669, 7], 0xDC3070: [0x677, 1], # Pothole Panic 0xDC3071: [0x677, 2], 0xDC3072: [0x677, 3], 0xDC3073: [0x677, 5], + 0xDC3170: [0x677, 7], 0xDC3074: [0x675, 1], # Ropey Rumpus 0xDC3075: [0x675, 2], 0xDC3076: [0x675, 3], 0xDC3077: [0x675, 5], + 0xDC3174: [0x675, 7], 0xDC3078: [0x67A, 1], # Konveyor Rope Klash 0xDC3079: [0x67A, 2], 0xDC307A: [0x67A, 3], 0xDC307B: [0x67A, 5], + 0xDC3178: [0x67A, 7], 0xDC307C: [0x678, 1], # Creepy Caverns 0xDC307D: [0x678, 2], 0xDC307E: [0x678, 3], 0xDC307F: [0x678, 5], + 0xDC317C: [0x678, 7], 0xDC3080: [0x665, 1], # Lightning Lookout 0xDC3081: [0x665, 2], 0xDC3082: [0x665, 3], 0xDC3083: [0x665, 5], + 0xDC3180: [0x665, 7], 0xDC3084: [0x679, 1], # Koindozer Klamber 0xDC3085: [0x679, 2], 0xDC3086: [0x679, 3], 0xDC3087: [0x679, 5], + 0xDC3184: [0x679, 7], 0xDC3088: [0x671, 1], # Poisonous Pipeline 0xDC3089: [0x671, 2], 0xDC308A: [0x671, 3], 0xDC308B: [0x671, 5], + 0xDC3188: [0x671, 7], 0xDC308C: [0x67B, 1], # Stampede Sprint @@ -199,23 +282,27 @@ location_rom_data = { 0xDC308E: [0x67B, 3], 0xDC308F: [0x67B, 4], 0xDC3090: [0x67B, 5], + 0xDC318C: [0x67B, 7], 0xDC3091: [0x67C, 1], # Criss Kross Cliffs 0xDC3092: [0x67C, 2], 0xDC3093: [0x67C, 3], 0xDC3094: [0x67C, 5], + 0xDC3191: [0x67C, 7], 0xDC3095: [0x67D, 1], # Tyrant Twin Tussle 0xDC3096: [0x67D, 2], 0xDC3097: [0x67D, 3], 0xDC3098: [0x67D, 4], 0xDC3099: [0x67D, 5], + 0xDC3195: [0x67D, 7], 0xDC309A: [0x663, 1], # Swoopy Salvo 0xDC309B: [0x663, 2], 0xDC309C: [0x663, 3], 0xDC309D: [0x663, 4], 0xDC309E: [0x663, 5], + 0xDC319A: [0x663, 7], 0xDC309F: [0x67E, 1], # Rocket Rush 0xDC30A0: [0x67E, 5], @@ -243,7 +330,7 @@ location_rom_data = { #0xDC30B4: [0x64D, 1], # Disabled until Trade Sequence 0xDC30B5: [0x64E, 1], - 0xDC30B6: [0x5FD, 4], # Banana Bird Mother + 0xDC30B6: [0x5FE, 4], # Banana Bird Mother # DKC3_TODO: Disabled until Trade Sequence #0xDC30B7: [0x615, 2, True], @@ -256,6 +343,18 @@ location_rom_data = { #0xDC30BE: [0x625, 4, True], } +boss_location_ids = [ + 0xDC30A1, + 0xDC30A2, + 0xDC30A3, + 0xDC30A4, + 0xDC30A5, + 0xDC30A6, + 0xDC30A7, + 0xDC30A8, + 0xDC30B6, +] + item_rom_data = { 0xDC3001: [0x5D5], # 1-Up Balloon @@ -400,7 +499,6 @@ def patch_rom(world, rom, player, active_level_list): rom.write_byte(0x3484DE, 0xEA) rom.write_byte(0x348528, 0x80) # Prevent Single-Ski Lock - # Make Swanky free rom.write_byte(0x348C48, 0x00) @@ -462,6 +560,25 @@ def patch_rom(world, rom, player, active_level_list): rom.write_byte(0x9130, world.starting_life_count[player].value) rom.write_byte(0x913B, world.starting_life_count[player].value) + # Cheat options + cheat_bytes = [0x00, 0x00] + + if world.merry[player]: + cheat_bytes[0] |= 0x01 + + if world.autosave[player]: + cheat_bytes[0] |= 0x02 + + if world.difficulty[player] == "tufst": + cheat_bytes[0] |= 0x80 + cheat_bytes[1] |= 0x80 + elif world.difficulty[player] == "hardr": + cheat_bytes[0] |= 0x00 + cheat_bytes[1] |= 0x00 + elif world.difficulty[player] == "norml": + cheat_bytes[1] |= 0x40 + + rom.write_bytes(0x8303, bytearray(cheat_bytes)) # Handle Level Shuffle Here if world.level_shuffle[player]: @@ -469,6 +586,9 @@ def patch_rom(world, rom, player, active_level_list): rom.write_byte(level_dict[level_list[i]].nameIDAddress, level_dict[active_level_list[i]].nameID) rom.write_byte(level_dict[level_list[i]].levelIDAddress, level_dict[active_level_list[i]].levelID) + rom.write_byte(0x3FF800 + level_dict[active_level_list[i]].levelID, level_dict[level_list[i]].levelID) + rom.write_byte(0x3FF860 + level_dict[level_list[i]].levelID, level_dict[active_level_list[i]].levelID) + # First levels of each world rom.write_byte(0x34BC3E, (0x32 + level_dict[active_level_list[0]].levelID)) rom.write_byte(0x34BC47, (0x32 + level_dict[active_level_list[5]].levelID)) @@ -495,6 +615,52 @@ def patch_rom(world, rom, player, active_level_list): rom.write_byte(0x32F339, 0x55) + # Handle KONGsanity Here + if world.kongsanity[player]: + # Arich's Hoard KONGsanity fix + rom.write_bytes(0x34BA8C, bytearray([0xEA, 0xEA])) + + # Don't hide the level flag if the 0x80 bit is set + rom.write_bytes(0x34CE92, bytearray([0x80])) + + # Use the `!` next to level name for indicating KONG letters + rom.write_bytes(0x34B8F0, bytearray([0x80])) + rom.write_bytes(0x34B8F3, bytearray([0x80])) + + # Hijack to code to set the 0x80 flag for the level when you complete KONG + rom.write_bytes(0x3BCD4B, bytearray([0x22, 0x80, 0xFA, 0XB8])) # JSL $B8FA80 + + rom.write_bytes(0x38FA80, bytearray([0xDA])) # PHX + rom.write_bytes(0x38FA81, bytearray([0x48])) # PHA + rom.write_bytes(0x38FA82, bytearray([0x08])) # PHP + rom.write_bytes(0x38FA83, bytearray([0xE2, 0x20])) # SEP #20 + rom.write_bytes(0x38FA85, bytearray([0x48])) # PHA + rom.write_bytes(0x38FA86, bytearray([0x18])) # CLC + rom.write_bytes(0x38FA87, bytearray([0x6D, 0xD3, 0x18])) # ADC $18D3 + rom.write_bytes(0x38FA8A, bytearray([0x8D, 0xD3, 0x18])) # STA $18D3 + rom.write_bytes(0x38FA8D, bytearray([0x68])) # PLA + rom.write_bytes(0x38FA8E, bytearray([0xC2, 0x20])) # REP 20 + rom.write_bytes(0x38FA90, bytearray([0X18])) # CLC + rom.write_bytes(0x38FA91, bytearray([0x6D, 0xD5, 0x05])) # ADC $05D5 + rom.write_bytes(0x38FA94, bytearray([0x8D, 0xD5, 0x05])) # STA $05D5 + rom.write_bytes(0x38FA97, bytearray([0xAE, 0xB9, 0x05])) # LDX $05B9 + rom.write_bytes(0x38FA9A, bytearray([0xBD, 0x32, 0x06])) # LDA $0632, X + rom.write_bytes(0x38FA9D, bytearray([0x09, 0x80, 0x00])) # ORA #8000 + rom.write_bytes(0x38FAA0, bytearray([0x9D, 0x32, 0x06])) # STA $0632, X + rom.write_bytes(0x38FAA3, bytearray([0xAD, 0xD5, 0x18])) # LDA $18D5 + rom.write_bytes(0x38FAA6, bytearray([0xD0, 0x03])) # BNE $80EA + rom.write_bytes(0x38FAA8, bytearray([0x9C, 0xD9, 0x18])) # STZ $18D9 + rom.write_bytes(0x38FAAB, bytearray([0xA9, 0x78, 0x00])) # LDA #0078 + rom.write_bytes(0x38FAAE, bytearray([0x8D, 0xD5, 0x18])) # STA $18D5 + rom.write_bytes(0x38FAB1, bytearray([0x28])) # PLP + rom.write_bytes(0x38FAB2, bytearray([0x68])) # PLA + rom.write_bytes(0x38FAB3, bytearray([0xFA])) # PLX + rom.write_bytes(0x38FAB4, bytearray([0x6B])) # RTL + # End Handle KONGsanity + + # Handle Credits + rom.write_bytes(0x32A5DF, bytearray([0x41, 0x52, 0x43, 0x48, 0x49, 0x50, 0x45, 0x4C, 0x41, 0x47, 0x4F, 0x20, 0x4D, 0x4F, 0xC4])) # "ARCHIPELAGO MOD" + rom.write_bytes(0x32A5EE, bytearray([0x00, 0x03, 0x50, 0x4F, 0x52, 0x59, 0x47, 0x4F, 0x4E, 0xC5])) # "PORYGONE" from Main import __version__ rom.name = bytearray(f'D3{__version__.replace(".", "")[0:3]}_{player}_{world.seed:11}\0', 'utf8')[:21] @@ -516,6 +682,17 @@ def patch_rom(world, rom, player, active_level_list): rom.write_byte(0x32DD63, 0xEA) rom.write_byte(0x32DD64, 0xEA) + # Don't grant Banana Birds at Bears + rom.write_byte(0x3492DB, 0xEA) + rom.write_byte(0x3492DC, 0xEA) + rom.write_byte(0x3492DD, 0xEA) + rom.write_byte(0x3493F4, 0xEA) + rom.write_byte(0x3493F5, 0xEA) + rom.write_byte(0x3493F6, 0xEA) + + # Don't grant present at Blizzard + rom.write_byte(0x8454, 0x00) + # Don't grant Patch and Skis from their bosses rom.write_byte(0x3F3762, 0x00) rom.write_byte(0x3F377B, 0x00) diff --git a/worlds/dkc3/__init__.py b/worlds/dkc3/__init__.py index f5b01ff723..5c575b85b5 100644 --- a/worlds/dkc3/__init__.py +++ b/worlds/dkc3/__init__.py @@ -4,7 +4,7 @@ import math import threading from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification -from .Items import DKC3Item, ItemData, item_table, inventory_table +from .Items import DKC3Item, ItemData, item_table, inventory_table, junk_table from .Locations import DKC3Location, all_locations, setup_locations from .Options import dkc3_options from .Regions import create_regions, connect_regions @@ -40,7 +40,7 @@ class DKC3World(World): game: str = "Donkey Kong Country 3" option_definitions = dkc3_options topology_present = False - data_version = 1 + data_version = 2 #hint_blacklist = {LocationName.rocket_rush_flag} item_name_to_id = {name: data.code for name, data in item_table.items()} @@ -99,10 +99,13 @@ class DKC3World(World): # Bosses total_required_locations += number_of_bosses - + # Secret Caves total_required_locations += 13 + if self.world.kongsanity[self.player]: + total_required_locations += 39 + ## Brothers Bear if False:#self.world.include_trade_sequence[self.player]: total_required_locations += 10 @@ -118,7 +121,11 @@ class DKC3World(World): total_junk_count = total_required_locations - len(itempool) - itempool += [self.create_item(ItemName.bear_coin)] * total_junk_count + junk_pool = [] + for item_name in self.world.random.choices(list(junk_table.keys()), k=total_junk_count): + junk_pool += [self.create_item(item_name)] + + itempool += junk_pool self.active_level_list = level_list.copy() From 83bcb441bfc52cf842d7d1f35df9247246644da4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 20 Aug 2022 19:22:03 +0200 Subject: [PATCH 04/91] Factorio: typo --- worlds/factorio/Mod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 37c503b047..9889e58bf3 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -107,7 +107,7 @@ def generate_mod(world, output_directory: str): random = multiworld.slot_seeds[player] def flop_random(low, high, base=None): - """Guarentees 50% below base and 50% above base, uniform distribution in each direction.""" + """Guarantees 50% below base and 50% above base, uniform distribution in each direction.""" if base: distance = random.random() if random.randint(0, 1): From 9341332379adb02f87921067ffed1afd87f3343a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 21 Aug 2022 00:58:46 +0200 Subject: [PATCH 05/91] WebHost: allow newlines in data-tooltip (#921) * WebHost: allow newlines in data-tooltip * WebHost: Tooltips: strip surrounding whitespace * WebHost: unify tooltips behaviour * WebHost: unify labels around tooltips * WebHost: changing tooltips width to max-width to allow small tooltips to not have empty space. * Minor modifications to tooltips - Reduce tooltip target to (?) spans - Set fixed width of 260px on tooltips - Add space between : and (?) on player-settings - Removed cursor:pointer on tooltips - Fix labels for checkboxes on generate.html Co-authored-by: Chris Wilson --- WebHostLib/options.py | 15 ++-- WebHostLib/static/assets/player-settings.js | 10 ++- WebHostLib/static/styles/generate.css | 4 -- WebHostLib/static/styles/globalStyles.css | 4 ++ WebHostLib/static/styles/tooltip.css | 4 +- WebHostLib/templates/generate.html | 79 ++++++++++----------- 6 files changed, 61 insertions(+), 55 deletions(-) diff --git a/WebHostLib/options.py b/WebHostLib/options.py index 3c481be62b..e2d362a570 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -49,6 +49,11 @@ def create(): return list(default_value) return default_value + def get_html_doc(option_type: type(Options.Option)) -> str: + if not option_type.__doc__: + return "Please document me!" + return "\n".join(line.strip() for line in option_type.__doc__.split("\n")).strip() + weighted_settings = { "baseOptions": { "description": "Generated by https://archipelago.gg/", @@ -88,7 +93,7 @@ def create(): game_options[option_name] = this_option = { "type": "select", "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": option.__doc__ if option.__doc__ else "Please document me!", + "description": get_html_doc(option), "defaultValue": None, "options": [] } @@ -114,7 +119,7 @@ def create(): game_options[option_name] = { "type": "range", "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": option.__doc__ if option.__doc__ else "Please document me!", + "description": get_html_doc(option), "defaultValue": option.default if hasattr( option, "default") and option.default != "random" else option.range_start, "min": option.range_start, @@ -131,14 +136,14 @@ def create(): game_options[option_name] = { "type": "items-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": option.__doc__ if option.__doc__ else "Please document me!", + "description": get_html_doc(option), } elif getattr(option, "verify_location_name", False): game_options[option_name] = { "type": "locations-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": option.__doc__ if option.__doc__ else "Please document me!", + "description": get_html_doc(option), } elif issubclass(option, Options.OptionList) or issubclass(option, Options.OptionSet): @@ -146,7 +151,7 @@ def create(): game_options[option_name] = { "type": "custom-list", "displayName": option.display_name if hasattr(option, "display_name") else option_name, - "description": option.__doc__ if option.__doc__ else "Please document me!", + "description": get_html_doc(option), "options": list(option.valid_keys), } diff --git a/WebHostLib/static/assets/player-settings.js b/WebHostLib/static/assets/player-settings.js index 21c6414df7..b77d4e877b 100644 --- a/WebHostLib/static/assets/player-settings.js +++ b/WebHostLib/static/assets/player-settings.js @@ -102,9 +102,15 @@ const buildOptionsTable = (settings, romOpts = false) => { // td Left const tdl = document.createElement('td'); const label = document.createElement('label'); + label.textContent = `${settings[setting].displayName}: `; label.setAttribute('for', setting); - label.setAttribute('data-tooltip', settings[setting].description); - label.innerText = `${settings[setting].displayName}:`; + + const questionSpan = document.createElement('span'); + questionSpan.classList.add('interactive'); + questionSpan.setAttribute('data-tooltip', settings[setting].description); + questionSpan.innerText = '(?)'; + + label.appendChild(questionSpan); tdl.appendChild(label); tr.appendChild(tdl); diff --git a/WebHostLib/static/styles/generate.css b/WebHostLib/static/styles/generate.css index 066fb8a7c5..478d444d40 100644 --- a/WebHostLib/static/styles/generate.css +++ b/WebHostLib/static/styles/generate.css @@ -56,7 +56,3 @@ #file-input{ display: none; } - -.interactive{ - color: #ffef00; -} diff --git a/WebHostLib/static/styles/globalStyles.css b/WebHostLib/static/styles/globalStyles.css index c20bab6b14..d8b10d1c50 100644 --- a/WebHostLib/static/styles/globalStyles.css +++ b/WebHostLib/static/styles/globalStyles.css @@ -105,3 +105,7 @@ h5, h6{ margin-bottom: 20px; background-color: #ffff00; } + +.interactive{ + color: #ffef00; +} \ No newline at end of file diff --git a/WebHostLib/static/styles/tooltip.css b/WebHostLib/static/styles/tooltip.css index 0c5c0c6969..7cd8463f64 100644 --- a/WebHostLib/static/styles/tooltip.css +++ b/WebHostLib/static/styles/tooltip.css @@ -14,7 +14,6 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, /* Base styles for the element that has a tooltip */ [data-tooltip], .tooltip { position: relative; - cursor: pointer; } /* Base styles for the entire tooltip */ @@ -55,14 +54,15 @@ give it one of the following classes: tooltip-left, tooltip-right, tooltip-top, /** Content styles */ .tooltip:after, [data-tooltip]:after { + width: 260px; z-index: 10000; padding: 8px; - width: 160px; border-radius: 4px; background-color: #000; background-color: hsla(0, 0%, 20%, 0.9); color: #fff; content: attr(data-tooltip); + white-space: pre-wrap; font-size: 14px; line-height: 1.2; } diff --git a/WebHostLib/templates/generate.html b/WebHostLib/templates/generate.html index 916ed72b8d..aa16a47d35 100644 --- a/WebHostLib/templates/generate.html +++ b/WebHostLib/templates/generate.html @@ -41,12 +41,11 @@ - - (?) - + @@ -85,12 +83,11 @@ - - (?) - + @@ -131,12 +128,11 @@ - - (?) - + @@ -162,23 +158,22 @@ - - (?) + Plando Options: + + (?) - +
- +
- +
- + From be8c3131d8423106e52e3682d0d36f1ba10d5827 Mon Sep 17 00:00:00 2001 From: KonoTyran Date: Sat, 20 Aug 2022 16:02:50 -0700 Subject: [PATCH 06/91] fix allay advancements requiring note block on the wrong one. (#896) --- worlds/minecraft/Rules.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/worlds/minecraft/Rules.py b/worlds/minecraft/Rules.py index 2a88087d0c..ecdb459769 100644 --- a/worlds/minecraft/Rules.py +++ b/worlds/minecraft/Rules.py @@ -276,10 +276,10 @@ def set_advancement_rules(world: MultiWorld, player: int): # 1.19 advancements - # can make a cake, and can reach a pillager outposts for allays - set_rule(world.get_location("Birthday Song", player), lambda state: state.can_reach("The Lie", "Location", player)) - # find allay and craft a noteblock - set_rule(world.get_location("You've Got a Friend in Me", player), lambda state: state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) + # can make a cake, and a noteblock, and can reach a pillager outposts for allays + set_rule(world.get_location("Birthday Song", player), lambda state: state.can_reach("The Lie", "Location", player) and state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player)) + # can get to outposts. + # set_rule(world.get_location("You've Got a Friend in Me", player), lambda state: True) # craft bucket and adventure to find frog spawning biome set_rule(world.get_location("Bukkit Bukkit", player), lambda state: state.has("Bucket", player) and state._mc_has_iron_ingots(player) and state._mc_can_adventure(player)) # I don't like this one its way to easy to get. just a pain to find. From fb122df5f5b9e35d99adba6d381043bb05eb07c2 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Sat, 20 Aug 2022 18:09:35 -0500 Subject: [PATCH 07/91] RoR2: code cleanup and styling consistency (#833) * build locations dict dynamically from the TotalLocations option. Minor styling cleanup * Minor items styling cleanup. remove unused event items * minor options cleanup. clarify preset toggle slightly better * make items.py more readable. add chaos weights dict to use as reference point for generation * small rules styling and consistency cleanup * create less regions and other init cleanup * move region creation to less function calls and move revivals calculation * typing * use enum instead of hardcoded ints. fix bug i introduced * better typing --- worlds/ror2/Items.py | 245 ++++++++++++++++++++------------------- worlds/ror2/Locations.py | 16 +-- worlds/ror2/Options.py | 11 +- worlds/ror2/Rules.py | 23 ++-- worlds/ror2/__init__.py | 114 +++++++++--------- 5 files changed, 206 insertions(+), 203 deletions(-) diff --git a/worlds/ror2/Items.py b/worlds/ror2/Items.py index c4ece1dcb3..9efbc713d8 100644 --- a/worlds/ror2/Items.py +++ b/worlds/ror2/Items.py @@ -1,143 +1,154 @@ +from typing import Dict from BaseClasses import Item -import typing +from .Options import ItemWeights + class RiskOfRainItem(Item): game: str = "Risk of Rain 2" + # 37000 - 38000 -item_table = { - "Dio's Best Friend": 37001, - "Common Item": 37002, - "Uncommon Item": 37003, - "Legendary Item": 37004, - "Boss Item": 37005, - "Lunar Item": 37006, - "Equipment": 37007, - "Item Scrap, White": 37008, - "Item Scrap, Green": 37009, - "Item Scrap, Red": 37010, - "Item Scrap, Yellow": 37011, - "Victory": None, - "Beat Level One": None, - "Beat Level Two": None, - "Beat Level Three": None, - "Beat Level Four": None, - "Beat Level Five": None, +item_table: Dict[str, int] = { + "Dio's Best Friend": 37001, + "Common Item": 37002, + "Uncommon Item": 37003, + "Legendary Item": 37004, + "Boss Item": 37005, + "Lunar Item": 37006, + "Equipment": 37007, + "Item Scrap, White": 37008, + "Item Scrap, Green": 37009, + "Item Scrap, Red": 37010, + "Item Scrap, Yellow": 37011 } -default_weights = { - "Item Scrap, Green": 16, - "Item Scrap, Red": 4, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 32, - "Common Item": 64, - "Uncommon Item": 32, - "Legendary Item": 8, - "Boss Item": 4, - "Lunar Item": 16, - "Equipment": 32 +default_weights: Dict[str, int] = { + "Item Scrap, Green": 16, + "Item Scrap, Red": 4, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 32, + "Common Item": 64, + "Uncommon Item": 32, + "Legendary Item": 8, + "Boss Item": 4, + "Lunar Item": 16, + "Equipment": 32 } -new_weights = { - "Item Scrap, Green": 15, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 75, - "Uncommon Item": 40, - "Legendary Item": 10, - "Boss Item": 5, - "Lunar Item": 10, - "Equipment": 20 +new_weights: Dict[str, int] = { + "Item Scrap, Green": 15, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 75, + "Uncommon Item": 40, + "Legendary Item": 10, + "Boss Item": 5, + "Lunar Item": 10, + "Equipment": 20 } -uncommon_weights = { - "Item Scrap, Green": 45, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 45, - "Uncommon Item": 100, - "Legendary Item": 10, - "Boss Item": 5, - "Lunar Item": 15, - "Equipment": 20 +uncommon_weights: Dict[str, int] = { + "Item Scrap, Green": 45, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 45, + "Uncommon Item": 100, + "Legendary Item": 10, + "Boss Item": 5, + "Lunar Item": 15, + "Equipment": 20 } -legendary_weights = { - "Item Scrap, Green": 15, - "Item Scrap, Red": 5, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 30, - "Common Item": 50, - "Uncommon Item": 25, - "Legendary Item": 100, - "Boss Item": 5, - "Lunar Item": 15, - "Equipment": 20 +legendary_weights: Dict[str, int] = { + "Item Scrap, Green": 15, + "Item Scrap, Red": 5, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 30, + "Common Item": 50, + "Uncommon Item": 25, + "Legendary Item": 100, + "Boss Item": 5, + "Lunar Item": 15, + "Equipment": 20 } -lunartic_weights = { - "Item Scrap, Green": 0, - "Item Scrap, Red": 0, - "Item Scrap, Yellow": 0, - "Item Scrap, White": 0, - "Common Item": 0, - "Uncommon Item": 0, - "Legendary Item": 0, - "Boss Item": 0, - "Lunar Item": 100, - "Equipment": 0 +lunartic_weights: Dict[str, int] = { + "Item Scrap, Green": 0, + "Item Scrap, Red": 0, + "Item Scrap, Yellow": 0, + "Item Scrap, White": 0, + "Common Item": 0, + "Uncommon Item": 0, + "Legendary Item": 0, + "Boss Item": 0, + "Lunar Item": 100, + "Equipment": 0 } -no_scraps_weights = { - "Item Scrap, Green": 0, - "Item Scrap, Red": 0, - "Item Scrap, Yellow": 0, - "Item Scrap, White": 0, - "Common Item": 100, - "Uncommon Item": 40, - "Legendary Item": 15, - "Boss Item": 5, - "Lunar Item": 10, - "Equipment": 25 +chaos_weights: Dict[str, int] = { + "Item Scrap, Green": 80, + "Item Scrap, Red": 45, + "Item Scrap, Yellow": 30, + "Item Scrap, White": 100, + "Common Item": 100, + "Uncommon Item": 70, + "Legendary Item": 30, + "Boss Item": 20, + "Lunar Item": 60, + "Equipment": 40 } -even_weights = { - "Item Scrap, Green": 1, - "Item Scrap, Red": 1, - "Item Scrap, Yellow": 1, - "Item Scrap, White": 1, - "Common Item": 1, - "Uncommon Item": 1, - "Legendary Item": 1, - "Boss Item": 1, - "Lunar Item": 1, - "Equipment": 1 +no_scraps_weights: Dict[str, int] = { + "Item Scrap, Green": 0, + "Item Scrap, Red": 0, + "Item Scrap, Yellow": 0, + "Item Scrap, White": 0, + "Common Item": 100, + "Uncommon Item": 40, + "Legendary Item": 15, + "Boss Item": 5, + "Lunar Item": 10, + "Equipment": 25 } -scraps_only = { - "Item Scrap, Green": 70, - "Item Scrap, White": 100, - "Item Scrap, Red": 30, - "Item Scrap, Yellow": 5, - "Common Item": 0, - "Uncommon Item": 0, - "Legendary Item": 0, - "Boss Item": 0, - "Lunar Item": 0, - "Equipment": 0 +even_weights: Dict[str, int] = { + "Item Scrap, Green": 1, + "Item Scrap, Red": 1, + "Item Scrap, Yellow": 1, + "Item Scrap, White": 1, + "Common Item": 1, + "Uncommon Item": 1, + "Legendary Item": 1, + "Boss Item": 1, + "Lunar Item": 1, + "Equipment": 1 } -item_pool_weights: typing.Dict[int, typing.Dict[str, int]] = { - 0: default_weights, - 1: new_weights, - 2: uncommon_weights, - 3: legendary_weights, - 4: lunartic_weights, - 6: no_scraps_weights, - 7: even_weights, - 8: scraps_only +scraps_only: Dict[str, int] = { + "Item Scrap, Green": 70, + "Item Scrap, White": 100, + "Item Scrap, Red": 30, + "Item Scrap, Yellow": 5, + "Common Item": 0, + "Uncommon Item": 0, + "Legendary Item": 0, + "Boss Item": 0, + "Lunar Item": 0, + "Equipment": 0 } -lookup_id_to_name: typing.Dict[int, str] = {id: name for name, id in item_table.items() if id} +item_pool_weights: Dict[int, Dict[str, int]] = { + ItemWeights.option_default: default_weights, + ItemWeights.option_new: new_weights, + ItemWeights.option_uncommon: uncommon_weights, + ItemWeights.option_legendary: legendary_weights, + ItemWeights.option_lunartic: lunartic_weights, + ItemWeights.option_chaos: chaos_weights, + ItemWeights.option_no_scraps: no_scraps_weights, + ItemWeights.option_even: even_weights, + ItemWeights.option_scraps_only: scraps_only +} + +lookup_id_to_name: Dict[int, str] = {id: name for name, id in item_table.items()} diff --git a/worlds/ror2/Locations.py b/worlds/ror2/Locations.py index ae6ccea2b2..e4ebe8ddfa 100644 --- a/worlds/ror2/Locations.py +++ b/worlds/ror2/Locations.py @@ -1,19 +1,13 @@ +from typing import Dict from BaseClasses import Location -import typing +from .Options import TotalLocations + class RiskOfRainLocation(Location): game: str = "Risk of Rain 2" -# 37000 - 38000 -base_location_table = { - "Victory": None, -} # 37006 - 37506 -item_pickups = { - f"ItemPickup{i}": 37005+i for i in range(1, 501) +item_pickups: Dict[str, int] = { + f"ItemPickup{i+1}": 37000+i for i in range(TotalLocations.range_end) } - -location_table = {**base_location_table, **item_pickups} - -lookup_id_to_name: typing.Dict[int, str] = {id: name for name, id in location_table.items()} diff --git a/worlds/ror2/Options.py b/worlds/ror2/Options.py index 727d01ffaa..a95cbf597a 100644 --- a/worlds/ror2/Options.py +++ b/worlds/ror2/Options.py @@ -1,4 +1,4 @@ -import typing +from typing import Dict from Options import Option, DefaultOnToggle, Range, Choice @@ -36,7 +36,8 @@ class AllowLunarItems(DefaultOnToggle): class StartWithRevive(DefaultOnToggle): """Start the game with a `Dio's Best Friend` item.""" display_name = "Start with a Revive" - + + class FinalStageDeath(DefaultOnToggle): """Death on the final boss stage counts as a win.""" display_name = "Final Stage Death is Win" @@ -124,7 +125,7 @@ class Equipment(Range): class ItemPoolPresetToggle(DefaultOnToggle): """Will use the item weight presets when set to true, otherwise will use the custom set item pool weights.""" - display_name = "Item Weight Presets" + display_name = "Use Item Weight Presets" class ItemWeights(Choice): @@ -150,7 +151,7 @@ class ItemWeights(Choice): # define a dictionary for the weights of the generated item pool. -ror2_weights: typing.Dict[str, type(Option)] = { +ror2_weights: Dict[str, type(Option)] = { "green_scrap": GreenScrap, "red_scrap": RedScrap, "yellow_scrap": YellowScrap, @@ -163,7 +164,7 @@ ror2_weights: typing.Dict[str, type(Option)] = { "equipment": Equipment } -ror2_options: typing.Dict[str, type(Option)] = { +ror2_options: Dict[str, type(Option)] = { "total_locations": TotalLocations, "total_revivals": TotalRevivals, "start_with_revive": StartWithRevive, diff --git a/worlds/ror2/Rules.py b/worlds/ror2/Rules.py index 64d741f99f..bf00f617d8 100644 --- a/worlds/ror2/Rules.py +++ b/worlds/ror2/Rules.py @@ -2,29 +2,32 @@ from BaseClasses import MultiWorld from worlds.generic.Rules import set_rule, add_rule -def set_rules(world: MultiWorld, player: int): - total_locations = world.total_locations[player] # total locations for current player +def set_rules(world: MultiWorld, player: int) -> None: + total_locations = world.total_locations[player].value # total locations for current player event_location_step = 25 # set an event location at these locations for "spheres" divisions = total_locations // event_location_step + total_revivals = world.worlds[player].total_revivals # pulling this info we calculated in generate_basic if divisions: for i in range(1, divisions): # since divisions is the floor of total_locations / 25 event_loc = world.get_location(f"Pickup{i * event_location_step}", player) - set_rule(event_loc, lambda state, i=i: state.can_reach(f"ItemPickup{i * event_location_step - 1}", "Location", player)) + set_rule(event_loc, + lambda state, i=i: state.can_reach(f"ItemPickup{i * event_location_step - 1}", "Location", player)) for n in range(i * event_location_step, (i + 1) * event_location_step): # we want to create a rule for each of the 25 locations per division if n == i * event_location_step: - set_rule(world.get_location(f"ItemPickup{n}", player), lambda state, event_item=event_loc.item.name: state.has(event_item, player)) + set_rule(world.get_location(f"ItemPickup{n}", player), + lambda state, event_item=event_loc.item.name: state.has(event_item, player)) else: set_rule(world.get_location(f"ItemPickup{n}", player), - lambda state, n = n: state.can_reach(f"ItemPickup{n - 1}", 'Location', player)) + lambda state, n=n: state.can_reach(f"ItemPickup{n - 1}", "Location", player)) for i in range(divisions * event_location_step, total_locations+1): - set_rule(world.get_location(f"ItemPickup{i}", player), lambda state, i=i: state.can_reach(f"ItemPickup{i - 1}", "Location", player)) - + set_rule(world.get_location(f"ItemPickup{i}", player), + lambda state, i=i: state.can_reach(f"ItemPickup{i - 1}", "Location", player)) set_rule(world.get_location("Victory", player), lambda state: state.can_reach(f"ItemPickup{total_locations}", "Location", player)) - if world.total_revivals[player] or world.start_with_revive[player]: - total_revivals = world.total_revivals[player] * world.total_locations[player] // 100 + if total_revivals or world.start_with_revive[player].value: add_rule(world.get_location("Victory", player), - lambda state: state.has("Dio's Best Friend", player, total_revivals + world.start_with_revive[player].value)) + lambda state: state.has("Dio's Best Friend", player, + total_revivals + world.start_with_revive[player])) world.completion_condition[player] = lambda state: state.has("Victory", player) diff --git a/worlds/ror2/__init__.py b/worlds/ror2/__init__.py index 9d0d693b61..af65a15ea4 100644 --- a/worlds/ror2/__init__.py +++ b/worlds/ror2/__init__.py @@ -1,10 +1,11 @@ import string +from typing import Dict, List from .Items import RiskOfRainItem, item_table, item_pool_weights -from .Locations import location_table, RiskOfRainLocation, base_location_table +from .Locations import RiskOfRainLocation, item_pickups from .Rules import set_rules from BaseClasses import Region, RegionType, Entrance, Item, ItemClassification, MultiWorld, Tutorial -from .Options import ror2_options +from .Options import ror2_options, ItemWeights from worlds.AutoWorld import World, WebWorld client_version = 1 @@ -32,34 +33,31 @@ class RiskOfRainWorld(World): topology_present = False item_name_to_id = item_table - location_name_to_id = location_table + location_name_to_id = item_pickups - data_version = 3 + data_version = 4 forced_auto_forfeit = True web = RiskOfWeb() + total_revivals: int - def generate_basic(self): + def generate_early(self) -> None: + # figure out how many revivals should exist in the pool + self.total_revivals = int(self.world.total_revivals[self.player].value / 100 * + self.world.total_locations[self.player].value) + + def generate_basic(self) -> None: # shortcut for starting_inventory... The start_with_revive option lets you start with a Dio's Best Friend if self.world.start_with_revive[self.player].value: self.world.push_precollected(self.world.create_item("Dio's Best Friend", self.player)) # if presets are enabled generate junk_pool from the selected preset pool_option = self.world.item_weights[self.player].value - if self.world.item_pool_presets[self.player].value: + junk_pool: Dict[str, int] = {} + if self.world.item_pool_presets[self.player]: # generate chaos weights if the preset is chosen - if pool_option == 5: - junk_pool = { - "Item Scrap, Green": self.world.random.randint(0, 80), - "Item Scrap, Red": self.world.random.randint(0, 45), - "Item Scrap, Yellow": self.world.random.randint(0, 30), - "Item Scrap, White": self.world.random.randint(0, 100), - "Common Item": self.world.random.randint(0, 100), - "Uncommon Item": self.world.random.randint(0, 70), - "Legendary Item": self.world.random.randint(0, 30), - "Boss Item": self.world.random.randint(0, 20), - "Lunar Item": self.world.random.randint(0, 60), - "Equipment": self.world.random.randint(0, 40) - } + if pool_option == ItemWeights.option_chaos: + for name, max_value in item_pool_weights[pool_option].items(): + junk_pool[name] = self.world.random.randint(0, max_value) else: junk_pool = item_pool_weights[pool_option].copy() else: # generate junk pool from user created presets @@ -77,37 +75,43 @@ class RiskOfRainWorld(World): } # remove lunar items from the pool if they're disabled in the yaml unless lunartic is rolled - if not self.world.enable_lunar[self.player]: - if not pool_option == 4: - junk_pool.pop("Lunar Item") + if not (self.world.enable_lunar[self.player] or pool_option == ItemWeights.option_lunartic): + junk_pool.pop("Lunar Item") # Generate item pool - itempool = [] - + itempool: List = [] # Add revive items for the player - itempool += ["Dio's Best Friend"] * int(self.world.total_revivals[self.player] / 100 * self.world.total_locations[self.player]) + itempool += ["Dio's Best Friend"] * self.total_revivals # Fill remaining items with randomly generated junk itempool += self.world.random.choices(list(junk_pool.keys()), weights=list(junk_pool.values()), - k=self.world.total_locations[self.player] - - int(self.world.total_revivals[self.player] / 100 * self.world.total_locations[self.player])) + k=self.world.total_locations[self.player].value - self.total_revivals) # Convert itempool into real items itempool = list(map(lambda name: self.create_item(name), itempool)) self.world.itempool += itempool - def set_rules(self): + def set_rules(self) -> None: set_rules(self.world, self.player) - def create_regions(self): - create_regions(self.world, self.player) - create_events(self.world, self.player, int(self.world.total_locations[self.player])) + def create_regions(self) -> None: + menu = create_region(self.world, self.player, "Menu") + petrichor = create_region(self.world, self.player, "Petrichor V", + [f"ItemPickup{i + 1}" for i in range(self.world.total_locations[self.player].value)]) + + connection = Entrance(self.player, "Lobby", menu) + menu.exits.append(connection) + connection.connect(petrichor) + + self.world.regions += [menu, petrichor] + + create_events(self.world, self.player) def fill_slot_data(self): return { "itemPickupStep": self.world.item_pickup_step[self.player].value, - "seed": "".join(self.world.slot_seeds[self.player].choice(string.digits) for i in range(16)), + "seed": "".join(self.world.slot_seeds[self.player].choice(string.digits) for _ in range(16)), "totalLocations": self.world.total_locations[self.player].value, "totalRevivals": self.world.total_revivals[self.player].value, "startWithDio": self.world.start_with_revive[self.player].value, @@ -116,49 +120,39 @@ class RiskOfRainWorld(World): def create_item(self, name: str) -> Item: item_id = item_table[name] - item = RiskOfRainItem(name, ItemClassification.filler, item_id, self.player) if name == "Dio's Best Friend": - item.classification = ItemClassification.progression + classification = ItemClassification.progression elif name in {"Equipment", "Legendary Item"}: - item.classification = ItemClassification.useful + classification = ItemClassification.useful + else: + classification = ItemClassification.filler + item = RiskOfRainItem(name, classification, item_id, self.player) return item -def create_events(world: MultiWorld, player: int, total_locations: int): +def create_events(world: MultiWorld, player: int) -> None: + total_locations = world.total_locations[player].value num_of_events = total_locations // 25 if total_locations / 25 == num_of_events: num_of_events -= 1 + world_region = world.get_region("Petrichor V", player) + for i in range(num_of_events): - event_loc = RiskOfRainLocation(player, f"Pickup{(i + 1) * 25}", None, world.get_region('Petrichor V', player)) + event_loc = RiskOfRainLocation(player, f"Pickup{(i + 1) * 25}", None, world_region) event_loc.place_locked_item(RiskOfRainItem(f"Pickup{(i + 1) * 25}", ItemClassification.progression, None, player)) event_loc.access_rule(lambda state, i=i: state.can_reach(f"ItemPickup{((i + 1) * 25) - 1}", player)) - world.get_region('Petrichor V', player).locations.append(event_loc) + world_region.locations.append(event_loc) + + victory_event = RiskOfRainLocation(player, "Victory", None, world_region) + victory_event.place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, None, player)) + world_region.locations.append(victory_event) -# generate locations based on player setting -def create_regions(world, player: int): - world.regions += [ - create_region(world, player, 'Menu', None, ['Lobby']), - create_region(world, player, 'Petrichor V', - [location for location in base_location_table] + - [f"ItemPickup{i}" for i in range(1, 1 + world.total_locations[player])]) - ] - - world.get_entrance("Lobby", player).connect(world.get_region("Petrichor V", player)) - world.get_location("Victory", player).place_locked_item(RiskOfRainItem("Victory", ItemClassification.progression, - None, player)) - - -def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): - ret = Region(name, RegionType.Generic, name, player) - ret.world = world +def create_region(world: MultiWorld, player: int, name: str, locations: List[str] = None) -> Region: + ret = Region(name, RegionType.Generic, name, player, world) if locations: for location in locations: - loc_id = location_table[location] + loc_id = item_pickups[location] location = RiskOfRainLocation(player, location, loc_id, ret) ret.locations.append(location) - if exits: - for exit in exits: - ret.exits.append(Entrance(player, exit, ret)) - return ret From bba82ccd6c0c97286288718b7f71150cdebbb507 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Sat, 20 Aug 2022 16:17:23 -0700 Subject: [PATCH 08/91] WebHost: Remove "Wiki" link from footer (#943) --- WebHostLib/templates/islandFooter.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/WebHostLib/templates/islandFooter.html b/WebHostLib/templates/islandFooter.html index 66995f011d..243eea7c1a 100644 --- a/WebHostLib/templates/islandFooter.html +++ b/WebHostLib/templates/islandFooter.html @@ -6,8 +6,6 @@ - Source Code - - Wiki - - Contributors - Bug Report From 484ee9f0651724469287da1bc78b14a9d2d8e391 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sat, 20 Aug 2022 16:55:41 -0700 Subject: [PATCH 09/91] OoT: More item.type bugs. (#930) --- worlds/oot/Hints.py | 2 ++ worlds/oot/Items.py | 6 ++++++ worlds/oot/Rules.py | 5 +++-- worlds/oot/__init__.py | 16 ++++++++-------- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/worlds/oot/Hints.py b/worlds/oot/Hints.py index b8ae7dfafc..aaafeb20b8 100644 --- a/worlds/oot/Hints.py +++ b/worlds/oot/Hints.py @@ -129,6 +129,8 @@ def getItemGenericName(item): def isRestrictedDungeonItem(dungeon, item): + if not isinstance(item, OOTItem): + return False if (item.map or item.compass) and dungeon.world.shuffle_mapcompass == 'dungeon': return item in dungeon.dungeon_items if item.type == 'SmallKey' and dungeon.world.shuffle_smallkeys == 'dungeon': diff --git a/worlds/oot/Items.py b/worlds/oot/Items.py index b4c0719700..31e6c31f62 100644 --- a/worlds/oot/Items.py +++ b/worlds/oot/Items.py @@ -22,6 +22,12 @@ def ap_id_to_oot_data(ap_id): raise Exception(f'Could not find desired item ID: {ap_id}') +def oot_is_item_of_type(item, item_type): + if not isinstance(item, OOTItem): + return False + return item.type == item_type + + class OOTItem(Item): game: str = "Ocarina of Time" type: str diff --git a/worlds/oot/Rules.py b/worlds/oot/Rules.py index c55407b485..915aae77c1 100644 --- a/worlds/oot/Rules.py +++ b/worlds/oot/Rules.py @@ -3,6 +3,7 @@ import logging from .SaveContext import SaveContext from .Regions import TimeOfDay +from .Items import oot_is_item_of_type from BaseClasses import CollectionState from worlds.generic.Rules import set_rule, add_rule, add_item_rule, forbid_item @@ -138,7 +139,7 @@ def set_rules(ootworld): # Sheik in Ice Cavern is the only song location in a dungeon; need to ensure that it cannot be anything else. # This is required if map/compass included, or any_dungeon shuffle. location = world.get_location('Sheik in Ice Cavern', player) - add_item_rule(location, lambda item: item.player == player and item.type == 'Song') + add_item_rule(location, lambda item: item.player == player and oot_is_item_of_type(item, 'Song')) if ootworld.skip_child_zelda: # If skip child zelda is on, the item at Song from Impa must be giveable by the save context. @@ -181,7 +182,7 @@ def set_shop_rules(ootworld): wallet = ootworld.parser.parse_rule('Progressive_Wallet') wallet2 = ootworld.parser.parse_rule('(Progressive_Wallet, 2)') - for location in filter(lambda location: location.item and location.item.type == 'Shop', ootworld.get_locations()): + for location in filter(lambda location: location.item and oot_is_item_of_type(location.item, 'Shop'), ootworld.get_locations()): # Add wallet requirements if location.item.name in ['Buy Arrows (50)', 'Buy Fish', 'Buy Goron Tunic', 'Buy Bombchu (20)', 'Buy Bombs (30)']: add_rule(location, wallet) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index fb90b04e77..b4635ad77f 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -8,7 +8,7 @@ logger = logging.getLogger("Ocarina of Time") from .Location import OOTLocation, LocationFactory, location_name_to_id from .Entrance import OOTEntrance from .EntranceShuffle import shuffle_random_entrances, entrance_shuffle_table, EntranceShuffleError -from .Items import OOTItem, item_table, oot_data_to_ap_id +from .Items import OOTItem, item_table, oot_data_to_ap_id, oot_is_item_of_type from .ItemPool import generate_itempool, add_dungeon_items, get_junk_item, get_junk_pool from .Regions import OOTRegion, TimeOfDay from .Rules import set_rules, set_shop_rules, set_entrances_based_rules @@ -793,7 +793,7 @@ class OOTWorld(World): # This includes all locations for which show_in_spoiler is false, and shuffled shop items. for loc in self.get_locations(): if loc.address is not None and ( - not loc.show_in_spoiler or (loc.item is not None and loc.item.type == 'Shop') + not loc.show_in_spoiler or oot_is_item_of_type(loc.item, 'Shop') or (self.skip_child_zelda and loc.name in ['HC Zeldas Letter', 'Song from Impa'])): loc.address = None @@ -869,11 +869,11 @@ class OOTWorld(World): autoworld.major_item_locations.append(loc) if loc.game == "Ocarina of Time" and loc.item.code and (not loc.locked or - (loc.item.type == 'Song' or - (loc.item.type == 'SmallKey' and world.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or - (loc.item.type == 'HideoutSmallKey' and world.worlds[loc.player].shuffle_fortresskeys == 'any_dungeon') or - (loc.item.type == 'BossKey' and world.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or - (loc.item.type == 'GanonBossKey' and world.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))): + (oot_is_item_of_type(loc.item, 'Song') or + (oot_is_item_of_type(loc.item, 'SmallKey') and world.worlds[loc.player].shuffle_smallkeys == 'any_dungeon') or + (oot_is_item_of_type(loc.item, 'HideoutSmallKey') and world.worlds[loc.player].shuffle_fortresskeys == 'any_dungeon') or + (oot_is_item_of_type(loc.item, 'BossKey') and world.worlds[loc.player].shuffle_bosskeys == 'any_dungeon') or + (oot_is_item_of_type(loc.item, 'GanonBossKey') and world.worlds[loc.player].shuffle_ganon_bosskey == 'any_dungeon'))): if loc.player in barren_hint_players: hint_area = get_hint_area(loc) items_by_region[loc.player][hint_area]['weight'] += 1 @@ -888,7 +888,7 @@ class OOTWorld(World): elif barren_hint_players or woth_hint_players: # Check only relevant oot locations for barren/woth for player in (barren_hint_players | woth_hint_players): for loc in world.worlds[player].get_locations(): - if loc.item.code and (not loc.locked or loc.item.type == 'Song'): + if loc.item.code and (not loc.locked or oot_is_item_of_type(loc.item, 'Song')): if player in barren_hint_players: hint_area = get_hint_area(loc) items_by_region[player][hint_area]['weight'] += 1 From bf217dcf85ce1c2b577c8825df76ef68669d850d Mon Sep 17 00:00:00 2001 From: wordfcuk <80899010+wordfcuk@users.noreply.github.com> Date: Sun, 21 Aug 2022 17:30:30 +0200 Subject: [PATCH 10/91] RoR2: Fixed the link to the game settings page (#945) --- worlds/ror2/docs/setup_en.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/ror2/docs/setup_en.md b/worlds/ror2/docs/setup_en.md index dc1c3769c7..e0088e47da 100644 --- a/worlds/ror2/docs/setup_en.md +++ b/worlds/ror2/docs/setup_en.md @@ -29,7 +29,7 @@ You can see the [basic multiworld setup guide](/tutorial/Archipelago/setup/en) h 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 Hollow Knight](/games/Hollow%20Knight/player-settings) here on the Archipelago +You can use the [game settings page](/games/Risk%20of%20Rain%202/player-settings) here on the Archipelago website to generate a YAML using a graphical interface. From a4a8894d22167ef80e261e1bef88b72107754f9f Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Sun, 21 Aug 2022 19:20:35 -0400 Subject: [PATCH 11/91] Add /SNI to .gitignore (#949) --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 58122d64a2..8a7246210f 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ README.html .vs/ EnemizerCLI/ /Players/ +/SNI/ /options.yaml /config.yaml /logs/ From 9553627136c37bd08d8b538c47ae4a54c96e907b Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Mon, 22 Aug 2022 05:50:01 +0200 Subject: [PATCH 12/91] Witness: More bug fixes (#937) * Fixed disable_non_randomized and other bugs * Slight performance & code sensibility increase * Added River Shortcut to Garden as a disabled check in disable_non_randomized * Changed no progression items exception to a warning * Added a list of disabled panels to slot_data for disable_non_randomized, so the client can automatically disable the right panels in the future * Made no progression exception conditional on playercount --- worlds/witness/__init__.py | 12 ++++++--- worlds/witness/locations.py | 2 ++ worlds/witness/player_logic.py | 27 ++++++++++++------- .../witness/settings/Disable_Unrandomized.txt | 12 +++++---- 4 files changed, 35 insertions(+), 18 deletions(-) diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index 19c9b97240..a6a9f9f6f8 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -13,6 +13,7 @@ from .rules import set_rules from .regions import WitnessRegions from .Options import is_option_enabled, the_witness_options, get_option_value from .utils import best_junk_to_add_based_on_weights +from logging import warning class WitnessWebWorld(WebWorld): @@ -56,15 +57,20 @@ class WitnessWorld(World): 'panelhex_to_id': self.locat.CHECK_PANELHEX_TO_ID, 'item_id_to_door_hexes': self.items.ITEM_ID_TO_DOOR_HEX, 'door_hexes': self.items.DOORS, - 'symbols_not_in_the_game': self.items.SYMBOLS_NOT_IN_THE_GAME + 'symbols_not_in_the_game': self.items.SYMBOLS_NOT_IN_THE_GAME, + 'disabled_panels': self.player_logic.COMPLETELY_DISABLED_CHECKS, } def generate_early(self): if not (is_option_enabled(self.world, self.player, "shuffle_symbols") or get_option_value(self.world, self.player, "shuffle_doors") or is_option_enabled(self.world, self.player, "shuffle_lasers")): - raise Exception("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle, Door" - " Shuffle or Laser Shuffle") + if self.world.players == 1: + warning("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle, Door" + " Shuffle or Laser Shuffle if that doesn't seem right.") + else: + raise Exception("This Witness world doesn't have any progression items. Please turn on Symbol Shuffle," + " Door Shuffle or Laser Shuffle.") self.player_logic = WitnessPlayerLogic(self.world, self.player) self.locat = WitnessPlayerLocations(self.world, self.player, self.player_logic) diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index f6fcad70ce..9b7d60ea16 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -277,6 +277,8 @@ class WitnessPlayerLocations: if not is_option_enabled(world, player, "shuffle_postgame"): self.CHECK_LOCATIONS -= postgame + self.CHECK_LOCATIONS.discard(StaticWitnessLogic.CHECKS_BY_HEX[player_logic.VICTORY_LOCATION]["checkName"]) + self.CHECK_LOCATIONS = self.CHECK_LOCATIONS - { StaticWitnessLogic.CHECKS_BY_HEX[check_hex]["checkName"] for check_hex in player_logic.COMPLETELY_DISABLED_CHECKS diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index efbb177f00..6fe45b107f 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -36,6 +36,9 @@ class WitnessPlayerLogic: Panels outside of the same region will still be checked manually. """ + if panel_hex in self.COMPLETELY_DISABLED_CHECKS: + return frozenset() + check_obj = StaticWitnessLogic.CHECKS_BY_HEX[panel_hex] these_items = frozenset({frozenset()}) @@ -72,7 +75,9 @@ class WitnessPlayerLogic: for option_panel in option: dep_obj = StaticWitnessLogic.CHECKS_BY_HEX.get(option_panel) - if option_panel in {"7 Lasers", "11 Lasers"}: + if option_panel in self.COMPLETELY_DISABLED_CHECKS: + new_items = frozenset() + elif option_panel in {"7 Lasers", "11 Lasers"}: new_items = frozenset({frozenset([option_panel])}) # If a panel turns on when a panel in a different region turns on, # the latter panel will be an "event panel", unless it ends up being @@ -204,9 +209,11 @@ class WitnessPlayerLogic: elif get_option_value(world, player, "victory_condition") == 3: self.VICTORY_LOCATION = "0xFFF00" - self.COMPLETELY_DISABLED_CHECKS.add( - self.VICTORY_LOCATION - ) + if get_option_value(world, player, "challenge_lasers") <= 7: + adjustment_linesets_in_order.append([ + "Requirement Changes:", + "0xFFF00 - 11 Lasers - True", + ]) if is_option_enabled(world, player, "disable_non_randomized_puzzles"): adjustment_linesets_in_order.append(get_disable_unrandomized_list()) @@ -356,6 +363,12 @@ class WitnessPlayerLogic: "0x2700B": "Open Door to Treehouse Laser House", "0x00055": "Orchard Apple Trees 4 Turns On", "0x17DDB": "Left Orange Bridge Fully Extended", + "0x03535": "Shipwreck Video Pattern Knowledge", + "0x03542": "Mountain Video Pattern Knowledge", + "0x0339E": "Desert Video Pattern Knowledge", + "0x03481": "Tutorial Video Pattern Knowledge", + "0x03702": "Jungle Video Pattern Knowledge", + "0x0356B": "Challenge Video Pattern Knowledge", } self.ALWAYS_EVENT_NAMES_BY_HEX = { @@ -371,12 +384,6 @@ class WitnessPlayerLogic: "0x0C2B2": "Bunker Laser Activation", "0x00BF6": "Swamp Laser Activation", "0x028A4": "Treehouse Laser Activation", - "0x03535": "Shipwreck Video Pattern Knowledge", - "0x03542": "Mountain Video Pattern Knowledge", - "0x0339E": "Desert Video Pattern Knowledge", - "0x03481": "Tutorial Video Pattern Knowledge", - "0x03702": "Jungle Video Pattern Knowledge", - "0x0356B": "Challenge Video Pattern Knowledge", "0x09F7F": "Mountaintop Trap Door Turns On", "0x17C34": "Mountain Access", } diff --git a/worlds/witness/settings/Disable_Unrandomized.txt b/worlds/witness/settings/Disable_Unrandomized.txt index 43c2596405..f03f5496c2 100644 --- a/worlds/witness/settings/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Disable_Unrandomized.txt @@ -1,15 +1,15 @@ Event Items: -Town Tower 4th Door Opens - 0x17CFB,0x3C12B,0x00B8D,0x17CF7 -Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9,0x17CA4 +Monastery Laser Activation - 0x00A5B,0x17CE7,0x17FA9 Bunker Laser Activation - 0x00061,0x17D01,0x17C42 Shadows Laser Activation - 0x00021,0x17D28,0x17C71 Requirement Changes: -0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9 | 0x17CA4 +0x17C65 - 0x00A5B | 0x17CE7 | 0x17FA9 0x0C2B2 - 0x00061 | 0x17D01 | 0x17C42 0x181B3 - 0x00021 | 0x17D28 | 0x17C71 0x28B39 - True - Reflection 0x17CAB - True - True +0x2779A - True - 0x17CFB | 0x3C12B | 0x17CF7 Disabled Locations: 0x03505 (Tutorial Gate Close) @@ -61,7 +61,7 @@ Disabled Locations: 0x193AA (Monastery Branch Avoid 2) 0x193AB (Monastery Branch Follow 1) 0x193A6 (Monastery Branch Follow 2) -0x17CA4 (Monastery Laser) - 0x193A6 - True +0x17CA4 (Monastery Laser) 0x18590 (Tree Outlines) - True - Symmetry & Environment 0x28AE3 (Vines Shadows Follow) - 0x18590 - Shadows Follow & Environment 0x28938 (Four-way Apple Tree) - 0x28AE3 - Environment @@ -103,4 +103,6 @@ Disabled Locations: 0x17E67 (Bunker Drop-Down Door Squares 2) 0x09DE0 (Bunker Laser) 0x0A079 (Bunker Elevator Control) -0x0042D (Mountaintop River Shape) \ No newline at end of file +0x0042D (Mountaintop River Shape) + +0x17CAA (River Door to Garden Panel) \ No newline at end of file From 6a6dfcbaffa1f745a1d27056f2740831d885a1e2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 22 Aug 2022 07:24:36 +0200 Subject: [PATCH 13/91] Core: add some types to generic.Rules --- worlds/generic/Rules.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/worlds/generic/Rules.py b/worlds/generic/Rules.py index f75b74d0b6..f53c417e1c 100644 --- a/worlds/generic/Rules.py +++ b/worlds/generic/Rules.py @@ -49,11 +49,11 @@ def exclusion_rules(world, player: int, exclude_locations: typing.Set[str]): location.progress_type = LocationProgressType.EXCLUDED -def set_rule(spot, rule: CollectionRule): +def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule): spot.access_rule = rule -def add_rule(spot, rule: CollectionRule, combine='and'): +def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine='and'): old_rule = spot.access_rule if combine == 'or': spot.access_rule = lambda state: rule(state) or old_rule(state) @@ -61,35 +61,37 @@ def add_rule(spot, rule: CollectionRule, combine='and'): spot.access_rule = lambda state: rule(state) and old_rule(state) -def forbid_item(location, item: str, player: int): +def forbid_item(location: "BaseClasses.Location", item: str, player: int): old_rule = location.item_rule location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i) -def forbid_items_for_player(location, items: typing.Set[str], player: int): +def forbid_items_for_player(location: "BaseClasses.Location", items: typing.Set[str], player: int): old_rule = location.item_rule location.item_rule = lambda i: (i.player != player or i.name not in items) and old_rule(i) -def forbid_items(location, items: typing.Set[str]): +def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]): """unused, but kept as a debugging tool.""" old_rule = location.item_rule location.item_rule = lambda i: i.name not in items and old_rule(i) -def add_item_rule(location, rule: ItemRule): +def add_item_rule(location: "BaseClasses.Location", rule: ItemRule): old_rule = location.item_rule location.item_rule = lambda item: rule(item) and old_rule(item) -def item_in_locations(state, item: str, player: int, locations: typing.Sequence): +def item_in_locations(state: "BaseClasses.CollectionState", item: str, player: int, + locations: typing.Sequence["BaseClasses.Location"]) -> bool: for location in locations: if item_name(state, location[0], location[1]) == (item, player): return True return False -def item_name(state, location: str, player: int) -> typing.Optional[typing.Tuple[str, int]]: +def item_name(state: "BaseClasses.CollectionState", location: str, player: int) -> \ + typing.Optional[typing.Tuple[str, int]]: location = state.world.get_location(location, player) if location.item is None: return None From 87d91aeef39657eb67ff09ec8d029f2d48f2f85e Mon Sep 17 00:00:00 2001 From: N00byKing Date: Sun, 21 Aug 2022 20:45:39 +0200 Subject: [PATCH 14/91] sm64ex: Option for 1Up Block Rando --- worlds/sm64ex/Locations.py | 47 ++++++++++++++++++++++++++++++-------- worlds/sm64ex/Options.py | 7 ++++++ worlds/sm64ex/Rules.py | 3 ++- worlds/sm64ex/__init__.py | 44 +++++++++++++++++++++++++++++------ 4 files changed, 84 insertions(+), 17 deletions(-) diff --git a/worlds/sm64ex/Locations.py b/worlds/sm64ex/Locations.py index f33c4306a1..1995abf425 100644 --- a/worlds/sm64ex/Locations.py +++ b/worlds/sm64ex/Locations.py @@ -46,6 +46,9 @@ locCCM_table = { "CCM: Snowman's Lost His Head": 3626025, "CCM: Wall Kicks Will Work": 3626026, "CCM: Bob-omb Buddy": 3626203, + "CCM: 1Up Block Near Snowman": 3626215, + "CCM: 1Up Block Ice Pillar": 3626216, + "CCM: 1Up Block Secret Slide": 3626217 } #Big Boo's Haunt @@ -55,7 +58,8 @@ locBBH_table = { "BBH: Secret of the Haunted Books": 3626030, "BBH: Seek the 8 Red Coins": 3626031, "BBH: Big Boo's Balcony": 3626032, - "BBH: Eye to Eye in the Secret Room": 3626033 + "BBH: Eye to Eye in the Secret Room": 3626033, + "BBH: 1Up Block Top of Mansion": 3626218 } #Hazy Maze Cave @@ -65,7 +69,9 @@ locHMC_table = { "HMC: Metal-Head Mario Can Move!": 3626037, "HMC: Navigating the Toxic Maze": 3626038, "HMC: A-Maze-Ing Emergency Exit": 3626039, - "HMC: Watch for Rolling Rocks": 3626040 + "HMC: Watch for Rolling Rocks": 3626040, + "HMC: 1Up Block above Pit": 3626219, + "HMC: 1Up Block Past Rolling Rocks": 3626220, } #Lethal Lava Land @@ -87,6 +93,9 @@ locSSL_table = { "SSL: Free Flying for 8 Red Coins": 3626053, "SSL: Pyramid Puzzle": 3626054, "SSL: Bob-omb Buddy": 3626207, + "SSL: 1Up Block Outside Pyramid": 3626221, + "SSL: 1Up Block Pyramid Left Path": 3626222, + "SSL: 1Up Block Pyramid Back": 3626223 } #Dire, Dire Docks @@ -108,6 +117,8 @@ locSL_table = { "SL: Shell Shreddin' for Red Coins": 3626067, "SL: Into the Igloo": 3626068, "SL: Bob-omb Buddy": 3626209, + "SL: 1Up Block Near Moneybags": 3626224, + "SL: 1Up Block inside Igloo": 3626225 } #Wet-Dry World @@ -119,6 +130,7 @@ locWDW_table = { "WDW: Go to Town for Red Coins": 3626074, "WDW: Quick Race Through Downtown!": 3626075, "WDW: Bob-omb Buddy": 3626210, + "WDW: 1Up Block in Downtown": 3626226 } #Tall, Tall Mountain @@ -130,6 +142,7 @@ locTTM_table = { "TTM: Breathtaking View from Bridge": 3626081, "TTM: Blast to the Lonely Mushroom": 3626082, "TTM: Bob-omb Buddy": 3626211, + "TTM: 1Up Block on Red Mushroom": 3626227 } #Tiny-Huge Island @@ -141,6 +154,9 @@ locTHI_table = { "THI: Wiggler's Red Coins": 3626088, "THI: Make Wiggler Squirm": 3626089, "THI: Bob-omb Buddy": 3626212, + "THI: 1Up Block THI Small near Start": 3626228, + "THI: 1Up Block THI Large near Start": 3626229, + "THI: 1Up Block Windy Area": 3626230 } #Tick Tock Clock @@ -150,7 +166,9 @@ locTTC_table = { "TTC: Get a Hand": 3626093, "TTC: Stomp on the Thwomp": 3626094, "TTC: Timed Jumps on Moving Bars": 3626095, - "TTC: Stop Time for Red Coins": 3626096 + "TTC: Stop Time for Red Coins": 3626096, + "TTC: 1Up Block Midway Up": 3626231, + "TTC: 1Up Block at the Top": 3626232 } #Rainbow Ride @@ -162,6 +180,9 @@ locRR_table = { "RR: Tricky Triangles!": 3626102, "RR: Somewhere Over the Rainbow": 3626103, "RR: Bob-omb Buddy": 3626214, + "RR: 1Up Block Top of Red Coin Maze": 3626233, + "RR: 1Up Block Under Fly Guy": 3626234, + "RR: 1Up Block On House in the Sky": 3626235 } loc100Coin_table = { @@ -193,7 +214,9 @@ locSA_table = { locBitDW_table = { "Bowser in the Dark World Red Coins": 3626105, - "Bowser in the Dark World Key": 3626178 + "Bowser in the Dark World Key": 3626178, + "Bowser in the Dark World 1Up Block on Tower": 3626236, + "Bowser in the Dark World 1Up Block near Goombas": 3626237 } locTotWC_table = { @@ -203,25 +226,31 @@ locTotWC_table = { locCotMC_table = { "Cavern of the Metal Cap Switch": 3626182, - "Cavern of the Metal Cap Red Coins": 3626133 + "Cavern of the Metal Cap Red Coins": 3626133, + "Cavern of the Metal Cap 1Up Block": 3626241 } locVCutM_table = { "Vanish Cap Under the Moat Switch": 3626183, - "Vanish Cap Under the Moat Red Coins": 3626147 + "Vanish Cap Under the Moat Red Coins": 3626147, + "Vanish Cap Under the Moat 1Up Block": 3626242 } locBitFS_table = { "Bowser in the Fire Sea Red Coins": 3626112, - "Bowser in the Fire Sea Key": 3626179 + "Bowser in the Fire Sea Key": 3626179, + "Bowser in the Fire Sea 1Up Block Swaying Stairs": 3626238, + "Bowser in the Fire Sea 1Up Block Near Poles": 3626239 } locWMotR_table = { - "Wing Mario Over the Rainbow": 3626154 + "Wing Mario Over the Rainbow Red Coins": 3626154, + "Wing Mario Over the Rainbow 1Up Block": 3626242 } locBitS_table = { - "Bowser in the Sky Red Coins": 3626119 + "Bowser in the Sky Red Coins": 3626119, + "Bowser in the Sky 1Up Block": 3626240 } #Secret Stars found inside the Castle diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index 594b0561c0..7d9a75dde9 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -68,6 +68,12 @@ class BuddyChecks(Toggle): """Bob-omb Buddies are checks, Cannon Unlocks are items""" display_name = "Bob-omb Buddy Checks" +class ExclamationBoxes(Choice): + """Include 1Up Exclamation Boxes during randomization""" + display_name = "Randomize 1Up !-Blocks" + option_Off = 0 + option_1Ups_Only = 1 + class ProgressiveKeys(DefaultOnToggle): """Keys will first grant you access to the Basement, then to the Secound Floor""" display_name = "Progressive Keys" @@ -87,4 +93,5 @@ sm64_options: typing.Dict[str,type(Option)] = { "StarsToFinish": StarsToFinish, "death_link": DeathLink, "BuddyChecks": BuddyChecks, + "ExclamationBoxes": ExclamationBoxes } \ No newline at end of file diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index a4a82b2737..eae9868583 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -109,7 +109,8 @@ def set_rules(world, player: int, area_connections): add_rule(world.get_location("BoB: 100 Coins", player), lambda state: state.has("Cannon Unlock BoB", player) or state.has("Wing Cap", player)) #Rules for Secret Stars - add_rule(world.get_location("Wing Mario Over the Rainbow", player), lambda state: state.has("Wing Cap", player)) + add_rule(world.get_location("Wing Mario Over the Rainbow Red Coins", player), lambda state: state.has("Wing Cap", player)) + add_rule(world.get_location("Wing Mario Over the Rainbow 1Up Block", player), lambda state: state.has("Wing Cap", player)) add_rule(world.get_location("Toad (Basement)", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, 12)) add_rule(world.get_location("Toad (Second Floor)", player), lambda state: state.can_reach("Second Floor", 'Region', player) and state.has("Power Star", player, 25)) add_rule(world.get_location("Toad (Third Floor)", player), lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Power Star", player, 35)) diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index e0f911fbd9..447a09d431 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -34,7 +34,7 @@ class SM64World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 6 + data_version = 7 required_client_version = (0, 3, 0) area_connections: typing.Dict[int, int] @@ -71,7 +71,6 @@ class SM64World(World): return item def generate_basic(self): - staritem = self.create_item("Power Star") starcount = self.world.AmountOfStars[self.player].value if (not self.world.EnableCoinStars[self.player].value): starcount = max(35,self.world.AmountOfStars[self.player].value-15) @@ -79,17 +78,15 @@ class SM64World(World): self.world.BasementStarDoorCost[self.player].value, self.world.SecondFloorStarDoorCost[self.player].value, self.world.MIPS1Cost[self.player].value, self.world.MIPS2Cost[self.player].value, self.world.StarsToFinish[self.player].value) - self.world.itempool += [staritem for i in range(0,starcount)] - mushroomitem = self.create_item("1Up Mushroom") - self.world.itempool += [mushroomitem for i in range(starcount,120 - (15 if not self.world.EnableCoinStars[self.player].value else 0))] + self.world.itempool += [self.create_item("Power Star") for i in range(0,starcount)] + self.world.itempool += [self.create_item("1Up Mushroom") for i in range(starcount,120 - (15 if not self.world.EnableCoinStars[self.player].value else 0))] if (not self.world.ProgressiveKeys[self.player].value): key1 = self.create_item("Basement Key") key2 = self.create_item("Second Floor Key") self.world.itempool += [key1,key2] else: - key = self.create_item("Progressive Key") - self.world.itempool += [key,key] + self.world.itempool += [self.create_item("Progressive Key") for i in range(0,2)] wingcap = self.create_item("Wing Cap") metalcap = self.create_item("Metal Cap") @@ -110,6 +107,39 @@ class SM64World(World): self.world.get_location("THI: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock THI")) self.world.get_location("RR: Bob-omb Buddy", self.player).place_locked_item(self.create_item("Cannon Unlock RR")) + if (self.world.ExclamationBoxes[self.player].value > 0): + self.world.itempool += [self.create_item("1Up Mushroom") for i in range(0,29)] + else: + self.world.get_location("CCM: 1Up Block Near Snowman", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("CCM: 1Up Block Ice Pillar", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("CCM: 1Up Block Secret Slide", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("BBH: 1Up Block Top of Mansion", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("HMC: 1Up Block above Pit", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("HMC: 1Up Block Past Rolling Rocks", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("SSL: 1Up Block Outside Pyramid", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("SSL: 1Up Block Pyramid Left Path", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("SSL: 1Up Block Pyramid Back", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("SL: 1Up Block Near Moneybags", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("SL: 1Up Block inside Igloo", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("WDW: 1Up Block in Downtown", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("TTM: 1Up Block on Red Mushroom", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("THI: 1Up Block THI Small near Start", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("THI: 1Up Block THI Large near Start", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("THI: 1Up Block Windy Area", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("TTC: 1Up Block Midway Up", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("TTC: 1Up Block at the Top", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("RR: 1Up Block Top of Red Coin Maze", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("RR: 1Up Block Under Fly Guy", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("RR: 1Up Block On House in the Sky", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Bowser in the Dark World 1Up Block on Tower", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Bowser in the Dark World 1Up Block near Goombas", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Cavern of the Metal Cap 1Up Block", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Vanish Cap Under the Moat 1Up Block", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Bowser in the Fire Sea 1Up Block Swaying Stairs", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Bowser in the Fire Sea 1Up Block Near Poles", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Wing Mario Over the Rainbow 1Up Block", self.player).place_locked_item(self.create_item("1Up Mushroom")) + self.world.get_location("Bowser in the Sky 1Up Block", self.player).place_locked_item(self.create_item("1Up Mushroom")) + def get_filler_item_name(self) -> str: return "1Up Mushroom" From 11cbc0b40b6a6b68e7dea99b3a6ad1c17e00c739 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Mon, 22 Aug 2022 14:30:42 -0700 Subject: [PATCH 15/91] Factorio: Make the energy bridge a different color. (#952) --- worlds/factorio/data/mod_template/data.lua | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/worlds/factorio/data/mod_template/data.lua b/worlds/factorio/data/mod_template/data.lua index c5f2745bd1..d790831478 100644 --- a/worlds/factorio/data/mod_template/data.lua +++ b/worlds/factorio/data/mod_template/data.lua @@ -1,4 +1,15 @@ {% from "macros.lua" import dict_to_lua %} +-- TODO: Replace the tinting code with an actual rendered picture of the energy bridge icon. +-- This tint is so that one is less likely to accidentally mass-produce energy-bridges, then wonder why their rocket is not building. +function energy_bridge_tint() + return { r = 0, g = 1, b = 0.667, a = 1} +end +function tint_icon(obj, tint) + obj.icons = { {icon = obj.icon, icon_size = obj.icon_size, icon_mipmaps = obj.icon_mipmaps, tint = tint} } + obj.icon = nil + obj.icon_size = nil + obj.icon_mipmaps = nil +end local energy_bridge = table.deepcopy(data.raw["accumulator"]["accumulator"]) energy_bridge.name = "ap-energy-bridge" energy_bridge.minable.result = "ap-energy-bridge" @@ -6,12 +17,20 @@ energy_bridge.localised_name = "Archipelago EnergyLink Bridge" energy_bridge.energy_source.buffer_capacity = "5MJ" energy_bridge.energy_source.input_flow_limit = "1MW" energy_bridge.energy_source.output_flow_limit = "1MW" +tint_icon(energy_bridge, energy_bridge_tint()) +energy_bridge.picture.layers[1].tint = energy_bridge_tint() +energy_bridge.picture.layers[1].hr_version.tint = energy_bridge_tint() +energy_bridge.charge_animation.layers[1].layers[1].tint = energy_bridge_tint() +energy_bridge.charge_animation.layers[1].layers[1].hr_version.tint = energy_bridge_tint() +energy_bridge.discharge_animation.layers[1].layers[1].tint = energy_bridge_tint() +energy_bridge.discharge_animation.layers[1].layers[1].hr_version.tint = energy_bridge_tint() data.raw["accumulator"]["ap-energy-bridge"] = energy_bridge local energy_bridge_item = table.deepcopy(data.raw["item"]["accumulator"]) energy_bridge_item.name = "ap-energy-bridge" energy_bridge_item.localised_name = "Archipelago EnergyLink Bridge" energy_bridge_item.place_result = energy_bridge.name +tint_icon(energy_bridge_item, energy_bridge_tint()) data.raw["item"]["ap-energy-bridge"] = energy_bridge_item local energy_bridge_recipe = table.deepcopy(data.raw["recipe"]["accumulator"]) From c695f91198f1094badfee98d15082024ebe00322 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 22 Aug 2022 23:35:41 +0200 Subject: [PATCH 16/91] Subnautica: add Options to Creature Scans (#950) --- worlds/subnautica/Creatures.py | 25 +++++++-- worlds/subnautica/Items.py | 4 +- worlds/subnautica/Options.py | 20 ++++++- worlds/subnautica/Rules.py | 100 +++++++++++++++++++-------------- worlds/subnautica/__init__.py | 11 +++- 5 files changed, 109 insertions(+), 51 deletions(-) diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/Creatures.py index 56e2a7efa1..a9f5e850e1 100644 --- a/worlds/subnautica/Creatures.py +++ b/worlds/subnautica/Creatures.py @@ -50,10 +50,8 @@ all_creatures: Dict[str, int] = { "Lava Larva": 1300, "Lava Lizard": 1300, "Sea Dragon Leviathan": 1300, - "Sea Emperor Leviathan": 1700, + "Cuddlefish": 300, "Sea Emperor Juvenile": 1700, - - # "Cuddlefish": 300, # maybe at some point, needs hatching in containment chamber (20 real-life minutes) } # be nice and make these require Stasis Rifle @@ -73,10 +71,29 @@ aggressive: Set[str] = { "River Prowler", } +containment: Set[str] = { # creatures that have to be raised from eggs + "Cuddlefish", +} + +hatchable: Set[str] = { # aggressive creatures that can be grown from eggs as alternative to stasis + "Ampeel", # warning: electric shocks + "Crabsquid", # warning: electric shocks + "Crabsnake", + "Boneshark", + "Crashfish", + "Gasopod", + "Lava Lizard", + "Mesmer", + "Sand Shark", + "Stalker", +} + suffix: str = " Scan" creature_locations: Dict[str, int] = { - creature+suffix: creature_id for creature_id, creature in enumerate(all_creatures, start=34000) + creature + suffix: creature_id for creature_id, creature in enumerate(all_creatures, start=34000) } all_creatures_presorted: List[str] = sorted(all_creatures) +all_creatures_presorted_without_containment = [name for name in all_creatures_presorted if name not in containment] + diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index 0f05d5e31a..9917921a4b 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -42,7 +42,7 @@ item_table: Dict[int, ItemDict] = { 'count': 1, 'name': 'Stillsuit', 'tech_type': 'Stillsuit'}, - 35008: {'classification': ItemClassification.filler, + 35008: {'classification': ItemClassification.progression, 'count': 2, 'name': 'Alien Containment Fragment', 'tech_type': 'BaseWaterParkFragment'}, @@ -222,7 +222,7 @@ item_table: Dict[int, ItemDict] = { 'count': 2, 'name': 'Observatory Fragment', 'tech_type': 'BaseObservatoryFragment'}, - 35053: {'classification': ItemClassification.useful, + 35053: {'classification': ItemClassification.progression, 'count': 2, 'name': 'Multipurpose Room', 'tech_type': 'BaseRoom'}, diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index f9f3f56756..020b5d1916 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,4 +1,4 @@ -from Options import Choice, Range, DeathLink +from Options import Choice, Range, DeathLink, Toggle from .Creatures import all_creatures @@ -33,12 +33,27 @@ class Goal(Choice): class CreatureScans(Range): - """Place items on specific creature scans. + """Place items on specific, randomly chosen, creature scans. Warning: Includes aggressive Leviathans.""" display_name = "Creature Scans" range_end = len(all_creatures) +class AggressiveScanLogic(Toggle): + """By default (Stasis), aggressive Creature Scans are logically expected only with a Stasis Rifle. + Containment: Removes Stasis Rifle as expected solution and expects Alien Containment instead. + Either: Creatures may be expected to be scanned via Stasis Rifle or Containment, whichever is found first. + None: Aggressive Creatures are assumed to not need any tools to scan. + + Note: Containment, Either and None adds Cuddlefish as an option for scans. + Note: This is purely a logic expectation, and does not affect gameplay, only placement.""" + display_name = "Aggressive Creature Scan Logic" + option_stasis = 0 + option_containment = 1 + option_either = 2 + option_none = 3 + + class SubnauticaDeathLink(DeathLink): """When you die, everyone dies. Of course the reverse is true too. Note: can be toggled via in-game console command "deathlink".""" @@ -48,5 +63,6 @@ options = { "item_pool": ItemPool, "goal": Goal, "creature_scans": CreatureScans, + "creature_scan_logic": AggressiveScanLogic, "death_link": SubnauticaDeathLink, } diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index b8f8f1a7b4..20c6a35c84 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -1,122 +1,128 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, Callable -from worlds.generic.Rules import set_rule +from worlds.generic.Rules import set_rule, add_rule from .Locations import location_table, LocationDict from .Creatures import all_creatures, aggressive, suffix +from .Options import AggressiveScanLogic import math if TYPE_CHECKING: from . import SubnauticaWorld + from BaseClasses import CollectionState, Location -def has_seaglide(state, player: int): +def has_seaglide(state: "CollectionState", player: int): return state.has("Seaglide Fragment", player, 2) -def has_modification_station(state, player: int): +def has_modification_station(state: "CollectionState", player: int): return state.has("Modification Station Fragment", player, 3) -def has_mobile_vehicle_bay(state, player: int): +def has_mobile_vehicle_bay(state: "CollectionState", player: int): return state.has("Mobile Vehicle Bay Fragment", player, 3) -def has_moonpool(state, player: int): +def has_moonpool(state: "CollectionState", player: int): return state.has("Moonpool Fragment", player, 2) -def has_vehicle_upgrade_console(state, player: int): +def has_vehicle_upgrade_console(state: "CollectionState", player: int): return state.has("Vehicle Upgrade Console", player) and \ has_moonpool(state, player) -def has_seamoth(state, player: int): +def has_seamoth(state: "CollectionState", player: int): return state.has("Seamoth Fragment", player, 3) and \ has_mobile_vehicle_bay(state, player) -def has_seamoth_depth_module_mk1(state, player: int): +def has_seamoth_depth_module_mk1(state: "CollectionState", player: int): return has_vehicle_upgrade_console(state, player) -def has_seamoth_depth_module_mk2(state, player: int): +def has_seamoth_depth_module_mk2(state: "CollectionState", player: int): return has_seamoth_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_seamoth_depth_module_mk3(state, player: int): +def has_seamoth_depth_module_mk3(state: "CollectionState", player: int): return has_seamoth_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_cyclops_bridge(state, player: int): +def has_cyclops_bridge(state: "CollectionState", player: int): return state.has("Cyclops Bridge Fragment", player, 3) -def has_cyclops_engine(state, player: int): +def has_cyclops_engine(state: "CollectionState", player: int): return state.has("Cyclops Engine Fragment", player, 3) -def has_cyclops_hull(state, player: int): +def has_cyclops_hull(state: "CollectionState", player: int): return state.has("Cyclops Hull Fragment", player, 3) -def has_cyclops(state, player: int): +def has_cyclops(state: "CollectionState", player: int): return has_cyclops_bridge(state, player) and \ has_cyclops_engine(state, player) and \ has_cyclops_hull(state, player) and \ has_mobile_vehicle_bay(state, player) -def has_cyclops_depth_module_mk1(state, player: int): +def has_cyclops_depth_module_mk1(state: "CollectionState", player: int): return state.has("Cyclops Depth Module MK1", player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk2(state, player: int): +def has_cyclops_depth_module_mk2(state: "CollectionState", player: int): return has_cyclops_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_cyclops_depth_module_mk3(state, player: int): +def has_cyclops_depth_module_mk3(state: "CollectionState", player: int): return has_cyclops_depth_module_mk2(state, player) and \ has_modification_station(state, player) -def has_prawn(state, player: int): +def has_prawn(state: "CollectionState", player: int): return state.has("Prawn Suit Fragment", player, 4) and \ has_mobile_vehicle_bay(state, player) -def has_praw_propulsion_arm(state, player: int): +def has_prawn_propulsion_arm(state: "CollectionState", player: int): return state.has("Prawn Suit Propulsion Cannon Fragment", player, 2) and \ has_vehicle_upgrade_console(state, player) -def has_prawn_depth_module_mk1(state, player: int): +def has_prawn_depth_module_mk1(state: "CollectionState", player: int): return has_vehicle_upgrade_console(state, player) -def has_prawn_depth_module_mk2(state, player: int): +def has_prawn_depth_module_mk2(state: "CollectionState", player: int): return has_prawn_depth_module_mk1(state, player) and \ has_modification_station(state, player) -def has_laser_cutter(state, player: int): +def has_laser_cutter(state: "CollectionState", player: int): return state.has("Laser Cutter Fragment", player, 3) -def has_stasis_rile(state, player: int): +def has_stasis_rifle(state: "CollectionState", player: int): return state.has("Stasis Rifle Fragment", player, 2) +def has_containment(state: "CollectionState", player: int): + return state.has("Alien Containment Fragment", player, 2) and state.has("Multipurpose Room", player) + + # Either we have propulsion cannon, or prawn + propulsion cannon arm -def has_propulsion_cannon(state, player: int): +def has_propulsion_cannon(state: "CollectionState", player: int): return state.has("Propulsion Cannon Fragment", player, 2) or \ - (has_prawn(state, player) and has_praw_propulsion_arm(state, player)) + (has_prawn(state, player) and has_prawn_propulsion_arm(state, player)) -def has_cyclops_shield(state, player: int): +def has_cyclops_shield(state: "CollectionState", player: int): return has_cyclops(state, player) and \ state.has("Cyclops Shield Generator", player) @@ -129,7 +135,7 @@ def has_cyclops_shield(state, player: int): # negligeable with from high capacity tank. 430m -> 460m # Fins are not used when using seaglide # -def get_max_swim_depth(state, player: int): +def get_max_swim_depth(state: "CollectionState", player: int): # TODO, Make this a difficulty setting. # Only go up to 200m without any submarines for now. return 200 @@ -140,7 +146,7 @@ def get_max_swim_depth(state, player: int): # has_ultra_glide_fins = state.has("Ultra Glide Fins", player) # max_depth = 400 # More like 430m. Give some room - # if has_seaglide(state, player: int): + # if has_seaglide(state: "CollectionState", player: int): # if has_ultra_high_capacity_tank: # max_depth = 750 # It's about 50m more. Give some room # else: @@ -156,7 +162,7 @@ def get_max_swim_depth(state, player: int): # return max_depth -def get_seamoth_max_depth(state, player: int): +def get_seamoth_max_depth(state: "CollectionState", player: int): if has_seamoth(state, player): if has_seamoth_depth_module_mk3(state, player): return 900 @@ -170,7 +176,7 @@ def get_seamoth_max_depth(state, player: int): return 0 -def get_cyclops_max_depth(state, player): +def get_cyclops_max_depth(state: "CollectionState", player): if has_cyclops(state, player): if has_cyclops_depth_module_mk3(state, player): return 1700 @@ -184,7 +190,7 @@ def get_cyclops_max_depth(state, player): return 0 -def get_prawn_max_depth(state, player): +def get_prawn_max_depth(state: "CollectionState", player): if has_prawn(state, player): if has_prawn_depth_module_mk2(state, player): return 1700 @@ -196,7 +202,7 @@ def get_prawn_max_depth(state, player): return 0 -def get_max_depth(state, player: int): +def get_max_depth(state: "CollectionState", player: int): # TODO, Difficulty option, we can add vehicle depth + swim depth # But at this point, we have to consider traver distance in caves, not # just depth @@ -206,7 +212,7 @@ def get_max_depth(state, player: int): get_prawn_max_depth(state, player)) -def can_access_location(state, player: int, loc: LocationDict) -> bool: +def can_access_location(state: "CollectionState", player: int, loc: LocationDict) -> bool: need_laser_cutter = loc.get("need_laser_cutter", False) if need_laser_cutter and not has_laser_cutter(state, player): return False @@ -239,17 +245,25 @@ def set_location_rule(world, player: int, loc: LocationDict): set_rule(world.get_location(loc["name"], player), lambda state: can_access_location(state, player, loc)) -def can_scan_creature(state, player: int, creature: str) -> bool: +def can_scan_creature(state: "CollectionState", player: int, creature: str) -> bool: if not has_seaglide(state, player): return False - if creature in aggressive and not has_stasis_rile(state, player): - return False return get_max_depth(state, player) >= all_creatures[creature] -def set_creature_rule(world, player, creature_name: str): - set_rule(world.get_location(creature_name + suffix, player), +def set_creature_rule(world, player: int, creature_name: str) -> "Location": + location = world.get_location(creature_name + suffix, player) + set_rule(location, lambda state: can_scan_creature(state, player, creature_name)) + return location + + +aggression_rules: Dict[int, Callable[["CollectionState", int], bool]] = { + AggressiveScanLogic.option_stasis: has_stasis_rifle, + AggressiveScanLogic.option_containment: has_containment, + AggressiveScanLogic.option_either: lambda state, player: + has_stasis_rifle(state, player) or has_containment(state, player) +} def set_rules(subnautica_world: "SubnauticaWorld"): @@ -259,8 +273,12 @@ def set_rules(subnautica_world: "SubnauticaWorld"): for loc in location_table.values(): set_location_rule(world, player, loc) - for creature_name in subnautica_world.creatures_to_scan: - set_creature_rule(world, player, creature_name) + if subnautica_world.creatures_to_scan: + aggressive_rule = aggression_rules.get(world.creature_scan_logic[player], None) + for creature_name in subnautica_world.creatures_to_scan: + location = set_creature_rule(world, player, creature_name) + if creature_name in aggressive and aggressive_rule: + add_rule(location, lambda state: aggressive_rule(state, player)) # Victory locations set_rule(world.get_location("Neptune Launch", player), lambda state: diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 6fa064d53a..6562d93db0 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -41,7 +41,7 @@ class SubnauticaWorld(World): location_name_to_id = all_locations option_definitions = Options.options - data_version = 5 + data_version = 6 required_client_version = (0, 3, 4) prefill_items: List[Item] @@ -52,7 +52,14 @@ class SubnauticaWorld(World): self.create_item("Seaglide Fragment"), self.create_item("Seaglide Fragment") ] - self.creatures_to_scan = self.world.random.sample(Creatures.all_creatures_presorted, + if self.world.creature_scan_logic[self.player] == Options.AggressiveScanLogic.option_stasis: + valid_creatures = Creatures.all_creatures_presorted_without_containment + self.world.creature_scans[self.player].value = min(len( + Creatures.all_creatures_presorted_without_containment), + self.world.creature_scans[self.player].value) + else: + valid_creatures = Creatures.all_creatures_presorted + self.creatures_to_scan = self.world.random.sample(valid_creatures, self.world.creature_scans[self.player].value) def create_regions(self): From b66a2657261dadc897f2f70cba2f49f70f20f0fc Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 22 Aug 2022 16:50:16 -0500 Subject: [PATCH 17/91] Docs: Make webworld attribute descriptions docstrings instead of comments for nice IDE things (#929) --- worlds/AutoWorld.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 1ca5b53422..02b94c5fb7 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -97,22 +97,22 @@ def call_stage(world: "MultiWorld", method_name: str, *args: Any) -> None: class WebWorld: """Webhost integration""" - # display a settings page. Can be a link to an out-of-ap settings tool too. + settings_page: Union[bool, str] = True - - # docs folder will be scanned for game info pages using this list in the format '{language}_{game_name}.md' + """display a settings page. Can be a link to a specific page or external tool.""" + game_info_languages: List[str] = ['en'] + """docs folder will be scanned for game info pages using this list in the format '{language}_{game_name}.md'""" - # docs folder will also be scanned for tutorial guides given the relevant information in this list. Each Tutorial - # class is to be used for one guide. tutorials: List["Tutorial"] + """docs folder will also be scanned for tutorial guides. Each Tutorial class is to be used for one guide.""" - # Choose a theme for your /game/* pages - # Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone theme = "grass" + """Choose a theme for you /game/* pages. + Available: dirt, grass, grassFlowers, ice, jungle, ocean, partyTime, stone""" - # display a link to a bug report page, most likely a link to a GitHub issue page. bug_report_page: Optional[str] + """display a link to a bug report page, most likely a link to a GitHub issue page.""" class World(metaclass=AutoWorldRegister): From d66f981be6213144c1dfbf77a93d3f45c72df562 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 22 Aug 2022 17:39:55 -0500 Subject: [PATCH 18/91] Github: templates and new user interface (#870) * move some docs out of readme and link with the headers * PR template * bug report template * task and feature request templates * md cleanup * forgot the template * make expected results separate section * move pr template to .github. remove assignment field on tasks * add headers to pr template * Requested changes * suggested changes from @black-sliver and @SoldierofOrder * Update docs/code_of_conduct.md Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * Update docs/contributing.md Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> * Update docs/contributing.md Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> Co-authored-by: Hussein Farran Co-authored-by: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug_report.yaml | 35 +++++++++++++++++++++ .github/ISSUE_TEMPLATE/feature_request.yaml | 17 ++++++++++ .github/ISSUE_TEMPLATE/task.yaml | 10 ++++++ .github/pull_request_template.md | 12 +++++++ README.md | 22 ++----------- docs/code_of_conduct.md | 11 +++++++ docs/contributing.md | 12 +++++++ 7 files changed, 100 insertions(+), 19 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yaml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yaml create mode 100644 .github/ISSUE_TEMPLATE/task.yaml create mode 100644 .github/pull_request_template.md create mode 100644 docs/code_of_conduct.md create mode 100644 docs/contributing.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000000..dff9a56651 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,35 @@ +name: Bug Report +description: File a bug report. +title: "Bug: " +labels: + - bug +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! If this bug occurred during local generation check your + Archipelago install for a log (probably `C:\ProgramData\Archipelago\logs`) + and upload it with this report, as well as all yaml files used. + - type: textarea + id: what-happened + attributes: + label: What happened? + validations: + required: true + - type: textarea + id: expected-results + attributes: + label: What were the expected results? + validations: + required: true + - type: dropdown + id: version + attributes: + label: Software + description: Where did this bug occur? + options: + - Website + - Local generation + - While playing + validations: + required: true \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 0000000000..84cee1b7f1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,17 @@ +name: Feature Request +description: Request a feature! +title: "Category: " +labels: + - enhancement +body: + - type: markdown + attributes: + value: | + Please replace `Category` in the title with what this feature will be targeting, such as Core generation, + website, documentation, or a game. + Note: this is not for requesting new games to be added. If you would like to request a game, the best place to + ask is about it is in the [discord](https://archipelago.gg/discord). + - type: textarea + id: feature + attributes: + label: What feature would you like to see? \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/task.yaml b/.github/ISSUE_TEMPLATE/task.yaml new file mode 100644 index 0000000000..fb677c684f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yaml @@ -0,0 +1,10 @@ +name: Task +description: Submit a task to be done. If this is not targeting core, it should likely be elsewhere. +title: "Core: " +labels: + - core + - enhancement +body: + - type: textarea + attributes: + label: What task needs to be completed? \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..c7c6471dd0 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,12 @@ +Please format your title with what portion of the project this pull request is +targeting and what it's changing. + +ex. "MyGame4: implement new game" or "Docs: add new guide for customizing MyGame3" + +## What is this fixing or adding? + + +## How was this tested? + + +## If this makes graphical changes, please attach screenshots. diff --git a/README.md b/README.md index 9403159c74..c8362dddd0 100644 --- a/README.md +++ b/README.md @@ -61,26 +61,10 @@ This project makes use of multiple other projects. We wouldn't be here without t * [Ocarina of Time Randomizer](https://github.com/TestRunnerSRL/OoT-Randomizer) ## Contributing -Contributions are welcome. We have a few asks of any new contributors. - -* Ensure that all changes which affect logic are covered by unit tests. -* Do not introduce any unit test failures/regressions. - -Otherwise, we tend to judge code on a case to case basis. It is a generally good idea to stick to PEP-8 guidelines to ensure consistency with existing code. (And to make the linter happy.) - -For adding a new game to Archipelago and other documentation on how Archipelago functions, please see [the docs folder](docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev channel in our discord. +For contribution guidelines, please see our [Contributing doc.](/docs/contributing.md) ## FAQ -For frequently asked questions see the website's [FAQ Page](https://archipelago.gg/faq/en/) +For Frequently asked questions, please see the website's [FAQ Page.](https://archipelago.gg/faq/en/) ## Code of Conduct -We conduct ourselves openly and inclusively here. Please do not contribute to an environment which makes other people uncomfortable. This means that we expect all contributors or participants here to: - -* Be welcoming and inclusive in tone and language. -* Be respectful of others and their abilities. -* Show empathy when speaking with others. -* Be gracious and accept feedback and constructive criticism. - -These guidelines apply to all channels of communication within this GitHub repository. Please be respectful in both public channels, such as issues, and private, such as private messaging or emails. - -Any incidents of abuse may be reported directly to Ijwu at hmfarran@gmail.com. +Please refer to our [code of conduct.](/docs/code_of_conduct.md) diff --git a/docs/code_of_conduct.md b/docs/code_of_conduct.md new file mode 100644 index 0000000000..dd3d154a02 --- /dev/null +++ b/docs/code_of_conduct.md @@ -0,0 +1,11 @@ +# Code of Conduct +We conduct ourselves openly and inclusively here. Please do not contribute to an environment which makes other people uncomfortable. This means that we expect all contributors or participants here to: + +* Be welcoming and inclusive in tone and language. +* Be respectful of others and their abilities. +* Show empathy when speaking with others. +* Be gracious and accept feedback and constructive criticism. + +These guidelines apply to all channels of communication within this GitHub repository. Please be respectful in both public channels, such as issues, and private ones, such as private messaging or emails. + +Any incidents of abuse may be reported directly to ijwu at hmfarran@gmail.com. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000000..adbbf0dea1 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,12 @@ +# Contributing +Contributions are welcome. We have a few requests of any new contributors. + +* Ensure that all changes which affect logic are covered by unit tests. +* Do not introduce any unit test failures/regressions. +* Follow styling as designated in our [styling documentation](/docs/style.md). + +Otherwise, we tend to judge code on a case to case basis. + +For adding a new game to Archipelago and other documentation on how Archipelago functions, please see +[the docs folder](docs/) for the relevant information and feel free to ask any questions in the #archipelago-dev +channel in our [Discord](https://archipelago.gg/discord). From 7f41cafffc3d97971aec36fa86d6c7576867c8bb Mon Sep 17 00:00:00 2001 From: Chris Wilson Date: Mon, 22 Aug 2022 19:01:21 -0400 Subject: [PATCH 19/91] Explaining the "Style Lockdown" (#940) * First pass at a contribution guide for the website. Suggestions are welcome. * Attempt to make the WebHost change guide describe the intent of the style restrictions more accurately. * Try to improve the explanation of the intention behind the style restrictions. --- WebHostLib/README.md | 46 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 WebHostLib/README.md diff --git a/WebHostLib/README.md b/WebHostLib/README.md new file mode 100644 index 0000000000..52d4963aee --- /dev/null +++ b/WebHostLib/README.md @@ -0,0 +1,46 @@ +# WebHost + +## Contribution Guidelines +**Thank you for your interest in contributing to the Archipelago website!** +Much of the content on the website is generated automatically, but there are some things +that need a personal touch. For those things, we rely on contributions from both the core +team and the community. The current primary maintainer of the website is Farrak Kilhn. +He may be found on Discord as `Farrak Kilhn#0418`, or on GitHub as `LegendaryLinux`. + +### Small Changes +Little changes like adding a button or a couple new select elements are perfectly fine. +Tweaks to style specific to a PR's content are also probably not a problem. For example, if +you build a new page which needs two side by side tables, and you need to write a CSS file +specific to your page, that is perfectly reasonable. + +### Content Additions +Once you develop a new feature or add new content the website, make a pull request. It will +be reviewed by the community and there will probably be some discussion around it. Depending +on the size of the feature, and if new styles are required, there may be an additional step +before the PR is accepted wherein Farrak works with the designer to implement styles. + +### Restrictions on Style Changes +A professional designer is paid to develop the styles and assets for the Archipelago website. +In an effort to maintain a consistent look and feel, pull requests which *exclusively* +change site styles are rejected. Please note this applies to code which changes the overall +look and feel of the site, not to small tweaks to CSS for your custom page. The intention +behind these restrictions is to maintain a curated feel for the design of the site. If +any PR affects the overall feel of the site but includes additive changes, there will +likely be a conversation about how to implement those changes without compromising the +curated site style. It is therefore worth noting there are a couple files which, if +changed in your pull request, will cause it to draw additional scrutiny. + +These closely guarded files are: +- `globalStyles.css` +- `islandFooter.css` +- `landing.css` +- `markdown.css` +- `tooltip.css` + +### Site Themes +There are several themes available for game pages. It is possible to request a new theme in +the `#art-and-design` channel on Discord. Because themes are created by the designer, they +are not free, and take some time to create. Farrak works closely with the designer to implement +these themes, and pays for the assets out of pocket. Therefore, only a couple themes per year +are added. If a proposed theme seems like a cool idea and the community likes it, there is a +good chance it will become a reality. From 0a5b24be2b714b741d837f4392a455788c4b07ce Mon Sep 17 00:00:00 2001 From: Jarno Date: Tue, 23 Aug 2022 01:02:10 +0200 Subject: [PATCH 20/91] [Core] Phase out Print packets and added Countdown type to print json (#812) * [Core] Added Countdown type to print json to distinct the count down message from other types * Added backward compatibility check * Fixed review comments * Updated header category * Apply suggestions from code review Co-authored-by: Hussein Farran * Completely phased out Print in favor of PrintJson * Updated docs to warn about phasing out of Print * Removed faulty import Co-authored-by: Hussein Farran --- MultiServer.py | 38 ++++++++++++++++++++++++++++++-------- docs/network protocol.md | 16 ++++++++++++++-- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 8a1844bf92..6354f8e7a9 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -36,6 +36,7 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ SlotType min_client_version = Version(0, 1, 6) +print_command_compatability_threshold = Version(0, 3, 5) # Remove backwards compatibility around 0.3.7 colorama.init() # functions callable on storable data on the server by clients @@ -291,20 +292,27 @@ class Context: # text - def notify_all(self, text): + def notify_all(self, text: str): logging.info("Notice (all): %s" % text) - self.broadcast_all([{"cmd": "Print", "text": text}]) + broadcast_text_all(self, text) def notify_client(self, client: Client, text: str): if not client.auth: return logging.info("Notice (Player %s in team %d): %s" % (client.name, client.team + 1, text)) - asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}])) + if client.version >= print_command_compatability_threshold: + asyncio.create_task(self.send_msgs(client, [{"cmd": "PrintJSON", "data": [{ "text": text }]}])) + else: + asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text}])) def notify_client_multiple(self, client: Client, texts: typing.List[str]): if not client.auth: return - asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts])) + if client.version >= print_command_compatability_threshold: + asyncio.create_task(self.send_msgs(client, + [{"cmd": "PrintJSON", "data": [{ "text": text }]} for text in texts])) + else: + asyncio.create_task(self.send_msgs(client, [{"cmd": "Print", "text": text} for text in texts])) # loading @@ -721,19 +729,33 @@ async def on_client_left(ctx: Context, client: Client): ctx.client_connection_timers[client.team, client.slot] = datetime.datetime.now(datetime.timezone.utc) -async def countdown(ctx: Context, timer): - ctx.notify_all(f'[Server]: Starting countdown of {timer}s') +async def countdown(ctx: Context, timer: int): + broadcast_countdown(ctx, timer, f"[Server]: Starting countdown of {timer}s") if ctx.countdown_timer: ctx.countdown_timer = timer # timer is already running, set it to a different time else: ctx.countdown_timer = timer while ctx.countdown_timer > 0: - ctx.notify_all(f'[Server]: {ctx.countdown_timer}') + broadcast_countdown(ctx, ctx.countdown_timer, f"[Server]: {ctx.countdown_timer}") ctx.countdown_timer -= 1 await asyncio.sleep(1) - ctx.notify_all(f'[Server]: GO') + broadcast_countdown(ctx, 0, f"[Server]: GO") ctx.countdown_timer = 0 +def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {}): + old_clients, new_clients = [], [] + + for teams in ctx.clients.values(): + for clients in teams.values(): + for client in clients: + new_clients.append(client) if client.version >= print_command_compatability_threshold \ + else old_clients.append(client) + + ctx.broadcast(old_clients, [{"cmd": "Print", "text": text }]) + ctx.broadcast(new_clients, [{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}]) + +def broadcast_countdown(ctx: Context, timer: int, message: str): + broadcast_text_all(ctx, message, { "type": "Countdown", "countdown": timer }) def get_players_string(ctx: Context): auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth} diff --git a/docs/network protocol.md b/docs/network protocol.md index b12768e2c9..342514248d 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -152,7 +152,8 @@ The arguments for RoomUpdate are identical to [RoomInfo](#RoomInfo) barring: All arguments for this packet are optional, only changes are sent. ### Print -Sent to clients purely to display a message to the player. +Sent to clients purely to display a message to the player. +* *Deprecation warning: clients that connect with version 0.3.5 or higher will nolonger recieve Print packets, instead all messsages are send as [PrintJSON](#PrintJSON)* #### Arguments | Name | Type | Notes | | ---- | ---- | ----- | @@ -164,10 +165,21 @@ Sent to clients purely to display a message to the player. This packet differs f | Name | Type | Notes | | ---- | ---- | ----- | | data | list\[[JSONMessagePart](#JSONMessagePart)\] | Type of this part of the message. | -| type | str | May be present to indicate the nature of this message. Known types are Hint and ItemSend. | +| type | str | May be present to indicate the [PrintJsonType](#PrintJsonType) of this message. | | receiving | int | Is present if type is Hint or ItemSend and marks the destination player's ID. | | item | [NetworkItem](#NetworkItem) | Is present if type is Hint or ItemSend and marks the source player id, location id, item id and item flags. | | found | bool | Is present if type is Hint, denotes whether the location hinted for was checked. | +| countdown | int | Is present if type is `Countdown`, denotes the amount of seconds remaining on the countdown. | + +##### PrintJsonType +PrintJsonType indicates the type of [PrintJson](#PrintJson) packet, different types can be handled differently by the client and can also contain additional arguments. When receiving an unknown type the data's list\[[JSONMessagePart](#JSONMessagePart)\] should still be printed as normal. + +Currently defined types are: +| Type | Notes | +| ---- | ----- | +| ItemSend | The message is in response to a player receiving an item. | +| Hint | The message is in response to a player hinting. | +| Countdown | The message contains information about the current server Countdown. | ### DataPackage Sent to clients to provide what is known as a 'data package' which contains information to enable a client to most easily communicate with the Archipelago server. Contents include things like location id to name mappings, among others; see [Data Package Contents](#Data-Package-Contents) for more info. From e548abd332ba68d208e67fee98c574596ad251e4 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Aug 2022 01:02:29 +0200 Subject: [PATCH 21/91] Subnautica: use correct option parent class (#954) * Subnautica: use correct option parent class * Update Options.py --- worlds/subnautica/Options.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index 020b5d1916..f68e12d2c0 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,4 +1,6 @@ -from Options import Choice, Range, DeathLink, Toggle +import typing + +from Options import Choice, Range, DeathLink from .Creatures import all_creatures @@ -39,7 +41,7 @@ class CreatureScans(Range): range_end = len(all_creatures) -class AggressiveScanLogic(Toggle): +class AggressiveScanLogic(Choice): """By default (Stasis), aggressive Creature Scans are logically expected only with a Stasis Rifle. Containment: Removes Stasis Rifle as expected solution and expects Alien Containment instead. Either: Creatures may be expected to be scanned via Stasis Rifle or Containment, whichever is found first. From c390801c4c03f83a7aba47912fe40bc4220d1a2c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Aug 2022 01:07:17 +0200 Subject: [PATCH 22/91] Test: verify file webhost file creations work to some degree (#953) WebHost: fix some file creation paths --- WebHostLib/options.py | 13 ++++++++----- test/webhost/TestFileGeneration.py | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 test/webhost/TestFileGeneration.py diff --git a/WebHostLib/options.py b/WebHostLib/options.py index e2d362a570..daa742d90e 100644 --- a/WebHostLib/options.py +++ b/WebHostLib/options.py @@ -1,6 +1,6 @@ import logging import os -from Utils import __version__ +from Utils import __version__, local_path from jinja2 import Template import yaml import json @@ -9,14 +9,13 @@ import typing from worlds.AutoWorld import AutoWorldRegister import Options -target_folder = os.path.join("WebHostLib", "static", "generated") - handled_in_js = {"start_inventory", "local_items", "non_local_items", "start_hints", "start_location_hints", "exclude_locations"} def create(): - os.makedirs(os.path.join(target_folder, 'configs'), exist_ok=True) + target_folder = local_path("WebHostLib", "static", "generated") + os.makedirs(os.path.join(target_folder, "configs"), exist_ok=True) def dictify_range(option: typing.Union[Options.Range, Options.SpecialRange]): data = {} @@ -66,12 +65,16 @@ def create(): for game_name, world in AutoWorldRegister.world_types.items(): all_options = {**Options.per_game_common_options, **world.option_definitions} - res = Template(open(os.path.join("WebHostLib", "templates", "options.yaml")).read()).render( + 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, default_converter=default_converter, ) + del file_data + with open(os.path.join(target_folder, 'configs', game_name + ".yaml"), "w") as f: f.write(res) diff --git a/test/webhost/TestFileGeneration.py b/test/webhost/TestFileGeneration.py new file mode 100644 index 0000000000..7f56864ea3 --- /dev/null +++ b/test/webhost/TestFileGeneration.py @@ -0,0 +1,23 @@ +"""Tests for successful generation of WebHost cached files. Can catch some other deeper errors.""" + +import os +import unittest + +import WebHost + + +class TestFileGeneration(unittest.TestCase): + def setUp(self) -> None: + self.correct_path = os.path.join(os.path.dirname(WebHost.__file__), "WebHostLib") + # should not create the folder *here* + self.incorrect_path = os.path.join(os.path.split(os.path.dirname(__file__))[0], "WebHostLib") + + def testOptions(self): + WebHost.create_options_files() + self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "configs"))) + self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs"))) + + def testTutorial(self): + WebHost.create_ordered_tutorials_file() + self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json"))) + self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "tutorials.json"))) From fab12dca0bc32720be596e4ad86fe78c0bcaeab7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 20 Aug 2022 19:20:22 +0200 Subject: [PATCH 23/91] SC2: add anti air to Devil's Playground Victory People seem to be on the mission long enough to get attacked by Mutalisks, so Victory should require anti air. Optional Objectives are doable quite comfortably before Mutalisks show up, allowing the anti-air to be on them for later in the mission. --- worlds/sc2wol/Locations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/sc2wol/Locations.py b/worlds/sc2wol/Locations.py index 3425dc7199..f69abd48e3 100644 --- a/worlds/sc2wol/Locations.py +++ b/worlds/sc2wol/Locations.py @@ -176,7 +176,8 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L state._sc2wol_has_competent_anti_air(world, player) and state.has('Science Vessel', player)), LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300, - lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), + lambda state: state._sc2wol_has_anti_air(world, player) and ( + state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))), LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301), LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302, lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)), From 33103b209d2b917f6975c69d4179a9886014b026 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Aug 2022 22:18:24 +0200 Subject: [PATCH 24/91] WebHost: fix error on save --- WebHostLib/customserver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index 01f1fd25e5..da7b54ba6d 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -103,7 +103,7 @@ class WebHostContext(Context): room.multisave = pickle.dumps(self.get_save()) # saving only occurs on activity, so we can "abuse" this information to mark this as last_activity if not exit_save: # we don't want to count a shutdown as activity, which would restart the server again - room.last_activity = datetime.utcnow() + room.last_activity = datetime.datetime.utcnow() return True def get_save(self) -> dict: From 295ea97544f0deb41e51e16d4ac221c630713b80 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Aug 2022 23:04:20 +0200 Subject: [PATCH 25/91] Subnautica: increment client version --- worlds/subnautica/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 6562d93db0..806c1b195e 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -42,7 +42,7 @@ class SubnauticaWorld(World): option_definitions = Options.options data_version = 6 - required_client_version = (0, 3, 4) + required_client_version = (0, 3, 5) prefill_items: List[Item] creatures_to_scan: List[str] From 1aaf89ff2cfbf440c9e2269ebfc1aec55ff04eb8 Mon Sep 17 00:00:00 2001 From: Magnemania <89949176+Magnemania@users.noreply.github.com> Date: Tue, 23 Aug 2022 17:20:39 -0400 Subject: [PATCH 26/91] SC2: Switched mission item group to a list comprehension to fix missile shuffle errors (#959) --- worlds/sc2wol/Items.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py index 59b59bc137..8da40de5ab 100644 --- a/worlds/sc2wol/Items.py +++ b/worlds/sc2wol/Items.py @@ -1,5 +1,6 @@ from BaseClasses import Item, ItemClassification import typing +from .MissionTables import vanilla_mission_req_table class ItemData(typing.NamedTuple): @@ -153,12 +154,7 @@ basic_unit: typing.Tuple[str, ...] = ( item_name_groups = {} for item, data in item_table.items(): item_name_groups.setdefault(data.type, []).append(item) -item_name_groups["Missions"] = ["Beat Liberation Day", "Beat The Outlaws", "Beat Zero Hour", "Beat Evacuation", - "None Outbreak", "Beat Safe Haven", "Beat Haven's Fall", "Beat Smash and Grab", "Beat The Dig", - "Beat The Moebius Factor", "Beat Supernova", "Beat Maw of the Void", "Beat Devil's Playground", - "Beat Welcome to the Jungle", "Beat Breakout", "Beat Ghost of a Chance", - "Beat The Great Train Robbery", "Beat Cutthroat", "Beat Engine of Destruction", - "Beat Media Blitz", "Beat Piercing the Shroud"] +item_name_groups["Missions"] = ["Beat " + mission_name for mission_name in vanilla_mission_req_table] filler_items: typing.Tuple[str, ...] = ( '+15 Starting Minerals', From 0d6cbd9093361f734c0d6af939c8bc1e32207c43 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 23 Aug 2022 23:33:30 +0200 Subject: [PATCH 27/91] Core: convert item name groups to frozenset Some worlds define them in lists, this speeds up lookup via state.has_group() or similar --- worlds/AutoWorld.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 02b94c5fb7..8d9a1b0829 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -27,7 +27,8 @@ class AutoWorldRegister(type): # build rest dct["item_names"] = frozenset(dct["item_name_to_id"]) - dct["item_name_groups"] = dct.get("item_name_groups", {}) + dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set + in dct.get("item_name_groups", {}).items()} dct["item_name_groups"]["Everything"] = dct["item_names"] dct["location_names"] = frozenset(dct["location_name_to_id"]) dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {}))) From a78863fde1ae6517d99917d54f8935966d151044 Mon Sep 17 00:00:00 2001 From: Zach Parks Date: Fri, 26 Aug 2022 02:12:37 -0500 Subject: [PATCH 28/91] Docs: Update community supported libraries in api doc (#788) * Docs: Update client supported libraries in api doc * left align table column * Update table of languages to include Haxe lib and remarks * Reformat table * Changed verbiage on SNI remark --- docs/network protocol.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/network protocol.md b/docs/network protocol.md index 342514248d..3315ddec2d 100644 --- a/docs/network protocol.md +++ b/docs/network protocol.md @@ -13,9 +13,18 @@ These steps should be followed in order to establish a gameplay connection with In the case that the client does not authenticate properly and receives a [ConnectionRefused](#ConnectionRefused) then the server will maintain the connection and allow for follow-up [Connect](#Connect) packet. -There are libraries available that implement this network protocol in [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py), [Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java), [.Net](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Net) and [C++](https://github.com/black-sliver/apclientpp) +There are also a number of community-supported libraries available that implement this network protocol to make integrating with Archipelago easier. -For Super Nintendo games there are clients available in either [Node](https://github.com/ArchipelagoMW/SuperNintendoClient) or [Python](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py), There are also game specific clients available for [The Legend of Zelda: Ocarina of Time](https://github.com/ArchipelagoMW/Z5Client) or [Final Fantasy 1](https://github.com/ArchipelagoMW/Archipelago/blob/main/FF1Client.py) +| Language/Runtime | Project | Remarks | +|-------------------------------|----------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------| +| Python | [Archipelago CommonClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/CommonClient.py) | | +| | [Archipelago SNIClient](https://github.com/ArchipelagoMW/Archipelago/blob/main/SNIClient.py) | For Super Nintendo Game Support; Utilizes [SNI](https://github.com/alttpo/sni). | +| JVM (Java / Kotlin) | [Archipelago.MultiClient.Java](https://github.com/ArchipelagoMW/Archipelago.MultiClient.Java) | | +| .NET (C# / C++ / F# / VB.NET) | [Archipelago.MultiClient.Net](https://www.nuget.org/packages/Archipelago.MultiClient.Net) | | +| C++ | [apclientpp](https://github.com/black-sliver/apclientpp) | almost-header-only | +| | [APCpp](https://github.com/N00byKing/APCpp) | CMake | +| JavaScript / TypeScript | [archipelago.js](https://www.npmjs.com/package/archipelago.js) | Browser and Node.js Supported | +| Haxe | [hxArchipelago](https://lib.haxe.org/p/hxArchipelago) | | ## Synchronizing Items When the client receives a [ReceivedItems](#ReceivedItems) packet, if the `index` argument does not match the next index that the client expects then it is expected that the client will re-sync items with the server. This can be accomplished by sending the server a [Sync](#Sync) packet and then a [LocationChecks](#LocationChecks) packet. From a175aa93e7185d929a471ac0ec4173e78be564cf Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Fri, 26 Aug 2022 01:31:30 -0700 Subject: [PATCH 29/91] Factorio: Detect if more than one AP factorio mod is loaded. (#964) --- worlds/factorio/data/mod_template/settings.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/worlds/factorio/data/mod_template/settings.lua b/worlds/factorio/data/mod_template/settings.lua index 7703ebe2e5..73e131a60e 100644 --- a/worlds/factorio/data/mod_template/settings.lua +++ b/worlds/factorio/data/mod_template/settings.lua @@ -1,3 +1,21 @@ +-- Find out if more than one AP mod is loaded, and if so, error out. +function mod_is_AP(str) + -- lua string.match is way more restrictive than regex. Regex would be "^AP-W?\d{20}-P[1-9]\d*-.+$" + local result = string.match(str, "^AP%-W?%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%d%-P[1-9]%d-%-.+$") + if result ~= nil then + log("Archipelago Mod: " .. result .. " is loaded.") + end + return result ~= nil +end +local ap_mod_count = 0 +for name, _ in pairs(mods) do + if mod_is_AP(name) then + ap_mod_count = ap_mod_count + 1 + if ap_mod_count > 1 then + error("More than one Archipelago Factorio mod is loaded.") + end + end +end data:extend({ { type = "bool-setting", From af19180ff0834bf8af09a1e989c44fa794cc3802 Mon Sep 17 00:00:00 2001 From: strotlog <49286967+strotlog@users.noreply.github.com> Date: Sat, 20 Aug 2022 04:35:46 +0000 Subject: [PATCH 30/91] SM: Fix rolling saves, add SRAM features - fix receiving items in an old save (issue #855) by moving receive queue's read pointer to a per-saveslot value - clear SRAM over $70:2000, and invalidate save data, when booting a new seed number for the first time - copy important ROM data to SRAM so future clients don't have to read ROM --- .../multiworld-basepatch.ips | Bin 17952 -> 18193 bytes .../data/SMBasepatch_prebuilt/multiworld.sym | 801 ++++++++++-------- .../sm-basepatch-symbols.json | 74 +- 3 files changed, 500 insertions(+), 375 deletions(-) diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips index d7fd17613e8f2af2b5cc6ed5db11efbe9d227f07..7ac3ea018475a0db495d6d540c9dda5f66f1391f 100644 GIT binary patch delta 499 zcmX|-PiPZC6vkh6(=?ca4VZ&}Vuq0{5d;TMo&_<~KTz}}9z$n`_Dqx~_b0GY=;FgE&o_ewRsM z6u;h2Vsnx4y>Sqp#q`_3L&n3jr=WX;ZkC?5{9Ao$>QZtAuoxjoY+a>2Bro&gDL-B#&u+)JeSKm9bBrWrO3JH=ddlGKyiAi&2&ro{n^*xHbPqe+AI% zIpOfSk=&Z!ob3|l1H4q*#Bo?Ma*i`MEEv&Y1pm8oUzAYe_%5R+P@kYCQCAsvPvkG1 zFX%gU0HYY)PSNF#6%wzXno~!8G&-ptSdX~S-JC&0^W-2w*(d*X53D_+n qEe}V|L_gfCnwGwXb8zSnQoG8bze6`HcSXCL7+MA`Tg|sxIrj%vd*R;z delta 239 zcmbQ($GD(}ae|S2LBkG)hVKk4G4~sHFl=UKU{jJgvE$V%M()iFEJ`ODHwLOPG1WJ2 zu4Hmo@?!jO$Y4Fg|8pN$lmr`gJTh3>pY3rgiUTP4Pp#nOzf&JnRx&UwVqlohpinTM zK?R64fLN#C#ft|FEEis`{CBD!s8FNoSH@&!H66M23~B}aEI@&bRSOsxvOPcs0lED^ zVU3LJ$-Zg@%s}Sk18ROu?;9oysEcuLXaMPv0;!m6sIDgH*|1|j!-E|kxc4(X*s)4! fQp4m7b$!174cY61IO`-b1sIf?8#d2XFOdfTlAm2m diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym index 751f470f53..5def2b7d9c 100644 --- a/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym +++ b/worlds/sm/data/SMBasepatch_prebuilt/multiworld.sym @@ -2,14 +2,14 @@ ; generated by asar [labels] -B8:8026 :neg_1_1 +B8:80C1 :neg_1_1 85:B9B4 :neg_1_2 85:B9E6 :neg_1_3 B8:C81F :neg_1_4 B8:C831 :neg_1_5 B8:C843 :neg_1_6 B8:800C :pos_1_0 -B8:81DE :pos_1_1 +B8:82D7 :pos_1_1 84:FA6B :pos_1_2 84:FA75 :pos_1_3 B8:C862 :pos_1_4 @@ -20,7 +20,7 @@ B8:C87C :pos_1_6 85:990F CLIPLEN_end 85:990C CLIPLEN_no_multi 85:FF1D CLIPSET -B8:80EF COLLECTTANK +B8:81E8 COLLECTTANK 85:FF45 MISCFX 84:8BF2 NORMAL 85:FF4E SETFX @@ -38,6 +38,11 @@ CE:FF00 config_multiworld CE:FF08 config_player_id CE:FF06 config_remote_items CE:FF02 config_sprite +B8:8119 copy_config_to_sram +B8:80FD copy_memory +B8:8117 copy_memory_done +B8:8109 copy_memory_even +B8:810F copy_memory_loop 84:F894 h_item 84:F8AD i_chozo_item 84:F8B4 i_hidden_item @@ -46,11 +51,11 @@ B8:885C i_item_setup_shared B8:8878 i_item_setup_shared_all_items B8:8883 i_item_setup_shared_alwaysloaded 84:FA79 i_live_pickup -B8:817F i_live_pickup_multiworld -B8:81C4 i_live_pickup_multiworld_end -B8:819B i_live_pickup_multiworld_local_item_or_offworld -B8:81B0 i_live_pickup_multiworld_own_item -B8:81BC i_live_pickup_multiworld_own_item1 +B8:8278 i_live_pickup_multiworld +B8:82BD i_live_pickup_multiworld_end +B8:8294 i_live_pickup_multiworld_local_item_or_offworld +B8:82A9 i_live_pickup_multiworld_own_item +B8:82B5 i_live_pickup_multiworld_own_item1 84:FA1E i_load_custom_graphics 84:FA39 i_load_custom_graphics_all_items 84:FA49 i_load_custom_graphics_alwaysloaded @@ -85,22 +90,27 @@ B8:81BC i_live_pickup_multiworld_own_item1 85:B9CA message_write_placeholders_loop 85:B9DC message_write_placeholders_notfound 85:B9DF message_write_placeholders_value_ok -B8:8092 mw_display_item_sent -B8:80FF mw_handle_queue -B8:8178 mw_handle_queue_end -B8:8101 mw_handle_queue_loop -B8:8151 mw_handle_queue_new_remote_item -B8:816D mw_handle_queue_next -B8:8163 mw_handle_queue_perform_receive -B8:81C8 mw_hook_main_game +B8:818B mw_display_item_sent +B8:81F8 mw_handle_queue +B8:8271 mw_handle_queue_end +B8:81FA mw_handle_queue_loop +B8:824A mw_handle_queue_new_remote_item +B8:8266 mw_handle_queue_next +B8:825C mw_handle_queue_perform_receive +B8:82C1 mw_hook_main_game B8:8011 mw_init -B8:8044 mw_init_end +B8:8066 mw_init_continuereset +B8:80EA mw_init_end B8:8000 mw_init_memory -B8:8083 mw_load_sram -B8:80B0 mw_receive_item -B8:80E8 mw_receive_item_end -B8:8070 mw_save_sram -B8:8049 mw_write_message +B8:803B mw_init_reset_sram +B8:8051 mw_init_smstringdata +B8:8174 mw_load_sram +B8:8182 mw_load_sram_done +B8:8185 mw_load_sram_setnewgame +B8:81A9 mw_receive_item +B8:81E1 mw_receive_item_end +B8:8169 mw_save_sram +B8:8142 mw_write_message 84:F888 nonprog_item_eight_palette_indices 89:9200 offworld_graphics_data_item 89:9100 offworld_graphics_data_progression_item @@ -125,7 +135,7 @@ B8:8049 mw_write_message 84:F96E p_visible_item_end 84:F95B p_visible_item_loop 84:F967 p_visible_item_trigger -B8:81DF patch_load_multiworld +B8:82D8 patch_load_multiworld 84:FA7E perform_item_pickup 84:F886 plm_graphics_entry_offworld_item 84:F87C plm_graphics_entry_offworld_progression_item @@ -144,17 +154,19 @@ B8:C808 start_item_data_minor B8:C818 start_item_data_reserve B8:C856 update_graphic 84:F890 v_item +B8:80EF write_repeated_memory +B8:80F4 write_repeated_memory_loop [source files] 0000 e25029c5 main.asm 0001 06780555 ../common/nofanfare.asm -0002 e76d1f83 ../common/multiworld.asm +0002 4f9a780e ../common/multiworld.asm 0003 613d24e1 ../common/itemextras.asm 0004 d6616c0c ../common/items.asm 0005 440b54fe ../common/startitem.asm [rom checksum] -09b134c5 +ad81eda1 [addr-to-line mapping] ff:ffff 0000:00000001 @@ -204,330 +216,423 @@ ff:ffff 0000:00000001 84:8bf2 0001:00000152 84:8bf6 0001:00000153 84:8bf7 0001:00000153 -b8:8000 0002:00000019 -b8:8002 0002:0000001a -b8:8006 0002:0000001b -b8:8008 0002:0000001c -b8:800c 0002:00000020 -b8:800e 0002:00000021 -b8:8010 0002:00000022 -b8:8011 0002:00000025 -b8:8012 0002:00000025 -b8:8013 0002:00000025 -b8:8014 0002:00000025 +b8:8000 0002:0000005a +b8:8002 0002:0000005b +b8:8006 0002:0000005c +b8:8008 0002:0000005d +b8:800c 0002:00000061 +b8:800e 0002:00000062 +b8:8010 0002:00000063 +b8:8011 0002:00000066 +b8:8012 0002:00000066 +b8:8013 0002:00000066 +b8:8014 0002:00000066 b8:8015 0000:00000013 -b8:8017 0002:00000029 -b8:801b 0002:0000002a -b8:801e 0002:0000002b -b8:8020 0002:0000002d -b8:8023 0002:0000002e -b8:8026 0002:00000031 -b8:802a 0002:00000032 -b8:802e 0002:00000033 -b8:8032 0002:00000034 -b8:8036 0002:00000035 -b8:8037 0002:00000035 -b8:8038 0002:00000036 -b8:803b 0002:00000037 -b8:803d 0002:00000039 -b8:8040 0002:0000003a -b8:8044 0002:0000003d -b8:8045 0002:0000003d -b8:8046 0002:0000003d -b8:8047 0002:0000003d -b8:8048 0002:0000003e -b8:8049 0002:00000043 -b8:804a 0002:00000043 -b8:804b 0002:00000044 -b8:804c 0002:00000044 -b8:804d 0002:00000045 -b8:8051 0002:00000046 -b8:8054 0002:00000046 -b8:8055 0002:00000047 -b8:8056 0002:00000048 -b8:805a 0002:00000049 -b8:805b 0002:0000004a -b8:805f 0002:0000004b -b8:8060 0002:0000004c -b8:8064 0002:0000004e -b8:8068 0002:0000004f -b8:8069 0002:00000050 -b8:806d 0002:00000051 -b8:806e 0002:00000051 -b8:806f 0002:00000052 -b8:8070 0002:00000055 -b8:8071 0002:00000055 -b8:8072 0000:00000013 -b8:8074 0002:00000057 -b8:8078 0002:00000058 -b8:807c 0002:00000059 -b8:807d 0002:00000059 -b8:807e 0002:0000005b -b8:807f 0002:0000005c -b8:8082 0002:0000005d -b8:8083 0002:00000060 -b8:8084 0002:00000060 -b8:8085 0000:00000013 -b8:8087 0002:00000062 -b8:808b 0002:00000063 -b8:808f 0002:00000064 -b8:8090 0002:00000064 -b8:8091 0002:00000065 -b8:8092 0002:0000006a -b8:8094 0002:0000006b -b8:8096 0002:0000006e -b8:8099 0002:0000006f -b8:809b 0002:00000070 -b8:809e 0002:00000071 -b8:80a0 0002:00000072 -b8:80a3 0002:00000073 -b8:80a7 0002:00000074 -b8:80a9 0002:00000075 -b8:80ab 0002:00000076 -b8:80ad 0002:00000077 -b8:80af 0002:00000078 -b8:80b0 0002:0000007c -b8:80b1 0002:0000007c -b8:80b2 0002:0000007d -b8:80b5 0002:0000007e -b8:80b7 0002:0000007f -b8:80ba 0002:00000080 -b8:80bc 0002:00000081 -b8:80bd 0002:00000082 -b8:80be 0002:00000084 -b8:80c1 0002:00000085 -b8:80c3 0002:00000086 -b8:80c6 0002:00000087 -b8:80c7 0002:00000088 -b8:80ca 0002:00000089 -b8:80cb 0002:00000089 -b8:80cc 0002:0000008a -b8:80d0 0002:0000008b -b8:80d1 0002:0000008c -b8:80d4 0002:0000008d -b8:80d8 0002:0000008e -b8:80da 0002:00000090 -b8:80dd 0002:00000091 -b8:80df 0002:00000092 -b8:80e2 0002:00000093 -b8:80e4 0002:00000095 -b8:80e8 0002:00000097 -b8:80ea 0002:00000098 -b8:80ec 0002:00000099 -b8:80ed 0002:00000099 -b8:80ee 0002:0000009a -b8:80ef 0002:000000a5 -b8:80f0 0002:000000a6 -b8:80f4 0002:000000a7 -b8:80f5 0002:000000a8 -b8:80f9 0002:000000a9 -b8:80fa 0002:000000ab -b8:80fe 0002:000000ac -b8:80ff 0002:000000de -b8:8100 0002:000000de -b8:8101 0002:000000e1 -b8:8105 0002:000000e2 -b8:8109 0002:000000e3 -b8:810b 0002:000000e5 -b8:810d 0002:000000e5 -b8:810e 0002:000000e8 -b8:8112 0002:000000e9 -b8:8114 0002:000000ea -b8:8118 0002:000000eb -b8:811a 0002:000000ec -b8:811e 0002:000000ed -b8:8121 0002:000000ee -b8:8123 0002:000000ef -b8:8125 0002:000000f0 -b8:8129 0002:000000f1 -b8:812b 0002:000000f2 -b8:812d 0002:000000f3 -b8:8130 0002:000000f4 -b8:8133 0002:000000f5 -b8:8135 0002:000000f6 -b8:813d 0002:000000fa -b8:813e 0002:000000fb -b8:813f 0002:000000fc -b8:8143 0002:000000ff -b8:8147 0002:00000100 -b8:814b 0002:00000101 -b8:814d 0002:00000103 -b8:814e 0002:00000104 -b8:814f 0002:00000105 -b8:8151 0002:0000010a -b8:8152 0002:0000010b -b8:8156 0002:0000010e -b8:815a 0002:0000010f -b8:815e 0002:00000110 -b8:8162 0002:00000111 -b8:8163 0002:00000115 -b8:8165 0002:00000116 -b8:8168 0002:00000117 -b8:816a 0002:00000118 -b8:816d 0002:0000011b -b8:8171 0002:0000011c -b8:8172 0002:0000011d -b8:8176 0002:0000011f -b8:8178 0002:00000122 -b8:817a 0002:00000123 -b8:817c 0002:00000124 -b8:817d 0002:00000124 -b8:817e 0002:00000125 -b8:817f 0002:00000129 -b8:8180 0002:00000129 -b8:8181 0002:00000129 -b8:8182 0002:0000012a -b8:8186 0002:0000012b -b8:8189 0002:0000012b -b8:818a 0002:0000012d -b8:818e 0002:0000012e -b8:818f 0002:0000012f -b8:8193 0002:00000130 -b8:8196 0002:00000131 -b8:8198 0002:00000133 -b8:819b 0002:00000136 -b8:819f 0002:00000137 -b8:81a3 0002:00000138 -b8:81a5 0002:0000013a -b8:81a9 0002:0000013b -b8:81aa 0002:0000013d -b8:81ae 0002:0000013e -b8:81b0 0002:00000141 -b8:81b4 0002:00000142 -b8:81b7 0002:00000143 -b8:81b9 0002:00000144 -b8:81bc 0002:00000147 -b8:81bd 0002:00000148 -b8:81be 0002:00000149 -b8:81c2 0002:0000014a -b8:81c4 0002:0000014d -b8:81c5 0002:0000014d -b8:81c6 0002:0000014d -b8:81c7 0002:0000014e -b8:81c8 0002:00000152 -b8:81cc 0002:00000153 -b8:81d0 0002:00000154 -b8:81d2 0002:00000155 -b8:81d6 0002:00000156 -b8:81d9 0002:00000157 -b8:81db 0002:00000158 -b8:81de 0002:0000015a -b8:81df 0002:0000015d -b8:81e3 0002:0000015e -b8:81e4 0002:0000015f -b8:81e7 0002:00000160 -b8:81eb 0002:00000162 -b8:81ec 0002:00000163 -b8:81ed 0002:00000164 -b8:81ee 0002:00000165 -b8:81ef 0002:00000166 -8b:914a 0002:0000016b -81:80f7 0002:0000016e -81:8027 0002:00000171 -82:8bb3 0002:00000174 -85:b9a3 0002:0000020e -85:b9a4 0002:0000020e -85:b9a5 0002:00000211 -85:b9a7 0002:00000212 -85:b9ad 0002:00000212 -85:b9ae 0002:00000213 -85:b9b1 0002:00000214 -85:b9b2 0002:00000215 -85:b9b3 0002:00000215 -85:b9b4 0002:00000219 -85:b9b7 0002:0000021a -85:b9bb 0002:0000021b -85:b9bd 0002:0000021b -85:b9bf 0002:0000021c -85:b9c2 0002:0000021d -85:b9c4 0002:0000021f -85:b9c5 0002:00000220 -85:b9c7 0002:00000224 -85:b9ca 0002:00000226 -85:b9cd 0002:00000227 -85:b9cf 0002:00000228 -85:b9d1 0002:00000229 -85:b9d5 0002:0000022a -85:b9d7 0002:0000022b -85:b9d9 0002:0000022c -85:b9da 0002:0000022d -85:b9dc 0002:0000022f -85:b9df 0002:00000231 -85:b9e2 0002:00000231 -85:b9e3 0002:00000232 -85:b9e6 0002:00000234 -85:b9ea 0002:00000235 -85:b9ed 0002:00000236 -85:b9ee 0002:00000237 -85:b9ef 0002:00000237 -85:b9f0 0002:00000238 -85:b9f4 0002:00000239 -85:b9f5 0002:0000023a -85:b9f9 0002:0000023b -85:b9fb 0002:0000023c -85:b9fc 0002:0000023d -85:b9fd 0002:0000023e -85:ba00 0002:0000023f -85:ba02 0002:00000240 -85:ba04 0002:00000243 -85:ba05 0002:00000243 -85:ba06 0002:00000244 -85:ba09 0002:00000245 -85:ba8a 0002:00000253 -85:ba8c 0002:00000254 -85:ba8f 0002:00000255 -85:ba92 0002:00000256 -85:ba95 0002:0000025e -85:ba96 0002:0000025f -85:ba98 0002:00000260 -85:ba9b 0002:00000261 -85:ba9d 0002:00000262 -85:ba9f 0002:00000263 -85:baa2 0002:00000264 -85:baa4 0002:00000265 -85:baa7 0002:00000266 -85:baa9 0002:00000269 -85:baaa 0002:0000026a -85:baab 0002:0000026b -85:baac 0002:0000026c -85:baae 0002:0000026d -85:baaf 0002:0000026e -85:bab0 0002:0000026f -85:bab1 0002:00000274 -85:bab4 0002:00000275 -85:bab5 0002:00000276 -85:bab8 0002:00000277 -85:bab9 0002:00000278 -85:baba 0002:00000279 -85:babb 0002:0000027a -85:babc 0002:00000285 -85:babd 0002:00000286 -85:babf 0002:00000287 -85:bac2 0002:00000288 -85:bac4 0002:00000289 -85:bac7 0002:0000028a -85:bac9 0002:0000028d -85:baca 0002:0000028e -85:bacb 0002:0000028f -85:bacd 0002:00000290 -85:bace 0002:00000292 -85:bacf 0002:00000293 -85:bad1 0002:00000294 -85:bad4 0002:00000295 -85:bad6 0002:00000296 -85:bad9 0002:00000297 -85:badb 0002:00000298 -85:badc 0002:0000029a -85:badd 0002:0000029b -85:badf 0002:0000029c -85:bae2 0002:0000029d -85:bae4 0002:0000029e -85:bae7 0002:0000029f -85:bae9 0002:000002a0 -85:8246 0002:000002a5 -85:8249 0002:000002a6 -85:824b 0002:000002a7 -85:82f9 0002:000002ab +b8:8017 0002:0000006a +b8:801b 0002:0000006b +b8:801e 0002:0000006c +b8:8020 0002:0000006d +b8:8024 0002:0000006e +b8:8028 0002:0000006f +b8:802a 0002:00000070 +b8:802e 0002:00000071 +b8:8032 0002:00000072 +b8:8034 0002:00000074 +b8:8038 0002:00000075 +b8:803b 0002:00000078 +b8:803c 0002:00000079 +b8:803f 0002:0000007a +b8:8042 0002:0000007b +b8:8045 0002:0000007c +b8:8048 0002:0000007d +b8:8049 0002:0000007e +b8:804a 0002:0000007f +b8:804e 0002:00000080 +b8:804f 0002:00000082 +b8:8066 0002:00000086 +b8:8068 0002:00000087 +b8:8069 0002:00000088 +b8:806a 0002:00000089 +b8:806c 0002:0000008a +b8:806e 0002:0000008b +b8:8070 0002:0000008c +b8:8072 0002:0000008d +b8:8075 0002:0000008e +b8:8077 0002:0000008f +b8:807a 0002:00000090 +b8:807d 0002:00000091 +b8:807f 0002:00000092 +b8:8083 0002:00000094 +b8:8085 0002:00000095 +b8:8087 0002:00000096 +b8:8089 0002:00000097 +b8:808b 0002:00000098 +b8:808d 0002:00000099 +b8:808f 0002:0000009a +b8:8092 0002:0000009b +b8:8094 0002:0000009c +b8:8097 0002:0000009d +b8:809a 0002:0000009e +b8:809c 0002:0000009f +b8:80a0 0002:000000a1 +b8:80a3 0002:000000a2 +b8:80a7 0002:000000a3 +b8:80ab 0002:000000a4 +b8:80af 0002:000000a5 +b8:80b3 0002:000000a6 +b8:80b7 0002:000000a8 +b8:80bb 0002:000000b0 +b8:80be 0002:000000b1 +b8:80c1 0002:000000b3 +b8:80c2 0002:000000b4 +b8:80c3 0002:000000b5 +b8:80c7 0002:000000b6 +b8:80cb 0002:000000b7 +b8:80cd 0002:000000c4 +b8:80d1 0002:000000c5 +b8:80d4 0002:000000c6 +b8:80d6 0002:000000c7 +b8:80da 0002:000000c8 +b8:80dd 0002:000000c9 +b8:80df 0002:000000ce +b8:80e2 0002:000000cf +b8:80e6 0002:000000d0 +b8:80ea 0002:000000d3 +b8:80eb 0002:000000d3 +b8:80ec 0002:000000d3 +b8:80ed 0002:000000d3 +b8:80ee 0002:000000d4 +b8:80ef 0002:000000db +b8:80f0 0002:000000dc +b8:80f1 0002:000000dd +b8:80f2 0002:000000de +b8:80f3 0002:000000df +b8:80f4 0002:000000e1 +b8:80f7 0002:000000e2 +b8:80f8 0002:000000e3 +b8:80f9 0002:000000e4 +b8:80fa 0002:000000e5 +b8:80fc 0002:000000e7 +b8:80fd 0002:000000ee +b8:80fe 0002:000000ef +b8:80ff 0002:000000f0 +b8:8100 0002:000000f1 +b8:8102 0002:000000f3 +b8:8104 0002:000000f4 +b8:8105 0002:000000f5 +b8:8107 0002:000000f6 +b8:8109 0002:000000f8 +b8:810b 0002:000000f9 +b8:810c 0002:000000fa +b8:810d 0002:000000fb +b8:810f 0002:000000fd +b8:8111 0002:000000fe +b8:8113 0002:000000ff +b8:8114 0002:00000100 +b8:8115 0002:00000101 +b8:8117 0002:00000103 +b8:8118 0002:00000104 +b8:8119 0002:00000108 +b8:811d 0002:00000109 +b8:8121 0002:0000010a +b8:8125 0002:0000010b +b8:8129 0002:0000010c +b8:812d 0002:0000010d +b8:8131 0002:0000010e +b8:8135 0002:0000010f +b8:8139 0002:00000110 +b8:813d 0002:00000111 +b8:8141 0002:00000112 +b8:8142 0002:00000118 +b8:8143 0002:00000118 +b8:8144 0002:00000119 +b8:8145 0002:00000119 +b8:8146 0002:0000011a +b8:814a 0002:0000011b +b8:814d 0002:0000011b +b8:814e 0002:0000011c +b8:814f 0002:0000011d +b8:8153 0002:0000011e +b8:8154 0002:0000011f +b8:8158 0002:00000120 +b8:8159 0002:00000121 +b8:815d 0002:00000123 +b8:8161 0002:00000124 +b8:8162 0002:00000125 +b8:8166 0002:00000126 +b8:8167 0002:00000126 +b8:8168 0002:00000127 +b8:8169 0002:0000012c +b8:816a 0002:0000012c +b8:816b 0000:00000013 +b8:816d 0002:0000012f +b8:816e 0002:0000012f +b8:816f 0002:00000131 +b8:8170 0002:00000132 +b8:8173 0002:00000133 +b8:8174 0002:00000138 +b8:8175 0002:00000138 +b8:8176 0000:00000013 +b8:8178 0002:0000013a +b8:817c 0002:0000013b +b8:8180 0002:0000013c +b8:8182 0002:0000013e +b8:8183 0002:0000013e +b8:8184 0002:0000013f +b8:8185 0002:00000147 +b8:8189 0002:00000148 +b8:818b 0002:0000014e +b8:818d 0002:0000014f +b8:818f 0002:00000152 +b8:8192 0002:00000153 +b8:8194 0002:00000154 +b8:8197 0002:00000155 +b8:8199 0002:00000156 +b8:819c 0002:00000157 +b8:81a0 0002:00000158 +b8:81a2 0002:00000159 +b8:81a4 0002:0000015a +b8:81a6 0002:0000015b +b8:81a8 0002:0000015c +b8:81a9 0002:00000160 +b8:81aa 0002:00000160 +b8:81ab 0002:00000161 +b8:81ae 0002:00000162 +b8:81b0 0002:00000163 +b8:81b3 0002:00000164 +b8:81b5 0002:00000165 +b8:81b6 0002:00000166 +b8:81b7 0002:00000168 +b8:81ba 0002:00000169 +b8:81bc 0002:0000016a +b8:81bf 0002:0000016b +b8:81c0 0002:0000016c +b8:81c3 0002:0000016d +b8:81c4 0002:0000016d +b8:81c5 0002:0000016e +b8:81c9 0002:0000016f +b8:81ca 0002:00000170 +b8:81cd 0002:00000171 +b8:81d1 0002:00000172 +b8:81d3 0002:00000174 +b8:81d6 0002:00000175 +b8:81d8 0002:00000176 +b8:81db 0002:00000177 +b8:81dd 0002:00000179 +b8:81e1 0002:0000017b +b8:81e3 0002:0000017c +b8:81e5 0002:0000017d +b8:81e6 0002:0000017d +b8:81e7 0002:0000017e +b8:81e8 0002:00000189 +b8:81e9 0002:0000018a +b8:81ed 0002:0000018b +b8:81ee 0002:0000018c +b8:81f2 0002:0000018d +b8:81f3 0002:0000018f +b8:81f7 0002:00000190 +b8:81f8 0002:000001c2 +b8:81f9 0002:000001c2 +b8:81fa 0002:000001c5 +b8:81fe 0002:000001c6 +b8:8202 0002:000001c7 +b8:8204 0002:000001c9 +b8:8206 0002:000001c9 +b8:8207 0002:000001cc +b8:820b 0002:000001cd +b8:820d 0002:000001ce +b8:8211 0002:000001cf +b8:8213 0002:000001d0 +b8:8217 0002:000001d1 +b8:821a 0002:000001d2 +b8:821c 0002:000001d3 +b8:821e 0002:000001d4 +b8:8222 0002:000001d5 +b8:8224 0002:000001d6 +b8:8226 0002:000001d7 +b8:8229 0002:000001d8 +b8:822c 0002:000001d9 +b8:822e 0002:000001da +b8:8236 0002:000001de +b8:8237 0002:000001df +b8:8238 0002:000001e0 +b8:823c 0002:000001e3 +b8:8240 0002:000001e4 +b8:8244 0002:000001e5 +b8:8246 0002:000001e7 +b8:8247 0002:000001e8 +b8:8248 0002:000001e9 +b8:824a 0002:000001ee +b8:824b 0002:000001ef +b8:824f 0002:000001f2 +b8:8253 0002:000001f3 +b8:8257 0002:000001f4 +b8:825b 0002:000001f5 +b8:825c 0002:000001f9 +b8:825e 0002:000001fa +b8:8261 0002:000001fb +b8:8263 0002:000001fc +b8:8266 0002:000001ff +b8:826a 0002:00000200 +b8:826b 0002:00000201 +b8:826f 0002:00000203 +b8:8271 0002:00000206 +b8:8273 0002:00000207 +b8:8275 0002:00000208 +b8:8276 0002:00000208 +b8:8277 0002:00000209 +b8:8278 0002:0000020d +b8:8279 0002:0000020d +b8:827a 0002:0000020d +b8:827b 0002:0000020e +b8:827f 0002:0000020f +b8:8282 0002:0000020f +b8:8283 0002:00000211 +b8:8287 0002:00000212 +b8:8288 0002:00000213 +b8:828c 0002:00000214 +b8:828f 0002:00000215 +b8:8291 0002:00000217 +b8:8294 0002:0000021a +b8:8298 0002:0000021b +b8:829c 0002:0000021c +b8:829e 0002:0000021e +b8:82a2 0002:0000021f +b8:82a3 0002:00000221 +b8:82a7 0002:00000222 +b8:82a9 0002:00000225 +b8:82ad 0002:00000226 +b8:82b0 0002:00000227 +b8:82b2 0002:00000228 +b8:82b5 0002:0000022b +b8:82b6 0002:0000022c +b8:82b7 0002:0000022d +b8:82bb 0002:0000022e +b8:82bd 0002:00000231 +b8:82be 0002:00000231 +b8:82bf 0002:00000231 +b8:82c0 0002:00000232 +b8:82c1 0002:00000236 +b8:82c5 0002:00000237 +b8:82c9 0002:00000238 +b8:82cb 0002:00000239 +b8:82cf 0002:0000023a +b8:82d2 0002:0000023b +b8:82d4 0002:0000023c +b8:82d7 0002:0000023e +b8:82d8 0002:00000241 +b8:82dc 0002:00000243 +b8:82dd 0002:00000244 +b8:82de 0002:00000245 +b8:82df 0002:00000246 +b8:82e0 0002:00000247 +8b:914a 0002:0000024c +81:80f7 0002:0000024f +81:8027 0002:00000252 +82:8bb3 0002:00000255 +85:b9a3 0002:000002ef +85:b9a4 0002:000002ef +85:b9a5 0002:000002f2 +85:b9a7 0002:000002f3 +85:b9ad 0002:000002f3 +85:b9ae 0002:000002f4 +85:b9b1 0002:000002f5 +85:b9b2 0002:000002f6 +85:b9b3 0002:000002f6 +85:b9b4 0002:000002fa +85:b9b7 0002:000002fb +85:b9bb 0002:000002fc +85:b9bd 0002:000002fc +85:b9bf 0002:000002fd +85:b9c2 0002:000002fe +85:b9c4 0002:00000300 +85:b9c5 0002:00000301 +85:b9c7 0002:00000305 +85:b9ca 0002:00000307 +85:b9cd 0002:00000308 +85:b9cf 0002:00000309 +85:b9d1 0002:0000030a +85:b9d5 0002:0000030b +85:b9d7 0002:0000030c +85:b9d9 0002:0000030d +85:b9da 0002:0000030e +85:b9dc 0002:00000310 +85:b9df 0002:00000312 +85:b9e2 0002:00000312 +85:b9e3 0002:00000313 +85:b9e6 0002:00000315 +85:b9ea 0002:00000316 +85:b9ed 0002:00000317 +85:b9ee 0002:00000318 +85:b9ef 0002:00000318 +85:b9f0 0002:00000319 +85:b9f4 0002:0000031a +85:b9f5 0002:0000031b +85:b9f9 0002:0000031c +85:b9fb 0002:0000031d +85:b9fc 0002:0000031e +85:b9fd 0002:0000031f +85:ba00 0002:00000320 +85:ba02 0002:00000321 +85:ba04 0002:00000324 +85:ba05 0002:00000324 +85:ba06 0002:00000325 +85:ba09 0002:00000326 +85:ba8a 0002:00000334 +85:ba8c 0002:00000335 +85:ba8f 0002:00000336 +85:ba92 0002:00000337 +85:ba95 0002:0000033f +85:ba96 0002:00000340 +85:ba98 0002:00000341 +85:ba9b 0002:00000342 +85:ba9d 0002:00000343 +85:ba9f 0002:00000344 +85:baa2 0002:00000345 +85:baa4 0002:00000346 +85:baa7 0002:00000347 +85:baa9 0002:0000034a +85:baaa 0002:0000034b +85:baab 0002:0000034c +85:baac 0002:0000034d +85:baae 0002:0000034e +85:baaf 0002:0000034f +85:bab0 0002:00000350 +85:bab1 0002:00000355 +85:bab4 0002:00000356 +85:bab5 0002:00000357 +85:bab8 0002:00000358 +85:bab9 0002:00000359 +85:baba 0002:0000035a +85:babb 0002:0000035b +85:babc 0002:00000366 +85:babd 0002:00000367 +85:babf 0002:00000368 +85:bac2 0002:00000369 +85:bac4 0002:0000036a +85:bac7 0002:0000036b +85:bac9 0002:0000036e +85:baca 0002:0000036f +85:bacb 0002:00000370 +85:bacd 0002:00000371 +85:bace 0002:00000373 +85:bacf 0002:00000374 +85:bad1 0002:00000375 +85:bad4 0002:00000376 +85:bad6 0002:00000377 +85:bad9 0002:00000378 +85:badb 0002:00000379 +85:badc 0002:0000037b +85:badd 0002:0000037c +85:badf 0002:0000037d +85:bae2 0002:0000037e +85:bae4 0002:0000037f +85:bae7 0002:00000380 +85:bae9 0002:00000381 +85:8246 0002:00000386 +85:8249 0002:00000387 +85:824b 0002:00000388 +85:82f9 0002:0000038c b8:885c 0003:00000045 b8:885d 0003:00000045 b8:885e 0003:00000046 diff --git a/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json index 63198cde72..222548ba1e 100644 --- a/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json +++ b/worlds/sm/data/SMBasepatch_prebuilt/sm-basepatch-symbols.json @@ -4,7 +4,7 @@ "CLIPLEN_end": "85:990F", "CLIPLEN_no_multi": "85:990C", "CLIPSET": "85:FF1D", - "COLLECTTANK": "B8:80EF", + "COLLECTTANK": "B8:81E8", "MISCFX": "85:FF45", "NORMAL": "84:8BF2", "SETFX": "85:FF4E", @@ -22,6 +22,11 @@ "config_player_id": "CE:FF08", "config_remote_items": "CE:FF06", "config_sprite": "CE:FF02", + "copy_config_to_sram": "B8:8119", + "copy_memory": "B8:80FD", + "copy_memory_done": "B8:8117", + "copy_memory_even": "B8:8109", + "copy_memory_loop": "B8:810F", "h_item": "84:F894", "i_chozo_item": "84:F8AD", "i_hidden_item": "84:F8B4", @@ -30,11 +35,11 @@ "i_item_setup_shared_all_items": "B8:8878", "i_item_setup_shared_alwaysloaded": "B8:8883", "i_live_pickup": "84:FA79", - "i_live_pickup_multiworld": "B8:817F", - "i_live_pickup_multiworld_end": "B8:81C4", - "i_live_pickup_multiworld_local_item_or_offworld": "B8:819B", - "i_live_pickup_multiworld_own_item": "B8:81B0", - "i_live_pickup_multiworld_own_item1": "B8:81BC", + "i_live_pickup_multiworld": "B8:8278", + "i_live_pickup_multiworld_end": "B8:82BD", + "i_live_pickup_multiworld_local_item_or_offworld": "B8:8294", + "i_live_pickup_multiworld_own_item": "B8:82A9", + "i_live_pickup_multiworld_own_item1": "B8:82B5", "i_load_custom_graphics": "84:FA1E", "i_load_custom_graphics_all_items": "84:FA39", "i_load_custom_graphics_alwaysloaded": "84:FA49", @@ -69,22 +74,27 @@ "message_write_placeholders_loop": "85:B9CA", "message_write_placeholders_notfound": "85:B9DC", "message_write_placeholders_value_ok": "85:B9DF", - "mw_display_item_sent": "B8:8092", - "mw_handle_queue": "B8:80FF", - "mw_handle_queue_end": "B8:8178", - "mw_handle_queue_loop": "B8:8101", - "mw_handle_queue_new_remote_item": "B8:8151", - "mw_handle_queue_next": "B8:816D", - "mw_handle_queue_perform_receive": "B8:8163", - "mw_hook_main_game": "B8:81C8", + "mw_display_item_sent": "B8:818B", + "mw_handle_queue": "B8:81F8", + "mw_handle_queue_end": "B8:8271", + "mw_handle_queue_loop": "B8:81FA", + "mw_handle_queue_new_remote_item": "B8:824A", + "mw_handle_queue_next": "B8:8266", + "mw_handle_queue_perform_receive": "B8:825C", + "mw_hook_main_game": "B8:82C1", "mw_init": "B8:8011", - "mw_init_end": "B8:8044", + "mw_init_continuereset": "B8:8066", + "mw_init_end": "B8:80EA", "mw_init_memory": "B8:8000", - "mw_load_sram": "B8:8083", - "mw_receive_item": "B8:80B0", - "mw_receive_item_end": "B8:80E8", - "mw_save_sram": "B8:8070", - "mw_write_message": "B8:8049", + "mw_init_reset_sram": "B8:803B", + "mw_init_smstringdata": "B8:8051", + "mw_load_sram": "B8:8174", + "mw_load_sram_done": "B8:8182", + "mw_load_sram_setnewgame": "B8:8185", + "mw_receive_item": "B8:81A9", + "mw_receive_item_end": "B8:81E1", + "mw_save_sram": "B8:8169", + "mw_write_message": "B8:8142", "nonprog_item_eight_palette_indices": "84:F888", "offworld_graphics_data_item": "89:9200", "offworld_graphics_data_progression_item": "89:9100", @@ -109,7 +119,7 @@ "p_visible_item_end": "84:F96E", "p_visible_item_loop": "84:F95B", "p_visible_item_trigger": "84:F967", - "patch_load_multiworld": "B8:81DF", + "patch_load_multiworld": "B8:82D8", "perform_item_pickup": "84:FA7E", "plm_graphics_entry_offworld_item": "84:F886", "plm_graphics_entry_offworld_progression_item": "84:F87C", @@ -128,14 +138,24 @@ "start_item_data_reserve": "B8:C818", "update_graphic": "B8:C856", "v_item": "84:F890", + "write_repeated_memory": "B8:80EF", + "write_repeated_memory_loop": "B8:80F4", "ITEM_RAM": "7E:09A2", "SRAM_MW_ITEMS_RECV": "70:2000", - "SRAM_MW_ITEMS_RECV_RPTR": "70:2600", - "SRAM_MW_ITEMS_RECV_WPTR": "70:2602", - "SRAM_MW_ITEMS_RECV_SPTR": "70:2604", - "SRAM_MW_ITEMS_SENT_RPTR": "70:2680", - "SRAM_MW_ITEMS_SENT_WPTR": "70:2682", + "SRAM_MW_ITEMS_RECV_WCOUNT": "70:2602", + "ReceiveQueueCompletedCount_InRamThatGetsSavedToSaveSlot": "7e:d8ae", + "SRAM_MW_ITEMS_SENT_RCOUNT": "70:2680", + "SRAM_MW_ITEMS_SENT_WCOUNT": "70:2682", "SRAM_MW_ITEMS_SENT": "70:2700", - "SRAM_MW_INITIALIZED": "70:26fe", + "SRAM_MW_SM": "70:3000", + "SRAM_MW_ROMTITLE": "70:3015", + "SRAM_MW_SEEDINT": "70:3060", + "SRAM_MW_INITIALIZED": "70:3064", + "SRAM_MW_CONFIG_ENABLED": "70:3070", + "SRAM_MW_CONFIG_CUSTOM_SPRITE": "70:3072", + "SRAM_MW_CONFIG_DEATHLINK": "70:3074", + "SRAM_MW_CONFIG_REMOTE_ITEMS": "70:3076", + "SRAM_MW_CONFIG_PLAYER_ID": "70:3078", + "varia_seedint_location": "df:ff00", "CollectedItems": "7E:D86E" } \ No newline at end of file From 4c94bb0ad57a1c3b7ab7caaf8f0ab249ba787d00 Mon Sep 17 00:00:00 2001 From: strotlog <49286967+strotlog@users.noreply.github.com> Date: Fri, 26 Aug 2022 14:44:09 +0000 Subject: [PATCH 31/91] WebHost: sort game list case-insensitively again --- Utils.py | 4 ++-- WebHost.py | 2 +- WebHostLib/templates/supportedGames.html | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Utils.py b/Utils.py index c621e31c9a..4b2300a870 100644 --- a/Utils.py +++ b/Utils.py @@ -619,7 +619,7 @@ def title_sorted(data: typing.Sequence, key=None, ignore: typing.Set = frozenset def sorter(element: str) -> str: parts = element.split(maxsplit=1) if parts[0].lower() in ignore: - return parts[1] + return parts[1].lower() else: - return element + return element.lower() return sorted(data, key=lambda i: sorter(key(i)) if key else sorter(i)) diff --git a/WebHost.py b/WebHost.py index db802193a6..2ce0764214 100644 --- a/WebHost.py +++ b/WebHost.py @@ -104,7 +104,7 @@ def create_ordered_tutorials_file() -> typing.List[typing.Dict[str, typing.Any]] for games in data: if 'Archipelago' in games['gameTitle']: generic_data = data.pop(data.index(games)) - sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"].lower()) + sorted_data = [generic_data] + Utils.title_sorted(data, key=lambda entry: entry["gameTitle"]) json.dump(sorted_data, json_target, indent=2, ensure_ascii=False) return sorted_data diff --git a/WebHostLib/templates/supportedGames.html b/WebHostLib/templates/supportedGames.html index fe81463a46..82f6348db2 100644 --- a/WebHostLib/templates/supportedGames.html +++ b/WebHostLib/templates/supportedGames.html @@ -1,7 +1,7 @@ {% extends 'pageWrapper.html' %} {% block head %} - Player Settings + Supported Games {% endblock %} From cc8ce32c61b30df6d8ee5f3563218fb8ddf76d0c Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 27 Aug 2022 09:21:47 +0200 Subject: [PATCH 32/91] Options: fix corner case where Toggle.value and Toggle.__int__ would be bool Which lead to a connect failure in Raft --- Options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Options.py b/Options.py index a4f559a532..7eb108c99d 100644 --- a/Options.py +++ b/Options.py @@ -298,7 +298,7 @@ class Toggle(NumericOption): if type(data) == str: return cls.from_text(data) else: - return cls(data) + return cls(int(data)) @classmethod def get_option_name(cls, value): From 6d6111de2a91c66a73f81bc6ad4c6021ac9f2aee Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sat, 27 Aug 2022 02:28:46 +0200 Subject: [PATCH 33/91] Launcher: add ModuleUpdate --- Launcher.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/Launcher.py b/Launcher.py index 53032ea251..92f43cd26c 100644 --- a/Launcher.py +++ b/Launcher.py @@ -10,16 +10,20 @@ Scroll down to components= to add components to the launcher as well as setup.py import argparse -from os.path import isfile -import sys -from typing import Iterable, Sequence, Callable, Union, Optional -import subprocess import itertools -from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox,\ - is_windows, is_macos, is_linux -from shutil import which import shlex +import subprocess +import sys from enum import Enum, auto +from os.path import isfile +from shutil import which +from typing import Iterable, Sequence, Callable, Union, Optional + +import ModuleUpdate +ModuleUpdate.update() + +from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \ + is_windows, is_macos, is_linux def open_host_yaml(): From b1ffbc49c97334bfaff06fb4951a2277c30c362e Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 28 Aug 2022 18:30:19 +0200 Subject: [PATCH 34/91] LttPAdjuster: fix GUI for invalid sprite files (#885) * LttPAdjuster: ignore invalid sprite files * LttPAdjuster: ignore .gitignore in sprites * LttPAdjuster: log and show message for invalid sprites * Alttp: set sprite.valid to False for bad zspr and apsprite ... ... when throwing exceptions Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- LttPAdjuster.py | 16 +++++++++- worlds/alttp/Rom.py | 74 +++++++++++++++++++++++++-------------------- 2 files changed, 57 insertions(+), 33 deletions(-) diff --git a/LttPAdjuster.py b/LttPAdjuster.py index 3de6e3b13a..f516a20ec0 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -752,6 +752,7 @@ class SpriteSelector(): self.window['pady'] = 5 self.spritesPerRow = 32 self.all_sprites = [] + self.invalid_sprites = [] self.sprite_pool = spritePool def open_custom_sprite_dir(_evt): @@ -833,6 +834,13 @@ class SpriteSelector(): self.window.focus() tkinter_center_window(self.window) + if self.invalid_sprites: + invalid = sorted(self.invalid_sprites) + logging.warning(f"The following sprites are invalid: {', '.join(invalid)}") + msg = f"{invalid[0]} " + msg += f"and {len(invalid)-1} more are invalid" if len(invalid) > 1 else "is invalid" + messagebox.showerror("Invalid sprites detected", msg, parent=self.window) + def remove_from_sprite_pool(self, button, spritename): self.callback(("remove", spritename)) self.spritePoolButtons.buttons.remove(button) @@ -897,7 +905,13 @@ class SpriteSelector(): sprites = [] for file in os.listdir(path): - sprites.append((file, Sprite(os.path.join(path, file)))) + if file == '.gitignore': + continue + sprite = Sprite(os.path.join(path, file)) + if sprite.valid: + sprites.append((file, sprite)) + else: + self.invalid_sprites.append(file) sprites.sort(key=lambda s: str.lower(s[1].name or "").strip()) diff --git a/worlds/alttp/Rom.py b/worlds/alttp/Rom.py index dd5cc8c4dc..9ca3a355e1 100644 --- a/worlds/alttp/Rom.py +++ b/worlds/alttp/Rom.py @@ -34,7 +34,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_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 +from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml 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 @@ -551,18 +551,22 @@ class Sprite(): Sprite.base_data = Sprite.sprite + Sprite.palette + Sprite.glove_palette def from_ap_sprite(self, filedata): - filedata = filedata.decode("utf-8-sig") - import yaml - obj = yaml.safe_load(filedata) - if obj["min_format_version"] > 1: - raise Exception("Sprite file requires an updated reader.") - self.author_name = obj["author"] - self.name = obj["name"] - if obj["data"]: # skip patching for vanilla content - data = bsdiff4.patch(Sprite.base_data, obj["data"]) - self.sprite = data[:self.sprite_size] - self.palette = data[self.sprite_size:self.palette_size] - self.glove_palette = data[self.sprite_size + self.palette_size:] + # noinspection PyBroadException + try: + obj = parse_yaml(filedata.decode("utf-8-sig")) + if obj["min_format_version"] > 1: + raise Exception("Sprite file requires an updated reader.") + self.author_name = obj["author"] + self.name = obj["name"] + if obj["data"]: # skip patching for vanilla content + data = bsdiff4.patch(Sprite.base_data, obj["data"]) + self.sprite = data[:self.sprite_size] + self.palette = data[self.sprite_size:self.palette_size] + self.glove_palette = data[self.sprite_size + self.palette_size:] + except Exception: + logger = logging.getLogger("apsprite") + logger.exception("Error parsing apsprite file") + self.valid = False @property def author_game_display(self) -> str: @@ -659,7 +663,7 @@ class Sprite(): @staticmethod def parse_zspr(filedata, expected_kind): - logger = logging.getLogger('ZSPR') + logger = logging.getLogger("ZSPR") headerstr = "<4xBHHIHIHH6x" headersize = struct.calcsize(headerstr) if len(filedata) < headersize: @@ -667,7 +671,7 @@ class Sprite(): version, csum, icsum, sprite_offset, sprite_size, palette_offset, palette_size, kind = struct.unpack_from( headerstr, filedata) if version not in [1]: - logger.error('Error parsing ZSPR file: Version %g not supported', version) + logger.error("Error parsing ZSPR file: Version %g not supported", version) return None if kind != expected_kind: return None @@ -676,36 +680,42 @@ class Sprite(): stream.seek(headersize) def read_utf16le(stream): - "Decodes a null-terminated UTF-16_LE string of unknown size from a stream" + """Decodes a null-terminated UTF-16_LE string of unknown size from a stream""" raw = bytearray() while True: char = stream.read(2) - if char in [b'', b'\x00\x00']: + if char in [b"", b"\x00\x00"]: break raw += char - return raw.decode('utf-16_le') + return raw.decode("utf-16_le") - sprite_name = read_utf16le(stream) - author_name = read_utf16le(stream) - author_credits_name = stream.read().split(b"\x00", 1)[0].decode() + # noinspection PyBroadException + try: + sprite_name = read_utf16le(stream) + author_name = read_utf16le(stream) + author_credits_name = stream.read().split(b"\x00", 1)[0].decode() - # Ignoring the Author Rom name for the time being. + # Ignoring the Author Rom name for the time being. - real_csum = sum(filedata) % 0x10000 - if real_csum != csum or real_csum ^ 0xFFFF != icsum: - logger.warning('ZSPR file has incorrect checksum. It may be corrupted.') + real_csum = sum(filedata) % 0x10000 + if real_csum != csum or real_csum ^ 0xFFFF != icsum: + logger.warning("ZSPR file has incorrect checksum. It may be corrupted.") - sprite = filedata[sprite_offset:sprite_offset + sprite_size] - palette = filedata[palette_offset:palette_offset + palette_size] + sprite = filedata[sprite_offset:sprite_offset + sprite_size] + palette = filedata[palette_offset:palette_offset + palette_size] - if len(sprite) != sprite_size or len(palette) != palette_size: - logger.error('Error parsing ZSPR file: Unexpected end of file') + if len(sprite) != sprite_size or len(palette) != palette_size: + logger.error("Error parsing ZSPR file: Unexpected end of file") + return None + + return sprite, palette, sprite_name, author_name, author_credits_name + + except Exception: + logger.exception("Error parsing ZSPR file") return None - return (sprite, palette, sprite_name, author_name, author_credits_name) - def decode_palette(self): - "Returns the palettes as an array of arrays of 15 colors" + """Returns the palettes as an array of arrays of 15 colors""" def array_chunk(arr, size): return list(zip(*[iter(arr)] * size)) From 26aed9351ee73696e34d2172f66748508ec87840 Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sun, 28 Aug 2022 20:58:26 -0700 Subject: [PATCH 35/91] Factorio: Fix a bug with single craft free samples. (#974) --- worlds/factorio/data/mod_template/control.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/factorio/data/mod_template/control.lua b/worlds/factorio/data/mod_template/control.lua index 63473808c6..51cd21e4da 100644 --- a/worlds/factorio/data/mod_template/control.lua +++ b/worlds/factorio/data/mod_template/control.lua @@ -249,6 +249,10 @@ script.on_event(defines.events.on_player_main_inventory_changed, update_player_e function add_samples(force, name, count) local function add_to_table(t) + if count <= 0 then + -- Fixes a bug with single craft, if a recipe gives 0 of a given item. + return + end t[name] = (t[name] or 0) + count end -- Add to global table of earned samples for future new players From 3eb9e7050f7dae3c8da5f53b38b1fb60b9385f24 Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Mon, 29 Aug 2022 14:04:02 -0400 Subject: [PATCH 36/91] DKC3: Fix Wrinkly Softlock (#963) --- worlds/dkc3/Rom.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/worlds/dkc3/Rom.py b/worlds/dkc3/Rom.py index 90c4507e44..2bb5221a60 100644 --- a/worlds/dkc3/Rom.py +++ b/worlds/dkc3/Rom.py @@ -502,6 +502,10 @@ def patch_rom(world, rom, player, active_level_list): # Make Swanky free rom.write_byte(0x348C48, 0x00) + rom.write_bytes(0x34AB70, bytearray([0xEA, 0xEA])) + rom.write_bytes(0x34ABF7, bytearray([0xEA, 0xEA])) + rom.write_bytes(0x34ACD0, bytearray([0xEA, 0xEA])) + # Banana Bird Costs if world.goal[player] == "banana_bird_hunt": banana_bird_cost = math.floor(world.number_of_banana_birds[player] * world.percentage_of_banana_birds[player] / 100.0) From 45fb7353208f272822cde22719aa4d404d66a3e0 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 29 Aug 2022 17:16:13 -0500 Subject: [PATCH 37/91] Clients: allow games without datapackage (#978) --- CommonClient.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CommonClient.py b/CommonClient.py index f830035425..5af8e8cd88 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -345,6 +345,8 @@ class CommonContext: cache_package = Utils.persistent_load().get("datapackage", {}).get("games", {}) needed_updates: typing.Set[str] = set() for game in relevant_games: + if game not in remote_datepackage_versions: + continue remote_version: int = remote_datepackage_versions[game] if remote_version == 0: # custom datapackage for this game From 4a2a184db11ff97a32c1f15106db191460cd234a Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 30 Aug 2022 17:12:33 +0200 Subject: [PATCH 38/91] Core: remove game-specific arguments from Generate (#971) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Generate.py | 11 +---------- Main.py | 1 - worlds/alttp/EntranceRandomizer.py | 2 -- worlds/alttp/__init__.py | 13 +++++++++---- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/Generate.py b/Generate.py index 1cad836345..d13a78b375 100644 --- a/Generate.py +++ b/Generate.py @@ -63,7 +63,7 @@ class PlandoSettings(enum.IntFlag): def __str__(self) -> str: if self.value: - return ", ".join((flag.name for flag in PlandoSettings if self.value & flag.value)) + return ", ".join(flag.name for flag in PlandoSettings if self.value & flag.value) return "Off" @@ -84,11 +84,6 @@ def mystery_argparse(): parser.add_argument('--seed', help='Define seed number to generate.', type=int) parser.add_argument('--multi', default=defaults["players"], type=lambda value: max(int(value), 1)) parser.add_argument('--spoiler', type=int, default=defaults["spoiler"]) - parser.add_argument('--lttp_rom', default=options["lttp_options"]["rom_file"], - help="Path to the 1.0 JP LttP Baserom.") # absolute, relative to cwd or relative to app path - parser.add_argument('--sm_rom', default=options["sm_options"]["rom_file"], - help="Path to the 1.0 JP SM Baserom.") - parser.add_argument('--enemizercli', default=resolve_path(defaults["enemizer_path"], local_path)) parser.add_argument('--outputpath', default=resolve_path(options["general_options"]["output_path"], user_path), help="Path to output folder. Absolute or relative to cwd.") # absolute or relative to cwd parser.add_argument('--race', action='store_true', default=defaults["race"]) @@ -183,10 +178,6 @@ def main(args=None, callback=ERmain): Utils.init_logging(f"Generate_{seed}", loglevel=args.log_level) - erargs.lttp_rom = args.lttp_rom - erargs.sm_rom = args.sm_rom - erargs.enemizercli = args.enemizercli - settings_cache: Dict[str, Tuple[argparse.Namespace, ...]] = \ {fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.samesettings else None) for fname, yamls in weights_cache.items()} diff --git a/Main.py b/Main.py index 48095e06bd..acff74595a 100644 --- a/Main.py +++ b/Main.py @@ -70,7 +70,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No world.required_medallions = args.required_medallions.copy() world.game = args.game.copy() world.player_name = args.name.copy() - world.enemizer = args.enemizercli world.sprite = args.sprite.copy() world.glitch_triforce = args.glitch_triforce # This is enabled/disabled globally, no per player option. diff --git a/worlds/alttp/EntranceRandomizer.py b/worlds/alttp/EntranceRandomizer.py index f1748b9a0f..47c36b6cde 100644 --- a/worlds/alttp/EntranceRandomizer.py +++ b/worlds/alttp/EntranceRandomizer.py @@ -212,9 +212,7 @@ def parse_arguments(argv, no_defaults=False): Alternatively, can be a ALttP Rom patched with a Link sprite that will be extracted. ''') - parser.add_argument('--gui', help='Launch the GUI', action='store_true') - parser.add_argument('--enemizercli', default=defval('EnemizerCLI/EnemizerCLI.Core')) parser.add_argument('--shufflebosses', default=defval('none'), choices=['none', 'basic', 'normal', 'chaos', "singularity"]) diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index e7f111c3b7..3e32584352 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -4,6 +4,7 @@ import random import threading import typing +import Utils from BaseClasses import Item, CollectionState, Tutorial from .Dungeons import create_dungeons from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect @@ -136,6 +137,10 @@ class ALTTPWorld(World): create_items = generate_itempool + enemizer_path: str = Utils.get_options()["generator"]["enemizer_path"] \ + if os.path.isabs(Utils.get_options()["generator"]["enemizer_path"]) \ + else Utils.local_path(Utils.get_options()["generator"]["enemizer_path"]) + def __init__(self, *args, **kwargs): self.dungeon_local_item_names = set() self.dungeon_specific_item_names = set() @@ -150,12 +155,12 @@ class ALTTPWorld(World): raise FileNotFoundError(rom_file) def generate_early(self): + if self.use_enemizer(): + check_enemizer(self.enemizer_path) + player = self.player world = self.world - if self.use_enemizer(): - check_enemizer(world.enemizer) - # system for sharing ER layouts self.er_seed = str(world.random.randint(0, 2 ** 64)) @@ -360,7 +365,7 @@ class ALTTPWorld(World): patch_rom(world, rom, player, use_enemizer) if use_enemizer: - patch_enemizer(world, player, rom, world.enemizer, output_directory) + patch_enemizer(world, player, rom, self.enemizer_path, output_directory) if world.is_race: patch_race_rom(rom, world, player) From 60d1a27079239bbcf08337e3322a4ab632480e2d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Tue, 30 Aug 2022 17:14:34 +0200 Subject: [PATCH 39/91] Subnautica: revamp aggressive creature scans (#966) * add forgotten aggressive creatures * fix logic requirements * added option to opt out of aggressive creature scans --- worlds/subnautica/Creatures.py | 29 +++++++++++++++++++++++++++-- worlds/subnautica/Locations.py | 7 ++++++- worlds/subnautica/Options.py | 15 ++++++++++++++- worlds/subnautica/Rules.py | 28 ++++++++++++++++++++++------ worlds/subnautica/__init__.py | 17 +++++++++-------- 5 files changed, 78 insertions(+), 18 deletions(-) diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/Creatures.py index a9f5e850e1..687c3732a9 100644 --- a/worlds/subnautica/Creatures.py +++ b/worlds/subnautica/Creatures.py @@ -1,3 +1,4 @@ +import functools from typing import Dict, Set, List # EN Locale Creature Name to rough depth in meters found at @@ -58,13 +59,18 @@ all_creatures: Dict[str, int] = { aggressive: Set[str] = { "Cave Crawler", # is very easy without Stasis Rifle, but included for consistency "Crashfish", + "Biter", "Bleeder", + "Blighter", + "Blood Crawler", "Mesmer", "Reaper Leviathan", "Crabsquid", "Warper", "Crabsnake", "Ampeel", + "Stalker", + "Sand Shark", "Boneshark", "Lava Lizard", "Sea Dragon Leviathan", @@ -94,6 +100,25 @@ creature_locations: Dict[str, int] = { creature + suffix: creature_id for creature_id, creature in enumerate(all_creatures, start=34000) } -all_creatures_presorted: List[str] = sorted(all_creatures) -all_creatures_presorted_without_containment = [name for name in all_creatures_presorted if name not in containment] +class Definitions: + """Only compute lists if needed and then cache them.""" + + @functools.cached_property + def all_creatures_presorted(self) -> List[str]: + return sorted(all_creatures) + + @functools.cached_property + def all_creatures_presorted_without_containment(self) -> List[str]: + return [name for name in self.all_creatures_presorted if name not in containment] + + @functools.cached_property + def all_creatures_presorted_without_stasis(self) -> List[str]: + return [name for name in self.all_creatures_presorted if name not in aggressive or name in hatchable] + + @functools.cached_property + def all_creatures_presorted_without_aggressive(self) -> List[str]: + return [name for name in self.all_creatures_presorted if name not in aggressive] + +# only singleton needed +Definitions: Definitions = Definitions() diff --git a/worlds/subnautica/Locations.py b/worlds/subnautica/Locations.py index 3effd1eac3..2dfeaf3bf3 100644 --- a/worlds/subnautica/Locations.py +++ b/worlds/subnautica/Locations.py @@ -15,7 +15,12 @@ class LocationDict(TypedDict, total=False): need_propulsion_cannon: bool -events: List[str] = ["Neptune Launch", "Disable Quarantine", "Full Infection", "Repair Aurora Drive"] +events: List[str] = [ + "Neptune Launch", + "Disable Quarantine", + "Full Infection", + "Repair Aurora Drive", +] location_table: Dict[int, LocationDict] = { 33000: {'can_slip_through': False, diff --git a/worlds/subnautica/Options.py b/worlds/subnautica/Options.py index f68e12d2c0..57bd23fdb7 100644 --- a/worlds/subnautica/Options.py +++ b/worlds/subnautica/Options.py @@ -1,7 +1,7 @@ import typing from Options import Choice, Range, DeathLink -from .Creatures import all_creatures +from .Creatures import all_creatures, Definitions class ItemPool(Choice): @@ -46,14 +46,27 @@ class AggressiveScanLogic(Choice): Containment: Removes Stasis Rifle as expected solution and expects Alien Containment instead. Either: Creatures may be expected to be scanned via Stasis Rifle or Containment, whichever is found first. None: Aggressive Creatures are assumed to not need any tools to scan. + Removed: No Creatures needing Stasis or Containment will be in the pool at all. Note: Containment, Either and None adds Cuddlefish as an option for scans. + Note: Stasis, Either and None adds unhatchable aggressive species, such as Warper. Note: This is purely a logic expectation, and does not affect gameplay, only placement.""" display_name = "Aggressive Creature Scan Logic" option_stasis = 0 option_containment = 1 option_either = 2 option_none = 3 + option_removed = 4 + + def get_pool(self) -> typing.List[str]: + if self == self.option_removed: + return Definitions.all_creatures_presorted_without_aggressive + elif self == self.option_stasis: + return Definitions.all_creatures_presorted_without_containment + elif self == self.option_containment: + return Definitions.all_creatures_presorted_without_stasis + else: + return Definitions.all_creatures_presorted class SubnauticaDeathLink(DeathLink): diff --git a/worlds/subnautica/Rules.py b/worlds/subnautica/Rules.py index 20c6a35c84..8925f1e829 100644 --- a/worlds/subnautica/Rules.py +++ b/worlds/subnautica/Rules.py @@ -1,8 +1,8 @@ -from typing import TYPE_CHECKING, Dict, Callable +from typing import TYPE_CHECKING, Dict, Callable, Optional from worlds.generic.Rules import set_rule, add_rule from .Locations import location_table, LocationDict -from .Creatures import all_creatures, aggressive, suffix +from .Creatures import all_creatures, aggressive, suffix, hatchable, containment from .Options import AggressiveScanLogic import math @@ -258,6 +258,15 @@ def set_creature_rule(world, player: int, creature_name: str) -> "Location": return location +def get_aggression_rule(option: AggressiveScanLogic, creature_name: str) -> \ + Optional[Callable[["CollectionState", int], bool]]: + """Get logic rule for a creature scan location.""" + if creature_name not in hatchable and option != option.option_none: # can only be done via stasis + return has_stasis_rifle + # otherwise allow option preference + return aggression_rules.get(option.value, None) + + aggression_rules: Dict[int, Callable[["CollectionState", int], bool]] = { AggressiveScanLogic.option_stasis: has_stasis_rifle, AggressiveScanLogic.option_containment: has_containment, @@ -274,14 +283,21 @@ def set_rules(subnautica_world: "SubnauticaWorld"): set_location_rule(world, player, loc) if subnautica_world.creatures_to_scan: - aggressive_rule = aggression_rules.get(world.creature_scan_logic[player], None) + option = world.creature_scan_logic[player] + for creature_name in subnautica_world.creatures_to_scan: location = set_creature_rule(world, player, creature_name) - if creature_name in aggressive and aggressive_rule: - add_rule(location, lambda state: aggressive_rule(state, player)) + if creature_name in containment: # there is no other way, hard-required containment + add_rule(location, lambda state: has_containment(state, player)) + elif creature_name in aggressive: + rule = get_aggression_rule(option, creature_name) + if rule: + add_rule(location, + lambda state, loc_rule=get_aggression_rule(option, creature_name): loc_rule(state, player)) # Victory locations - set_rule(world.get_location("Neptune Launch", player), lambda state: + set_rule(world.get_location("Neptune Launch", player), + lambda state: get_max_depth(state, player) >= 1444 and has_mobile_vehicle_bay(state, player) and state.has("Neptune Launch Platform", player) and diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index 806c1b195e..a4447ccbc1 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -52,14 +52,15 @@ class SubnauticaWorld(World): self.create_item("Seaglide Fragment"), self.create_item("Seaglide Fragment") ] - if self.world.creature_scan_logic[self.player] == Options.AggressiveScanLogic.option_stasis: - valid_creatures = Creatures.all_creatures_presorted_without_containment - self.world.creature_scans[self.player].value = min(len( - Creatures.all_creatures_presorted_without_containment), - self.world.creature_scans[self.player].value) - else: - valid_creatures = Creatures.all_creatures_presorted - self.creatures_to_scan = self.world.random.sample(valid_creatures, + scan_option: Options.AggressiveScanLogic = self.world.creature_scan_logic[self.player] + creature_pool = scan_option.get_pool() + + self.world.creature_scans[self.player].value = min( + len(creature_pool), + self.world.creature_scans[self.player].value + ) + + self.creatures_to_scan = self.world.random.sample(creature_pool, self.world.creature_scans[self.player].value) def create_regions(self): From 2a7babce6871797a1f037be4e5a8bd828fe2c505 Mon Sep 17 00:00:00 2001 From: strotlog <49286967+strotlog@users.noreply.github.com> Date: Tue, 30 Aug 2022 08:16:21 -0700 Subject: [PATCH 40/91] SM+SMZ3: don't abandon checks that happen while disconnected from AP (#946) --- SNIClient.py | 47 +++++++++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index aad231691b..c97d0d6c0d 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -149,8 +149,8 @@ class Context(CommonContext): def event_invalid_slot(self): if self.snes_socket is not None and not self.snes_socket.closed: asyncio.create_task(self.snes_socket.close()) - raise Exception('Invalid ROM detected, ' - 'please verify that you have loaded the correct rom and reconnect your snes (/snes)') + raise Exception("Invalid ROM detected, " + "please verify that you have loaded the correct rom and reconnect your snes (/snes)") async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -158,7 +158,7 @@ class Context(CommonContext): if self.rom is None: self.awaiting_rom = True snes_logger.info( - 'No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)') + "No ROM detected, awaiting snes connection to authenticate to the multiworld server (/snes)") return self.awaiting_rom = False self.auth = self.rom @@ -262,7 +262,7 @@ async def deathlink_kill_player(ctx: Context): SNES_RECONNECT_DELAY = 5 -# LttP +# FXPAK Pro protocol memory mapping used by SNI ROM_START = 0x000000 WRAM_START = 0xF50000 WRAM_SIZE = 0x20000 @@ -293,21 +293,24 @@ SHOP_LEN = (len(Shops.shop_table) * 3) + 5 DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte # SM -SM_ROMNAME_START = 0x007FC0 +SM_ROMNAME_START = ROM_START + 0x007FC0 SM_INGAME_MODES = {0x07, 0x09, 0x0b} SM_ENDGAME_MODES = {0x26, 0x27} SM_DEATH_MODES = {0x15, 0x17, 0x18, 0x19, 0x1A} -SM_RECV_PROGRESS_ADDR = SRAM_START + 0x2000 # 2 bytes -SM_RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte -SM_RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte +# RECV and SEND are from the gameplay's perspective: SNIClient writes to RECV queue and reads from SEND queue +SM_RECV_QUEUE_START = SRAM_START + 0x2000 +SM_RECV_QUEUE_WCOUNT = SRAM_START + 0x2602 +SM_SEND_QUEUE_START = SRAM_START + 0x2700 +SM_SEND_QUEUE_RCOUNT = SRAM_START + 0x2680 +SM_SEND_QUEUE_WCOUNT = SRAM_START + 0x2682 SM_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x277f04 # 1 byte SM_REMOTE_ITEM_FLAG_ADDR = ROM_START + 0x277f06 # 1 byte # SMZ3 -SMZ3_ROMNAME_START = 0x00FFC0 +SMZ3_ROMNAME_START = ROM_START + 0x00FFC0 SMZ3_INGAME_MODES = {0x07, 0x09, 0x0b} SMZ3_ENDGAME_MODES = {0x26, 0x27} @@ -1083,6 +1086,9 @@ async def game_watcher(ctx: Context): if ctx.awaiting_rom: await ctx.server_auth(False) + elif ctx.server is None: + snes_logger.warning("ROM detected but no active multiworld server connection. " + + "Connect using command: /connect server:port") if ctx.auth and ctx.auth != ctx.rom: snes_logger.warning("ROM change detected, please reconnect to the multiworld server") @@ -1159,6 +1165,9 @@ async def game_watcher(ctx: Context): await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}]) await track_locations(ctx, roomid, roomdata) elif ctx.game == GAME_SM: + if ctx.server is None or ctx.slot is None: + # not successfully connected to a multiworld server, cannot process the game sending items + continue gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1) if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time(): currently_dead = gamemode[0] in SM_DEATH_MODES @@ -1169,22 +1178,22 @@ async def game_watcher(ctx: Context): ctx.finished_game = True continue - data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x680, 4) + data = await snes_read(ctx, SM_SEND_QUEUE_RCOUNT, 4) if data is None: continue recv_index = data[0] | (data[1] << 8) - recv_item = data[2] | (data[3] << 8) + recv_item = data[2] | (data[3] << 8) # this is actually SM_SEND_QUEUE_WCOUNT while (recv_index < recv_item): itemAdress = recv_index * 8 - message = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x700 + itemAdress, 8) + message = await snes_read(ctx, SM_SEND_QUEUE_START + itemAdress, 8) # worldId = message[0] | (message[1] << 8) # unused # itemId = message[2] | (message[3] << 8) # unused itemIndex = (message[4] | (message[5] << 8)) >> 3 recv_index += 1 - snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x680, + snes_buffered_write(ctx, SM_SEND_QUEUE_RCOUNT, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) from worlds.sm.Locations import locations_start_id @@ -1196,12 +1205,11 @@ async def game_watcher(ctx: Context): f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})') await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}]) - data = await snes_read(ctx, SM_RECV_PROGRESS_ADDR + 0x600, 4) + data = await snes_read(ctx, SM_RECV_QUEUE_WCOUNT, 2) if data is None: continue - # recv_itemOutPtr = data[0] | (data[1] << 8) # unused - itemOutPtr = data[2] | (data[3] << 8) + itemOutPtr = data[0] | (data[1] << 8) from worlds.sm.Items import items_start_id from worlds.sm.Locations import locations_start_id @@ -1214,10 +1222,10 @@ async def game_watcher(ctx: Context): locationId = 0x00 #backward compat playerID = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0 - snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + itemOutPtr * 4, bytes( + snes_buffered_write(ctx, SM_RECV_QUEUE_START + itemOutPtr * 4, bytes( [playerID & 0xFF, (playerID >> 8) & 0xFF, itemId & 0xFF, locationId & 0xFF])) itemOutPtr += 1 - snes_buffered_write(ctx, SM_RECV_PROGRESS_ADDR + 0x602, + snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT, bytes([itemOutPtr & 0xFF, (itemOutPtr >> 8) & 0xFF])) logging.info('Received %s from %s (%s) (%d/%d in list)' % ( color(ctx.item_names[item.item], 'red', 'bold'), @@ -1225,6 +1233,9 @@ async def game_watcher(ctx: Context): ctx.location_names[item.location], itemOutPtr, len(ctx.items_received))) await snes_flush_writes(ctx) elif ctx.game == GAME_SMZ3: + if ctx.server is None or ctx.slot is None: + # not successfully connected to a multiworld server, cannot process the game sending items + continue currentGame = await snes_read(ctx, SRAM_START + 0x33FE, 2) if (currentGame is not None): if (currentGame[0] != 0): From a753905ee4536e4c28ef25dd0b230fe03fa683b1 Mon Sep 17 00:00:00 2001 From: espeon65536 <81029175+espeon65536@users.noreply.github.com> Date: Tue, 30 Aug 2022 11:54:40 -0700 Subject: [PATCH 41/91] OoT bug fixes (#955) * OoT: fix shop patching crash due to Item changes * OoT: more informative failure in triforce piece replacement * OoT: in triforce hunt, remove ganon BK from pool and lock the door * OoT: no longer store trap information on the item --- worlds/oot/ItemPool.py | 4 ++++ worlds/oot/Items.py | 1 - worlds/oot/Options.py | 4 ++-- worlds/oot/Patches.py | 30 ++++++++++++++---------------- worlds/oot/__init__.py | 11 ++++++++--- 5 files changed, 28 insertions(+), 22 deletions(-) diff --git a/worlds/oot/ItemPool.py b/worlds/oot/ItemPool.py index 301c502a7e..12c9c26292 100644 --- a/worlds/oot/ItemPool.py +++ b/worlds/oot/ItemPool.py @@ -1388,6 +1388,10 @@ def get_pool_core(world): remove_junk_pool = list(remove_junk_pool) + ['Recovery Heart', 'Bombs (20)', 'Arrows (30)', 'Ice Trap'] junk_candidates = [item for item in pool if item in remove_junk_pool] + if len(pending_junk_pool) > len(junk_candidates): + excess = len(pending_junk_pool) - len(junk_candidates) + if world.triforce_hunt: + raise RuntimeError(f"Items in the pool for player {world.player} exceed locations. Add {excess} location(s) or remove {excess} triforce piece(s).") while pending_junk_pool: pending_item = pending_junk_pool.pop() if not junk_candidates: diff --git a/worlds/oot/Items.py b/worlds/oot/Items.py index 31e6c31f62..06164091a7 100644 --- a/worlds/oot/Items.py +++ b/worlds/oot/Items.py @@ -49,7 +49,6 @@ class OOTItem(Item): self.type = type self.index = index self.special = special or {} - self.looks_like_item = None self.price = special.get('price', None) if special else None self.internal = False diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index 50b6c26c86..ea9a8160fb 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -158,12 +158,12 @@ class TriforceGoal(Range): """Number of Triforce pieces required to complete the game.""" display_name = "Required Triforce Pieces" range_start = 1 - range_end = 100 + range_end = 80 default = 20 class ExtraTriforces(Range): - """Percentage of additional Triforce pieces in the pool, separate from the item pool setting.""" + """Percentage of additional Triforce pieces in the pool. With high numbers, you may need to randomize additional locations to have enough items.""" display_name = "Percentage of Extra Triforce Pieces" range_start = 0 range_end = 100 diff --git a/worlds/oot/Patches.py b/worlds/oot/Patches.py index 7bf31c4f7a..322d2d838a 100644 --- a/worlds/oot/Patches.py +++ b/worlds/oot/Patches.py @@ -1844,7 +1844,7 @@ def write_rom_item(rom, item_id, item): def get_override_table(world): - return list(filter(lambda val: val != None, map(partial(get_override_entry, world.player), world.world.get_filled_locations(world.player)))) + return list(filter(lambda val: val != None, map(partial(get_override_entry, world), world.world.get_filled_locations(world.player)))) override_struct = struct.Struct('>xBBBHBB') # match override_t in get_items.c @@ -1852,10 +1852,10 @@ def get_override_table_bytes(override_table): return b''.join(sorted(itertools.starmap(override_struct.pack, override_table))) -def get_override_entry(player_id, location): +def get_override_entry(ootworld, location): scene = location.scene default = location.default - player_id = 0 if player_id == location.item.player else min(location.item.player, 255) + player_id = 0 if ootworld.player == location.item.player else min(location.item.player, 255) if location.item.game != 'Ocarina of Time': # This is an AP sendable. It's guaranteed to not be None. if location.item.advancement: @@ -1869,7 +1869,7 @@ def get_override_entry(player_id, location): if location.item.trap: item_id = 0x7C # Ice Trap ID, to get "X is a fool" message - looks_like_item_id = location.item.looks_like_item.index + looks_like_item_id = ootworld.trap_appearances[location.address].index else: looks_like_item_id = 0 @@ -2091,7 +2091,8 @@ def get_locked_doors(rom, world): return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits] # If boss door, set the door's unlock flag - if (world.shuffle_bosskeys == 'remove' and scene != 0x0A) or (world.shuffle_ganon_bosskey == 'remove' and scene == 0x0A): + if (world.shuffle_bosskeys == 'remove' and scene != 0x0A) or ( + world.shuffle_ganon_bosskey == 'remove' and scene == 0x0A and not world.triforce_hunt): if actor_id == 0x002E and actor_type == 0x05: return [0x00D4 + scene * 0x1C + 0x04 + flag_byte, flag_bits] @@ -2109,23 +2110,20 @@ def place_shop_items(rom, world, shop_items, messages, locations, init_shop_id=F rom.write_int16(location.address1, location.item.index) else: if location.item.trap: - item_display = location.item.looks_like_item - elif location.item.game != "Ocarina of Time": - item_display = location.item - if location.item.advancement: - item_display.index = 0xCB - else: - item_display.index = 0xCC - item_display.special = {} + item_display = world.trap_appearances[location.address] else: item_display = location.item # bottles in shops should look like empty bottles # so that that are different than normal shop refils - if 'shop_object' in item_display.special: - rom_item = read_rom_item(rom, item_display.special['shop_object']) + if location.item.trap or location.item.game == "Ocarina of Time": + if 'shop_object' in item_display.special: + rom_item = read_rom_item(rom, item_display.special['shop_object']) + else: + rom_item = read_rom_item(rom, item_display.index) else: - rom_item = read_rom_item(rom, item_display.index) + display_index = 0xCB if location.item.advancement else 0xCC + rom_item = read_rom_item(rom, display_index) shop_objs.add(rom_item['object_id']) shop_id = world.current_shop_id diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index b4635ad77f..a9b7d5a1b6 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -178,6 +178,10 @@ class OOTWorld(World): if self.skip_child_zelda: self.shuffle_weird_egg = False + # Ganon boss key should not be in itempool in triforce hunt + if self.triforce_hunt: + self.shuffle_ganon_bosskey = 'remove' + # Determine skipped trials in GT # This needs to be done before the logic rules in GT are parsed trial_list = ['Forest', 'Fire', 'Water', 'Spirit', 'Shadow', 'Light'] @@ -803,9 +807,10 @@ class OOTWorld(World): with i_o_limiter: # Make traps appear as other random items - ice_traps = [loc.item for loc in self.get_locations() if loc.item.trap] - for trap in ice_traps: - trap.looks_like_item = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name) + trap_location_ids = [loc.address for loc in self.get_locations() if loc.item.trap] + self.trap_appearances = {} + for loc_id in trap_location_ids: + self.trap_appearances[loc_id] = self.create_item(self.world.slot_seeds[self.player].choice(self.fake_items).name) # Seed hint RNG, used for ganon text lines also self.hint_rng = self.world.slot_seeds[self.player] From fcfc2c2e100189e363cff2f47a30b0bbc8154c57 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Wed, 31 Aug 2022 00:10:18 +0200 Subject: [PATCH 42/91] WebHost: fix local_path on python 3.8 (#981) * WebHost: fix local_path on python 3.8 `__file__` is relative in 3.8, so `os.path.dirname(__file__)` ends up being an empty string breaking calls to `local_path()` (without arguments) * WebHost: add comment to local_path override --- WebHost.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WebHost.py b/WebHost.py index 2ce0764214..4c07e8b185 100644 --- a/WebHost.py +++ b/WebHost.py @@ -12,7 +12,7 @@ ModuleUpdate.update() # in case app gets imported by something like gunicorn import Utils -Utils.local_path.cached_path = os.path.dirname(__file__) +Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8 from WebHostLib import register, app as raw_app from waitress import serve From 8da1cfeeb752b1aee83ee1ec53e9da8493bc544e Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Wed, 31 Aug 2022 00:14:17 -0400 Subject: [PATCH 43/91] SM: remove events from data package (#973) --- SNIClient.py | 6 +++--- worlds/sm/Items.py | 14 -------------- worlds/sm/Locations.py | 14 -------------- worlds/sm/__init__.py | 17 ++++++++--------- 4 files changed, 11 insertions(+), 40 deletions(-) delete mode 100644 worlds/sm/Items.py delete mode 100644 worlds/sm/Locations.py diff --git a/SNIClient.py b/SNIClient.py index c97d0d6c0d..3d90fafc17 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -1196,7 +1196,7 @@ async def game_watcher(ctx: Context): snes_buffered_write(ctx, SM_SEND_QUEUE_RCOUNT, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF])) - from worlds.sm.Locations import locations_start_id + from worlds.sm import locations_start_id location_id = locations_start_id + itemIndex ctx.locations_checked.add(location_id) @@ -1211,8 +1211,8 @@ async def game_watcher(ctx: Context): itemOutPtr = data[0] | (data[1] << 8) - from worlds.sm.Items import items_start_id - from worlds.sm.Locations import locations_start_id + from worlds.sm import items_start_id + from worlds.sm import locations_start_id if itemOutPtr < len(ctx.items_received): item = ctx.items_received[itemOutPtr] itemId = item.item - items_start_id diff --git a/worlds/sm/Items.py b/worlds/sm/Items.py deleted file mode 100644 index ff8970b64d..0000000000 --- a/worlds/sm/Items.py +++ /dev/null @@ -1,14 +0,0 @@ -from worlds.sm.variaRandomizer.rando.Items import ItemManager - -items_start_id = 83000 - -def gen_special_id(): - special_id_value_start = 32 - while True: - yield special_id_value_start - special_id_value_start += 1 - -gen_run = gen_special_id() - -lookup_id_to_name = dict((items_start_id + (value.Id if value.Id != None else next(gen_run)), value.Name) for key, value in ItemManager.Items.items()) -lookup_name_to_id = {item_name: item_id for item_id, item_name in lookup_id_to_name.items()} \ No newline at end of file diff --git a/worlds/sm/Locations.py b/worlds/sm/Locations.py deleted file mode 100644 index 4e80ab00e6..0000000000 --- a/worlds/sm/Locations.py +++ /dev/null @@ -1,14 +0,0 @@ -from worlds.sm.variaRandomizer.graph.location import locationsDict - -locations_start_id = 82000 - -def gen_boss_id(): - boss_id_value_start = 256 - while True: - yield boss_id_value_start - boss_id_value_start += 1 - -gen_run = gen_boss_id() - -lookup_id_to_name = dict((locations_start_id + (value.Id if value.Id != None else next(gen_run)), key) for key, value in locationsDict.items()) -lookup_name_to_id = {location_name: location_id for location_id, location_name in lookup_id_to_name.items()} \ No newline at end of file diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index 5da1c40f75..fbf3825e0f 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -11,8 +11,6 @@ from worlds.sm.variaRandomizer.graph.graph_utils import GraphUtils logger = logging.getLogger("Super Metroid") -from .Locations import lookup_name_to_id as locations_lookup_name_to_id -from .Items import lookup_name_to_id as items_lookup_name_to_id from .Regions import create_regions from .Rules import set_rules, add_entrance_rule from .Options import sm_options @@ -68,6 +66,8 @@ class SMWeb(WebWorld): ["Farrak Kilhn"] )] +locations_start_id = 82000 +items_start_id = 83000 class SMWorld(World): """ @@ -78,12 +78,11 @@ class SMWorld(World): game: str = "Super Metroid" topology_present = True - data_version = 1 + data_version = 2 option_definitions = sm_options - item_names: Set[str] = frozenset(items_lookup_name_to_id) - location_names: Set[str] = frozenset(locations_lookup_name_to_id) - item_name_to_id = items_lookup_name_to_id - location_name_to_id = locations_lookup_name_to_id + + item_name_to_id = {value.Name: items_start_id + value.Id for key, value in ItemManager.Items.items() if value.Id != None} + location_name_to_id = {key: locations_start_id + value.Id for key, value in locationsDict.items() if value.Id != None} web = SMWeb() remote_items: bool = False @@ -701,8 +700,8 @@ 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, id in locations_lookup_name_to_id.items(): - self.locations[name] = SMLocation(player, name, id) + 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): From c617bba95993a8b7099a678b59ff1062d932266d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 31 Aug 2022 20:55:15 +0200 Subject: [PATCH 44/91] SC2: client revamp (#967) SC2 client now relies almost entirely on the map file and server for the locations and just facilitates them, should make it significantly more resilient to objectives being added or removed * SC2: fix client crash on printjson messages with more [ than ] * SC2: move text to queue, that actually clears memory * SC2: Announce which mission is being loaded Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- CommonClient.py | 7 +- Starcraft2Client.py | 431 ++++++++++++++------------------- Utils.py | 2 +- worlds/sc2wol/MissionTables.py | 4 +- worlds/sc2wol/__init__.py | 1 + 5 files changed, 187 insertions(+), 258 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 5af8e8cd88..574da16f2a 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -152,8 +152,9 @@ class CommonContext: # locations locations_checked: typing.Set[int] # local state locations_scouted: typing.Set[int] - missing_locations: typing.Set[int] + missing_locations: typing.Set[int] # server state checked_locations: typing.Set[int] # server state + server_locations: typing.Set[int] # all locations the server knows of, missing_location | checked_locations locations_info: typing.Dict[int, NetworkItem] # internals @@ -184,8 +185,9 @@ class CommonContext: self.locations_checked = set() # local state self.locations_scouted = set() self.items_received = [] - self.missing_locations = set() + self.missing_locations = set() # server state self.checked_locations = set() # server state + self.server_locations = set() # all locations the server knows of, missing_location | checked_locations self.locations_info = {} self.input_queue = asyncio.Queue() @@ -634,6 +636,7 @@ async def process_server_cmd(ctx: CommonContext, args: dict): # when /missing is used for the client side view of what is missing. ctx.missing_locations = set(args["missing_locations"]) ctx.checked_locations = set(args["checked_locations"]) + ctx.server_locations = ctx.missing_locations | ctx. checked_locations elif cmd == 'ReceivedItems': start_index = args["index"] diff --git a/Starcraft2Client.py b/Starcraft2Client.py index dc63e9a456..b8f6086914 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -1,31 +1,31 @@ from __future__ import annotations -import multiprocessing -import logging import asyncio +import copy +import ctypes +import logging +import multiprocessing import os.path +import re +import sys +import typing +import queue +from pathlib import Path import nest_asyncio import sc2 - -from sc2.main import run_game -from sc2.data import Race from sc2.bot_ai import BotAI +from sc2.data import Race +from sc2.main import run_game from sc2.player import Bot -from worlds.sc2wol.Regions import MissionInfo -from worlds.sc2wol.MissionTables import lookup_id_to_mission +from MultiServer import mark_raw +from Utils import init_logging, is_windows +from worlds.sc2wol import SC2WoLWorld from worlds.sc2wol.Items import lookup_id_to_name, item_table from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET -from worlds.sc2wol import SC2WoLWorld - -from pathlib import Path -import re -from MultiServer import mark_raw -import ctypes -import sys - -from Utils import init_logging, is_windows +from worlds.sc2wol.MissionTables import lookup_id_to_mission +from worlds.sc2wol.Regions import MissionInfo if __name__ == "__main__": init_logging("SC2Client", exception_logger="Client") @@ -35,10 +35,12 @@ sc2_logger = logging.getLogger("Starcraft2") import colorama -from NetUtils import * +from NetUtils import ClientStatus, RawJSONtoTextParser from CommonClient import CommonContext, server_loop, ClientCommandProcessor, gui_enabled, get_base_parser nest_asyncio.apply() +max_bonus: int = 8 +victory_modulo: int = 100 class StarcraftClientProcessor(ClientCommandProcessor): @@ -98,13 +100,13 @@ class StarcraftClientProcessor(ClientCommandProcessor): def _cmd_available(self) -> bool: """Get what missions are currently available to play""" - request_available_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui) + request_available_missions(self.ctx) return True def _cmd_unfinished(self) -> bool: """Get what missions are currently available to play and have not had all locations checked""" - request_unfinished_missions(self.ctx.checked_locations, self.ctx.mission_req_table, self.ctx.ui, self.ctx) + request_unfinished_missions(self.ctx) return True @mark_raw @@ -125,18 +127,19 @@ class SC2Context(CommonContext): items_handling = 0b111 difficulty = -1 all_in_choice = 0 - mission_req_table = None - items_rec_to_announce = [] - rec_announce_pos = 0 - items_sent_to_announce = [] - sent_announce_pos = 0 - announcements = [] - announcement_pos = 0 + mission_req_table: typing.Dict[str, MissionInfo] = {} + announcements = queue.Queue() sc2_run_task: typing.Optional[asyncio.Task] = None - missions_unlocked = False + missions_unlocked: bool = False # allow launching missions ignoring requirements current_tooltip = None last_loc_list = None difficulty_override = -1 + mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {} + raw_text_parser: RawJSONtoTextParser + + def __init__(self, *args, **kwargs): + super(SC2Context, self).__init__(*args, **kwargs) + self.raw_text_parser = RawJSONtoTextParser(self) async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -149,30 +152,32 @@ class SC2Context(CommonContext): self.difficulty = args["slot_data"]["game_difficulty"] self.all_in_choice = args["slot_data"]["all_in_map"] slot_req_table = args["slot_data"]["mission_req"] - self.mission_req_table = {} - # Compatibility for 0.3.2 server data. - if "category" not in next(iter(slot_req_table)): - for i, mission_data in enumerate(slot_req_table.values()): - mission_data["category"] = wol_default_categories[i] - for mission in slot_req_table: - self.mission_req_table[mission] = MissionInfo(**slot_req_table[mission]) + self.mission_req_table = { + mission: MissionInfo(**slot_req_table[mission]) for mission in slot_req_table + } + + self.build_location_to_mission_mapping() # Look for and set SC2PATH. # check_game_install_path() returns True if and only if it finds + sets SC2PATH. if "SC2PATH" not in os.environ and check_game_install_path(): check_mod_install() - if cmd in {"PrintJSON"}: - if "receiving" in args: - if self.slot_concerns_self(args["receiving"]): - self.announcements.append(args["data"]) - return - if "item" in args: - if self.slot_concerns_self(args["item"].player): - self.announcements.append(args["data"]) + def on_print_json(self, args: dict): + if "receiving" in args and self.slot_concerns_self(args["receiving"]): + relevant = True + elif "item" in args and self.slot_concerns_self(args["item"].player): + relevant = True + else: + relevant = False + + if relevant: + self.announcements.put(self.raw_text_parser(copy.deepcopy(args["data"]))) + + super(SC2Context, self).on_print_json(args) def run_gui(self): - from kvui import GameManager, HoverBehavior, ServerToolTip, fade_in_animation + from kvui import GameManager, HoverBehavior, ServerToolTip from kivy.app import App from kivy.clock import Clock from kivy.uix.tabbedpanel import TabbedPanelItem @@ -190,6 +195,7 @@ class SC2Context(CommonContext): class MissionButton(HoverableButton): tooltip_text = StringProperty("Test") + ctx: SC2Context def __init__(self, *args, **kwargs): super(HoverableButton, self).__init__(*args, **kwargs) @@ -210,10 +216,7 @@ class SC2Context(CommonContext): self.ctx.current_tooltip = self.layout def on_leave(self): - if self.ctx.current_tooltip: - App.get_running_app().root.remove_widget(self.ctx.current_tooltip) - - self.ctx.current_tooltip = None + self.ctx.ui.clear_tooltip() @property def ctx(self) -> CommonContext: @@ -235,13 +238,20 @@ class SC2Context(CommonContext): mission_panel = None last_checked_locations = {} mission_id_to_button = {} - launching = False + launching: typing.Union[bool, int] = False # if int -> mission ID refresh_from_launching = True first_check = True + ctx: SC2Context def __init__(self, ctx): super().__init__(ctx) + def clear_tooltip(self): + if self.ctx.current_tooltip: + App.get_running_app().root.remove_widget(self.ctx.current_tooltip) + + self.ctx.current_tooltip = None + def build(self): container = super().build() @@ -256,7 +266,7 @@ class SC2Context(CommonContext): def build_mission_table(self, dt): if (not self.launching and (not self.last_checked_locations == self.ctx.checked_locations or - not self.refresh_from_launching)) or self.first_check: + not self.refresh_from_launching)) or self.first_check: self.refresh_from_launching = True self.mission_panel.clear_widgets() @@ -267,12 +277,7 @@ class SC2Context(CommonContext): self.mission_id_to_button = {} categories = {} - available_missions = [] - unfinished_locations = initialize_blank_mission_dict(self.ctx.mission_req_table) - unfinished_missions = calc_unfinished_missions(self.ctx.checked_locations, - self.ctx.mission_req_table, - self.ctx, available_missions=available_missions, - unfinished_locations=unfinished_locations) + available_missions, unfinished_missions = calc_unfinished_missions(self.ctx) # separate missions into categories for mission in self.ctx.mission_req_table: @@ -283,7 +288,8 @@ class SC2Context(CommonContext): for category in categories: category_panel = MissionCategory() - category_panel.add_widget(Label(text=category, size_hint_y=None, height=50, outline_width=1)) + category_panel.add_widget( + Label(text=category, size_hint_y=None, height=50, outline_width=1)) # Map is completed for mission in categories[category]: @@ -295,7 +301,9 @@ class SC2Context(CommonContext): text = f"[color=6495ED]{text}[/color]" tooltip = f"Uncollected locations:\n" - tooltip += "\n".join(location for location in unfinished_locations[mission]) + tooltip += "\n".join([self.ctx.location_names[loc] for loc in + self.ctx.locations_for_mission(mission) + if loc in self.ctx.missing_locations]) elif mission in available_missions: text = f"[color=FFFFFF]{text}[/color]" # Map requirements not met @@ -303,7 +311,7 @@ class SC2Context(CommonContext): text = f"[color=a9a9a9]{text}[/color]" tooltip = f"Requires: " if len(self.ctx.mission_req_table[mission].required_world) > 0: - tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission-1] for + tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for req_mission in self.ctx.mission_req_table[mission].required_world) @@ -325,13 +333,17 @@ class SC2Context(CommonContext): self.refresh_from_launching = False self.mission_panel.clear_widgets() - self.mission_panel.add_widget(Label(text="Launching Mission")) + self.mission_panel.add_widget(Label(text="Launching Mission: " + + lookup_id_to_mission[self.launching])) + if self.ctx.ui: + self.ctx.ui.clear_tooltip() def mission_callback(self, button): if not self.launching: - self.ctx.play_mission(list(self.mission_id_to_button.keys()) - [list(self.mission_id_to_button.values()).index(button)]) - self.launching = True + mission_id: int = list(self.mission_id_to_button.values()).index(button) + self.ctx.play_mission(list(self.mission_id_to_button) + [mission_id]) + self.launching = mission_id Clock.schedule_once(self.finish_launching, 10) def finish_launching(self, dt): @@ -349,7 +361,7 @@ class SC2Context(CommonContext): def play_mission(self, mission_id): if self.missions_unlocked or \ - is_mission_available(mission_id, self.checked_locations, self.mission_req_table): + is_mission_available(self, mission_id): if self.sc2_run_task: if not self.sc2_run_task.done(): sc2_logger.warning("Starcraft 2 Client is still running!") @@ -358,12 +370,29 @@ class SC2Context(CommonContext): sc2_logger.warning("Launching Mission without Archipelago authentication, " "checks will not be registered to server.") self.sc2_run_task = asyncio.create_task(starcraft_launch(self, mission_id), - name="Starcraft 2 Launch") + name="Starcraft 2 Launch") else: sc2_logger.info( f"{lookup_id_to_mission[mission_id]} is not currently unlocked. " f"Use /unfinished or /available to see what is available.") + def build_location_to_mission_mapping(self): + mission_id_to_location_ids: typing.Dict[int, typing.Set[int]] = { + mission_info.id: set() for mission_info in self.mission_req_table.values() + } + + for loc in self.server_locations: + mission_id, objective = divmod(loc - SC2WOL_LOC_ID_OFFSET, victory_modulo) + mission_id_to_location_ids[mission_id].add(objective) + self.mission_id_to_location_ids = {mission_id: sorted(objectives) for mission_id, objectives in + mission_id_to_location_ids.items()} + + def locations_for_mission(self, mission: str): + mission_id: int = self.mission_req_table[mission].id + objectives = self.mission_id_to_location_ids[self.mission_req_table[mission].id] + for objective in objectives: + yield SC2WOL_LOC_ID_OFFSET + mission_id * 100 + objective + async def main(): multiprocessing.freeze_support() @@ -459,11 +488,7 @@ def calc_difficulty(difficulty): return 'X' -async def starcraft_launch(ctx: SC2Context, mission_id): - ctx.rec_announce_pos = len(ctx.items_rec_to_announce) - ctx.sent_announce_pos = len(ctx.items_sent_to_announce) - ctx.announcements_pos = len(ctx.announcements) - +async def starcraft_launch(ctx: SC2Context, mission_id: int): sc2_logger.info(f"Launching {lookup_id_to_mission[mission_id]}. If game does not launch check log file for errors.") with DllDirectory(None): @@ -472,32 +497,29 @@ async def starcraft_launch(ctx: SC2Context, mission_id): class ArchipelagoBot(sc2.bot_ai.BotAI): - game_running = False - mission_completed = False - first_bonus = False - second_bonus = False - third_bonus = False - fourth_bonus = False - fifth_bonus = False - sixth_bonus = False - seventh_bonus = False - eight_bonus = False - ctx: SC2Context = None - mission_id = 0 + game_running: bool = False + mission_completed: bool = False + boni: typing.List[bool] + setup_done: bool + ctx: SC2Context + mission_id: int can_read_game = False - last_received_update = 0 + last_received_update: int = 0 def __init__(self, ctx: SC2Context, mission_id): + self.setup_done = False self.ctx = ctx self.mission_id = mission_id + self.boni = [False for _ in range(max_bonus)] super(ArchipelagoBot, self).__init__() async def on_step(self, iteration: int): game_state = 0 - if iteration == 0: + if not self.setup_done: + self.setup_done = True start_items = calculate_items(self.ctx.items_received) if self.ctx.difficulty_override >= 0: difficulty = calc_difficulty(self.ctx.difficulty_override) @@ -511,36 +533,10 @@ class ArchipelagoBot(sc2.bot_ai.BotAI): self.last_received_update = len(self.ctx.items_received) else: - if self.ctx.announcement_pos < len(self.ctx.announcements): - index = 0 - message = "" - while index < len(self.ctx.announcements[self.ctx.announcement_pos]): - message += self.ctx.announcements[self.ctx.announcement_pos][index]["text"] - index += 1 - - index = 0 - start_rem_pos = -1 - # Remove unneeded [Color] tags - while index < len(message): - if message[index] == '[': - start_rem_pos = index - index += 1 - elif message[index] == ']' and start_rem_pos > -1: - temp_msg = "" - - if start_rem_pos > 0: - temp_msg = message[:start_rem_pos] - if index < len(message) - 1: - temp_msg += message[index + 1:] - - message = temp_msg - index += start_rem_pos - index - start_rem_pos = -1 - else: - index += 1 - + if not self.ctx.announcements.empty(): + message = self.ctx.announcements.get(timeout=1) await self.chat_send("SendMessage " + message) - self.ctx.announcement_pos += 1 + self.ctx.announcements.task_done() # Archipelago reads the health for unit in self.all_own_units(): @@ -568,169 +564,97 @@ class ArchipelagoBot(sc2.bot_ai.BotAI): if game_state & (1 << 1) and not self.mission_completed: if self.mission_id != 29: print("Mission Completed") - await self.ctx.send_msgs([ - {"cmd": 'LocationChecks', "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id]}]) + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', + "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id]}]) self.mission_completed = True else: print("Game Complete") await self.ctx.send_msgs([{"cmd": 'StatusUpdate', "status": ClientStatus.CLIENT_GOAL}]) self.mission_completed = True - if game_state & (1 << 2) and not self.first_bonus: - print("1st Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 1]}]) - self.first_bonus = True - - if not self.second_bonus and game_state & (1 << 3): - print("2nd Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 2]}]) - self.second_bonus = True - - if not self.third_bonus and game_state & (1 << 4): - print("3rd Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 3]}]) - self.third_bonus = True - - if not self.fourth_bonus and game_state & (1 << 5): - print("4th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 4]}]) - self.fourth_bonus = True - - if not self.fifth_bonus and game_state & (1 << 6): - print("5th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 5]}]) - self.fifth_bonus = True - - if not self.sixth_bonus and game_state & (1 << 7): - print("6th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 6]}]) - self.sixth_bonus = True - - if not self.seventh_bonus and game_state & (1 << 8): - print("6th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 7]}]) - self.seventh_bonus = True - - if not self.eight_bonus and game_state & (1 << 9): - print("6th Bonus Collected") - await self.ctx.send_msgs( - [{"cmd": 'LocationChecks', - "locations": [SC2WOL_LOC_ID_OFFSET + 100 * self.mission_id + 8]}]) - self.eight_bonus = True + for x, completed in enumerate(self.boni): + if not completed and game_state & (1 << (x + 2)): + await self.ctx.send_msgs( + [{"cmd": 'LocationChecks', + "locations": [SC2WOL_LOC_ID_OFFSET + victory_modulo * self.mission_id + x + 1]}]) + self.boni[x] = True else: await self.chat_send("LostConnection - Lost connection to game.") -def calc_objectives_completed(mission, missions_info, locations_done, unfinished_locations, ctx): - objectives_complete = 0 - - if missions_info[mission].extra_locations > 0: - for i in range(missions_info[mission].extra_locations): - if (missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i) in locations_done: - objectives_complete += 1 - else: - unfinished_locations[mission].append(ctx.location_names[ - missions_info[mission].id * 100 + SC2WOL_LOC_ID_OFFSET + i]) - - return objectives_complete - - else: - return -1 - - -def request_unfinished_missions(locations_done, location_table, ui, ctx): - if location_table: +def request_unfinished_missions(ctx: SC2Context): + if ctx.mission_req_table: message = "Unfinished Missions: " - unlocks = initialize_blank_mission_dict(location_table) - unfinished_locations = initialize_blank_mission_dict(location_table) + unlocks = initialize_blank_mission_dict(ctx.mission_req_table) + unfinished_locations = initialize_blank_mission_dict(ctx.mission_req_table) - unfinished_missions = calc_unfinished_missions(locations_done, location_table, ctx, unlocks=unlocks, - unfinished_locations=unfinished_locations) + _, unfinished_missions = calc_unfinished_missions(ctx, unlocks=unlocks) - message += ", ".join(f"{mark_up_mission_name(mission, location_table, ui,unlocks)}[{location_table[mission].id}] " + + message += ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}[{ctx.mission_req_table[mission].id}] " + mark_up_objectives( - f"[{unfinished_missions[mission]}/{location_table[mission].extra_locations}]", + f"[{len(unfinished_missions[mission])}/" + f"{sum(1 for _ in ctx.locations_for_mission(mission))}]", ctx, unfinished_locations, mission) for mission in unfinished_missions) - if ui: - ui.log_panels['All'].on_message_markup(message) - ui.log_panels['Starcraft2'].on_message_markup(message) + if ctx.ui: + ctx.ui.log_panels['All'].on_message_markup(message) + ctx.ui.log_panels['Starcraft2'].on_message_markup(message) else: sc2_logger.info(message) else: sc2_logger.warning("No mission table found, you are likely not connected to a server.") -def calc_unfinished_missions(locations_done, locations, ctx, unlocks=None, unfinished_locations=None, - available_missions=[]): +def calc_unfinished_missions(ctx: SC2Context, unlocks=None): unfinished_missions = [] locations_completed = [] if not unlocks: - unlocks = initialize_blank_mission_dict(locations) + unlocks = initialize_blank_mission_dict(ctx.mission_req_table) - if not unfinished_locations: - unfinished_locations = initialize_blank_mission_dict(locations) - - if len(available_missions) > 0: - available_missions = [] - - available_missions.extend(calc_available_missions(locations_done, locations, unlocks)) + available_missions = calc_available_missions(ctx, unlocks) for name in available_missions: - if not locations[name].extra_locations == -1: - objectives_completed = calc_objectives_completed(name, locations, locations_done, unfinished_locations, ctx) - - if objectives_completed < locations[name].extra_locations: + objectives = set(ctx.locations_for_mission(name)) + if objectives: + objectives_completed = ctx.checked_locations & objectives + if len(objectives_completed) < len(objectives): unfinished_missions.append(name) locations_completed.append(objectives_completed) - else: + else: # infer that this is the final mission as it has no objectives unfinished_missions.append(name) locations_completed.append(-1) - return {unfinished_missions[i]: locations_completed[i] for i in range(len(unfinished_missions))} + return available_missions, dict(zip(unfinished_missions, locations_completed)) -def is_mission_available(mission_id_to_check, locations_done, locations): - unfinished_missions = calc_available_missions(locations_done, locations) +def is_mission_available(ctx: SC2Context, mission_id_to_check): + unfinished_missions = calc_available_missions(ctx) - return any(mission_id_to_check == locations[mission].id for mission in unfinished_missions) + return any(mission_id_to_check == ctx.mission_req_table[mission].id for mission in unfinished_missions) -def mark_up_mission_name(mission, location_table, ui, unlock_table): +def mark_up_mission_name(ctx: SC2Context, mission, unlock_table): """Checks if the mission is required for game completion and adds '*' to the name to mark that.""" - if location_table[mission].completion_critical: - if ui: + if ctx.mission_req_table[mission].completion_critical: + if ctx.ui: message = "[color=AF99EF]" + mission + "[/color]" else: message = "*" + mission + "*" else: message = mission - if ui: + if ctx.ui: unlocks = unlock_table[mission] if len(unlocks) > 0: - pre_message = f"[ref={list(location_table).index(mission)}|Unlocks: " - pre_message += ", ".join(f"{unlock}({location_table[unlock].id})" for unlock in unlocks) + pre_message = f"[ref={list(ctx.mission_req_table).index(mission)}|Unlocks: " + pre_message += ", ".join(f"{unlock}({ctx.mission_req_table[unlock].id})" for unlock in unlocks) pre_message += f"]" message = pre_message + message + "[/ref]" @@ -743,7 +667,7 @@ def mark_up_objectives(message, ctx, unfinished_locations, mission): if ctx.ui: locations = unfinished_locations[mission] - pre_message = f"[ref={list(ctx.mission_req_table).index(mission)+30}|" + pre_message = f"[ref={list(ctx.mission_req_table).index(mission) + 30}|" pre_message += "
".join(location for location in locations) pre_message += f"]" formatted_message = pre_message + message + "[/ref]" @@ -751,90 +675,91 @@ def mark_up_objectives(message, ctx, unfinished_locations, mission): return formatted_message -def request_available_missions(locations_done, location_table, ui): - if location_table: +def request_available_missions(ctx: SC2Context): + if ctx.mission_req_table: message = "Available Missions: " # Initialize mission unlock table - unlocks = initialize_blank_mission_dict(location_table) + unlocks = initialize_blank_mission_dict(ctx.mission_req_table) - missions = calc_available_missions(locations_done, location_table, unlocks) + missions = calc_available_missions(ctx, unlocks) message += \ - ", ".join(f"{mark_up_mission_name(mission, location_table, ui, unlocks)}[{location_table[mission].id}]" + ", ".join(f"{mark_up_mission_name(ctx, mission, unlocks)}" + f"[{ctx.mission_req_table[mission].id}]" for mission in missions) - if ui: - ui.log_panels['All'].on_message_markup(message) - ui.log_panels['Starcraft2'].on_message_markup(message) + if ctx.ui: + ctx.ui.log_panels['All'].on_message_markup(message) + ctx.ui.log_panels['Starcraft2'].on_message_markup(message) else: sc2_logger.info(message) else: sc2_logger.warning("No mission table found, you are likely not connected to a server.") -def calc_available_missions(locations_done, locations, unlocks=None): +def calc_available_missions(ctx: SC2Context, unlocks=None): available_missions = [] missions_complete = 0 # Get number of missions completed - for loc in locations_done: - if loc % 100 == 0: + for loc in ctx.checked_locations: + if loc % victory_modulo == 0: missions_complete += 1 - for name in locations: + for name in ctx.mission_req_table: # Go through the required missions for each mission and fill up unlock table used later for hover-over tooltips if unlocks: - for unlock in locations[name].required_world: - unlocks[list(locations)[unlock-1]].append(name) + for unlock in ctx.mission_req_table[name].required_world: + unlocks[list(ctx.mission_req_table)[unlock - 1]].append(name) - if mission_reqs_completed(name, missions_complete, locations_done, locations): + if mission_reqs_completed(ctx, name, missions_complete): available_missions.append(name) return available_missions -def mission_reqs_completed(location_to_check, missions_complete, locations_done, locations): +def mission_reqs_completed(ctx: SC2Context, mission_name: str, missions_complete): """Returns a bool signifying if the mission has all requirements complete and can be done - Keyword arguments: + Arguments: + ctx -- instance of SC2Context locations_to_check -- the mission string name to check missions_complete -- an int of how many missions have been completed - locations_done -- a list of the location ids that have been complete - locations -- a dict of MissionInfo for mission requirements for this world""" - if len(locations[location_to_check].required_world) >= 1: +""" + if len(ctx.mission_req_table[mission_name].required_world) >= 1: # A check for when the requirements are being or'd or_success = False # Loop through required missions - for req_mission in locations[location_to_check].required_world: + for req_mission in ctx.mission_req_table[mission_name].required_world: req_success = True # Check if required mission has been completed - if not (locations[list(locations)[req_mission-1]].id * 100 + SC2WOL_LOC_ID_OFFSET) in locations_done: - if not locations[location_to_check].or_requirements: + if not (ctx.mission_req_table[list(ctx.mission_req_table)[req_mission - 1]].id * + victory_modulo + SC2WOL_LOC_ID_OFFSET) in ctx.checked_locations: + if not ctx.mission_req_table[mission_name].or_requirements: return False else: req_success = False # Recursively check required mission to see if it's requirements are met, in case !collect has been done - if not mission_reqs_completed(list(locations)[req_mission-1], missions_complete, locations_done, - locations): - if not locations[location_to_check].or_requirements: + if not mission_reqs_completed(ctx, list(ctx.mission_req_table)[req_mission - 1], missions_complete): + if not ctx.mission_req_table[mission_name].or_requirements: return False else: req_success = False # If requirement check succeeded mark or as satisfied - if locations[location_to_check].or_requirements and req_success: + if ctx.mission_req_table[mission_name].or_requirements and req_success: or_success = True - if locations[location_to_check].or_requirements: + if ctx.mission_req_table[mission_name].or_requirements: # Return false if or requirements not met if not or_success: return False # Check number of missions - if missions_complete >= locations[location_to_check].number: + if missions_complete >= ctx.mission_req_table[mission_name].number: return True else: return False @@ -929,7 +854,7 @@ class DllDirectory: self.set(self._old) @staticmethod - def get() -> str: + def get() -> typing.Optional[str]: if sys.platform == "win32": n = ctypes.windll.kernel32.GetDllDirectoryW(0, None) buf = ctypes.create_unicode_buffer(n) diff --git a/Utils.py b/Utils.py index 4b2300a870..c362131d75 100644 --- a/Utils.py +++ b/Utils.py @@ -35,7 +35,7 @@ class Version(typing.NamedTuple): build: int -__version__ = "0.3.4" +__version__ = "0.3.5" version_tuple = tuplize_version(__version__) is_linux = sys.platform.startswith("linux") diff --git a/worlds/sc2wol/MissionTables.py b/worlds/sc2wol/MissionTables.py index ecd1da4922..4f1b1157ec 100644 --- a/worlds/sc2wol/MissionTables.py +++ b/worlds/sc2wol/MissionTables.py @@ -69,8 +69,8 @@ vanilla_mission_req_table = { "Zero Hour": MissionInfo(3, 4, [2], "Mar Sara", completion_critical=True), "Evacuation": MissionInfo(4, 4, [3], "Colonist"), "Outbreak": MissionInfo(5, 3, [4], "Colonist"), - "Safe Haven": MissionInfo(6, 1, [5], "Colonist", number=7), - "Haven's Fall": MissionInfo(7, 1, [5], "Colonist", number=7), + "Safe Haven": MissionInfo(6, 4, [5], "Colonist", number=7), + "Haven's Fall": MissionInfo(7, 4, [5], "Colonist", number=7), "Smash and Grab": MissionInfo(8, 5, [3], "Artifact", completion_critical=True), "The Dig": MissionInfo(9, 4, [8], "Artifact", number=8, completion_critical=True), "The Moebius Factor": MissionInfo(10, 9, [9], "Artifact", number=11, completion_critical=True), diff --git a/worlds/sc2wol/__init__.py b/worlds/sc2wol/__init__.py index cf3175bd6e..4f9b33609f 100644 --- a/worlds/sc2wol/__init__.py +++ b/worlds/sc2wol/__init__.py @@ -43,6 +43,7 @@ class SC2WoLWorld(World): locked_locations: typing.List[str] location_cache: typing.List[Location] mission_req_table = {} + required_client_version = 0, 3, 5 def __init__(self, world: MultiWorld, player: int): super(SC2WoLWorld, self).__init__(world, player) From 0444fdc379aab5d41e4052686e55b896f96f3de6 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Wed, 31 Aug 2022 20:20:30 -0400 Subject: [PATCH 45/91] SM: wasteland ap (#983) --- .../variaRandomizer/graph/vanilla/graph_access.py | 13 ++++++++++++- .../graph/vanilla/graph_locations.py | 4 ++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py index eebff84c52..b74b69026e 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_access.py @@ -294,7 +294,18 @@ accessPoints = [ sm.canGetBackFromRidleyZone(), sm.canPassWastelandDessgeegas(), sm.canPassRedKiHunters())), - 'RidleyRoomOut': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])) + 'RidleyRoomOut': Cache.ldeco(lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main'])), + 'Wasteland': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']), + sm.canGetBackFromRidleyZone(), + sm.canPassWastelandDessgeegas())) + }, internal=True), + AccessPoint('Wasteland', 'LowerNorfair', { + # no transition to firefleas to exlude pb of shame location when starting at firefleas top + 'Ridley Zone': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']), + sm.traverse('WastelandLeft'), + sm.canGetBackFromRidleyZone(), + sm.canPassWastelandDessgeegas(), + sm.canPassNinjaPirates())) }, internal=True), AccessPoint('Three Muskateers Room Left', 'LowerNorfair', { 'Firefleas': Cache.ldeco(lambda sm: sm.wand(sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']), diff --git a/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py b/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py index b8a1d3f44e..671368e831 100644 --- a/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py +++ b/worlds/sm/variaRandomizer/graph/vanilla/graph_locations.py @@ -797,10 +797,10 @@ locationsDict["Power Bomb (lower Norfair above fire flea room)"].Available = ( lambda sm: SMBool(True) ) locationsDict["Power Bomb (Power Bombs of shame)"].AccessFrom = { - 'Ridley Zone': lambda sm: sm.canUsePowerBombs() + 'Wasteland': lambda sm: sm.canUsePowerBombs() } locationsDict["Power Bomb (Power Bombs of shame)"].Available = ( - lambda sm: sm.canHellRun(**Settings.hellRunsTable['LowerNorfair']['Main']) + lambda sm: SMBool(True) ) locationsDict["Missile (lower Norfair near Wave Beam)"].AccessFrom = { 'Firefleas': lambda sm: SMBool(True) From b115bdafe78e34261b8fd8c83fdde54db469e50d Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Thu, 1 Sep 2022 09:30:28 +0200 Subject: [PATCH 46/91] CI/Doc: Use pytest subtests (#986) * CI/Doc: use pytest-subtests * CI: clean up pip installs a bit * make lint and unittests install the same stuff * make sure to install wheel, which is a recommended (not required) dependency for everything pip --- .github/workflows/lint.yml | 4 ++-- .github/workflows/unittests.yml | 4 ++-- docs/running from source.md | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index d7cc3c7439..28adb50026 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,8 +18,8 @@ jobs: python-version: 3.9 - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install flake8 pytest + python -m pip install --upgrade pip wheel + pip install flake8 pytest pytest-subtests if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index 1c8ab10c70..4d0ceaec87 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -32,8 +32,8 @@ jobs: python-version: ${{ matrix.python.version }} - name: Install dependencies run: | - python -m pip install --upgrade pip - pip install flake8 pytest + python -m pip install --upgrade pip wheel + pip install flake8 pytest pytest-subtests python ModuleUpdate.py --yes --force --append "WebHostLib/requirements.txt" - name: Unittests run: | diff --git a/docs/running from source.md b/docs/running from source.md index 4360b28c16..39addd0a28 100644 --- a/docs/running from source.md +++ b/docs/running from source.md @@ -56,3 +56,8 @@ SNI is required to use SNIClient. If not integrated into the project, it has to You can get the latest SNI release at [SNI Github releases](https://github.com/alttpo/sni/releases). It should be dropped as "SNI" into the root folder of the project. Alternatively, you can point the sni setting in host.yaml at your SNI folder. + + +## Running tests + +Run `pip install pytest pytest-subtests`, then use your IDE to run tests or run `pytest` from the source folder. From 03f66a922d42f492ff751fede981d94e14dd4ee4 Mon Sep 17 00:00:00 2001 From: Yussur Mustafa Oraji Date: Thu, 1 Sep 2022 21:21:53 +0200 Subject: [PATCH 47/91] sm64ex: Fix a Location (#979) --- worlds/sm64ex/Locations.py | 4 ++-- worlds/sm64ex/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/worlds/sm64ex/Locations.py b/worlds/sm64ex/Locations.py index 1995abf425..9a3db29c26 100644 --- a/worlds/sm64ex/Locations.py +++ b/worlds/sm64ex/Locations.py @@ -245,7 +245,7 @@ locBitFS_table = { locWMotR_table = { "Wing Mario Over the Rainbow Red Coins": 3626154, - "Wing Mario Over the Rainbow 1Up Block": 3626242 + "Wing Mario Over the Rainbow 1Up Block": 3626243 } locBitS_table = { @@ -268,4 +268,4 @@ location_table = {**locBoB_table,**locWhomp_table,**locJRB_table,**locCCM_table, **locWDW_table,**locTTM_table,**locTHI_table,**locTTC_table,**locRR_table, \ **loc100Coin_table,**locPSS_table,**locSA_table,**locBitDW_table,**locTotWC_table, \ **locCotMC_table, **locVCutM_table, **locBitFS_table, **locWMotR_table, **locBitS_table, \ - **locSS_table} \ No newline at end of file + **locSS_table} diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index 447a09d431..cf8a8e875d 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -34,7 +34,7 @@ class SM64World(World): item_name_to_id = item_table location_name_to_id = location_table - data_version = 7 + data_version = 8 required_client_version = (0, 3, 0) area_connections: typing.Dict[int, int] From e413619c26c30322110f963e36fef044b64073b2 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Thu, 1 Sep 2022 21:25:06 +0200 Subject: [PATCH 48/91] Tests: verify that a world doesn't use the same ID multiple times (#985) --- test/general/TestIDs.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/general/TestIDs.py b/test/general/TestIDs.py index f91775c8de..db1c9461b9 100644 --- a/test/general/TestIDs.py +++ b/test/general/TestIDs.py @@ -52,3 +52,13 @@ class TestIDs(unittest.TestCase): else: for location_id in world_type.location_id_to_name: self.assertGreater(location_id, 0) + + def testDuplicateItemIDs(self): + for gamename, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=gamename): + self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id)) + + def testDuplicateLocationIDs(self): + for gamename, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=gamename): + self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id)) From 8d2333006a43407e2fa7fb5ad88fc00b08ef124a Mon Sep 17 00:00:00 2001 From: skrawpie <21212370+skrawpie@users.noreply.github.com> Date: Thu, 1 Sep 2022 15:26:04 -0400 Subject: [PATCH 49/91] Minecraft: Added shuffled recipe list to en_Minecraft.md (#980) Co-authored-by: KonoTyran --- worlds/minecraft/docs/en_Minecraft.md | 87 ++++++++++++++++++++++++++- 1 file changed, 85 insertions(+), 2 deletions(-) diff --git a/worlds/minecraft/docs/en_Minecraft.md b/worlds/minecraft/docs/en_Minecraft.md index b67107a8fc..2d4f063b79 100644 --- a/worlds/minecraft/docs/en_Minecraft.md +++ b/worlds/minecraft/docs/en_Minecraft.md @@ -7,9 +7,9 @@ config file. ## What does randomization do to this game? -Recipes are removed from the crafting book and shuffled into the item pool. It can also optionally change which +Some recipes are locked from being able to be crafted and shuffled into the item pool. It can also optionally change which structures appear in each dimension. Crafting recipes are re-learned when they are received from other players as item -checks, and occasionally when completing your own achievements. +checks, and occasionally when completing your own achievements. See below for which recipes are shuffled. ## What is considered a location check in minecraft? @@ -25,3 +25,86 @@ inventory directly. Victory is achieved when the player kills the Ender Dragon, enters the portal in The End, and completes the credits sequence either by skipping it or watching hit play out. + +## Which recipes are locked? + +* Archery + * Bow + * Arrow + * Crossbow +* Brewing + * Blaze Powder + * Brewing Stand +* Enchanting + * Enchanting Table + * Bookshelf +* Bucket +* Flint & Steel +* All Beds +* Bottles +* Shield +* Fishing Rod + * Fishing Rod + * Carrot on a Stick + * Warped Fungus on a Stick +* Campfire + * Campfire + * Soul Campfire +* Spyglass +* Lead +* Progressive Weapons + * Tier I + * Stone Sword + * Stone Axe + * Tier II + * Iron Sword + * Iron Axe + * Tier III + * Diamond Sword + * Diamond Axe +* Progessive Tools + * Tier I + * Stone Shovel + * Stone Hoe + * Tier II + * Iron Shovel + * Iron Hoe + * Tier III + * Diamond Shovel + * Diamond Hoe + * Netherite Ingot +* Progressive Armor + * Tier I + * Iron Helmet + * Iron Chestplate + * Iron Leggings + * Iron Boots + * Tier II + * Diamond Helmet + * Diamond Chestplate + * Diamond Leggings + * Diamond Boots +* Progressive Resource Crafting + * Tier I + * Iron Ingot from Nuggets + * Iron Nugget + * Gold Ingot from Nuggets + * Gold Nugget + * Furnace + * Blast Furnace + * Tier II + * Redstone + * Redstone Block + * Glowstone + * Iron Ingot from Iron Block + * Iron Block + * Gold Ingot from Gold Block + * Gold Block + * Diamond + * Diamond Block + * Netherite Block + * Netherite Ingot from Netherite Block + * Anvil + * Emerald + * Emerald Block + * Copper Block From b14d694e1e22cdd901d317837bc2bf533fa2e444 Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Thu, 1 Sep 2022 13:45:14 -0500 Subject: [PATCH 50/91] templates: fix bug report label --- .github/ISSUE_TEMPLATE/bug_report.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index dff9a56651..d4c8702da0 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -2,7 +2,7 @@ name: Bug Report description: File a bug report. title: "Bug: " labels: - - bug + - bug / fix body: - type: markdown attributes: @@ -32,4 +32,4 @@ body: - Local generation - While playing validations: - required: true \ No newline at end of file + required: true From f7d107fc0c89a4d34f6ae6a4c4b563a14b50c650 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 2 Sep 2022 03:35:41 +0200 Subject: [PATCH 51/91] Subnautica: add some more missed aggressive creatures --- worlds/subnautica/Creatures.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/subnautica/Creatures.py b/worlds/subnautica/Creatures.py index 687c3732a9..cb34f261f5 100644 --- a/worlds/subnautica/Creatures.py +++ b/worlds/subnautica/Creatures.py @@ -55,7 +55,6 @@ all_creatures: Dict[str, int] = { "Sea Emperor Juvenile": 1700, } -# be nice and make these require Stasis Rifle aggressive: Set[str] = { "Cave Crawler", # is very easy without Stasis Rifle, but included for consistency "Crashfish", @@ -75,6 +74,8 @@ aggressive: Set[str] = { "Lava Lizard", "Sea Dragon Leviathan", "River Prowler", + "Ghost Leviathan Juvenile", + "Ghost Leviathan" } containment: Set[str] = { # creatures that have to be raised from eggs From b45d8bf22164d266c1d0cf2079df45af26cff96e Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 2 Sep 2022 17:37:37 -0400 Subject: [PATCH 52/91] Patch: Save patch file extension in archipelago.json (#968) --- Patch.py | 3 ++- WebHostLib/downloads.py | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Patch.py b/Patch.py index f90e376656..aaa4fc2404 100644 --- a/Patch.py +++ b/Patch.py @@ -17,7 +17,7 @@ ModuleUpdate.update() import Utils -current_patch_version = 4 +current_patch_version = 5 class AutoPatchRegister(type): @@ -128,6 +128,7 @@ class APDeltaPatch(APContainer, metaclass=AutoPatchRegister): manifest = super(APDeltaPatch, self).get_manifest() manifest["base_checksum"] = self.hash manifest["result_file_ending"] = self.result_file_ending + manifest["patch_file_ending"] = self.patch_file_ending return manifest @classmethod diff --git a/WebHostLib/downloads.py b/WebHostLib/downloads.py index 528cbe5ec0..c3a373c2e9 100644 --- a/WebHostLib/downloads.py +++ b/WebHostLib/downloads.py @@ -32,9 +32,12 @@ def download_patch(room_id, patch_id): new_zip.writestr("archipelago.json", json.dumps(manifest)) else: new_zip.writestr(file.filename, zf.read(file), file.compress_type, 9) - + if "patch_file_ending" in manifest: + patch_file_ending = manifest["patch_file_ending"] + else: + patch_file_ending = AutoPatchRegister.patch_types[patch.game].patch_file_ending fname = f"P{patch.player_id}_{patch.player_name}_{app.jinja_env.filters['suuid'](room_id)}" \ - f"{AutoPatchRegister.patch_types[patch.game].patch_file_ending}" + f"{patch_file_ending}" new_file.seek(0) return send_file(new_file, as_attachment=True, download_name=fname) else: From 4b6d46fd743d9246f400d743786505a90cd3d7af Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 2 Sep 2022 11:04:23 +0200 Subject: [PATCH 53/91] Core: update modules --- WebHostLib/requirements.txt | 6 +++--- requirements.txt | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/WebHostLib/requirements.txt b/WebHostLib/requirements.txt index 52d0316b2a..a4dd710e83 100644 --- a/WebHostLib/requirements.txt +++ b/WebHostLib/requirements.txt @@ -1,7 +1,7 @@ -flask>=2.1.3 +flask>=2.2.2 pony>=0.7.16 -waitress>=2.1.1 +waitress>=2.1.2 Flask-Caching>=2.0.1 Flask-Compress>=1.12 -Flask-Limiter>=2.5.0 +Flask-Limiter>=2.6.2 bokeh>=2.4.3 diff --git a/requirements.txt b/requirements.txt index 661209e072..6c9e3b9d2d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,6 @@ websockets>=10.3 PyYAML>=6.0 jellyfish>=0.9.0 jinja2>=3.1.2 -schema>=0.7.4 +schema>=0.7.5 kivy>=2.1.0 bsdiff4>=1.2.2 \ No newline at end of file From b7cfcc9272efdc39f6cab4e8c4e5ad9466463c2b Mon Sep 17 00:00:00 2001 From: Sunny Bat Date: Sat, 3 Sep 2022 12:25:04 -0700 Subject: [PATCH 54/91] New features and fixes for Raft (#984) * Add DeathLink, small logic changes * Fix generation, rules, use bool for slotData * Add more island options * Update Shovel-related logic * Update docs --- worlds/raft/Options.py | 40 ++++++++++++----------- worlds/raft/Rules.py | 61 ++++++++++++++++++++-------------- worlds/raft/__init__.py | 55 ++++++++++++++++++++++++------- worlds/raft/docs/en_Raft.md | 2 +- worlds/raft/docs/setup_en.md | 63 ++++++++++++++++++------------------ worlds/raft/regions.json | 16 ++++----- 6 files changed, 143 insertions(+), 94 deletions(-) diff --git a/worlds/raft/Options.py b/worlds/raft/Options.py index 482f1d343a..5f09db07c0 100644 --- a/worlds/raft/Options.py +++ b/worlds/raft/Options.py @@ -1,8 +1,4 @@ -from Options import Range, Toggle, DefaultOnToggle, Choice - -class UseResourcePacks(DefaultOnToggle): - """Uses Resource Packs to fill out the item pool from Raft. Resource Packs have basic earlygame items such as planks, plastic, or food.""" - display_name = "Use resource packs" +from Options import Range, Toggle, DefaultOnToggle, Choice, DeathLink class MinimumResourcePackAmount(Range): """The minimum amount of resources available in a resource pack""" @@ -19,23 +15,30 @@ class MaximumResourcePackAmount(Range): default = 5 class DuplicateItems(Choice): - """Adds duplicates of items to the item pool. These will be selected alongside - Resource Packs (if configured). Note that there are not many progression items, - and selecting Progression may produce many of the same duplicate item.""" + """Adds duplicates of items to the item pool (if configured in Filler items). These will be selected alongside Resource Packs (if configured). Note that there are not many progression items, and selecting Progression may produce many of the same duplicate item.""" display_name = "Duplicate items" - option_disabled = 0 - option_progression = 1 - option_non_progression = 2 - option_any = 3 + option_progression = 0 + option_non_progression = 1 + option_any = 2 + default = 2 + +class FillerItemTypes(Choice): + """Determines whether to use Resource Packs, Duplicate Items (as configured), or both.""" + display_name = "Filler items" + option_resource_packs = 0 + option_duplicates = 1 + option_both = 2 class IslandFrequencyLocations(Choice): """Sets where frequencies for story islands are located.""" display_name = "Frequency locations" option_vanilla = 0 option_random_on_island = 1 - option_progressive = 2 - option_anywhere = 3 - default = 1 + option_random_island_order = 2 + option_random_on_island_random_order = 3 + option_progressive = 4 + option_anywhere = 5 + default = 2 class IslandGenerationDistance(Choice): """Sets how far away islands spawn from you when you input their coordinates into the Receiver.""" @@ -56,7 +59,7 @@ class ProgressiveItems(DefaultOnToggle): display_name = "Progressive items" class BigIslandEarlyCrafting(Toggle): - """Allows recipes that require items from big islands (eg leather) to lock earlygame items like the Receiver, Bolt, or Smelter.""" + """Allows recipes that require items from big islands (eg leather) to lock earlygame items like the Receiver, Bolt, or Smelter. Big islands are available from the start of the game, however it can take a long time to find them.""" display_name = "Early recipes behind big islands" class PaddleboardMode(Toggle): @@ -64,14 +67,15 @@ class PaddleboardMode(Toggle): display_name = "Paddleboard Mode" raft_options = { - "use_resource_packs": UseResourcePacks, "minimum_resource_pack_amount": MinimumResourcePackAmount, "maximum_resource_pack_amount": MaximumResourcePackAmount, "duplicate_items": DuplicateItems, + "filler_item_types": FillerItemTypes, "island_frequency_locations": IslandFrequencyLocations, "island_generation_distance": IslandGenerationDistance, "expensive_research": ExpensiveResearch, "progressive_items": ProgressiveItems, "big_island_early_crafting": BigIslandEarlyCrafting, - "paddleboard_mode": PaddleboardMode + "paddleboard_mode": PaddleboardMode, + "death_link": DeathLink } diff --git a/worlds/raft/Rules.py b/worlds/raft/Rules.py index 3ca565d9d6..0432c2806f 100644 --- a/worlds/raft/Rules.py +++ b/worlds/raft/Rules.py @@ -12,9 +12,6 @@ class RaftLogic(LogicMixin): def raft_can_smelt_items(self, player): return self.has("Smelter", player) - - def raft_can_find_titanium(self, player): - return self.has("Metal detector", player) def raft_can_craft_bolt(self, player): return self.raft_can_smelt_items(player) and self.has("Bolt", player) @@ -27,12 +24,19 @@ class RaftLogic(LogicMixin): def raft_can_craft_circuitBoard(self, player): return self.raft_can_smelt_items(player) and self.has("Circuit board", player) + + def raft_can_craft_shovel(self, player): + return self.raft_can_smelt_items(player) and self.has("Shovel", player) and self.raft_can_craft_bolt(player) def raft_can_craft_reciever(self, player): return self.raft_can_craft_circuitBoard(player) and self.raft_can_craft_hinge(player) and self.has("Receiver", player) def raft_can_craft_antenna(self, player): return self.raft_can_craft_circuitBoard(player) and self.raft_can_craft_bolt(player) and self.has("Antenna", player) + + def raft_can_find_titanium(self, player): + return (self.has("Metal detector", player) and self.raft_can_craft_battery(player) + and self.raft_can_craft_shovel(player)) def raft_can_craft_plasticBottle(self, player): return self.raft_can_smelt_items(player) and self.has("Empty bottle", player) @@ -60,7 +64,7 @@ class RaftLogic(LogicMixin): return self.raft_can_craft_hinge(player) and self.raft_can_craft_bolt(player) and self.has("Zipline tool", player) def raft_can_get_dirt(self, player): - return self.raft_can_smelt_items(player) and self.raft_can_craft_bolt(player) and self.has("Shovel", player) + return self.raft_can_craft_shovel(player) and self.raft_big_islands_available(player) def raft_can_craft_grassPlot(self, player): return self.raft_can_get_dirt(player) and self.has("Grass plot", player) @@ -88,60 +92,69 @@ class RaftLogic(LogicMixin): return self.raft_can_access_radio_tower(player) def raft_can_access_vasagatan(self, player): - return self.raft_can_complete_radio_tower(player) and self.raft_can_navigate(player) and self.has("Vasagatan Frequency", player) + return self.raft_can_navigate(player) and self.has("Vasagatan Frequency", player) def raft_can_complete_vasagatan(self, player): return self.raft_can_access_vasagatan(player) def raft_can_access_balboa_island(self, player): - return (self.raft_can_complete_vasagatan(player) - and self.raft_can_drive(player) - and self.has("Balboa Island Frequency", player)) + return self.raft_can_drive(player) and self.has("Balboa Island Frequency", player) def raft_can_complete_balboa_island(self, player): return self.raft_can_access_balboa_island(player) and self.raft_can_craft_machete(player) def raft_can_access_caravan_island(self, player): - return self.raft_can_complete_balboa_island(player) and self.raft_can_drive(player) and self.has("Caravan Island Frequency", player) + return self.raft_can_drive(player) and self.has("Caravan Island Frequency", player) def raft_can_complete_caravan_island(self, player): return self.raft_can_access_caravan_island(player) and self.raft_can_craft_ziplineTool(player) def raft_can_access_tangaroa(self, player): - return self.raft_can_complete_caravan_island(player) and self.raft_can_drive(player) and self.has("Tangaroa Frequency", player) + return self.raft_can_drive(player) and self.has("Tangaroa Frequency", player) def raft_can_complete_tangaroa(self, player): - return self.raft_can_access_tangaroa(player) + return self.raft_can_access_tangaroa(player) and self.raft_can_craft_ziplineTool(player) def raft_can_access_varuna_point(self, player): - return self.raft_can_complete_tangaroa(player) and self.raft_can_drive(player) and self.has("Varuna Point Frequency", player) + return self.raft_can_drive(player) and self.has("Varuna Point Frequency", player) def raft_can_complete_varuna_point(self, player): - return self.raft_can_access_varuna_point(player) + return self.raft_can_access_varuna_point(player) and self.raft_can_craft_ziplineTool(player) def raft_can_access_temperance(self, player): - return self.raft_can_complete_varuna_point(player) and self.raft_can_drive(player) and self.has("Temperance Frequency", player) + return self.raft_can_drive(player) and self.has("Temperance Frequency", player) def raft_can_complete_temperance(self, player): - return self.raft_can_access_temperance(player) + return self.raft_can_access_temperance(player) # No zipline required on Temperance def raft_can_access_utopia(self, player): - return self.raft_can_complete_temperance(player) and self.raft_can_drive(player) and self.has("Utopia Frequency", player) + return (self.raft_can_drive(player) + # Access checks are to prevent frequencies for other + # islands from appearing in Utopia + and self.raft_can_access_radio_tower(player) + and self.raft_can_access_vasagatan(player) + and self.raft_can_access_balboa_island(player) + and self.raft_can_access_caravan_island(player) + and self.raft_can_access_tangaroa(player) + and self.raft_can_access_varuna_point(player) + and self.raft_can_access_temperance(player) + and self.has("Utopia Frequency", player) + and self.raft_can_craft_shovel(player)) # Shovels are available but we don't want to softlock players def raft_can_complete_utopia(self, player): - return self.raft_can_access_utopia(player) + return self.raft_can_access_utopia(player) and self.raft_can_craft_ziplineTool(player) def set_rules(world, player): regionChecks = { "Raft": lambda state: True, "ResearchTable": lambda state: True, "RadioTower": lambda state: state.raft_can_access_radio_tower(player), # All can_access functions have state as implicit parameter for function - "Vasagatan": lambda state: state.raft_can_complete_radio_tower(player) and state.raft_can_access_vasagatan(player), - "BalboaIsland": lambda state: state.raft_can_complete_vasagatan(player) and state.raft_can_access_balboa_island(player), - "CaravanIsland": lambda state: state.raft_can_complete_balboa_island(player) and state.raft_can_access_caravan_island(player), - "Tangaroa": lambda state: state.raft_can_complete_caravan_island(player) and state.raft_can_access_tangaroa(player), - "Varuna Point": lambda state: state.raft_can_complete_tangaroa(player) and state.raft_can_access_varuna_point(player), - "Temperance": lambda state: state.raft_can_complete_varuna_point(player) and state.raft_can_access_temperance(player), + "Vasagatan": lambda state: state.raft_can_access_vasagatan(player), + "BalboaIsland": lambda state: state.raft_can_access_balboa_island(player), + "CaravanIsland": lambda state: state.raft_can_access_caravan_island(player), + "Tangaroa": lambda state: state.raft_can_access_tangaroa(player), + "Varuna Point": lambda state: state.raft_can_access_varuna_point(player), + "Temperance": lambda state: state.raft_can_access_temperance(player), "Utopia": lambda state: state.raft_can_complete_temperance(player) and state.raft_can_access_utopia(player) } itemChecks = { @@ -183,7 +196,7 @@ def set_rules(world, player): if region != "Menu": for exitRegion in world.get_region(region, player).exits: set_rule(world.get_entrance(exitRegion.name, player), regionChecks[region]) - + # Location access rules for location in location_table: locFromWorld = world.get_location(location["name"], player) diff --git a/worlds/raft/__init__.py b/worlds/raft/__init__.py index da4b58f24f..860ba9aab5 100644 --- a/worlds/raft/__init__.py +++ b/worlds/raft/__init__.py @@ -56,21 +56,21 @@ class RaftWorld(World): extraItemNamePool = [] extras = len(location_table) - len(item_table) - 1 # Victory takes up 1 unaccounted-for slot if extras > 0: - if (self.world.use_resource_packs[self.player].value): + if (self.world.filler_item_types[self.player].value != 1): # Use resource packs for packItem in resourcePackItems: for i in range(minimumResourcePackAmount, maximumResourcePackAmount + 1): extraItemNamePool.append(createResourcePackName(i, packItem)) - if self.world.duplicate_items[self.player].value != 0: + if self.world.filler_item_types[self.player].value != 0: # Use duplicate items dupeItemPool = item_table.copy() # Remove frequencies if necessary - if self.world.island_frequency_locations[self.player].value != 3: # Not completely random locations + if self.world.island_frequency_locations[self.player].value != 5: # Not completely random locations dupeItemPool = (itm for itm in dupeItemPool if "Frequency" not in itm["name"]) # Remove progression or non-progression items if necessary - if (self.world.duplicate_items[self.player].value == 1): # Progression only + if (self.world.duplicate_items[self.player].value == 0): # Progression only dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == True) - elif (self.world.duplicate_items[self.player].value == 2): # Non-progression only + elif (self.world.duplicate_items[self.player].value == 1): # Non-progression only dupeItemPool = (itm for itm in dupeItemPool if itm["progression"] == False) dupeItemPool = list(dupeItemPool) @@ -91,19 +91,15 @@ class RaftWorld(World): def create_regions(self): create_regions(self.world, self.player) - - def fill_slot_data(self): - slot_data = {} - return slot_data def get_pre_fill_items(self): - if self.world.island_frequency_locations[self.player] in [0, 1]: + if self.world.island_frequency_locations[self.player] in [0, 1, 2, 3]: return [loc.item for loc in self.world.get_filled_locations()] return [] def create_item_replaceAsNecessary(self, name: str) -> Item: isFrequency = "Frequency" in name - shouldUseProgressive = ((isFrequency and self.world.island_frequency_locations[self.player].value == 2) + shouldUseProgressive = ((isFrequency and self.world.island_frequency_locations[self.player].value == 4) or (not isFrequency and self.world.progressive_items[self.player].value)) if shouldUseProgressive and name in progressive_table: name = progressive_table[name] @@ -148,6 +144,40 @@ class RaftWorld(World): self.setLocationItemFromRegion("Tangaroa", "Varuna Point Frequency") self.setLocationItemFromRegion("Varuna Point", "Temperance Frequency") self.setLocationItemFromRegion("Temperance", "Utopia Frequency") + elif self.world.island_frequency_locations[self.player] in [2, 3]: + locationToFrequencyItemMap = { + "Vasagatan": "Vasagatan Frequency", + "BalboaIsland": "Balboa Island Frequency", + "CaravanIsland": "Caravan Island Frequency", + "Tangaroa": "Tangaroa Frequency", + "Varuna Point": "Varuna Point Frequency", + "Temperance": "Temperance Frequency", + "Utopia": "Utopia Frequency" + } + locationToVanillaFrequencyLocationMap = { + "RadioTower": "Radio Tower Frequency to Vasagatan", + "Vasagatan": "Vasagatan Frequency to Balboa", + "BalboaIsland": "Relay Station quest", + "CaravanIsland": "Caravan Island Frequency to Tangaroa", + "Tangaroa": "Tangaroa Frequency to Varuna Point", + "Varuna Point": "Varuna Point Frequency to Temperance", + "Temperance": "Temperance Frequency to Utopia" + } + # Utopia is never chosen until the end, otherwise these are chosen randomly + availableLocationList = ["Vasagatan", "BalboaIsland", "CaravanIsland", "Tangaroa", "Varuna Point", "Temperance", "Utopia"] + previousLocation = "RadioTower" + while (len(availableLocationList) > 0): + if (len(availableLocationList) > 1): + currentLocation = availableLocationList[random.randint(0, len(availableLocationList) - 2)] + else: + currentLocation = availableLocationList[0] # Utopia (only one left in list) + availableLocationList.remove(currentLocation) + if self.world.island_frequency_locations[self.player] == 2: + self.setLocationItem(locationToVanillaFrequencyLocationMap[previousLocation], locationToFrequencyItemMap[currentLocation]) + elif self.world.island_frequency_locations[self.player] == 3: + self.setLocationItemFromRegion(previousLocation, locationToFrequencyItemMap[currentLocation]) + previousLocation = currentLocation + # Victory item self.world.get_location("Utopia Complete", self.player).place_locked_item( RaftItem("Victory", ItemClassification.progression, None, player=self.player)) @@ -166,7 +196,8 @@ class RaftWorld(World): def fill_slot_data(self): return { "IslandGenerationDistance": self.world.island_generation_distance[self.player].value, - "ExpensiveResearch": self.world.expensive_research[self.player].value + "ExpensiveResearch": bool(self.world.expensive_research[self.player].value), + "DeathLink": bool(self.world.death_link[self.player].value) } def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): diff --git a/worlds/raft/docs/en_Raft.md b/worlds/raft/docs/en_Raft.md index adcf5ea6cb..385377d456 100644 --- a/worlds/raft/docs/en_Raft.md +++ b/worlds/raft/docs/en_Raft.md @@ -22,7 +22,7 @@ Decoration Packages are unchanged. Researches and pickups remain visually unchanged, regardless of what the unlock is. ## When the player receives an item, what happens? -A Raft notification will appear with the item information. The unlock will also appear in the chat. Unlocks that would normally give you the item (eg Machete) will NOT give it to you, but must instead be crafted. +A Raft notification will appear with the item information. The unlock will also appear in the chat. Unlocks that would normally give you the item (eg Zipline) will NOT give it to you, but must instead be crafted. ## Are there any limitations compared to vanilla Raft? - Mods that add new researchable technologies, modify story islands, or give items like blueprints are likely incompatible with Raftipelago. diff --git a/worlds/raft/docs/setup_en.md b/worlds/raft/docs/setup_en.md index ad69eacdf8..4d84ef0f65 100644 --- a/worlds/raft/docs/setup_en.md +++ b/worlds/raft/docs/setup_en.md @@ -4,23 +4,50 @@ - [Raft](https://store.steampowered.com/app/648800/Raft/) - [Raft Mod Loader](https://www.raftmodding.com/loader) ("*RML*") +- [ModUtils mod](https://www.raftmodding.com/mods/modutils) - [Raftipelago mod](https://www.raftmodding.com/mods/raftipelago) ## Installation Procedures -1. Install Raft. The currently-supported Raft version is Version 1.0: The Final Chapter. If you plan on playing Raft mainly with Archipelago, it's recommended to disable Raft auto-updating through Steam, as there is no beta channel to get old builds. +1. Install Raft. The currently-supported Raft version is Version 1.0: The Final Chapter. Any minor version (such as 1.08) should be compatible. 2. Install RML. -3. Install the Raftipelago mod from the Raft Modding website. You should open the auto-installation link on the webpage through RML. Alternatively, you can download the .rmod file and place it in the Mods folder manually. +3. Install the Raftipelago and ModUtils mods from the Raft Modding website. You should open the auto-installation link on the webpage through RML. Alternatively, you can download the .rmod file and place it in the Mods folder manually. -4. Open RML and click Play. If you've already installed it, the shortcut in the Start Menu is called "RMLLauncher.exe". Raft should start. +4. Open RML and click Play. If you've already installed it, the executable that was used to install RML ("RMLLauncher.exe" unless renamed) should be used to run RML. Raft should start after clicking Play. 5. Open the RML menu. This should open automatically when Raft first loads. If it does not, and you see RML information in the top center of the Raft main menu, press F9 to open it. 6. Navigate to the "Mod manager" tab in the left-hand menu. -7. Click on the plug icon for Raftipelago to load the mod. +7. Click on the plug icon for ModUtils to load the mod. You can also click on the (i) next to the plug icon, then check the "Load this mod at startup" button. This will make the mod always load at startup. + +8. Click on the plug icon for Raftipelago to load the mod. While it's possible to also make this mod load at startup, it's recommended *not* to do so; if this mod loads before ModUtils, the mod will fail to load properly. + +## Joining a MultiWorld Game + +1. Ensure you're on the Main Menu with Raftipelago loaded. + +2. Open the Debug Console by pressing F10. + +3. Type */connect {serverAddress} {username} {password}* into the console and hit Enter. + - Example: */connect archipelago.gg:12345 SunnyBat* + - If there is no password, the password argument may be omitted (as is the case in the above example). + - serverAddress must not contain spaces. + - If your username or password contains spaces, surround that value with quotation marks ("). Adding quotation marks even when not necessary (eg "SunnyBat") is fine. + - If your username or password starts with a quotation mark, surround the value with an additional set of quotation marks (eg the value *"myP@s$w0rD* would be entered as *""myP@s$w0rD"*). + +4. Start a new game or load an existing one. It's recommended to avoid using an existing game that was not created with your current run of Raftipelago (either vanilla or a different Raftipelago run). It will work, but if anything is unlocked, it will be automatically registered with Archipelago once the world is loaded. This is irreversible. + +5. You can disconnect from an Archipelago server by typing */disconnect confirmDisconnect* into the console and hitting Enter. + +## Multiplayer Raft + +You're able to have multiple Raft players on a single Raftipelago world. This will work, with a few notes: +- Only the player that creates/loads the world can connect to Archipelago (this is the "host" of the Raft world). Other players do not need to connect; everything will be routed through the the host. +- Players other than the host will be labeled as a "Raft Player (Steam name)" when using ingame chat, which will be routed through Archipelago chat. +- Ingame chat will only work when the host is connected to the Archipelago server. ## Installation Troubleshooting @@ -45,38 +72,12 @@ If this happens, then RML is configured to only start a new instance of Raft, th You can either: - Close the existing instance of Raft then click Play - Check the box next to the "Disable Automatic Game Start" setting in the Settings menu then click Play. - -## Joining a MultiWorld Game - -1. Ensure you're on the Main Menu with Raftipelago loaded. - -2. Open the Debug Console by pressing F10. - -3. Type */connect {serverAddress} {username} {password}* into the console and hit Enter. - - Example: */connect archipelago.gg:12345 SunnyBat* - - serverAddress must not contain spaces. - - If your username or password contains spaces, surround that value with quotation marks ("). Adding quotation marks even when not necessary (eg "SunnyBat") is fine. - - If your username or password starts with a quotation mark, surround the value with an additional set of quotation marks (eg the value *"myP@s$w0rD* would be entered as *""myP@s$w0rD"*). - -4. Start a new game or load an existing one. - - Raftipelago save games are marked as *incompatible* with vanilla Raft. This means when Raftipelago is not loaded, saves made with Raftipelago will show as corrupt/unselectable. - - Avoid using an existing game that was not created with your current run of Raftipelago (either vanilla or a different Raftipelago run). It will work, but if anything is unlocked, it will be automatically registered with Archipelago once the world is loaded. This is irreversible. - -5. You can disconnect from an Archipelago server by typing */disconnect confirmDisconnect* into the console and hitting Enter. - -## Multiplayer Raft - -You're able to have multiple Raft players on a single Raftipelago world. This will work, with a few notes: -- Only the player that creates/loads the world can connect to Archipelago (this is the "host" of the Raft world). Other players do not need to connect; everything will be routed through the the host. -- Resource Packs are only received by the host and any other players connected to the Raft world when the resource pack is received. -- Players other than the host will be labeled as a "Raft Player (Steam name)" when using ingame chat, which will be routed through Archipelago chat. -- Ingame chat will only work when the host is connected to the Archipelago server. ## Game Troubleshooting ### The "Load game" button is disabled for my world / my world is corrupt -Be sure that you click the "Load game" button **after** you load Raftipelago. You can click the Load Game button again to reload all of the saves in your folder (there is no need to restart Raft if the mod loaded successfully). +Be sure that you click the "Load game" button **after** you load Raftipelago. You can click the Load Game button again to refresh all of the saves in your folder (there is no need to restart Raft if the mod loaded successfully). ### I'm certain I'm doing things correctly, but the world is still not loadable diff --git a/worlds/raft/regions.json b/worlds/raft/regions.json index b2737a1298..e593a0ab61 100644 --- a/worlds/raft/regions.json +++ b/worlds/raft/regions.json @@ -1,12 +1,12 @@ { - "Raft": ["RadioTower", "ResearchTable"], + "Raft": ["ResearchTable", "RadioTower", "Vasagatan", "BalboaIsland", "CaravanIsland", "Tangaroa", "Varuna Point", "Temperance", "Utopia"], "ResearchTable": [], - "RadioTower": ["Vasagatan"], - "Vasagatan": ["BalboaIsland"], - "BalboaIsland": ["CaravanIsland"], - "CaravanIsland": ["Tangaroa"], - "Tangaroa": ["Varuna Point"], - "Varuna Point": ["Temperance"], - "Temperance": ["Utopia"], + "RadioTower": [], + "Vasagatan": [], + "BalboaIsland": [], + "CaravanIsland": [], + "Tangaroa": [], + "Varuna Point": [], + "Temperance": [], "Utopia": [] } \ No newline at end of file From f9e28004a078f4925f8ba8694d7785043162a4bb Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sat, 3 Sep 2022 15:25:55 -0400 Subject: [PATCH 55/91] SMZ3: item link gt fill fix (#995) --- worlds/smz3/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index 15ac85c7c3..b9aab50e22 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -526,9 +526,11 @@ class SMZ3World(World): def JunkFillGT(self, factor): poolLength = len(self.world.itempool) + playerGroups = self.world.get_player_groups(self.player) + playerGroups.add(self.player) junkPoolIdx = [i for i in range(0, poolLength) if self.world.itempool[i].classification in (ItemClassification.filler, ItemClassification.trap) and - self.world.itempool[i].player == self.player] + self.world.itempool[i].player in playerGroups] toRemove = [] for loc in self.locations.values(): # commenting this for now since doing a partial GT pre fill would allow for non SMZ3 progression in GT From 539d2e80f1eff44cdbfb8263d179d316141b05c4 Mon Sep 17 00:00:00 2001 From: espeon65536 Date: Sat, 3 Sep 2022 10:02:20 -0500 Subject: [PATCH 56/91] OoT: prevent glitched + mq dungeons this combo is not allowed on main ootr, so we won't have it here either --- worlds/oot/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/worlds/oot/__init__.py b/worlds/oot/__init__.py index a9b7d5a1b6..b65882c874 100644 --- a/worlds/oot/__init__.py +++ b/worlds/oot/__init__.py @@ -190,7 +190,11 @@ class OOTWorld(World): # Determine which dungeons are MQ # Possible future plan: allow user to pick which dungeons are MQ - mq_dungeons = self.world.random.sample(dungeon_table, self.mq_dungeons) + if self.logic_rules == 'glitchless': + mq_dungeons = self.world.random.sample(dungeon_table, self.mq_dungeons) + else: + self.mq_dungeons = 0 + mq_dungeons = [] self.dungeon_mq = {item['name']: (item in mq_dungeons) for item in dungeon_table} # Determine tricks in logic From 0cbb3c283917094ec5dad42628fe7d93a75025c0 Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Sat, 3 Sep 2022 17:52:09 -0400 Subject: [PATCH 57/91] SMZ3: data package fix (#996) --- SNIClient.py | 3 ++- worlds/smz3/__init__.py | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/SNIClient.py b/SNIClient.py index 3d90fafc17..ccac3998a1 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -1271,7 +1271,8 @@ async def game_watcher(ctx: Context): 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 - location_id = locations_start_id + itemIndex + from worlds.smz3 import convertLocSMZ3IDToAPID + location_id = locations_start_id + convertLocSMZ3IDToAPID(itemIndex) ctx.locations_checked.add(location_id) location = ctx.location_names[location_id] diff --git a/worlds/smz3/__init__.py b/worlds/smz3/__init__.py index b9aab50e22..b796c2a43c 100644 --- a/worlds/smz3/__init__.py +++ b/worlds/smz3/__init__.py @@ -25,6 +25,10 @@ from Options import Accessibility world_folder = os.path.dirname(__file__) logger = logging.getLogger("SMZ3") +# Location IDs in the range 256+196 to 256+202 shifted +34 between 11.2 and 11.3 +# this is required to keep backward compatibility +def convertLocSMZ3IDToAPID(value): + return (value - 34) if value >= 256+230 and value <= 256+236 else value class SMZ3CollectionState(metaclass=AutoLogicRegister): def init_mixin(self, parent: MultiWorld): @@ -61,12 +65,13 @@ class SMZ3World(World): """ game: str = "SMZ3" topology_present = False - data_version = 2 + data_version = 3 option_definitions = smz3_options item_names: Set[str] = frozenset(TotalSMZ3Item.lookup_name_to_id) location_names: Set[str] item_name_to_id = TotalSMZ3Item.lookup_name_to_id - location_name_to_id: Dict[str, int] = {key : locations_start_id + value.Id for key, value in TotalSMZ3World(Config(), "", 0, "").locationLookup.items()} + location_name_to_id: Dict[str, int] = {key : locations_start_id + convertLocSMZ3IDToAPID(value.Id) + for key, value in TotalSMZ3World(Config(), "", 0, "").locationLookup.items()} web = SMZ3Web() remote_items: bool = False From 2acc129381135dedea93d58cf1411124e78cca5b Mon Sep 17 00:00:00 2001 From: PoryGone <98504756+PoryGone@users.noreply.github.com> Date: Sun, 4 Sep 2022 08:45:45 -0400 Subject: [PATCH 58/91] SA2B: Fix typo in doc string (#997) --- worlds/sa2b/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index 84a38f221c..e9b75bbeb8 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -46,7 +46,7 @@ def check_for_impossible_shuffle(shuffled_levels: typing.List[int], gate_0_range class SA2BWorld(World): """ - Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rogue, and Eggman across 31 stages and prevent the destruction of the earth. + Sonic Adventure 2 Battle is an action platforming game. Play as Sonic, Tails, Knuckles, Shadow, Rouge, and Eggman across 31 stages and prevent the destruction of the earth. """ game: str = "Sonic Adventure 2 Battle" option_definitions = sa2b_options From 5e8ac74b2a168568a4d26c7cddef0878372af5fc Mon Sep 17 00:00:00 2001 From: wildham0 <64616385+wildham0@users.noreply.github.com> Date: Mon, 5 Sep 2022 03:21:00 -0400 Subject: [PATCH 59/91] FFR: fix NoOverworld mode (#999) * Add Sigil/Mark to item list --- data/lua/FF1/ff1_connector.lua | 12 ++++++++++-- worlds/ff1/Items.py | 2 +- worlds/ff1/__init__.py | 2 +- worlds/ff1/data/items.json | 4 +++- 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/data/lua/FF1/ff1_connector.lua b/data/lua/FF1/ff1_connector.lua index ed3ee1ca60..6b2eec269a 100644 --- a/data/lua/FF1/ff1_connector.lua +++ b/data/lua/FF1/ff1_connector.lua @@ -97,6 +97,11 @@ local extensionConsumableLookup = { [443] = 0x3F } +local noOverworldItemsLookup = { + [499] = 0x2B, + [500] = 0x12, +} + local itemMessages = {} local consumableStacks = nil local prevstate = "" @@ -341,7 +346,7 @@ function processBlock(block) -- This is a key item memoryLocation = memoryLocation - 0x0E0 wU8(memoryLocation, 0x01) - elseif v >= 0x1E0 then + elseif v >= 0x1E0 and v <= 0x1F2 then -- This is a movement item -- Minus Offset (0x100) - movement offset (0xE0) memoryLocation = memoryLocation - 0x1E0 @@ -351,7 +356,10 @@ function processBlock(block) else wU8(memoryLocation, 0x01) end - + elseif v >= 0x1F3 and v <= 0x1F4 then + -- NoOverworld special items + memoryLocation = noOverworldItemsLookup[v] + wU8(memoryLocation, 0x01) elseif v >= 0x16C and v <= 0x1AF then -- This is a gold item amountToAdd = goldLookup[v] diff --git a/worlds/ff1/Items.py b/worlds/ff1/Items.py index e045fbaf18..469cf6f051 100644 --- a/worlds/ff1/Items.py +++ b/worlds/ff1/Items.py @@ -20,7 +20,7 @@ FF1_STARTER_ITEMS = [ FF1_PROGRESSION_LIST = [ "Rod", "Cube", "Lute", "Key", "Chime", "Oxyale", - "Ship", "Canoe", "Floater", "Canal", + "Ship", "Canoe", "Floater", "Mark", "Sigil", "Canal", "Crown", "Crystal", "Herb", "Tnt", "Adamant", "Slab", "Ruby", "Bottle", "Shard", "EarthOrb", "FireOrb", "WaterOrb", "AirOrb" diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index 0d731ace4b..b9f90a2ea1 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -31,7 +31,7 @@ class FF1World(World): game = "Final Fantasy" topology_present = False remote_items = True - data_version = 1 + data_version = 2 remote_start_inventory = True ff1_items = FF1Items() diff --git a/worlds/ff1/data/items.json b/worlds/ff1/data/items.json index 99611837fb..333080973d 100644 --- a/worlds/ff1/data/items.json +++ b/worlds/ff1/data/items.json @@ -190,5 +190,7 @@ "Ship": 480, "Bridge": 488, "Canal": 492, - "Canoe": 498 + "Canoe": 498, + "Sigil": 499, + "Mark": 500 } From 1792b66b3a8355e98304a690bced48363cff49d0 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 4 Sep 2022 23:43:03 +0200 Subject: [PATCH 60/91] CI: fix automated builds, update SNI and Enemizer * Launcher.py always running ModuleUpdate breaks setup.py build --yes * Use env variables in github workflows * Update SNI and Enemizer versions in github workflows * Minor cleanup in workflows * Silence pycharm warning in Launcher.py --- .github/workflows/build.yml | 17 ++++++++++++----- .github/workflows/release.yml | 14 ++++++++++---- Launcher.py | 6 ++++-- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4138f93f04..be053bdc2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,6 +4,11 @@ name: Build on: workflow_dispatch +env: + SNI_VERSION: v0.0.84 + ENEMIZER_VERSION: 7.1 + APPIMAGETOOL_VERSION: 13 + jobs: # build-release-macos: # LF volunteer @@ -17,9 +22,9 @@ jobs: python-version: '3.8' - name: Download run-time dependencies run: | - Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-windows-amd64.zip -OutFile sni.zip + Invoke-WebRequest -Uri https://github.com/alttpo/sni/releases/download/${Env:SNI_VERSION}/sni-${Env:SNI_VERSION}-windows-amd64.zip -OutFile sni.zip Expand-Archive -Path sni.zip -DestinationPath SNI -Force - Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/7.0.1/win-x64.zip -OutFile enemizer.zip + Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force - name: Build run: | @@ -43,6 +48,7 @@ jobs: build-ubuntu1804: runs-on: ubuntu-18.04 steps: + # - copy code below to release.yml - - uses: actions/checkout@v2 - name: Install base dependencies run: | @@ -56,18 +62,18 @@ jobs: - name: Install build-time dependencies run: | echo "PYTHON=python3.9" >> $GITHUB_ENV - wget -nv https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage + wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage chmod a+rx appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage --appimage-extract echo -e '#/bin/sh\n./squashfs-root/AppRun "$@"' > appimagetool chmod a+rx appimagetool - name: Download run-time dependencies run: | - wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz + wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz tar xf sni-*.tar.xz rm sni-*.tar.xz mv sni-* SNI - wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z + wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z 7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z - name: Build run: | @@ -84,6 +90,7 @@ jobs: (cd build && DIR_NAME="`ls | grep exe`" && mv "$DIR_NAME" Archipelago && tar -czvf ../dist/$TAR_NAME Archipelago && mv Archipelago "$DIR_NAME") echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV + # - copy code above to release.yml - - name: Store AppImage uses: actions/upload-artifact@v2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index aa82883ff1..23f018caf2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,6 +7,11 @@ on: tags: - '*.*.*' +env: + SNI_VERSION: v0.0.84 + ENEMIZER_VERSION: 7.1 + APPIMAGETOOL_VERSION: 13 + jobs: create-release: runs-on: ubuntu-latest @@ -44,22 +49,23 @@ jobs: - name: Install build-time dependencies run: | echo "PYTHON=python3.9" >> $GITHUB_ENV - wget -nv https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage + wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage chmod a+rx appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage --appimage-extract echo -e '#/bin/sh\n./squashfs-root/AppRun "$@"' > appimagetool chmod a+rx appimagetool - name: Download run-time dependencies run: | - wget -nv https://github.com/alttpo/sni/releases/download/v0.0.82/sni-v0.0.82-manylinux2014-amd64.tar.xz + wget -nv https://github.com/alttpo/sni/releases/download/$SNI_VERSION/sni-$SNI_VERSION-manylinux2014-amd64.tar.xz tar xf sni-*.tar.xz rm sni-*.tar.xz mv sni-* SNI - wget -nv https://github.com/Ijwu/Enemizer/releases/download/7.0.1/ubuntu.16.04-x64.7z + wget -nv https://github.com/Ijwu/Enemizer/releases/download/$ENEMIZER_VERSION/ubuntu.16.04-x64.7z 7za x -oEnemizerCLI/ ubuntu.16.04-x64.7z - name: Build run: | - "${{ env.PYTHON }}" -m pip install --upgrade pip setuptools virtualenv PyGObject # pygobject should probably move to requirements + # pygobject is an optional dependency for kivy that's not in requirements + "${{ env.PYTHON }}" -m pip install --upgrade pip virtualenv PyGObject setuptools "${{ env.PYTHON }}" -m venv venv source venv/bin/activate pip install -r requirements.txt diff --git a/Launcher.py b/Launcher.py index 92f43cd26c..8a3d53f866 100644 --- a/Launcher.py +++ b/Launcher.py @@ -19,8 +19,9 @@ from os.path import isfile from shutil import which from typing import Iterable, Sequence, Callable, Union, Optional -import ModuleUpdate -ModuleUpdate.update() +if __name__ == "__main__": + import ModuleUpdate + ModuleUpdate.update() from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \ is_windows, is_macos, is_linux @@ -69,6 +70,7 @@ def browse_files(): webbrowser.open(file) +# noinspection PyArgumentList class Type(Enum): TOOL = auto() FUNC = auto() # not a real component From 8aad75ed239d851c8b3dce5c3a868102eeb597f2 Mon Sep 17 00:00:00 2001 From: toasterparty Date: Mon, 5 Sep 2022 01:02:40 -0700 Subject: [PATCH 61/91] Tests: Check for Holes in the Item Pool (#992) * test for holes in the item pool * Update test/general/TestItems.py Co-authored-by: alwaysintreble * Update test/general/TestItems.py Co-authored-by: alwaysintreble * Update test/general/TestItems.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * Update test/general/TestItems.py Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: alwaysintreble Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- test/general/TestItems.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/general/TestItems.py b/test/general/TestItems.py index bd1bd50665..3dd589995e 100644 --- a/test/general/TestItems.py +++ b/test/general/TestItems.py @@ -1,5 +1,6 @@ import unittest from worlds.AutoWorld import AutoWorldRegister +from . import setup_default_world class TestBase(unittest.TestCase): @@ -29,3 +30,17 @@ class TestBase(unittest.TestCase): with self.subTest(group_name, group_name=group_name): for item in items: self.assertIn(item, world_type.item_name_to_id) + + def testItemCountGreaterEqualLocations(self): + for game_name, world_type in AutoWorldRegister.world_types.items(): + + if game_name in {"Final Fantasy"}: + continue + with self.subTest("Game", game=game_name): + world = setup_default_world(world_type) + location_count = sum(0 if location.event or location.item else 1 for location in world.get_locations()) + self.assertGreaterEqual( + len(world.itempool), + location_count, + f"{game_name} Item count MUST meet or exceede the number of locations", + ) From baf51e59591c7c4f051345c8f5cafc22e27c4442 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 5 Sep 2022 21:09:03 +0200 Subject: [PATCH 62/91] SC2: fix Launching Mission: text pulling the unshuffled ID. (#1001) Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Starcraft2Client.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Starcraft2Client.py b/Starcraft2Client.py index b8f6086914..ce4d9b046c 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -340,9 +340,8 @@ class SC2Context(CommonContext): def mission_callback(self, button): if not self.launching: - mission_id: int = list(self.mission_id_to_button.values()).index(button) - self.ctx.play_mission(list(self.mission_id_to_button) - [mission_id]) + mission_id: int = next(k for k, v in self.mission_id_to_button.items() if v == button) + self.ctx.play_mission(mission_id) self.launching = mission_id Clock.schedule_once(self.finish_launching, 10) @@ -359,7 +358,7 @@ class SC2Context(CommonContext): if self.sc2_run_task: self.sc2_run_task.cancel() - def play_mission(self, mission_id): + def play_mission(self, mission_id: int): if self.missions_unlocked or \ is_mission_available(self, mission_id): if self.sc2_run_task: From 7c04e7e06fc724639ef43a317d1c1de96cd047a7 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 5 Sep 2022 10:07:10 +0200 Subject: [PATCH 63/91] MultiServer: save goal completion flag --- MultiServer.py | 1 + 1 file changed, 1 insertion(+) diff --git a/MultiServer.py b/MultiServer.py index 6354f8e7a9..fc6e17dd20 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -593,6 +593,7 @@ class Context: forfeit_player(self, client.team, client.slot) elif self.forced_auto_forfeits[self.games[client.slot]]: forfeit_player(self, client.team, client.slot) + self.save() # save goal completion flag def notify_hints(ctx: Context, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False): From ade82e3d60cbb5786d9e62efb19f6605b12441ea Mon Sep 17 00:00:00 2001 From: lordlou <87331798+lordlou@users.noreply.github.com> Date: Tue, 6 Sep 2022 13:56:23 -0400 Subject: [PATCH 64/91] SM: varia tracker fix (#1006) --- .../multiworld-basepatch.ips | Bin 18193 -> 18193 bytes .../data/SMBasepatch_prebuilt/multiworld.sym | 704 +++++++++--------- .../sm-basepatch-symbols.json | 70 +- 3 files changed, 387 insertions(+), 387 deletions(-) diff --git a/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips b/worlds/sm/data/SMBasepatch_prebuilt/multiworld-basepatch.ips index 7ac3ea018475a0db495d6d540c9dda5f66f1391f..f8fba9b0cfba91ed4adfc2c8d8032796683271d7 100644 GIT binary patch delta 200 zcmbQ($2hT%ae}d2%MONy?+h$4H(GZvY-VO)Q<6Ec6cnspt@OTm zGQX<2c5^Gk0#SyQ3P!|W18tMiDEj#uzJlOGpdq2~I9jlbOTPA0y8?d%yE0wfto~vFW4*=dmO!oi) delta 200 zcmbQ($2hT%ae}d2;|_*~?+h$4H=1@ZY-VO)Q<6Ec6cnspt@OTO zGQX<2c0((}0#SyQ3YP!|W18tMiDjXU-;JlOGpdq2~I9jlbO8z*O|8?ZKJE0r{Eo~vFW4* Date: Wed, 7 Sep 2022 11:16:32 -0700 Subject: [PATCH 65/91] ALttP: remove link_palettes option (#1004) * ALttP: remove link_palettes option It doesn't work anyway so better to have it not visible. --- LttPAdjuster.py | 6 +++--- worlds/alttp/Options.py | 6 +++--- worlds/alttp/__init__.py | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/LttPAdjuster.py b/LttPAdjuster.py index f516a20ec0..469e8920b3 100644 --- a/LttPAdjuster.py +++ b/LttPAdjuster.py @@ -83,9 +83,9 @@ def main(): parser.add_argument('--ow_palettes', default='default', choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy', 'sick']) - parser.add_argument('--link_palettes', default='default', - choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy', - 'sick']) + # parser.add_argument('--link_palettes', default='default', + # choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy', + # 'sick']) parser.add_argument('--shield_palettes', default='default', choices=['default', 'random', 'blackout', 'puke', 'classic', 'grayscale', 'negative', 'dizzy', 'sick']) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 183f3eda91..b40becbdbb 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -282,8 +282,8 @@ class ShieldPalette(Palette): display_name = "Shield Palette" -class LinkPalette(Palette): - display_name = "Link Palette" +# class LinkPalette(Palette): +# display_name = "Link Palette" class HeartBeep(Choice): @@ -387,7 +387,7 @@ alttp_options: typing.Dict[str, type(Option)] = { "hud_palettes": HUDPalette, "sword_palettes": SwordPalette, "shield_palettes": ShieldPalette, - "link_palettes": LinkPalette, + # "link_palettes": LinkPalette, "heartbeep": HeartBeep, "heartcolor": HeartColor, "quickswap": QuickSwap, diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 3e32584352..abb1f0a9e6 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -378,7 +378,7 @@ class ALTTPWorld(World): 'hud': world.hud_palettes[player], 'sword': world.sword_palettes[player], 'shield': world.shield_palettes[player], - 'link': world.link_palettes[player] + # 'link': world.link_palettes[player] } palettes_options = {key: option.current_key for key, option in palettes_options.items()} From 88a225764a541bb89a37037ed199dd685d51ef85 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 9 Sep 2022 21:28:24 +0200 Subject: [PATCH 66/91] FF1: fix printjson --- CommonClient.py | 8 +++++++- FF1Client.py | 45 ++++++++++++++++++++------------------------- MultiServer.py | 5 ++++- SNIClient.py | 3 --- Starcraft2Client.py | 8 +++----- 5 files changed, 34 insertions(+), 35 deletions(-) diff --git a/CommonClient.py b/CommonClient.py index 574da16f2a..94d4359dd1 100644 --- a/CommonClient.py +++ b/CommonClient.py @@ -5,6 +5,7 @@ import urllib.parse import sys import typing import time +import functools import ModuleUpdate ModuleUpdate.update() @@ -17,7 +18,8 @@ if __name__ == "__main__": Utils.init_logging("TextClient", exception_logger="Client") from MultiServer import CommandProcessor -from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot +from NetUtils import Endpoint, decode, NetworkItem, encode, JSONtoTextParser, \ + ClientStatus, Permission, NetworkSlot, RawJSONtoTextParser from Utils import Version, stream_input from worlds import network_data_package, AutoWorldRegister import os @@ -204,6 +206,10 @@ class CommonContext: # execution self.keep_alive_task = asyncio.create_task(keep_alive(self), name="Bouncy") + @functools.cached_property + def raw_text_parser(self) -> RawJSONtoTextParser: + return RawJSONtoTextParser(self) + @property def total_locations(self) -> typing.Optional[int]: """Will return None until connected.""" diff --git a/FF1Client.py b/FF1Client.py index c280fa3035..6973c9f403 100644 --- a/FF1Client.py +++ b/FF1Client.py @@ -1,4 +1,5 @@ import asyncio +import copy import json import time from asyncio import StreamReader, StreamWriter @@ -6,7 +7,7 @@ from typing import List import Utils -from CommonClient import CommonContext, server_loop, gui_enabled, console_loop, ClientCommandProcessor, logger, \ +from CommonClient import CommonContext, server_loop, gui_enabled, ClientCommandProcessor, logger, \ get_base_parser SYSTEM_MESSAGE_ID = 0 @@ -64,7 +65,7 @@ class FF1Context(CommonContext): def _set_message(self, msg: str, msg_id: int): if DISPLAY_MSGS: - self.messages[(time.time(), msg_id)] = msg + self.messages[time.time(), msg_id] = msg def on_package(self, cmd: str, args: dict): if cmd == 'Connected': @@ -76,29 +77,23 @@ class FF1Context(CommonContext): elif cmd == "ReceivedItems": msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}" self._set_message(msg, SYSTEM_MESSAGE_ID) - elif cmd == 'PrintJSON': - print_type = args['type'] - item = args['item'] - receiving_player_id = args['receiving'] - receiving_player_name = self.player_names[receiving_player_id] - sending_player_id = item.player - sending_player_name = self.player_names[item.player] - if print_type == 'Hint': - msg = f"Hint: Your {self.item_names[item.item]} is at" \ - f" {self.player_names[item.player]}'s {self.location_names[item.location]}" - self._set_message(msg, item.item) - elif print_type == 'ItemSend' and receiving_player_id != self.slot: - if sending_player_id == self.slot: - if receiving_player_id == self.slot: - msg = f"You found your own {self.item_names[item.item]}" - else: - msg = f"You sent {self.item_names[item.item]} to {receiving_player_name}" - else: - if receiving_player_id == sending_player_id: - msg = f"{sending_player_name} found their {self.item_names[item.item]}" - else: - msg = f"{sending_player_name} sent {self.item_names[item.item]} to " \ - f"{receiving_player_name}" + + def on_print_json(self, args: dict): + relevant = args.get("type", None) in {"Hint", "ItemSend"} + if relevant: + item = args["item"] + # goes to this world + if self.slot_concerns_self(args["receiving"]): + relevant = True + # found in this world + elif self.slot_concerns_self(item.player): + relevant = True + # not related + else: + relevant = False + if relevant: + item = args["item"] + msg = self.raw_text_parser(copy.deepcopy(args["data"])) self._set_message(msg, item.item) def run_gui(self): diff --git a/MultiServer.py b/MultiServer.py index fc6e17dd20..2838609a2b 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -743,6 +743,7 @@ async def countdown(ctx: Context, timer: int): broadcast_countdown(ctx, 0, f"[Server]: GO") ctx.countdown_timer = 0 + def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {}): old_clients, new_clients = [], [] @@ -755,8 +756,10 @@ def broadcast_text_all(ctx: Context, text: str, additional_arguments: dict = {}) ctx.broadcast(old_clients, [{"cmd": "Print", "text": text }]) ctx.broadcast(new_clients, [{**{"cmd": "PrintJSON", "data": [{ "text": text }]}, **additional_arguments}]) + def broadcast_countdown(ctx: Context, timer: int, message: str): - broadcast_text_all(ctx, message, { "type": "Countdown", "countdown": timer }) + broadcast_text_all(ctx, message, {"type": "Countdown", "countdown": timer}) + def get_players_string(ctx: Context): auth_clients = {(c.team, c.slot) for c in ctx.endpoints if c.auth} diff --git a/SNIClient.py b/SNIClient.py index ccac3998a1..477cde86a2 100644 --- a/SNIClient.py +++ b/SNIClient.py @@ -15,9 +15,6 @@ import typing from json import loads, dumps -import ModuleUpdate -ModuleUpdate.update() - from Utils import init_logging, messagebox if __name__ == "__main__": diff --git a/Starcraft2Client.py b/Starcraft2Client.py index ce4d9b046c..f81d4ec34a 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -135,11 +135,6 @@ class SC2Context(CommonContext): last_loc_list = None difficulty_override = -1 mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {} - raw_text_parser: RawJSONtoTextParser - - def __init__(self, *args, **kwargs): - super(SC2Context, self).__init__(*args, **kwargs) - self.raw_text_parser = RawJSONtoTextParser(self) async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -164,10 +159,13 @@ class SC2Context(CommonContext): check_mod_install() def on_print_json(self, args: dict): + # goes to this world if "receiving" in args and self.slot_concerns_self(args["receiving"]): relevant = True + # found in this world elif "item" in args and self.slot_concerns_self(args["item"].player): relevant = True + # not related else: relevant = False From bb386d3bd7e51c8e23384e0fc68ce1c109b434a8 Mon Sep 17 00:00:00 2001 From: TheBigSalarius <60804015+TheBigSalarius@users.noreply.github.com> Date: Fri, 9 Sep 2022 16:30:50 -0400 Subject: [PATCH 67/91] FF1: fix FF1Client messaging and scoped lua messaging with printjson Corrects the issue causing the client and lua messaging not displaying properly after the printjson changes --- FF1Client.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/FF1Client.py b/FF1Client.py index 6973c9f403..5a56d0dd08 100644 --- a/FF1Client.py +++ b/FF1Client.py @@ -74,11 +74,13 @@ class FF1Context(CommonContext): msg = args['text'] if ': !' not in msg: self._set_message(msg, SYSTEM_MESSAGE_ID) - elif cmd == "ReceivedItems": - msg = f"Received {', '.join([self.item_names[item.item] for item in args['items']])}" - self._set_message(msg, SYSTEM_MESSAGE_ID) def on_print_json(self, args: dict): + if self.ui: + self.ui.print_json(copy.deepcopy(args["data"])) + else: + text = self.jsontotextparser(copy.deepcopy(args["data"])) + logger.info(text) relevant = args.get("type", None) in {"Hint", "ItemSend"} if relevant: item = args["item"] From e204a0fce6207e33df731438de3d5b48cee77d0d Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 11 Sep 2022 01:57:22 +0200 Subject: [PATCH 68/91] Subnautica: fix missed item and correct other item pool counts to fit it --- worlds/subnautica/Items.py | 17 +++++++++++------ worlds/subnautica/__init__.py | 2 +- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/worlds/subnautica/Items.py b/worlds/subnautica/Items.py index 9917921a4b..4201cf3910 100644 --- a/worlds/subnautica/Items.py +++ b/worlds/subnautica/Items.py @@ -139,7 +139,7 @@ item_table: Dict[int, ItemDict] = { 'name': 'Power Transmitter Fragment', 'tech_type': 'PowerTransmitterFragment'}, 35032: {'classification': ItemClassification.progression, - 'count': 4, + 'count': 5, 'name': 'Prawn Suit Fragment', 'tech_type': 'ExosuitFragment'}, 35033: {'classification': ItemClassification.useful, @@ -163,7 +163,7 @@ item_table: Dict[int, ItemDict] = { 'name': 'Scanner Room Fragment', 'tech_type': 'BaseMapRoomFragment'}, 35038: {'classification': ItemClassification.progression, - 'count': 5, + 'count': 4, 'name': 'Seamoth Fragment', 'tech_type': 'SeamothFragment'}, 35039: {'classification': ItemClassification.progression, @@ -203,9 +203,9 @@ item_table: Dict[int, ItemDict] = { 'name': 'Picture Frame', 'tech_type': 'PictureFrameFragment'}, 35048: {'classification': ItemClassification.filler, - 'count': 2, - 'name': 'Bench Fragment', - 'tech_type': 'BenchFragment'}, + 'count': 1, + 'name': 'Bench', + 'tech_type': 'Bench'}, 35049: {'classification': ItemClassification.filler, 'count': 1, 'name': 'Basic Plant Pot', @@ -333,7 +333,12 @@ item_table: Dict[int, ItemDict] = { 35080: {'classification': ItemClassification.filler, 'count': 1, 'name': 'Water Filtration Machine', - 'tech_type': 'BaseFiltrationMachine'}} + 'tech_type': 'BaseFiltrationMachine'}, + 35081: {'classification': ItemClassification.progression, + 'count': 1, + 'name': 'Ultra High Capacity Tank', + 'tech_type': 'HighCapacityTank'}, +} advancement_item_names: Set[str] = set() non_advancement_item_names: Set[str] = set() diff --git a/worlds/subnautica/__init__.py b/worlds/subnautica/__init__.py index a4447ccbc1..7dc23bf405 100644 --- a/worlds/subnautica/__init__.py +++ b/worlds/subnautica/__init__.py @@ -41,7 +41,7 @@ class SubnauticaWorld(World): location_name_to_id = all_locations option_definitions = Options.options - data_version = 6 + data_version = 7 required_client_version = (0, 3, 5) prefill_items: List[Item] From b2aa251c4755870006de20ddba655c0a140b0838 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 12 Sep 2022 01:51:25 +0200 Subject: [PATCH 69/91] SC2: fix bitflag overflow when multiple instances of an Item are acquired (#1008) --- Starcraft2Client.py | 59 ++++++++++++++---------------------------- worlds/sc2wol/Items.py | 14 ++++++++++ 2 files changed, 34 insertions(+), 39 deletions(-) diff --git a/Starcraft2Client.py b/Starcraft2Client.py index f81d4ec34a..a9ed9c041b 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -19,10 +19,11 @@ from sc2.data import Race from sc2.main import run_game from sc2.player import Bot +import NetUtils from MultiServer import mark_raw from Utils import init_logging, is_windows from worlds.sc2wol import SC2WoLWorld -from worlds.sc2wol.Items import lookup_id_to_name, item_table +from worlds.sc2wol.Items import lookup_id_to_name, item_table, ItemData, type_flaggroups from worlds.sc2wol.Locations import SC2WOL_LOC_ID_OFFSET from worlds.sc2wol.MissionTables import lookup_id_to_mission from worlds.sc2wol.Regions import MissionInfo @@ -429,47 +430,27 @@ wol_default_categories = [ ] -def calculate_items(items): - unit_unlocks = 0 - armory1_unlocks = 0 - armory2_unlocks = 0 - upgrade_unlocks = 0 - building_unlocks = 0 - merc_unlocks = 0 - lab_unlocks = 0 - protoss_unlock = 0 - minerals = 0 - vespene = 0 - supply = 0 +def calculate_items(items: typing.List[NetUtils.NetworkItem]) -> typing.List[int]: + network_item: NetUtils.NetworkItem + accumulators: typing.List[int] = [0 for _ in type_flaggroups] - for item in items: - data = lookup_id_to_name[item.item] + for network_item in items: + name: str = lookup_id_to_name[network_item.item] + item_data: ItemData = item_table[name] - if item_table[data].type == "Unit": - unit_unlocks += (1 << item_table[data].number) - elif item_table[data].type == "Upgrade": - upgrade_unlocks += (1 << item_table[data].number) - elif item_table[data].type == "Armory 1": - armory1_unlocks += (1 << item_table[data].number) - elif item_table[data].type == "Armory 2": - armory2_unlocks += (1 << item_table[data].number) - elif item_table[data].type == "Building": - building_unlocks += (1 << item_table[data].number) - elif item_table[data].type == "Mercenary": - merc_unlocks += (1 << item_table[data].number) - elif item_table[data].type == "Laboratory": - lab_unlocks += (1 << item_table[data].number) - elif item_table[data].type == "Protoss": - protoss_unlock += (1 << item_table[data].number) - elif item_table[data].type == "Minerals": - minerals += item_table[data].number - elif item_table[data].type == "Vespene": - vespene += item_table[data].number - elif item_table[data].type == "Supply": - supply += item_table[data].number + # exists exactly once + if item_data.quantity == 1: + accumulators[type_flaggroups[item_data.type]] |= 1 << item_data.number - return [unit_unlocks, upgrade_unlocks, armory1_unlocks, armory2_unlocks, building_unlocks, merc_unlocks, - lab_unlocks, protoss_unlock, minerals, vespene, supply] + # exists multiple times + elif item_data.type == "Upgrade": + accumulators[type_flaggroups[item_data.type]] += 1 << item_data.number + + # sum + else: + accumulators[type_flaggroups[item_data.type]] += item_data.number + + return accumulators def calc_difficulty(difficulty): diff --git a/worlds/sc2wol/Items.py b/worlds/sc2wol/Items.py index 8da40de5ab..6bb74076fb 100644 --- a/worlds/sc2wol/Items.py +++ b/worlds/sc2wol/Items.py @@ -163,3 +163,17 @@ filler_items: typing.Tuple[str, ...] = ( lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if data.code} +# Map type to expected int +type_flaggroups: typing.Dict[str, int] = { + "Unit": 0, + "Upgrade": 1, + "Armory 1": 2, + "Armory 2": 3, + "Building": 4, + "Mercenary": 5, + "Laboratory": 6, + "Protoss": 7, + "Minerals": 8, + "Vespene": 9, + "Supply": 10, +} From ef46979bd800cb2f9bef8e93f0c7179562cec577 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Mon, 12 Sep 2022 02:08:33 +0200 Subject: [PATCH 70/91] SC2: fix client freezing on exit while SC2 is running (#1011) --- Starcraft2Client.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Starcraft2Client.py b/Starcraft2Client.py index a9ed9c041b..7f4bb8f404 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -136,6 +136,11 @@ class SC2Context(CommonContext): last_loc_list = None difficulty_override = -1 mission_id_to_location_ids: typing.Dict[int, typing.List[int]] = {} + last_bot: typing.Optional[ArchipelagoBot] = None + + def __init__(self, *args, **kwargs): + super(SC2Context, self).__init__(*args, **kwargs) + self.raw_text_parser = RawJSONtoTextParser(self) async def server_auth(self, password_requested: bool = False): if password_requested and not self.password: @@ -354,6 +359,8 @@ class SC2Context(CommonContext): async def shutdown(self): await super(SC2Context, self).shutdown() + if self.last_bot: + self.last_bot.want_close = True if self.sc2_run_task: self.sc2_run_task.cancel() @@ -481,7 +488,7 @@ class ArchipelagoBot(sc2.bot_ai.BotAI): setup_done: bool ctx: SC2Context mission_id: int - + want_close: bool = False can_read_game = False last_received_update: int = 0 @@ -489,12 +496,17 @@ class ArchipelagoBot(sc2.bot_ai.BotAI): def __init__(self, ctx: SC2Context, mission_id): self.setup_done = False self.ctx = ctx + self.ctx.last_bot = self self.mission_id = mission_id self.boni = [False for _ in range(max_bonus)] super(ArchipelagoBot, self).__init__() async def on_step(self, iteration: int): + if self.want_close: + self.want_close = False + await self._client.leave() + return game_state = 0 if not self.setup_done: self.setup_done = True From 156e9e0e4305ef007ce4a5ba0db17cf474c3b21f Mon Sep 17 00:00:00 2001 From: TheBigSalarius <60804015+TheBigSalarius@users.noreply.github.com> Date: Sun, 11 Sep 2022 19:55:11 -0400 Subject: [PATCH 71/91] FF1: Throw exception for Noverworld --- worlds/ff1/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/worlds/ff1/__init__.py b/worlds/ff1/__init__.py index b9f90a2ea1..24b8e7081b 100644 --- a/worlds/ff1/__init__.py +++ b/worlds/ff1/__init__.py @@ -66,7 +66,10 @@ class FF1World(World): def goal_rule_and_shards(state): return goal_rule(state) and state.has("Shard", self.player, 32) terminated_event.access_rule = goal_rule_and_shards - + if "MARK" in items.keys(): + # Fail generation for Noverworld and provide link to old FFR website + raise Exception("FFR Noverworld seeds must be generated on an older version of FFR. Please ensure you generated the settings using " + "4-4-0.finalfantasyrandomizer.com") menu_region.locations.append(terminated_event) self.world.regions += [menu_region] From af11fa51501fc8bd6162b6cff817fcb2b72c0d50 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Fri, 16 Sep 2022 00:32:30 +0200 Subject: [PATCH 72/91] Core: auto alias (#1022) * Test: check that default templates can be parsed into Option objects --- Options.py | 7 ++++++- docs/world api.md | 6 ++---- test/webhost/TestFileGeneration.py | 13 +++++++++++- worlds/alttp/Options.py | 8 -------- worlds/factorio/Options.py | 2 -- worlds/hk/Options.py | 3 --- worlds/oot/Options.py | 9 --------- worlds/sm/Options.py | 2 -- worlds/sm64ex/Options.py | 22 ++++++++++++++++---- worlds/sm64ex/__init__.py | 1 + worlds/smz3/Options.py | 1 - worlds/soe/Options.py | 2 -- worlds/timespinner/Options.py | 32 +++++++++++++++++++++++++++++- 13 files changed, 70 insertions(+), 38 deletions(-) diff --git a/Options.py b/Options.py index 7eb108c99d..56b54fe317 100644 --- a/Options.py +++ b/Options.py @@ -26,13 +26,18 @@ class AssembleOptions(abc.ABCMeta): attrs["name_lookup"].update({option_id: name for name, option_id in new_options.items()}) options.update(new_options) - # apply aliases, without name_lookup aliases = {name[6:].lower(): option_id for name, option_id in attrs.items() if name.startswith("alias_")} assert "random" not in aliases, "Choice option 'random' cannot be manually assigned." + # auto-alias Off and On being parsed as True and False + if "off" in options: + options["false"] = options["off"] + if "on" in options: + options["true"] = options["on"] + options.update(aliases) # auto-validate schema on __init__ diff --git a/docs/world api.md b/docs/world api.md index ffc0749e8c..fd0a3711f3 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -274,14 +274,12 @@ Define a property `option_ = ` per selectable value and `default = ` to set the default selection. Aliases can be set by defining a property `alias_ = `. -One special case where aliases are required is when option name is `yes`, `no`, -`on` or `off` because they parse to `True` or `False`: ```python option_off = 0 option_on = 1 option_some = 2 -alias_false = 0 -alias_true = 1 +alias_disabled = 0 +alias_enabled = 1 default = 0 ``` diff --git a/test/webhost/TestFileGeneration.py b/test/webhost/TestFileGeneration.py index 7f56864ea3..656206cdec 100644 --- a/test/webhost/TestFileGeneration.py +++ b/test/webhost/TestFileGeneration.py @@ -14,9 +14,20 @@ class TestFileGeneration(unittest.TestCase): def testOptions(self): WebHost.create_options_files() - self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "configs"))) + target = os.path.join(self.correct_path, "static", "generated", "configs") + self.assertTrue(os.path.exists(target)) self.assertFalse(os.path.exists(os.path.join(self.incorrect_path, "static", "generated", "configs"))) + # folder seems fine, so now we try to generate Options based on the default file + from WebHostLib.check import roll_options + file: os.DirEntry + 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: + 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.") + def testTutorial(self): WebHost.create_ordered_tutorials_file() self.assertTrue(os.path.exists(os.path.join(self.correct_path, "static", "generated", "tutorials.json"))) diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index b40becbdbb..184b8f3a20 100644 --- a/worlds/alttp/Options.py +++ b/worlds/alttp/Options.py @@ -39,8 +39,6 @@ class OpenPyramid(Choice): option_auto = 3 default = option_goal - alias_true = option_open - alias_false = option_closed alias_yes = option_open alias_no = option_closed @@ -159,8 +157,6 @@ class Progressive(Choice): option_off = 0 option_grouped_random = 1 option_on = 2 - alias_false = 0 - alias_true = 2 default = 2 def want_progressives(self, random): @@ -202,8 +198,6 @@ class Hints(Choice): option_on = 2 option_full = 3 default = 2 - alias_false = 0 - alias_true = 2 class Scams(Choice): @@ -213,7 +207,6 @@ class Scams(Choice): option_king_zora = 1 option_bottle_merchant = 2 option_all = 3 - alias_false = 0 @property def gives_king_zora_hint(self): @@ -293,7 +286,6 @@ class HeartBeep(Choice): option_half = 2 option_quarter = 3 option_off = 4 - alias_false = 4 class HeartColor(Choice): diff --git a/worlds/factorio/Options.py b/worlds/factorio/Options.py index b7d13c6464..64e03c4338 100644 --- a/worlds/factorio/Options.py +++ b/worlds/factorio/Options.py @@ -137,8 +137,6 @@ class Progressive(Choice): option_off = 0 option_grouped_random = 1 option_on = 2 - alias_false = 0 - alias_true = 2 default = 2 def want_progressives(self, random): diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index fd8d036175..04923e70f8 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -409,7 +409,6 @@ class DeathLink(Choice): shade: DeathLink functions like a normal death if you do not already have a shade, shadeless otherwise. """ option_off = 0 - alias_false = 0 alias_no = 0 alias_true = 1 alias_on = 1 @@ -435,10 +434,8 @@ class CostSanity(Choice): These costs can be in Geo (except Grubfather, Seer and Eggshop), Grubs, Charms, Essence and/or Rancid Eggs """ option_off = 0 - alias_false = 0 alias_no = 0 option_on = 1 - alias_true = 1 alias_yes = 1 option_shopsonly = 2 option_notshops = 3 diff --git a/worlds/oot/Options.py b/worlds/oot/Options.py index ea9a8160fb..deee558755 100644 --- a/worlds/oot/Options.py +++ b/worlds/oot/Options.py @@ -101,7 +101,6 @@ class InteriorEntrances(Choice): option_off = 0 option_simple = 1 option_all = 2 - alias_false = 0 alias_true = 2 @@ -141,7 +140,6 @@ class MixEntrancePools(Choice): option_off = 0 option_indoor = 1 option_all = 2 - alias_false = 0 class DecoupleEntrances(Toggle): @@ -308,7 +306,6 @@ class ShopShuffle(Choice): option_off = 0 option_fixed_number = 1 option_random_number = 2 - alias_false = 0 class ShopSlots(Range): @@ -326,7 +323,6 @@ class TokenShuffle(Choice): option_dungeons = 1 option_overworld = 2 option_all = 3 - alias_false = 0 class ScrubShuffle(Choice): @@ -336,7 +332,6 @@ class ScrubShuffle(Choice): option_low = 1 option_regular = 2 option_random_prices = 3 - alias_false = 0 alias_affordable = 1 alias_expensive = 2 @@ -569,7 +564,6 @@ class Hints(Choice): option_agony = 2 option_always = 3 default = 3 - alias_false = 0 class MiscHints(DefaultOnToggle): @@ -673,8 +667,6 @@ class IceTraps(Choice): option_mayhem = 3 option_onslaught = 4 default = 1 - alias_false = 0 - alias_true = 2 alias_extra = 2 @@ -742,7 +734,6 @@ class Music(Choice): option_normal = 0 option_off = 1 option_randomized = 2 - alias_false = 1 class BackgroundMusic(Music): diff --git a/worlds/sm/Options.py b/worlds/sm/Options.py index 814b19f4d4..2f0c70a242 100644 --- a/worlds/sm/Options.py +++ b/worlds/sm/Options.py @@ -122,8 +122,6 @@ class AreaRandomization(Choice): option_off = 0 option_light = 1 option_on = 2 - alias_false = 0 - alias_true = 2 default = 0 class AreaLayout(Toggle): diff --git a/worlds/sm64ex/Options.py b/worlds/sm64ex/Options.py index 7d9a75dde9..f29a65c58d 100644 --- a/worlds/sm64ex/Options.py +++ b/worlds/sm64ex/Options.py @@ -1,48 +1,57 @@ import typing from Options import Option, DefaultOnToggle, Range, Toggle, DeathLink, Choice + class EnableCoinStars(DefaultOnToggle): """Disable to Ignore 100 Coin Stars. You can still collect them, but they don't do anything""" display_name = "Enable 100 Coin Stars" + class StrictCapRequirements(DefaultOnToggle): """If disabled, Stars that expect special caps may have to be acquired without the caps""" display_name = "Strict Cap Requirements" + class StrictCannonRequirements(DefaultOnToggle): """If disabled, Stars that expect cannons may have to be acquired without them. Only makes a difference if Buddy Checks are enabled""" display_name = "Strict Cannon Requirements" + class FirstBowserStarDoorCost(Range): """How many stars are required at the Star Door to Bowser in the Dark World""" range_start = 0 range_end = 50 default = 8 + class BasementStarDoorCost(Range): """How many stars are required at the Star Door in the Basement""" range_start = 0 range_end = 70 default = 30 + class SecondFloorStarDoorCost(Range): """How many stars are required to access the third floor""" range_start = 0 range_end = 90 default = 50 + class MIPS1Cost(Range): """How many stars are required to spawn MIPS the first time""" range_start = 0 range_end = 40 default = 15 + class MIPS2Cost(Range): """How many stars are required to spawn MIPS the secound time. Must be bigger or equal MIPS1Cost""" range_start = 0 range_end = 80 default = 50 + class StarsToFinish(Range): """How many stars are required at the infinite stairs""" display_name = "Endless Stairs Stars" @@ -50,35 +59,40 @@ class StarsToFinish(Range): range_end = 100 default = 70 + class AmountOfStars(Range): """How many stars exist. Disabling 100 Coin Stars removes 15 from the Pool. At least max of any Cost set""" range_start = 35 range_end = 120 default = 120 + class AreaRandomizer(Choice): """Randomize Entrances""" display_name = "Entrance Randomizer" - alias_false = 0 option_Off = 0 option_Courses_Only = 1 option_Courses_and_Secrets = 2 + class BuddyChecks(Toggle): """Bob-omb Buddies are checks, Cannon Unlocks are items""" display_name = "Bob-omb Buddy Checks" + class ExclamationBoxes(Choice): """Include 1Up Exclamation Boxes during randomization""" display_name = "Randomize 1Up !-Blocks" option_Off = 0 option_1Ups_Only = 1 + class ProgressiveKeys(DefaultOnToggle): """Keys will first grant you access to the Basement, then to the Secound Floor""" display_name = "Progressive Keys" -sm64_options: typing.Dict[str,type(Option)] = { + +sm64_options: typing.Dict[str, type(Option)] = { "AreaRandomizer": AreaRandomizer, "ProgressiveKeys": ProgressiveKeys, "EnableCoinStars": EnableCoinStars, @@ -93,5 +107,5 @@ sm64_options: typing.Dict[str,type(Option)] = { "StarsToFinish": StarsToFinish, "death_link": DeathLink, "BuddyChecks": BuddyChecks, - "ExclamationBoxes": ExclamationBoxes -} \ No newline at end of file + "ExclamationBoxes": ExclamationBoxes, +} diff --git a/worlds/sm64ex/__init__.py b/worlds/sm64ex/__init__.py index cf8a8e875d..88b2261a23 100644 --- a/worlds/sm64ex/__init__.py +++ b/worlds/sm64ex/__init__.py @@ -9,6 +9,7 @@ from .Regions import create_regions, sm64courses, sm64entrances_s, sm64_internal from BaseClasses import Item, Tutorial, ItemClassification from ..AutoWorld import World, WebWorld + class SM64Web(WebWorld): tutorials = [Tutorial( "Multiworld Setup Guide", diff --git a/worlds/smz3/Options.py b/worlds/smz3/Options.py index 2bbddf7ab3..e3d8b8dd10 100644 --- a/worlds/smz3/Options.py +++ b/worlds/smz3/Options.py @@ -107,7 +107,6 @@ class HeartBeepSpeed(Choice): option_Half = 2 option_Normal = 3 option_Double = 4 - alias_false = 0 default = 3 class HeartColor(Choice): diff --git a/worlds/soe/Options.py b/worlds/soe/Options.py index 4ec0ce2bcc..c718cb4abd 100644 --- a/worlds/soe/Options.py +++ b/worlds/soe/Options.py @@ -21,8 +21,6 @@ class OffOnFullChoice(Choice): option_on = 1 option_full = 2 alias_chaos = 2 - alias_false = 0 - alias_true = 1 class Difficulty(EvermizerFlags, Choice): diff --git a/worlds/timespinner/Options.py b/worlds/timespinner/Options.py index 588416dd52..574259c9e0 100644 --- a/worlds/timespinner/Options.py +++ b/worlds/timespinner/Options.py @@ -3,66 +3,82 @@ from BaseClasses import MultiWorld from Options import Toggle, DefaultOnToggle, DeathLink, Choice, Range, Option, OptionDict from schema import Schema, And, Optional + class StartWithJewelryBox(Toggle): "Start with Jewelry Box unlocked" display_name = "Start with Jewelry Box" + #class ProgressiveVerticalMovement(Toggle): # "Always find vertical movement in the following order Succubus Hairpin -> Light Wall -> Celestial Sash" # display_name = "Progressive vertical movement" + #class ProgressiveKeycards(Toggle): # "Always find Security Keycard's in the following order D -> C -> B -> A" # display_name = "Progressive keycards" + class DownloadableItems(DefaultOnToggle): "With the tablet you will be able to download items at terminals" display_name = "Downloadable items" + class EyeSpy(Toggle): "Requires Oculus Ring in inventory to be able to break hidden walls." display_name = "Eye Spy" + class StartWithMeyef(Toggle): "Start with Meyef, ideal for when you want to play multiplayer." display_name = "Start with Meyef" + class QuickSeed(Toggle): "Start with Talaria Attachment, Nyoom!" display_name = "Quick seed" + class SpecificKeycards(Toggle): "Keycards can only open corresponding doors" display_name = "Specific Keycards" + class Inverted(Toggle): "Start in the past" display_name = "Inverted" + #class StinkyMaw(Toggle): # "Require gasmask for Maw" # display_name = "Stinky Maw" + class GyreArchives(Toggle): "Gyre locations are in logic. New warps are gated by Merchant Crow and Kobo" display_name = "Gyre Archives" + class Cantoran(Toggle): "Cantoran's fight and check are available upon revisiting his room" display_name = "Cantoran" + class LoreChecks(Toggle): "Memories and journal entries contain items." display_name = "Lore Checks" + class BossRando(Toggle): "Shuffles the positions of all bosses." display_name = "Boss Randomization" + class BossScaling(DefaultOnToggle): "When Boss Rando is enabled, scales the bosses' HP, XP, and ATK to the stats of the location they replace (Reccomended)" display_name = "Scale Random Boss Stats" + class DamageRando(Choice): "Randomly nerfs and buffs some orbs and their associated spells as well as some associated rings." display_name = "Damage Rando" @@ -73,9 +89,9 @@ class DamageRando(Choice): option_mostlybuffs = 4 option_allbuffs = 5 option_manual = 6 - alias_false = 0 alias_true = 2 + class DamageRandoOverrides(OptionDict): "Manual +/-/normal odds for an orb. Put 0 if you don't want a certain nerf or buff to be a possibility. Orbs that you don't specify will roll with 1/1/1 as odds" schema = Schema({ @@ -180,6 +196,7 @@ class DamageRandoOverrides(OptionDict): "Radiant": { "MinusOdds": 1, "NormalOdds": 1, "PlusOdds": 1 }, } + class HpCap(Range): "Sets the number that Lunais's HP maxes out at." display_name = "HP Cap" @@ -187,10 +204,12 @@ class HpCap(Range): range_end = 999 default = 999 + class BossHealing(DefaultOnToggle): "Enables/disables healing after boss fights. NOTE: Currently only applicable when Boss Rando is enabled." display_name = "Heal After Bosses" + class ShopFill(Choice): """Sets the items for sale in Merchant Crow's shops. Default: No sunglasses or trendy jacket, but sand vials for sale. @@ -203,10 +222,12 @@ class ShopFill(Choice): option_vanilla = 2 option_empty = 3 + class ShopWarpShards(DefaultOnToggle): "Shops always sell warp shards (when keys possessed), ignoring inventory setting." display_name = "Always Sell Warp Shards" + class ShopMultiplier(Range): "Multiplier for the cost of items in the shop. Set to 0 for free shops." display_name = "Shop Price Multiplier" @@ -214,6 +235,7 @@ class ShopMultiplier(Range): range_end = 10 default = 1 + class LootPool(Choice): """Sets the items that drop from enemies (does not apply to boss reward checks) Vanilla: Drops are the same as the base game @@ -224,6 +246,7 @@ class LootPool(Choice): option_randomized = 1 option_empty = 2 + class DropRateCategory(Choice): """Sets the drop rate when 'Loot Pool' is set to 'Random' Tiered: Based on item rarity/value @@ -237,6 +260,7 @@ class DropRateCategory(Choice): option_randomized = 2 option_fixed = 3 + class FixedDropRate(Range): "Base drop rate percentage when 'Drop Rate Category' is set to 'Fixed'" display_name = "Fixed Drop Rate" @@ -244,6 +268,7 @@ class FixedDropRate(Range): range_end = 100 default = 5 + class LootTierDistro(Choice): """Sets how often items of each rarity tier are placed when 'Loot Pool' is set to 'Random' Default Weight: Rarer items will be assigned to enemy drop slots less frequently than common items @@ -255,14 +280,17 @@ class LootTierDistro(Choice): option_full_random = 1 option_inverted_weight = 2 + class ShowBestiary(Toggle): "All entries in the bestiary are visible, without needing to kill one of a given enemy first" display_name = "Show Bestiary Entries" + class ShowDrops(Toggle): "All item drops in the bestiary are visible, without needing an enemy to drop one of a given item first" display_name = "Show Bestiary Item Drops" + # Some options that are available in the timespinner randomizer arent currently implemented timespinner_options: Dict[str, Option] = { "StartWithJewelryBox": StartWithJewelryBox, @@ -296,9 +324,11 @@ timespinner_options: Dict[str, Option] = { "DeathLink": DeathLink, } + def is_option_enabled(world: MultiWorld, player: int, name: str) -> bool: return get_option_value(world, player, name) > 0 + def get_option_value(world: MultiWorld, player: int, name: str) -> Union[int, dict]: option = getattr(world, name, None) if option == None: From 9daa64741baa0a5e305f3d7919fffaea61333577 Mon Sep 17 00:00:00 2001 From: Alchav <59858495+Alchav@users.noreply.github.com> Date: Fri, 16 Sep 2022 20:06:25 -0400 Subject: [PATCH 73/91] New, smarter fast_fill function (#646) Co-authored-by: Fabian Dill --- Fill.py | 150 ++++++++++++++++++++++----------------- test/general/TestFill.py | 8 +-- worlds/AutoWorld.py | 6 +- worlds/alttp/__init__.py | 18 +---- worlds/sa2b/__init__.py | 3 +- worlds/sm/__init__.py | 3 +- 6 files changed, 97 insertions(+), 91 deletions(-) diff --git a/Fill.py b/Fill.py index e44c80e720..c62eaabde8 100644 --- a/Fill.py +++ b/Fill.py @@ -136,33 +136,98 @@ def fill_restrictive(world: MultiWorld, base_state: CollectionState, locations: itempool.extend(unplaced_items) +def remaining_fill(world: MultiWorld, + locations: typing.List[Location], + itempool: typing.List[Item]) -> None: + unplaced_items: typing.List[Item] = [] + placements: typing.List[Location] = [] + swapped_items: typing.Counter[typing.Tuple[int, str]] = Counter() + while locations and itempool: + item_to_place = itempool.pop() + spot_to_fill: typing.Optional[Location] = None + + for i, location in enumerate(locations): + if location.item_rule(item_to_place): + # popping by index is faster than removing by content, + spot_to_fill = locations.pop(i) + # skipping a scan for the element + break + + else: + # we filled all reachable spots. + # try swapping this item with previously placed items + + for (i, location) in enumerate(placements): + placed_item = location.item + # Unplaceable items can sometimes be swapped infinitely. Limit the + # number of times we will swap an individual item to prevent this + + if swapped_items[placed_item.player, + placed_item.name] > 1: + continue + + location.item = None + placed_item.location = None + if location.item_rule(item_to_place): + # Add this item to the existing placement, and + # add the old item to the back of the queue + spot_to_fill = placements.pop(i) + + swapped_items[placed_item.player, + placed_item.name] += 1 + + itempool.append(placed_item) + + break + + # Item can't be placed here, restore original item + location.item = placed_item + placed_item.location = location + + if spot_to_fill is None: + # Can't place this item, move on to the next + unplaced_items.append(item_to_place) + continue + + world.push_item(spot_to_fill, item_to_place, False) + placements.append(spot_to_fill) + + if unplaced_items and locations: + # There are leftover unplaceable items and locations that won't accept them + raise FillError(f'No more spots to place {unplaced_items}, locations {locations} are invalid. ' + f'Already placed {len(placements)}: {", ".join(str(place) for place in placements)}') + + itempool.extend(unplaced_items) + + +def fast_fill(world: MultiWorld, + item_pool: typing.List[Item], + fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]: + placing = min(len(item_pool), len(fill_locations)) + for item, location in zip(item_pool, fill_locations): + world.push_item(location, item, False) + return item_pool[placing:], fill_locations[placing:] + + def distribute_items_restrictive(world: MultiWorld) -> None: fill_locations = sorted(world.get_unfilled_locations()) world.random.shuffle(fill_locations) - # get items to distribute itempool = sorted(world.itempool) world.random.shuffle(itempool) progitempool: typing.List[Item] = [] - nonexcludeditempool: typing.List[Item] = [] - localrestitempool: typing.Dict[int, typing.List[Item]] = {player: [] for player in range(1, world.players + 1)} - nonlocalrestitempool: typing.List[Item] = [] - restitempool: typing.List[Item] = [] + usefulitempool: typing.List[Item] = [] + filleritempool: typing.List[Item] = [] for item in itempool: if item.advancement: progitempool.append(item) - elif item.useful: # this only gets nonprogression items which should not appear in excluded locations - nonexcludeditempool.append(item) - elif item.name in world.local_items[item.player].value: - localrestitempool[item.player].append(item) - elif item.name in world.non_local_items[item.player].value: - nonlocalrestitempool.append(item) + elif item.useful: + usefulitempool.append(item) else: - restitempool.append(item) + filleritempool.append(item) - call_all(world, "fill_hook", progitempool, nonexcludeditempool, - localrestitempool, nonlocalrestitempool, restitempool, fill_locations) + call_all(world, "fill_hook", progitempool, usefulitempool, filleritempool, fill_locations) locations: typing.Dict[LocationProgressType, typing.List[Location]] = { loc_type: [] for loc_type in LocationProgressType} @@ -184,50 +249,16 @@ def distribute_items_restrictive(world: MultiWorld) -> None: raise FillError( f'Not enough locations for progress items. There are {len(progitempool)} more items than locations') - if nonexcludeditempool: - world.random.shuffle(defaultlocations) - # needs logical fill to not conflict with local items - fill_restrictive( - world, world.state, defaultlocations, nonexcludeditempool) - if nonexcludeditempool: - raise FillError( - f'Not enough locations for non-excluded items. There are {len(nonexcludeditempool)} more items than locations') + remaining_fill(world, excludedlocations, filleritempool) + if excludedlocations: + raise FillError( + f"Not enough filler items for excluded locations. There are {len(excludedlocations)} more locations than items") - defaultlocations = defaultlocations + excludedlocations - world.random.shuffle(defaultlocations) + restitempool = usefulitempool + filleritempool - if any(localrestitempool.values()): # we need to make sure some fills are limited to certain worlds - local_locations: typing.Dict[int, typing.List[Location]] = {player: [] for player in world.player_ids} - for location in defaultlocations: - local_locations[location.player].append(location) - for player_locations in local_locations.values(): - world.random.shuffle(player_locations) + remaining_fill(world, defaultlocations, restitempool) - for player, items in localrestitempool.items(): # items already shuffled - player_local_locations = local_locations[player] - for item_to_place in items: - if not player_local_locations: - logging.warning(f"Ran out of local locations for player {player}, " - f"cannot place {item_to_place}.") - break - spot_to_fill = player_local_locations.pop() - world.push_item(spot_to_fill, item_to_place, False) - defaultlocations.remove(spot_to_fill) - - for item_to_place in nonlocalrestitempool: - for i, location in enumerate(defaultlocations): - if location.player != item_to_place.player: - world.push_item(defaultlocations.pop(i), item_to_place, False) - break - else: - raise Exception(f"Could not place non_local_item {item_to_place} among {defaultlocations}. " - f"Too many non-local items for too few remaining locations.") - - world.random.shuffle(defaultlocations) - - restitempool, defaultlocations = fast_fill( - world, restitempool, defaultlocations) - unplaced = progitempool + restitempool + unplaced = restitempool unfilled = defaultlocations if unplaced or unfilled: @@ -241,15 +272,6 @@ def distribute_items_restrictive(world: MultiWorld) -> None: logging.info(f'Per-Player counts: {print_data})') -def fast_fill(world: MultiWorld, - item_pool: typing.List[Item], - fill_locations: typing.List[Location]) -> typing.Tuple[typing.List[Item], typing.List[Location]]: - placing = min(len(item_pool), len(fill_locations)) - for item, location in zip(item_pool, fill_locations): - world.push_item(location, item, False) - return item_pool[placing:], fill_locations[placing:] - - def flood_items(world: MultiWorld) -> None: # get items to distribute world.random.shuffle(world.itempool) diff --git a/test/general/TestFill.py b/test/general/TestFill.py index 189aafafb2..8ce5b3b281 100644 --- a/test/general/TestFill.py +++ b/test/general/TestFill.py @@ -371,13 +371,13 @@ class TestDistributeItemsRestrictive(unittest.TestCase): distribute_items_restrictive(multi_world) - self.assertEqual(locations[0].item, basic_items[0]) + self.assertEqual(locations[0].item, basic_items[1]) self.assertFalse(locations[0].event) self.assertEqual(locations[1].item, prog_items[0]) self.assertTrue(locations[1].event) self.assertEqual(locations[2].item, prog_items[1]) self.assertTrue(locations[2].event) - self.assertEqual(locations[3].item, basic_items[1]) + self.assertEqual(locations[3].item, basic_items[0]) self.assertFalse(locations[3].event) def test_excluded_distribute(self): @@ -500,8 +500,8 @@ class TestDistributeItemsRestrictive(unittest.TestCase): removed_item: list[Item] = [] removed_location: list[Location] = [] - def fill_hook(progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, restitempool, fill_locations): - removed_item.append(restitempool.pop(0)) + def fill_hook(progitempool, usefulitempool, filleritempool, fill_locations): + removed_item.append(filleritempool.pop(0)) removed_location.append(fill_locations.pop(0)) multi_world.worlds[player1.id].fill_hook = fill_hook diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 8d9a1b0829..959bc858a0 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -221,10 +221,8 @@ class World(metaclass=AutoWorldRegister): @classmethod def fill_hook(cls, progitempool: List["Item"], - nonexcludeditempool: List["Item"], - localrestitempool: Dict[int, List["Item"]], - nonlocalrestitempool: Dict[int, List["Item"]], - restitempool: List["Item"], + usefulitempool: List["Item"], + filleritempool: List["Item"], fill_locations: List["Location"]) -> None: """Special method that gets called as part of distribute_items_restrictive (main fill). This gets called once per present world type.""" diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index abb1f0a9e6..cb66ac4ff1 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -424,8 +424,7 @@ class ALTTPWorld(World): return ALttPItem(name, self.player, **item_init_table[name]) @classmethod - def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, - restitempool, fill_locations): + def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations): trash_counts = {} standard_keyshuffle_players = set() for player in world.get_game_players("A Link to the Past"): @@ -472,26 +471,15 @@ class ALTTPWorld(World): for player, trash_count in trash_counts.items(): gtower_locations = locations_mapping[player] world.random.shuffle(gtower_locations) - localrest = localrestitempool[player] - if localrest: - gt_item_pool = restitempool + localrest - world.random.shuffle(gt_item_pool) - else: - gt_item_pool = restitempool.copy() - while gtower_locations and gt_item_pool and trash_count > 0: + while gtower_locations and filleritempool and trash_count > 0: spot_to_fill = gtower_locations.pop() - item_to_place = gt_item_pool.pop() + item_to_place = filleritempool.pop() if spot_to_fill.item_rule(item_to_place): - if item_to_place in localrest: - localrest.remove(item_to_place) - else: - restitempool.remove(item_to_place) world.push_item(spot_to_fill, item_to_place, False) fill_locations.remove(spot_to_fill) # very slow, unfortunately trash_count -= 1 - def get_filler_item_name(self) -> str: if self.world.goal[self.player] == "icerodhunt": item = "Nothing" diff --git a/worlds/sa2b/__init__.py b/worlds/sa2b/__init__.py index e9b75bbeb8..ea24809521 100644 --- a/worlds/sa2b/__init__.py +++ b/worlds/sa2b/__init__.py @@ -282,8 +282,7 @@ class SA2BWorld(World): spoiler_handle.writelines(text) @classmethod - def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, - restitempool, fill_locations): + def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations): if world.get_game_players("Sonic Adventure 2 Battle"): progitempool.sort( key=lambda item: 0 if (item.name != 'Emblem') else 1) diff --git a/worlds/sm/__init__.py b/worlds/sm/__init__.py index fbf3825e0f..0bf12ca7eb 100644 --- a/worlds/sm/__init__.py +++ b/worlds/sm/__init__.py @@ -660,8 +660,7 @@ class SMWorld(World): loc.address = loc.item.code = None @classmethod - def stage_fill_hook(cls, world, progitempool, nonexcludeditempool, localrestitempool, nonlocalrestitempool, - restitempool, fill_locations): + def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations): if world.get_game_players("Super Metroid"): progitempool.sort( key=lambda item: 1 if (item.name == 'Morph Ball') else 0) From 516a52c041980174da3069be3541db4d5d87b98d Mon Sep 17 00:00:00 2001 From: N00byKing Date: Fri, 16 Sep 2022 20:58:28 +0200 Subject: [PATCH 74/91] sm64ex: Fix WDW 1Up Block Logic --- worlds/sm64ex/Rules.py | 1 + 1 file changed, 1 insertion(+) diff --git a/worlds/sm64ex/Rules.py b/worlds/sm64ex/Rules.py index eae9868583..2462206246 100644 --- a/worlds/sm64ex/Rules.py +++ b/worlds/sm64ex/Rules.py @@ -86,6 +86,7 @@ def set_rules(world, player: int, area_connections): # which would make it impossible to reach downtown area without the cannon. add_rule(world.get_location("WDW: Quick Race Through Downtown!", player), lambda state: state.has("Cannon Unlock WDW", player)) add_rule(world.get_location("WDW: Go to Town for Red Coins", player), lambda state: state.has("Cannon Unlock WDW", player)) + add_rule(world.get_location("WDW: 1Up Block in Downtown", player), lambda state: state.has("Cannon Unlock WDW", player)) if world.StrictCapRequirements[player]: add_rule(world.get_location("BoB: Mario Wings to the Sky", player), lambda state: state.has("Wing Cap", player)) From ff05e9d7d53a2db46070f869b0537ecb78c9139c Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 16 Sep 2022 10:04:17 +0200 Subject: [PATCH 75/91] MultiServer: produce nicer output ... ... for headless and when cancelling the file open dialog --- MultiServer.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index 2838609a2b..b4ddb936d6 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -2044,15 +2044,28 @@ async def main(args: argparse.Namespace): args.auto_shutdown, args.compatibility, args.log_network) data_filename = args.multidata - try: - if not data_filename: + if not data_filename: + try: filetypes = (("Multiworld data", (".archipelago", ".zip")),) data_filename = Utils.open_filename("Select multiworld data", filetypes) + except Exception as e: + if isinstance(e, ImportError) or (e.__class__.__name__ == "TclError" and "no display" in str(e)): + if not isinstance(e, ImportError): + logging.error(f"Failed to load tkinter ({e})") + logging.info("Pass a multidata filename on command line to run headless.") + exit(1) + raise + + if not data_filename: + logging.info("No file selected. Exiting.") + exit(1) + + try: ctx.load(data_filename, args.use_embedded_options) except Exception as e: - logging.exception('Failed to read multiworld data (%s)' % e) + logging.exception(f"Failed to read multiworld data ({e})") raise ctx.init_save(not args.disable_save) From 8d51205e8f13e144a3c6a85fd84a317ff9181888 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Fri, 16 Sep 2022 09:18:11 +0200 Subject: [PATCH 76/91] Alttp: only check item.type for own items with retro_cave --- worlds/alttp/ItemPool.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/worlds/alttp/ItemPool.py b/worlds/alttp/ItemPool.py index d7ab3b8491..aa8d7ef3f0 100644 --- a/worlds/alttp/ItemPool.py +++ b/worlds/alttp/ItemPool.py @@ -480,7 +480,7 @@ def set_up_take_anys(world, player): old_man_take_any.shop = TakeAny(old_man_take_any, 0x0112, 0xE2, True, True, total_shop_slots) world.shops.append(old_man_take_any.shop) - swords = [item for item in world.itempool if item.type == 'Sword' and item.player == player] + swords = [item for item in world.itempool if item.player == player and item.type == 'Sword'] if swords: sword = world.random.choice(swords) world.itempool.remove(sword) From 332dde154fd2a88ded5592f0d5a85b48e5c1ff3a Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Fri, 16 Sep 2022 19:55:33 -0500 Subject: [PATCH 77/91] core: new freetext and textchoice options (#728) * add freetext and freetextchoice options * fix textchoice. create plando_bosses bool so worlds can check if boss plando is enabled * remove strange unneccessary \ escapes * lttp: rip boss plando out of core * fix broken text methods so they read the data correctly * revert `None` key in boss_shuffle_options. fix failing tests * lttp: rewrite boss plando * lttp: rewrite boss shuffle * add generic verification step and allow options to set a plando module * add default typing to plando_options set * use PlandoSettings intflag for lttp boss plando * fix plandosettings boss flag check * minor lttp init cleanup * make suggested changes. account for "random" existing within plando boss options * override eq operator * Please document me! * Forgot to mention it supports plando * remove auto_display_name * Throw warning alerting user to which shuffle is being used if plando is off. Set the remaining boss shuffle in init and boss placement cleanup * move the convoluted string matching to `from_text` * remove unneccessary text lowering and actually turn off plando option when it's disabled * typing * strong typing for verify method and reorder * typing is your friend * log warning correctly * 3.8 support :( * also list apparently * rip out old boss shuffle spoiler code * verification step for plando bosses and locations * update plando guide to reference new supported behavior * empty string is not `None`. remove unneccessary error throw * Fix bad ordering * validate boss_shuffle only contains a normal boss option at the end * get random choice from a list dummy * >:( Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> * minor textchoice cleanup Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- BaseClasses.py | 1 - Generate.py | 60 +------------- Options.py | 99 ++++++++++++++++++++++- worlds/alttp/Bosses.py | 143 ++++++++++++++++++--------------- worlds/alttp/Options.py | 143 +++++++++++++++++++++++++++++++-- worlds/alttp/__init__.py | 2 +- worlds/alttp/docs/plando_en.md | 6 +- 7 files changed, 323 insertions(+), 131 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index cea1d48e6f..7a7abc8bad 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -1422,7 +1422,6 @@ class Spoiler(): "f" in self.world.shop_shuffle[player])) outfile.write('Custom Potion Shop: %s\n' % bool_to_text("w" in self.world.shop_shuffle[player])) - outfile.write('Boss shuffle: %s\n' % self.world.boss_shuffle[player]) outfile.write('Enemy health: %s\n' % self.world.enemy_health[player]) outfile.write('Enemy damage: %s\n' % self.world.enemy_damage[player]) outfile.write('Prize shuffle %s\n' % diff --git a/Generate.py b/Generate.py index d13a78b375..763471e90b 100644 --- a/Generate.py +++ b/Generate.py @@ -23,7 +23,6 @@ from worlds.alttp.EntranceRandomizer import parse_arguments from Main import main as ERmain from BaseClasses import seeddigits, get_seed import Options -from worlds.alttp import Bosses from worlds.alttp.Text import TextTable from worlds.AutoWorld import AutoWorldRegister import copy @@ -337,19 +336,6 @@ def prefer_int(input_data: str) -> Union[str, int]: return input_data -available_boss_names: Set[str] = {boss.lower() for boss in Bosses.boss_table if boss not in - {'Agahnim', 'Agahnim2', 'Ganon'}} -available_boss_locations: Set[str] = {f"{loc.lower()}{f' {level}' if level else ''}" for loc, level in - Bosses.boss_location_table} - -boss_shuffle_options = {None: 'none', - 'none': 'none', - 'basic': 'basic', - 'full': 'full', - 'chaos': 'chaos', - 'singularity': 'singularity' - } - goals = { 'ganon': 'ganon', 'crystals': 'crystals', @@ -456,42 +442,7 @@ def roll_triggers(weights: dict, triggers: list) -> dict: return weights -def get_plando_bosses(boss_shuffle: str, plando_options: Set[str]) -> str: - if boss_shuffle in boss_shuffle_options: - return boss_shuffle_options[boss_shuffle] - elif PlandoSettings.bosses in plando_options: - options = boss_shuffle.lower().split(";") - remainder_shuffle = "none" # vanilla - bosses = [] - for boss in options: - if boss in boss_shuffle_options: - remainder_shuffle = boss_shuffle_options[boss] - elif "-" in boss: - loc, boss_name = boss.split("-") - if boss_name not in available_boss_names: - raise ValueError(f"Unknown Boss name {boss_name}") - if loc not in available_boss_locations: - raise ValueError(f"Unknown Boss Location {loc}") - level = '' - if loc.split(" ")[-1] in {"top", "middle", "bottom"}: - # split off level - loc = loc.split(" ") - level = f" {loc[-1]}" - loc = " ".join(loc[:-1]) - loc = loc.title().replace("Of", "of") - if not Bosses.can_place_boss(boss_name.title(), loc, level): - raise ValueError(f"Cannot place {boss_name} at {loc}{level}") - bosses.append(boss) - elif boss not in available_boss_names: - raise ValueError(f"Unknown Boss name or Boss shuffle option {boss}.") - else: - bosses.append(boss) - return ";".join(bosses + [remainder_shuffle]) - else: - raise Exception(f"Boss Shuffle {boss_shuffle} is unknown and boss plando is turned off.") - - -def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option)): +def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoSettings): if option_key in game_weights: try: if not option.supports_weighting: @@ -502,8 +453,7 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, except Exception as e: raise Exception(f"Error generating option {option_key} in {ret.game}") from e else: - if hasattr(player_option, "verify"): - player_option.verify(AutoWorldRegister.world_types[ret.game]) + player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options) else: setattr(ret, option_key, option(option.default)) @@ -549,11 +499,11 @@ def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings if ret.game in AutoWorldRegister.world_types: for option_key, option in world_type.option_definitions.items(): - handle_option(ret, game_weights, option_key, option) + handle_option(ret, game_weights, option_key, option, plando_options) for option_key, option in Options.per_game_common_options.items(): # skip setting this option if already set from common_options, defaulting to root option if not (option_key in Options.common_options and option_key not in game_weights): - handle_option(ret, game_weights, option_key, option) + handle_option(ret, game_weights, option_key, option, plando_options) if PlandoSettings.items in plando_options: ret.plando_items = game_weights.get("plando_items", []) if ret.game == "Minecraft" or ret.game == "Ocarina of Time": @@ -636,8 +586,6 @@ def roll_alttp_settings(ret: argparse.Namespace, weights, plando_options): ret.item_functionality = get_choice_legacy('item_functionality', weights) - boss_shuffle = get_choice_legacy('boss_shuffle', weights) - ret.shufflebosses = get_plando_bosses(boss_shuffle, plando_options) ret.enemy_damage = {None: 'default', 'default': 'default', diff --git a/Options.py b/Options.py index 56b54fe317..11ec46f2e9 100644 --- a/Options.py +++ b/Options.py @@ -40,6 +40,17 @@ class AssembleOptions(abc.ABCMeta): options.update(aliases) + if "verify" not in attrs: + # not overridden by class -> look up bases + verifiers = [f for f in (getattr(base, "verify", None) for base in bases) if f] + if len(verifiers) > 1: # verify multiple bases/mixins + def verify(self, *args, **kwargs) -> None: + for f in verifiers: + f(self, *args, **kwargs) + attrs["verify"] = verify + else: + assert verifiers, "class Option is supposed to implement def verify" + # auto-validate schema on __init__ if "schema" in attrs.keys(): @@ -117,6 +128,41 @@ class Option(typing.Generic[T], metaclass=AssembleOptions): def from_any(cls, data: typing.Any) -> Option[T]: raise NotImplementedError + if typing.TYPE_CHECKING: + from Generate import PlandoSettings + from worlds.AutoWorld import World + + def verify(self, world: World, player_name: str, plando_options: PlandoSettings) -> None: + pass + else: + def verify(self, *args, **kwargs) -> None: + pass + + +class FreeText(Option): + """Text option that allows users to enter strings. + Needs to be validated by the world or option definition.""" + + def __init__(self, value: str): + assert isinstance(value, str), "value of FreeText must be a string" + self.value = value + + @property + def current_key(self) -> str: + return self.value + + @classmethod + def from_text(cls, text: str) -> FreeText: + return cls(text) + + @classmethod + def from_any(cls, data: typing.Any) -> FreeText: + return cls.from_text(str(data)) + + @classmethod + def get_option_name(cls, value: T) -> str: + return value + class NumericOption(Option[int], numbers.Integral): # note: some of the `typing.Any`` here is a result of unresolved issue in python standards @@ -373,6 +419,53 @@ class Choice(NumericOption): __hash__ = Option.__hash__ # see https://docs.python.org/3/reference/datamodel.html#object.__hash__ +class TextChoice(Choice): + """Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string""" + + def __init__(self, value: typing.Union[str, int]): + assert isinstance(value, str) or isinstance(value, int), \ + f"{value} is not a valid option for {self.__class__.__name__}" + self.value = value + super(TextChoice, self).__init__() + + @property + def current_key(self) -> str: + if isinstance(self.value, str): + return self.value + else: + return self.name_lookup[self.value] + + @classmethod + def from_text(cls, text: str) -> TextChoice: + if text.lower() == "random": # chooses a random defined option but won't use any free text options + return cls(random.choice(list(cls.name_lookup))) + for option_name, value in cls.options.items(): + if option_name.lower() == text.lower(): + return cls(value) + return cls(text) + + @classmethod + def get_option_name(cls, value: T) -> str: + if isinstance(value, str): + return value + return cls.name_lookup[value] + + def __eq__(self, other: typing.Any): + if isinstance(other, self.__class__): + return other.value == self.value + elif isinstance(other, str): + if other in self.options: + return other == self.current_key + return other == self.value + elif isinstance(other, int): + assert other in self.name_lookup, f"compared against an int that could never be equal. {self} == {other}" + return other == self.value + elif isinstance(other, bool): + return other == bool(self.value) + else: + raise TypeError(f"Can't compare {self.__class__.__name__} with {other.__class__.__name__}") + + class Range(NumericOption): range_start = 0 range_end = 1 @@ -512,7 +605,7 @@ class VerifyKeys: raise Exception(f"Found unexpected key {', '.join(extra)} in {cls}. " f"Allowed keys: {cls.valid_keys}.") - def verify(self, world): + def verify(self, world, player_name: str, plando_options) -> None: if self.convert_name_groups and self.verify_item_name: new_value = type(self.value)() # empty container of whatever value is for item_name in self.value: @@ -737,8 +830,8 @@ class ItemLinks(OptionList): pool |= {item_name} return pool - def verify(self, world): - super(ItemLinks, self).verify(world) + def verify(self, world, player_name: str, plando_options) -> None: + super(ItemLinks, self).verify(world, player_name, plando_options) existing_links = set() for link in self.value: if link["name"] in existing_links: diff --git a/worlds/alttp/Bosses.py b/worlds/alttp/Bosses.py index 5ea7aba5fc..1c381b9a1c 100644 --- a/worlds/alttp/Bosses.py +++ b/worlds/alttp/Bosses.py @@ -1,8 +1,9 @@ import logging -from typing import Optional +from typing import Optional, Union, List, Tuple, Callable, Dict from BaseClasses import Boss from Fill import FillError +from .Options import Bosses def BossFactory(boss: str, player: int) -> Optional[Boss]: @@ -12,7 +13,7 @@ def BossFactory(boss: str, player: int) -> Optional[Boss]: raise Exception('Unknown Boss: %s', boss) -def ArmosKnightsDefeatRule(state, player: int): +def ArmosKnightsDefeatRule(state, player: int) -> bool: # Magic amounts are probably a bit overkill return ( state.has_melee_weapon(player) or @@ -25,7 +26,7 @@ def ArmosKnightsDefeatRule(state, player: int): state.has('Red Boomerang', player)) -def LanmolasDefeatRule(state, player: int): +def LanmolasDefeatRule(state, player: int) -> bool: return ( state.has_melee_weapon(player) or state.has('Fire Rod', player) or @@ -35,16 +36,16 @@ def LanmolasDefeatRule(state, player: int): state.can_shoot_arrows(player)) -def MoldormDefeatRule(state, player: int): +def MoldormDefeatRule(state, player: int) -> bool: return state.has_melee_weapon(player) -def HelmasaurKingDefeatRule(state, player: int): +def HelmasaurKingDefeatRule(state, player: int) -> bool: # TODO: technically possible with the hammer return state.has_sword(player) or state.can_shoot_arrows(player) -def ArrghusDefeatRule(state, player: int): +def ArrghusDefeatRule(state, player: int) -> bool: if not state.has('Hookshot', player): return False # TODO: ideally we would have a check for bow and silvers, which combined with the @@ -58,7 +59,7 @@ def ArrghusDefeatRule(state, player: int): (state.has('Ice Rod', player) and (state.can_shoot_arrows(player) or state.can_extend_magic(player, 16)))) -def MothulaDefeatRule(state, player: int): +def MothulaDefeatRule(state, player: int) -> bool: return ( state.has_melee_weapon(player) or (state.has('Fire Rod', player) and state.can_extend_magic(player, 10)) or @@ -70,11 +71,11 @@ def MothulaDefeatRule(state, player: int): ) -def BlindDefeatRule(state, player: int): +def BlindDefeatRule(state, player: int) -> bool: return state.has_melee_weapon(player) or state.has('Cane of Somaria', player) or state.has('Cane of Byrna', player) -def KholdstareDefeatRule(state, player: int): +def KholdstareDefeatRule(state, player: int) -> bool: return ( ( state.has('Fire Rod', player) or @@ -96,11 +97,11 @@ def KholdstareDefeatRule(state, player: int): ) -def VitreousDefeatRule(state, player: int): +def VitreousDefeatRule(state, player: int) -> bool: return state.can_shoot_arrows(player) or state.has_melee_weapon(player) -def TrinexxDefeatRule(state, player: int): +def TrinexxDefeatRule(state, player: int) -> bool: if not (state.has('Fire Rod', player) and state.has('Ice Rod', player)): return False return state.has('Hammer', player) or state.has('Tempered Sword', player) or state.has('Golden Sword', player) or \ @@ -108,11 +109,11 @@ def TrinexxDefeatRule(state, player: int): (state.has_sword(player) and state.can_extend_magic(player, 32)) -def AgahnimDefeatRule(state, player: int): +def AgahnimDefeatRule(state, player: int) -> bool: return state.has_sword(player) or state.has('Hammer', player) or state.has('Bug Catching Net', player) -def GanonDefeatRule(state, player: int): +def GanonDefeatRule(state, player: int) -> bool: if state.world.swordless[player]: return state.has('Hammer', player) and \ state.has_fire_source(player) and \ @@ -132,7 +133,7 @@ def GanonDefeatRule(state, player: int): return common and state.has('Silver Bow', player) and state.can_shoot_arrows(player) -boss_table = { +boss_table: Dict[str, Tuple[str, Optional[Callable]]] = { 'Armos Knights': ('Armos', ArmosKnightsDefeatRule), 'Lanmolas': ('Lanmola', LanmolasDefeatRule), 'Moldorm': ('Moldorm', MoldormDefeatRule), @@ -147,7 +148,7 @@ boss_table = { 'Agahnim2': ('Agahnim2', AgahnimDefeatRule) } -boss_location_table = [ +boss_location_table: List[Tuple[str, str]] = [ ('Ganons Tower', 'top'), ('Tower of Hera', None), ('Skull Woods', None), @@ -164,6 +165,34 @@ boss_location_table = [ ] +def place_plando_bosses(bosses: List[str], world, player: int) -> Tuple[List[str], List[Tuple[str, str]]]: + # Most to least restrictive order + boss_locations = boss_location_table.copy() + world.random.shuffle(boss_locations) + boss_locations.sort(key=lambda location: -int(restrictive_boss_locations[location])) + already_placed_bosses: List[str] = [] + + for boss in bosses: + if "-" in boss: # handle plando locations + loc, boss = boss.split("-") + boss = boss.title() + level: str = None + if loc.split(" ")[-1] in {"top", "middle", "bottom"}: + # split off level + loc = loc.split(" ") + level = loc[-1] + loc = " ".join(loc[:-1]) + loc = loc.title().replace("Of", "of") + place_boss(world, player, boss, loc, level) + already_placed_bosses.append(boss) + boss_locations.remove((loc, level)) + else: # boss chosen with no specified locations + boss = boss.title() + boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations) + + return already_placed_bosses, boss_locations + + def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> bool: # blacklist approach if boss in {"Agahnim", "Agahnim2", "Ganon"}: @@ -187,62 +216,50 @@ def can_place_boss(boss: str, dungeon_name: str, level: Optional[str] = None) -> return True -restrictive_boss_locations = {} + +restrictive_boss_locations: Dict[Tuple[str, str], bool] = {} for location in boss_location_table: restrictive_boss_locations[location] = not all(can_place_boss(boss, *location) for boss in boss_table if not boss.startswith("Agahnim")) -def place_boss(world, player: int, boss: str, location: str, level: Optional[str]): + +def place_boss(world, player: int, boss: str, location: str, level: Optional[str]) -> None: if location == 'Ganons Tower' and world.mode[player] == 'inverted': location = 'Inverted Ganons Tower' logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else '')) world.get_dungeon(location, player).bosses[level] = BossFactory(boss, player) -def format_boss_location(location, level): + +def format_boss_location(location: str, level: str) -> str: return location + (' (' + level + ')' if level else '') -def place_bosses(world, player: int): - if world.boss_shuffle[player] == 'none': + +def place_bosses(world, player: int) -> None: + # will either be an int or a lower case string with ';' between options + boss_shuffle: Union[str, int] = world.boss_shuffle[player].value + already_placed_bosses: List[str] = [] + remaining_locations: List[Tuple[str, str]] = [] + # handle plando + if isinstance(boss_shuffle, str): + # figure out our remaining mode, convert it to an int and remove it from plando_args + options = boss_shuffle.split(";") + boss_shuffle = Bosses.options[options.pop()] + # place our plando bosses + already_placed_bosses, remaining_locations = place_plando_bosses(options, world, player) + if boss_shuffle == Bosses.option_none: # vanilla boss locations return + # Most to least restrictive order - boss_locations = boss_location_table.copy() - world.random.shuffle(boss_locations) - boss_locations.sort(key= lambda location: -int(restrictive_boss_locations[location])) + if not remaining_locations and not already_placed_bosses: + remaining_locations = boss_location_table.copy() + world.random.shuffle(remaining_locations) + remaining_locations.sort(key=lambda location: -int(restrictive_boss_locations[location])) all_bosses = sorted(boss_table.keys()) # sorted to be deterministic on older pythons placeable_bosses = [boss for boss in all_bosses if boss not in ['Agahnim', 'Agahnim2', 'Ganon']] - shuffle_mode = world.boss_shuffle[player] - already_placed_bosses = [] - if ";" in shuffle_mode: - bosses = shuffle_mode.split(";") - shuffle_mode = bosses.pop() - for boss in bosses: - if "-" in boss: - loc, boss = boss.split("-") - boss = boss.title() - level = None - if loc.split(" ")[-1] in {"top", "middle", "bottom"}: - # split off level - loc = loc.split(" ") - level = loc[-1] - loc = " ".join(loc[:-1]) - loc = loc.title().replace("Of", "of") - if can_place_boss(boss, loc, level) and (loc, level) in boss_locations: - place_boss(world, player, boss, loc, level) - already_placed_bosses.append(boss) - boss_locations.remove((loc, level)) - else: - raise Exception(f"Cannot place {boss} at {format_boss_location(loc, level)} for player {player}.") - else: - boss = boss.title() - boss_locations, already_placed_bosses = place_where_possible(world, player, boss, boss_locations) - - if shuffle_mode == "none": - return # vanilla bosses come pre-placed - - if shuffle_mode in ["basic", "full"]: - if world.boss_shuffle[player] == "basic": # vanilla bosses shuffled + if boss_shuffle == Bosses.option_basic or boss_shuffle == Bosses.option_full: + if boss_shuffle == Bosses.option_basic: # vanilla bosses shuffled bosses = placeable_bosses + ['Armos Knights', 'Lanmolas', 'Moldorm'] else: # all bosses present, the three duplicates chosen at random bosses = placeable_bosses + world.random.sample(placeable_bosses, 3) @@ -258,7 +275,7 @@ def place_bosses(world, player: int): logging.debug('Bosses chosen %s', bosses) world.random.shuffle(bosses) - for loc, level in boss_locations: + for loc, level in remaining_locations: for _ in range(len(bosses)): boss = bosses.pop() if can_place_boss(boss, loc, level): @@ -272,8 +289,8 @@ def place_bosses(world, player: int): place_boss(world, player, boss, loc, level) - elif shuffle_mode == "chaos": # all bosses chosen at random - for loc, level in boss_locations: + elif boss_shuffle == Bosses.option_chaos: # all bosses chosen at random + for loc, level in remaining_locations: try: boss = world.random.choice( [b for b in placeable_bosses if can_place_boss(b, loc, level)]) @@ -282,9 +299,9 @@ def place_bosses(world, player: int): else: place_boss(world, player, boss, loc, level) - elif shuffle_mode == "singularity": + elif boss_shuffle == Bosses.option_singularity: primary_boss = world.random.choice(placeable_bosses) - remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, boss_locations) + remaining_boss_locations, _ = place_where_possible(world, player, primary_boss, remaining_locations) if remaining_boss_locations: # pick a boss to go into the remaining locations remaining_boss = world.random.choice([boss for boss in placeable_bosses if all( @@ -293,12 +310,12 @@ def place_bosses(world, player: int): if remaining_boss_locations: raise Exception("Unfilled boss locations!") else: - raise FillError(f"Could not find boss shuffle mode {shuffle_mode}") + raise FillError(f"Could not find boss shuffle mode {boss_shuffle}") -def place_where_possible(world, player: int, boss: str, boss_locations): - remainder = [] - placed_bosses = [] +def place_where_possible(world, player: int, boss: str, boss_locations) -> Tuple[List[Tuple[str, str]], List[str]]: + remainder: List[Tuple[str, str]] = [] + placed_bosses: List[str] = [] for loc, level in boss_locations: # place that boss where it can go if can_place_boss(boss, loc, level): diff --git a/worlds/alttp/Options.py b/worlds/alttp/Options.py index 184b8f3a20..b13d99f1e7 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 +from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice class Logic(Choice): @@ -138,13 +138,143 @@ class WorldState(Choice): option_inverted = 2 -class Bosses(Choice): - option_vanilla = 0 - option_simple = 1 +class Bosses(TextChoice): + """Shuffles bosses around to different locations. + Basic will shuffle all bosses except Ganon and Agahnim anywhere they can be placed. + Full chooses 3 bosses at random to be placed twice instead of Lanmolas, Moldorm, and Helmasaur. + Chaos allows any boss to appear any number of times. + Singularity places a single boss in as many places as possible, and a second boss in any remaining locations. + Supports plando placement. Formatting here: https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/plando/en""" + display_name = "Boss Shuffle" + option_none = 0 + option_basic = 1 option_full = 2 option_chaos = 3 option_singularity = 4 + bosses: set = { + "Armos Knights", + "Lanmolas", + "Moldorm", + "Helmasaur King", + "Arrghus", + "Mothula", + "Blind", + "Kholdstare", + "Vitreous", + "Trinexx", + } + + locations: set = { + "Ganons Tower Top", + "Tower of Hera", + "Skull Woods", + "Ganons Tower Middle", + "Eastern Palace", + "Desert Palace", + "Palace of Darkness", + "Swamp Palace", + "Thieves Town", + "Ice Palace", + "Misery Mire", + "Turtle Rock", + "Ganons Tower Bottom" + } + + def __init__(self, value: typing.Union[str, int]): + assert isinstance(value, str) or isinstance(value, int), \ + f"{value} is not a valid option for {self.__class__.__name__}" + self.value = value + + @classmethod + def from_text(cls, text: str): + import random + # set all of our text to lower case for name checking + text = text.lower() + cls.bosses = {boss_name.lower() for boss_name in cls.bosses} + cls.locations = {boss_location.lower() for boss_location in cls.locations} + if text == "random": + return cls(random.choice(list(cls.options.values()))) + for option_name, value in cls.options.items(): + if option_name == text: + return cls(value) + options = text.split(";") + + # since plando exists in the option verify the plando values given are valid + cls.validate_plando_bosses(options) + + # find out what type of boss shuffle we should use for placing bosses after plando + # and add as a string to look nice in the spoiler + if "random" in options: + shuffle = random.choice(list(cls.options)) + options.remove("random") + options = ";".join(options) + ";" + shuffle + boss_class = cls(options) + else: + for option in options: + if option in cls.options: + boss_class = cls(";".join(options)) + break + else: + if len(options) == 1: + if cls.valid_boss_name(options[0]): + options = options[0] + ";singularity" + boss_class = cls(options) + else: + options = options[0] + ";none" + boss_class = cls(options) + else: + options = ";".join(options) + ";none" + boss_class = cls(options) + return boss_class + + @classmethod + def validate_plando_bosses(cls, options: typing.List[str]) -> None: + from .Bosses import can_place_boss, format_boss_location + for option in options: + if option == "random" or option in cls.options: + if option != options[-1]: + raise ValueError(f"{option} option must be at the end of the boss_shuffle options!") + continue + if "-" in option: + location, boss = option.split("-") + level = '' + if not cls.valid_boss_name(boss): + raise ValueError(f"{boss} is not a valid boss name for location {location}.") + if not cls.valid_location_name(location): + raise ValueError(f"{location} is not a valid boss location name.") + if location.split(" ")[-1] in ("top", "middle", "bottom"): + location = location.split(" ") + level = location[-1] + location = " ".join(location[:-1]) + location = location.title().replace("Of", "of") + if not can_place_boss(boss.title(), location, level): + raise ValueError(f"{format_boss_location(location, level)} " + f"is not a valid location for {boss.title()}.") + else: + if not cls.valid_boss_name(option): + raise ValueError(f"{option} is not a valid boss name.") + + @classmethod + def valid_boss_name(cls, value: str) -> bool: + return value.lower() in cls.bosses + + @classmethod + def valid_location_name(cls, value: str) -> bool: + return value in cls.locations + + def verify(self, world, player_name: str, plando_options) -> None: + if isinstance(self.value, int): + return + from Generate import PlandoSettings + if not(PlandoSettings.bosses & plando_options): + import logging + # plando is disabled but plando options were given so pull the option and change it to an int + option = self.value.split(";")[-1] + self.value = self.options[option] + logging.warning(f"The plando bosses module is turned off, so {self.name_lookup[self.value].title()} " + f"boss shuffle will be used for player {player_name}.") + class Enemies(Choice): option_vanilla = 0 @@ -164,8 +294,8 @@ class Progressive(Choice): class Swordless(Toggle): - """No swords. Curtains in Skull Woods and Agahnim\'s - Tower are removed, Agahnim\'s Tower barrier can be + """No swords. Curtains in Skull Woods and Agahnim's + Tower are removed, Agahnim's Tower barrier can be destroyed with hammer. Misery Mire and Turtle Rock can be opened without a sword. Hammer damages Ganon. Ether and Bombos Tablet can be activated with Hammer @@ -367,6 +497,7 @@ alttp_options: typing.Dict[str, type(Option)] = { "hints": Hints, "scams": Scams, "restrict_dungeon_item_on_boss": RestrictBossItem, + "boss_shuffle": Bosses, "pot_shuffle": PotShuffle, "enemy_shuffle": EnemyShuffle, "killable_thieves": KillableThieves, diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index cb66ac4ff1..2aeeec3951 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -349,7 +349,7 @@ class ALTTPWorld(World): def use_enemizer(self): world = self.world player = self.player - return (world.boss_shuffle[player] != 'none' or world.enemy_shuffle[player] + return (world.boss_shuffle[player] or world.enemy_shuffle[player] or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default' or world.pot_shuffle[player] or world.bush_shuffle[player] or world.killable_thieves[player]) diff --git a/worlds/alttp/docs/plando_en.md b/worlds/alttp/docs/plando_en.md index 36322d9933..af8cbfe1b0 100644 --- a/worlds/alttp/docs/plando_en.md +++ b/worlds/alttp/docs/plando_en.md @@ -26,10 +26,14 @@ - Example: `Trinexx` - Takes a particular boss and places that boss in any remaining slots in which this boss can function. - In this example, it would fill Desert Palace, but not Tower of Hera. + - If no other options are provided this will follow normal singularity rules with that boss. - Boss Shuffle: - - Example: `simple` + - Example: `basic` - Runs a particular boss shuffle mode to finish construction instead of vanilla placement, typically used as a last instruction. + - Supports `random` which will choose a random option from the normal choices. + - If one is not supplied any remaining locations will be unshuffled unless a single specific boss is + supplied in which case it will use singularity as noted above. - [Available Bosses](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L135) - [Available Arenas](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/Bosses.py#L150) From 4fcde135e5cf015ebfa276ecaf078d2e7295d368 Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sun, 18 Sep 2022 04:20:59 +0200 Subject: [PATCH 78/91] The Witness: Renaming, Options, Logic Fixes (#1000) Fixes to postgame detection for "shuffle_postgame" Renamed many locations to be symbol-independent ("Outside Tutorial Dots Introduction" becomes "Outside Tutorial Shed Row"). This is to set up future alternate modes, like Sigma Expert, which use completely different symbols. Renamed most door items to be shorter, more consistent, and less... stupid. ("Bunker Bunker Entry Door" -> "Bunker Entry") Removed "shuffle_uncommon" Many logic fixes --- worlds/witness/Options.py | 17 +- worlds/witness/WitnessItems.txt | 232 ++-- worlds/witness/WitnessLogic.txt | 1086 +++++++++-------- worlds/witness/__init__.py | 2 +- worlds/witness/locations.py | 197 +-- worlds/witness/player_logic.py | 29 +- worlds/witness/regions.py | 2 +- .../witness/settings/Disable_Unrandomized.txt | 120 +- .../witness/settings/Door_Panel_Shuffle.txt | 34 +- worlds/witness/settings/Doors_Complex.txt | 343 +++--- worlds/witness/settings/Doors_Max.txt | 347 +++--- worlds/witness/settings/Doors_Simple.txt | 191 ++- worlds/witness/settings/Early_UTM.txt | 4 +- 13 files changed, 1307 insertions(+), 1297 deletions(-) diff --git a/worlds/witness/Options.py b/worlds/witness/Options.py index 631a5bc076..2cb9ade007 100644 --- a/worlds/witness/Options.py +++ b/worlds/witness/Options.py @@ -15,9 +15,9 @@ class DisableNonRandomizedPuzzles(DefaultOnToggle): class EarlySecretArea(Toggle): - """Opens the Mountainside shortcut to the Mountain Secret Area from the start. + """Opens the Mountainside shortcut to the Caves from the start. (Otherwise known as "UTM", "Caves" or the "Challenge Area")""" - display_name = "Early Secret Area" + display_name = "Early Caves" class ShuffleSymbols(DefaultOnToggle): @@ -58,15 +58,9 @@ class ShuffleVaultBoxes(Toggle): display_name = "Shuffle Vault Boxes" -class ShuffleUncommonLocations(Toggle): - """Adds some optional puzzles that are somewhat difficult or out of the way. - Examples: Mountaintop River Shape, Tutorial Patio Floor, Theater Videos""" - display_name = "Shuffle Uncommon Locations" - - class ShufflePostgame(Toggle): - """Adds locations into the pool that are guaranteed to be locked behind your goal. Use this if you don't play with - forfeit on victory.""" + """Adds locations into the pool that are guaranteed to become accessible before or at the same time as your goal. + Use this if you don't play with forfeit on victory.""" display_name = "Shuffle Postgame" @@ -90,7 +84,7 @@ class MountainLasers(Range): class ChallengeLasers(Range): - """Sets the amount of beams required to enter the secret area through the Mountain Bottom Layer Discard.""" + """Sets the amount of beams required to enter the Caves through the Mountain Bottom Floor Discard.""" display_name = "Required Lasers for Challenge" range_start = 1 range_end = 11 @@ -122,7 +116,6 @@ the_witness_options: Dict[str, type] = { "disable_non_randomized_puzzles": DisableNonRandomizedPuzzles, "shuffle_discarded_panels": ShuffleDiscardedPanels, "shuffle_vault_boxes": ShuffleVaultBoxes, - "shuffle_uncommon": ShuffleUncommonLocations, "shuffle_postgame": ShufflePostgame, "victory_condition": VictoryCondition, "mountain_lasers": MountainLasers, diff --git a/worlds/witness/WitnessItems.txt b/worlds/witness/WitnessItems.txt index 4449602529..fd9b10f97a 100644 --- a/worlds/witness/WitnessItems.txt +++ b/worlds/witness/WitnessItems.txt @@ -28,38 +28,38 @@ Traps: 610 - Power Surge Doors: -1100 - Glass Factory Entry Door (Panel) - 0x01A54 -1105 - Door to Symmetry Island Lower (Panel) - 0x000B0 -1107 - Door to Symmetry Island Upper (Panel) - 0x1C349 -1110 - Door to Desert Flood Light Room (Panel) - 0x0C339 -1111 - Desert Flood Room Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B -1119 - Quarry Door to Mill (Panel) - 0x01E5A,0x01E59 +1100 - Glass Factory Entry (Panel) - 0x01A54 +1105 - Symmetry Island Lower (Panel) - 0x000B0 +1107 - Symmetry Island Upper (Panel) - 0x1C349 +1110 - Desert Light Room Entry (Panel) - 0x0C339 +1111 - Desert Flood Controls (Panel) - 0x1C2DF,0x1831E,0x1C260,0x1831C,0x1C2F3,0x1831D,0x1C2B1,0x1831B +1119 - Quarry Mill Entry (Panel) - 0x01E5A,0x01E59 1120 - Quarry Mill Ramp Controls (Panel) - 0x03678,0x03676 -1122 - Quarry Mill Elevator Controls (Panel) - 0x03679,0x03675 +1122 - Quarry Mill Lift Controls (Panel) - 0x03679,0x03675 1125 - Quarry Boathouse Ramp Height Control (Panel) - 0x03852 1127 - Quarry Boathouse Ramp Horizontal Control (Panel) - 0x03858 1131 - Shadows Door Timer (Panel) - 0x334DB,0x334DC -1150 - Monastery Entry Door Left (Panel) - 0x00B10 -1151 - Monastery Entry Door Right (Panel) - 0x00C92 -1162 - Town Door to RGB House (Panel) - 0x28998 -1163 - Town Door to Church (Panel) - 0x28A0D +1150 - Monastery Entry Left (Panel) - 0x00B10 +1151 - Monastery Entry Right (Panel) - 0x00C92 +1162 - Town Tinted Glass Door (Panel) - 0x28998 +1163 - Town Church Entry (Panel) - 0x28A0D 1166 - Town Maze Panel (Drop-Down Staircase) (Panel) - 0x28A79 -1169 - Windmill Door (Panel) - 0x17F5F +1169 - Windmill Entry (Panel) - 0x17F5F 1200 - Treehouse First & Second Doors (Panel) - 0x0288C,0x02886 1202 - Treehouse Third Door (Panel) - 0x0A182 1205 - Treehouse Laser House Door Timer (Panel) - 0x2700B,0x334DC -1208 - Treehouse Shortcut Drop-Down Bridge (Panel) - 0x17CBC +1208 - Treehouse Drawbridge (Panel) - 0x17CBC 1175 - Jungle Popup Wall (Panel) - 0x17CAB -1180 - Bunker Entry Door (Panel) - 0x17C2E -1183 - Inside Bunker Door to Bunker Proper (Panel) - 0x0A099 +1180 - Bunker Entry (Panel) - 0x17C2E +1183 - Bunker Tinted Glass Door (Panel) - 0x0A099 1186 - Bunker Elevator Control (Panel) - 0x0A079 -1190 - Swamp Entry Door (Panel) - 0x0056E +1190 - Swamp Entry (Panel) - 0x0056E 1192 - Swamp Sliding Bridge (Panel) - 0x00609,0x18488 1195 - Swamp Rotating Bridge (Panel) - 0x181F5 1197 - Swamp Maze Control (Panel) - 0x17C0A 1310 - Boat - 0x17CDF,0x17CC8,0x17CA6,0x09DB8,0x17C95,0x0A054 -1400 - Caves Mountain Shortcut - 0x2D73F +1400 - Caves Mountain Shortcut (Door) - 0x2D73F 1500 - Symmetry Laser - 0x00509 1501 - Desert Laser - 0x012FB,0x01317 @@ -73,101 +73,101 @@ Doors: 1509 - Swamp Laser - 0x00BF6 1510 - Treehouse Laser - 0x028A4 -1600 - Outside Tutorial Optional Door - 0x03BA2 -1603 - Outside Tutorial Outpost Entry Door - 0x0A170 -1606 - Outside Tutorial Outpost Exit Door - 0x04CA3 -1609 - Glass Factory Entry Door - 0x01A29 -1612 - Glass Factory Back Wall - 0x0D7ED -1615 - Symmetry Island Lower Door - 0x17F3E -1618 - Symmetry Island Upper Door - 0x18269 -1619 - Orchard Middle Gate - 0x03307 -1620 - Orchard Final Gate - 0x03313 -1621 - Desert Door to Flood Light Room - 0x09FEE -1624 - Desert Door to Pond Room - 0x0C2C3 -1627 - Desert Door to Water Levels Room - 0x0A24B -1630 - Desert Door to Elevator Room - 0x0C316 -1633 - Quarry Main Entry 1 - 0x09D6F -1636 - Quarry Main Entry 2 - 0x17C07 -1639 - Quarry Door to Mill - 0x02010 -1642 - Quarry Mill Side Door - 0x275FF -1645 - Quarry Mill Rooftop Shortcut - 0x17CE8 -1648 - Quarry Mill Stairs - 0x0368A -1651 - Quarry Boathouse Boat Staircase - 0x2769B,0x27163 -1653 - Quarry Boathouse First Barrier - 0x17C50 -1654 - Quarry Boathouse Shortcut - 0x3865F -1656 - Shadows Timed Door - 0x19B24 -1657 - Shadows Laser Room Right Door - 0x194B2 -1660 - Shadows Laser Room Left Door - 0x19665 -1663 - Shadows Barrier to Quarry - 0x19865,0x0A2DF -1666 - Shadows Barrier to Ledge - 0x1855B,0x19ADE -1669 - Keep Hedge Maze 1 Exit Door - 0x01954 -1672 - Keep Pressure Plates 1 Exit Door - 0x01BEC -1675 - Keep Hedge Maze 2 Shortcut - 0x018CE -1678 - Keep Hedge Maze 2 Exit Door - 0x019D8 -1681 - Keep Hedge Maze 3 Shortcut - 0x019B5 -1684 - Keep Hedge Maze 3 Exit Door - 0x019E6 -1687 - Keep Hedge Maze 4 Shortcut - 0x0199A -1690 - Keep Hedge Maze 4 Exit Door - 0x01A0E -1693 - Keep Pressure Plates 2 Exit Door - 0x01BEA -1696 - Keep Pressure Plates 3 Exit Door - 0x01CD5 -1699 - Keep Pressure Plates 4 Exit Door - 0x01D40 -1702 - Keep Shortcut to Shadows - 0x09E3D -1705 - Keep Tower Shortcut - 0x04F8F -1708 - Monastery Shortcut - 0x0364E -1711 - Monastery Inner Door - 0x0C128 -1714 - Monastery Outer Door - 0x0C153 -1717 - Monastery Door to Garden - 0x03750 -1718 - Town Cargo Box Door - 0x0A0C9 -1720 - Town Wooden Roof Staircase - 0x034F5 -1723 - Town Tinted Door to RGB House - 0x28A61 -1726 - Town Door to Church - 0x03BB0 -1729 - Town Maze Staircase - 0x28AA2 -1732 - Town Windmill Door - 0x1845B -1735 - Town RGB House Staircase - 0x2897B -1738 - Town Tower Blue Panels Door - 0x27798 -1741 - Town Tower Lattice Door - 0x27799 -1744 - Town Tower Environmental Set Door - 0x2779A -1747 - Town Tower Wooden Roof Set Door - 0x2779C -1750 - Theater Entry Door - 0x17F88 -1753 - Theater Exit Door Left - 0x0A16D -1756 - Theater Exit Door Right - 0x3CCDF -1759 - Jungle Bamboo Shortcut to River - 0x3873B -1760 - Jungle Popup Wall - 0x1475B -1762 - River Shortcut to Monastery Garden - 0x0CF2A -1765 - Bunker Bunker Entry Door - 0x0C2A4 -1768 - Bunker Tinted Glass Door - 0x17C79 -1771 - Bunker Door to Ultraviolet Room - 0x0C2A3 -1774 - Bunker Door to Elevator - 0x0A08D -1777 - Swamp Entry Door - 0x00C1C -1780 - Swamp Door to Broken Shapers - 0x184B7 +1600 - Outside Tutorial Outpost Path (Door) - 0x03BA2 +1603 - Outside Tutorial Outpost Entry (Door) - 0x0A170 +1606 - Outside Tutorial Outpost Exit (Door) - 0x04CA3 +1609 - Glass Factory Entry (Door) - 0x01A29 +1612 - Glass Factory Back Wall (Door) - 0x0D7ED +1615 - Symmetry Island Lower (Door) - 0x17F3E +1618 - Symmetry Island Upper (Door) - 0x18269 +1619 - Orchard First Gate (Door) - 0x03307 +1620 - Orchard Second Gate (Door) - 0x03313 +1621 - Desert Light Room Entry (Door) - 0x09FEE +1624 - Desert Pond Room Entry (Door) - 0x0C2C3 +1627 - Desert Flood Room Entry (Door) - 0x0A24B +1630 - Desert Elevator Room Entry (Door) - 0x0C316 +1633 - Quarry Entry 1 (Door) - 0x09D6F +1636 - Quarry Entry 2 (Door) - 0x17C07 +1639 - Quarry Mill Entry (Door) - 0x02010 +1642 - Quarry Mill Side Exit (Door) - 0x275FF +1645 - Quarry Mill Roof Exit (Door) - 0x17CE8 +1648 - Quarry Mill Stairs (Door) - 0x0368A +1651 - Quarry Boathouse Dock (Door) - 0x2769B,0x27163 +1653 - Quarry Boathouse First Barrier (Door) - 0x17C50 +1654 - Quarry Boathouse Second Barrier (Door) - 0x3865F +1656 - Shadows Timed Door (Door) - 0x19B24 +1657 - Shadows Laser Entry Right (Door) - 0x194B2 +1660 - Shadows Laser Entry Left (Door) - 0x19665 +1663 - Shadows Quarry Barrier (Door) - 0x19865,0x0A2DF +1666 - Shadows Ledge Barrier (Door) - 0x1855B,0x19ADE +1669 - Keep Hedge Maze 1 Exit (Door) - 0x01954 +1672 - Keep Pressure Plates 1 Exit (Door) - 0x01BEC +1675 - Keep Hedge Maze 2 Shortcut (Door) - 0x018CE +1678 - Keep Hedge Maze 2 Exit (Door) - 0x019D8 +1681 - Keep Hedge Maze 3 Shortcut (Door) - 0x019B5 +1684 - Keep Hedge Maze 3 Exit (Door) - 0x019E6 +1687 - Keep Hedge Maze 4 Shortcut (Door) - 0x0199A +1690 - Keep Hedge Maze 4 Exit (Door) - 0x01A0E +1693 - Keep Pressure Plates 2 Exit (Door) - 0x01BEA +1696 - Keep Pressure Plates 3 Exit (Door) - 0x01CD5 +1699 - Keep Pressure Plates 4 Exit (Door) - 0x01D40 +1702 - Keep Shadows Shortcut (Door) - 0x09E3D +1705 - Keep Tower Shortcut (Door) - 0x04F8F +1708 - Monastery Shortcut (Door) - 0x0364E +1711 - Monastery Entry Inner (Door) - 0x0C128 +1714 - Monastery Entry Outer (Door) - 0x0C153 +1717 - Monastery Garden Entry (Door) - 0x03750 +1718 - Town Cargo Box Entry (Door) - 0x0A0C9 +1720 - Town Wooden Roof Stairs (Door) - 0x034F5 +1723 - Town Tinted Glass Door (Door) - 0x28A61 +1726 - Town Church Entry (Door) - 0x03BB0 +1729 - Town Maze Stairs (Door) - 0x28AA2 +1732 - Town Windmill Entry (Door) - 0x1845B +1735 - Town RGB House Stairs (Door) - 0x2897B +1738 - Town Tower First Door (Door) - 0x27798 +1741 - Town Tower Third Door (Door) - 0x27799 +1744 - Town Tower Fourth Door (Door) - 0x2779A +1747 - Town Tower Second Door (Door) - 0x2779C +1750 - Theater Entry (Door) - 0x17F88 +1753 - Theater Exit Left (Door) - 0x0A16D +1756 - Theater Exit Right (Door) - 0x3CCDF +1759 - Jungle Bamboo Laser Shortcut (Door) - 0x3873B +1760 - Jungle Popup Wall (Door) - 0x1475B +1762 - River Monastery Shortcut (Door) - 0x0CF2A +1765 - Bunker Entry (Door) - 0x0C2A4 +1768 - Bunker Tinted Glass Door (Door) - 0x17C79 +1771 - Bunker UV Room Entry (Door) - 0x0C2A3 +1774 - Bunker Elevator Room Entry (Door) - 0x0A08D +1777 - Swamp Entry (Door) - 0x00C1C +1780 - Swamp Between Bridges First Door - 0x184B7 1783 - Swamp Platform Shortcut Door - 0x38AE6 -1786 - Swamp Cyan Water Pump - 0x04B7F -1789 - Swamp Door to Rotated Shapers - 0x18507 -1792 - Swamp Red Water Pump - 0x183F2 -1795 - Swamp Red Underwater Exit - 0x305D5 -1798 - Swamp Blue Water Pump - 0x18482 -1801 - Swamp Purple Water Pump - 0x0A1D6 -1804 - Swamp Near Laser Shortcut - 0x2D880 -1807 - Treehouse First Door - 0x0C309 -1810 - Treehouse Second Door - 0x0C310 -1813 - Treehouse Beyond Yellow Bridge Door - 0x0A181 -1816 - Treehouse Drawbridge - 0x0C32D -1819 - Treehouse Timed Door to Laser House - 0x0C323 -1822 - Inside Mountain First Layer Exit Door - 0x09E54 -1825 - Inside Mountain Second Layer Staircase Near - 0x09FFB -1828 - Inside Mountain Second Layer Exit Door - 0x09EDD -1831 - Inside Mountain Second Layer Staircase Far - 0x09E07 -1834 - Inside Mountain Giant Puzzle Exit Door - 0x09F89 -1840 - Inside Mountain Door to Final Room - 0x0C141 -1843 - Inside Mountain Bottom Layer Rock - 0x17F33 -1846 - Inside Mountain Door to Secret Area - 0x2D77D -1849 - Caves Pillar Door - 0x019A5 -1855 - Caves Swamp Shortcut - 0x2D859 -1858 - Challenge Entry Door - 0x0A19A -1861 - Challenge Door to Theater Walkway - 0x0348A -1864 - Theater Walkway Door to Windmill Interior - 0x27739 -1867 - Theater Walkway Door to Desert Elevator Room - 0x27263 -1870 - Theater Walkway Door to Town - 0x09E87 +1786 - Swamp Cyan Water Pump (Door) - 0x04B7F +1789 - Swamp Between Bridges Second Door - 0x18507 +1792 - Swamp Red Water Pump (Door) - 0x183F2 +1795 - Swamp Red Underwater Exit (Door) - 0x305D5 +1798 - Swamp Blue Water Pump (Door) - 0x18482 +1801 - Swamp Purple Water Pump (Door) - 0x0A1D6 +1804 - Swamp Laser Shortcut (Door) - 0x2D880 +1807 - Treehouse First Door (Door) - 0x0C309 +1810 - Treehouse Second Door (Door) - 0x0C310 +1813 - Treehouse Third Door (Door) - 0x0A181 +1816 - Treehouse Drawbridge (Door) - 0x0C32D +1819 - Treehouse Laser House Entry (Door) - 0x0C323 +1822 - Mountain Floor 1 Exit (Door) - 0x09E54 +1825 - Mountain Floor 2 Staircase Near (Door) - 0x09FFB +1828 - Mountain Floor 2 Exit (Door) - 0x09EDD +1831 - Mountain Floor 2 Staircase Far (Door) - 0x09E07 +1834 - Mountain Bottom Floor Giant Puzzle Exit (Door) - 0x09F89 +1840 - Mountain Bottom Floor Final Room Entry (Door) - 0x0C141 +1843 - Mountain Bottom Floor Rock (Door) - 0x17F33 +1846 - Caves Entry (Door) - 0x2D77D +1849 - Caves Pillar Door (Door) - 0x019A5 +1855 - Caves Swamp Shortcut (Door) - 0x2D859 +1858 - Challenge Entry (Door) - 0x0A19A +1861 - Challenge Tunnels Entry (Door) - 0x0348A +1864 - Tunnels Theater Shortcut (Door) - 0x27739 +1867 - Tunnels Desert Shortcut (Door) - 0x27263 +1870 - Tunnels Town Shortcut (Door) - 0x09E87 1903 - Outside Tutorial Outpost Doors - 0x03BA2,0x0A170,0x04CA3 1906 - Symmetry Island Doors - 0x17F3E,0x18269 @@ -181,18 +181,18 @@ Doors: 1930 - Keep Hedge Maze Doors - 0x01954,0x018CE,0x019D8,0x019B5,0x019E6,0x0199A,0x01A0E 1933 - Keep Pressure Plates Doors - 0x01BEC,0x01BEA,0x01CD5,0x01D40 1936 - Keep Shortcuts - 0x09E3D,0x04F8F -1939 - Monastery Entry Door - 0x0C128,0x0C153 +1939 - Monastery Entry - 0x0C128,0x0C153 1942 - Monastery Shortcuts - 0x0364E,0x03750 1945 - Town Doors - 0x0A0C9,0x034F5,0x28A61,0x03BB0,0x28AA2,0x1845B,0x2897B 1948 - Town Tower Doors - 0x27798,0x27799,0x2779A,0x2779C -1951 - Theater Exit Door - 0x0A16D,0x3CCDF +1951 - Theater Exit - 0x0A16D,0x3CCDF 1954 - Jungle & River Shortcuts - 0x3873B,0x0CF2A 1957 - Bunker Doors - 0x0C2A4,0x17C79,0x0C2A3,0x0A08D 1960 - Swamp Doors - 0x00C1C,0x184B7,0x38AE6,0x18507 1963 - Swamp Water Pumps - 0x04B7F,0x183F2,0x305D5,0x18482,0x0A1D6 1966 - Treehouse Entry Doors - 0x0C309,0x0C310,0x0A181 -1975 - Inside Mountain Second Layer Stairs & Doors - 0x09FFB,0x09EDD,0x09E07 -1978 - Inside Mountain Bottom Layer Doors to Caves - 0x17F33,0x2D77D +1975 - Mountain Floor 2 Stairs & Doors - 0x09FFB,0x09EDD,0x09E07 +1978 - Mountain Bottom Floor Doors to Caves - 0x17F33,0x2D77D 1981 - Caves Doors to Challenge - 0x019A5,0x0A19A 1984 - Caves Exits to Main Island - 0x2D859,0x2D73F -1987 - Theater Walkway Doors - 0x27739,0x27263,0x09E87 \ No newline at end of file +1987 - Tunnels Doors - 0x27739,0x27263,0x09E87 \ No newline at end of file diff --git a/worlds/witness/WitnessLogic.txt b/worlds/witness/WitnessLogic.txt index c98257fb73..650cf14c52 100644 --- a/worlds/witness/WitnessLogic.txt +++ b/worlds/witness/WitnessLogic.txt @@ -14,49 +14,49 @@ Tutorial (Tutorial) - Outside Tutorial - 0x03629: 158010 - 0x0C373 (Patio Floor) - 0x0C335 - Dots Outside Tutorial (Outside Tutorial) - Outside Tutorial Path To Outpost - 0x03BA2: -158650 - 0x033D4 (Vault) - True - Dots & Squares & Black/White Squares +158650 - 0x033D4 (Vault) - True - Dots & Black/White Squares 158651 - 0x03481 (Vault Box) - 0x033D4 - True -158013 - 0x0005D (Dots Introduction 1) - True - Dots -158014 - 0x0005E (Dots Introduction 2) - 0x0005D - Dots -158015 - 0x0005F (Dots Introduction 3) - 0x0005E - Dots -158016 - 0x00060 (Dots Introduction 4) - 0x0005F - Dots -158017 - 0x00061 (Dots Introduction 5) - 0x00060 - Dots -158018 - 0x018AF (Squares Introduction 1) - True - Squares & Black/White Squares -158019 - 0x0001B (Squares Introduction 2) - 0x018AF - Squares & Black/White Squares -158020 - 0x012C9 (Squares Introduction 3) - 0x0001B - Squares & Black/White Squares -158021 - 0x0001C (Squares Introduction 4) - 0x012C9 - Squares & Black/White Squares -158022 - 0x0001D (Squares Introduction 5) - 0x0001C - Squares & Black/White Squares -158023 - 0x0001E (Squares Introduction 6) - 0x0001D - Squares & Black/White Squares -158024 - 0x0001F (Squares Introduction 7) - 0x0001E - Squares & Black/White Squares -158025 - 0x00020 (Squares Introduction 8) - 0x0001F - Squares & Black/White Squares -158026 - 0x00021 (Squares Introduction 9) - 0x00020 - Squares & Black/White Squares -Door - 0x03BA2 (Optional Door 1) - 0x0A3B5 +158013 - 0x0005D (Shed Row 1) - True - Dots +158014 - 0x0005E (Shed Row 2) - 0x0005D - Dots +158015 - 0x0005F (Shed Row 3) - 0x0005E - Dots +158016 - 0x00060 (Shed Row 4) - 0x0005F - Dots +158017 - 0x00061 (Shed Row 5) - 0x00060 - Dots +158018 - 0x018AF (Tree Row 1) - True - Black/White Squares +158019 - 0x0001B (Tree Row 2) - 0x018AF - Black/White Squares +158020 - 0x012C9 (Tree Row 3) - 0x0001B - Black/White Squares +158021 - 0x0001C (Tree Row 4) - 0x012C9 - Black/White Squares +158022 - 0x0001D (Tree Row 5) - 0x0001C - Black/White Squares +158023 - 0x0001E (Tree Row 6) - 0x0001D - Black/White Squares +158024 - 0x0001F (Tree Row 7) - 0x0001E - Black/White Squares +158025 - 0x00020 (Tree Row 8) - 0x0001F - Black/White Squares +158026 - 0x00021 (Tree Row 9) - 0x00020 - Black/White Squares +Door - 0x03BA2 (Outpost Path) - 0x0A3B5 Outside Tutorial Path To Outpost (Outside Tutorial) - Outside Tutorial Outpost - 0x0A170: -158011 - 0x0A171 (Door to Outpost Panel) - True - Dots -Door - 0x0A170 (Door to Outpost) - 0x0A171 +158011 - 0x0A171 (Outpost Entry Panel) - True - Dots +Door - 0x0A170 (Outpost Entry) - 0x0A171 Outside Tutorial Outpost (Outside Tutorial) - Outside Tutorial - 0x04CA3: -158012 - 0x04CA4 (Exit Door from Outpost Panel) - True - Dots & Squares & Black/White Squares -Door - 0x04CA3 (Exit Door from Outpost) - 0x04CA4 +158012 - 0x04CA4 (Outpost Exit Panel) - True - Dots & Black/White Squares +Door - 0x04CA3 (Outpost Exit) - 0x04CA4 158600 - 0x17CFB (Discard) - True - Triangles Main Island () - Outside Tutorial - True: Outside Glass Factory (Glass Factory) - Main Island - True - Inside Glass Factory - 0x01A29: -158027 - 0x01A54 (Entry Door Panel) - True - Symmetry -Door - 0x01A29 (Entry Door) - 0x01A54 +158027 - 0x01A54 (Entry Panel) - True - Symmetry +Door - 0x01A29 (Entry) - 0x01A54 158601 - 0x3C12B (Discard) - True - Triangles Inside Glass Factory (Glass Factory) - Inside Glass Factory Behind Back Wall - 0x0D7ED: -158028 - 0x00086 (Vertical Symmetry 1) - True - Symmetry -158029 - 0x00087 (Vertical Symmetry 2) - 0x00086 - Symmetry -158030 - 0x00059 (Vertical Symmetry 3) - 0x00087 - Symmetry -158031 - 0x00062 (Vertical Symmetry 4) - 0x00059 - Symmetry -158032 - 0x0005C (Vertical Symmetry 5) - 0x00062 - Symmetry -158033 - 0x0008D (Rotational Symmetry 1) - 0x0005C - Symmetry -158034 - 0x00081 (Rotational Symmetry 2) - 0x0008D - Symmetry -158035 - 0x00083 (Rotational Symmetry 3) - 0x00081 - Symmetry +158028 - 0x00086 (Back Wall 1) - True - Symmetry +158029 - 0x00087 (Back Wall 2) - 0x00086 - Symmetry +158030 - 0x00059 (Back Wall 3) - 0x00087 - Symmetry +158031 - 0x00062 (Back Wall 4) - 0x00059 - Symmetry +158032 - 0x0005C (Back Wall 5) - 0x00062 - Symmetry +158033 - 0x0008D (Front 1) - 0x0005C - Symmetry +158034 - 0x00081 (Front 2) - 0x0008D - Symmetry +158035 - 0x00083 (Front 3) - 0x00081 - Symmetry 158036 - 0x00084 (Melting 1) - 0x00083 - Symmetry 158037 - 0x00082 (Melting 2) - 0x00084 - Symmetry 158038 - 0x0343A (Melting 3) - 0x00082 - Symmetry @@ -66,35 +66,35 @@ Inside Glass Factory Behind Back Wall (Glass Factory) - Boat - 0x17CC8: 158039 - 0x17CC8 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat Outside Symmetry Island (Symmetry Island) - Main Island - True - Symmetry Island Lower - 0x17F3E: -158040 - 0x000B0 (Door to Symmetry Island Lower Panel) - 0x0343A - Dots -Door - 0x17F3E (Door to Symmetry Island Lower) - 0x000B0 +158040 - 0x000B0 (Lower Panel) - 0x0343A - Dots +Door - 0x17F3E (Lower) - 0x000B0 Symmetry Island Lower (Symmetry Island) - Symmetry Island Upper - 0x18269: -158041 - 0x00022 (Black Dots 1) - True - Symmetry & Dots -158042 - 0x00023 (Black Dots 2) - 0x00022 - Symmetry & Dots -158043 - 0x00024 (Black Dots 3) - 0x00023 - Symmetry & Dots -158044 - 0x00025 (Black Dots 4) - 0x00024 - Symmetry & Dots -158045 - 0x00026 (Black Dots 5) - 0x00025 - Symmetry & Dots -158046 - 0x0007C (Colored Dots 1) - 0x00026 - Symmetry & Colored Dots -158047 - 0x0007E (Colored Dots 2) - 0x0007C - Symmetry & Colored Dots -158048 - 0x00075 (Colored Dots 3) - 0x0007E - Symmetry & Colored Dots -158049 - 0x00073 (Colored Dots 4) - 0x00075 - Symmetry & Colored Dots -158050 - 0x00077 (Colored Dots 5) - 0x00073 - Symmetry & Colored Dots -158051 - 0x00079 (Colored Dots 6) - 0x00077 - Symmetry & Colored Dots -158052 - 0x00065 (Fading Lines 1) - 0x00079 - Symmetry & Colored Dots -158053 - 0x0006D (Fading Lines 2) - 0x00065 - Symmetry & Colored Dots -158054 - 0x00072 (Fading Lines 3) - 0x0006D - Symmetry & Colored Dots -158055 - 0x0006F (Fading Lines 4) - 0x00072 - Symmetry & Colored Dots -158056 - 0x00070 (Fading Lines 5) - 0x0006F - Symmetry & Colored Dots -158057 - 0x00071 (Fading Lines 6) - 0x00070 - Symmetry & Colored Dots -158058 - 0x00076 (Fading Lines 7) - 0x00071 - Symmetry & Colored Dots -158059 - 0x009B8 (Scenery Outlines 1) - True - Symmetry & Environment -158060 - 0x003E8 (Scenery Outlines 2) - 0x009B8 - Symmetry & Environment -158061 - 0x00A15 (Scenery Outlines 3) - 0x003E8 - Symmetry & Environment -158062 - 0x00B53 (Scenery Outlines 4) - 0x00A15 - Symmetry & Environment -158063 - 0x00B8D (Scenery Outlines 5) - 0x00B53 - Symmetry & Environment -158064 - 0x1C349 (Door to Symmetry Island Upper Panel) - 0x00076 - Symmetry & Dots -Door - 0x18269 (Door to Symmetry Island Upper) - 0x1C349 +158041 - 0x00022 (Right 1) - True - Symmetry & Dots +158042 - 0x00023 (Right 2) - 0x00022 - Symmetry & Dots +158043 - 0x00024 (Right 3) - 0x00023 - Symmetry & Dots +158044 - 0x00025 (Right 4) - 0x00024 - Symmetry & Dots +158045 - 0x00026 (Right 5) - 0x00025 - Symmetry & Dots +158046 - 0x0007C (Back 1) - 0x00026 - Symmetry & Colored Dots +158047 - 0x0007E (Back 2) - 0x0007C - Symmetry & Colored Dots +158048 - 0x00075 (Back 3) - 0x0007E - Symmetry & Colored Dots +158049 - 0x00073 (Back 4) - 0x00075 - Symmetry & Colored Dots +158050 - 0x00077 (Back 5) - 0x00073 - Symmetry & Colored Dots +158051 - 0x00079 (Back 6) - 0x00077 - Symmetry & Colored Dots +158052 - 0x00065 (Left 1) - 0x00079 - Symmetry & Colored Dots +158053 - 0x0006D (Left 2) - 0x00065 - Symmetry & Colored Dots +158054 - 0x00072 (Left 3) - 0x0006D - Symmetry & Colored Dots +158055 - 0x0006F (Left 4) - 0x00072 - Symmetry & Colored Dots +158056 - 0x00070 (Left 5) - 0x0006F - Symmetry & Colored Dots +158057 - 0x00071 (Left 6) - 0x00070 - Symmetry & Colored Dots +158058 - 0x00076 (Left 7) - 0x00071 - Symmetry & Colored Dots +158059 - 0x009B8 (Scenery Outlines 1) - True - Symmetry +158060 - 0x003E8 (Scenery Outlines 2) - 0x009B8 - Symmetry +158061 - 0x00A15 (Scenery Outlines 3) - 0x003E8 - Symmetry +158062 - 0x00B53 (Scenery Outlines 4) - 0x00A15 - Symmetry +158063 - 0x00B8D (Scenery Outlines 5) - 0x00B53 - Symmetry +158064 - 0x1C349 (Upper Panel) - 0x00076 - Symmetry & Dots +Door - 0x18269 (Upper) - 0x1C349 Symmetry Island Upper (Symmetry Island): 158065 - 0x00A52 (Yellow 1) - True - Symmetry & Colored Dots @@ -107,15 +107,15 @@ Symmetry Island Upper (Symmetry Island): Laser - 0x00509 (Laser) - 0x0360D - True Orchard (Orchard) - Main Island - True - Orchard Beyond First Gate - 0x03307: -158071 - 0x00143 (Apple Tree 1) - True - Environment -158072 - 0x0003B (Apple Tree 2) - 0x00143 - Environment -158073 - 0x00055 (Apple Tree 3) - 0x0003B - Environment -Door - 0x03307 (Mid Gate) - 0x00055 +158071 - 0x00143 (Apple Tree 1) - True - True +158072 - 0x0003B (Apple Tree 2) - 0x00143 - True +158073 - 0x00055 (Apple Tree 3) - 0x0003B - True +Door - 0x03307 (First Gate) - 0x00055 Orchard Beyond First Gate (Orchard) - Orchard End - 0x03313: -158074 - 0x032F7 (Apple Tree 4) - 0x00055 - Environment -158075 - 0x032FF (Apple Tree 5) - 0x032F7 - Environment -Door - 0x03313 (Final Gate) - 0x032FF +158074 - 0x032F7 (Apple Tree 4) - 0x00055 - True +158075 - 0x032FF (Apple Tree 5) - 0x032F7 - True +Door - 0x03313 (Second Gate) - 0x032FF Orchard End (Orchard): @@ -123,34 +123,36 @@ Desert Outside (Desert) - Main Island - True - Desert Floodlight Room - 0x09FEE: 158652 - 0x0CC7B (Vault) - True - Dots & Shapers & Rotated Shapers & Negative Shapers 158653 - 0x0339E (Vault Box) - 0x0CC7B - True 158602 - 0x17CE7 (Discard) - True - Triangles -158076 - 0x00698 (Sun Reflection 1) - True - Reflection -158077 - 0x0048F (Sun Reflection 2) - 0x00698 - Reflection -158078 - 0x09F92 (Sun Reflection 3) - 0x0048F & 0x09FA0 - Reflection -158079 - 0x09FA0 (Reflection 3 Control) - 0x0048F - True -158080 - 0x0A036 (Sun Reflection 4) - 0x09F92 - Reflection -158081 - 0x09DA6 (Sun Reflection 5) - 0x09F92 - Reflection -158082 - 0x0A049 (Sun Reflection 6) - 0x09F92 - Reflection -158083 - 0x0A053 (Sun Reflection 7) - 0x0A036 & 0x09DA6 & 0x0A049 - Reflection -158084 - 0x09F94 (Sun Reflection 8) - 0x0A053 & 0x09F86 - Reflection -158085 - 0x09F86 (Reflection 8 Control) - 0x0A053 - True -158086 - 0x0C339 (Door to Desert Flood Light Room Panel) - 0x09F94 - True -Door - 0x09FEE (Door to Desert Flood Light Room) - 0x0C339 - True +158076 - 0x00698 (Surface 1) - True - True +158077 - 0x0048F (Surface 2) - 0x00698 - True +158078 - 0x09F92 (Surface 3) - 0x0048F & 0x09FA0 - True +158079 - 0x09FA0 (Surface 3 Control) - 0x0048F - True +158080 - 0x0A036 (Surface 4) - 0x09F92 - True +158081 - 0x09DA6 (Surface 5) - 0x09F92 - True +158082 - 0x0A049 (Surface 6) - 0x09F92 - True +158083 - 0x0A053 (Surface 7) - 0x0A036 & 0x09DA6 & 0x0A049 - True +158084 - 0x09F94 (Surface 8) - 0x0A053 & 0x09F86 - True +158085 - 0x09F86 (Surface 8 Control) - 0x0A053 - True +158086 - 0x0C339 (Light Room Entry Panel) - 0x09F94 - True +Door - 0x09FEE (Light Room Entry) - 0x0C339 - True +158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True +Laser - 0x012FB (Laser) - 0x03608 Desert Floodlight Room (Desert) - Desert Pond Room - 0x0C2C3: 158087 - 0x09FAA (Light Control) - True - True -158088 - 0x00422 (Artificial Light Reflection 1) - 0x09FAA - Reflection -158089 - 0x006E3 (Artificial Light Reflection 2) - 0x09FAA - Reflection -158090 - 0x0A02D (Artificial Light Reflection 3) - 0x09FAA & 0x00422 & 0x006E3 - Reflection -Door - 0x0C2C3 (Door to Pond Room) - 0x0A02D +158088 - 0x00422 (Light Room 1) - 0x09FAA - True +158089 - 0x006E3 (Light Room 2) - 0x09FAA - True +158090 - 0x0A02D (Light Room 3) - 0x09FAA & 0x00422 & 0x006E3 - True +Door - 0x0C2C3 (Pond Room Entry) - 0x0A02D Desert Pond Room (Desert) - Desert Water Levels Room - 0x0A24B: -158091 - 0x00C72 (Pond Reflection 1) - True - Reflection -158092 - 0x0129D (Pond Reflection 2) - 0x00C72 - Reflection -158093 - 0x008BB (Pond Reflection 3) - 0x0129D - Reflection -158094 - 0x0078D (Pond Reflection 4) - 0x008BB - Reflection -158095 - 0x18313 (Pond Reflection 5) - 0x0078D - Reflection -158096 - 0x0A249 (Door to Water Levels Room Panel) - 0x18313 - Reflection -Door - 0x0A24B (Door to Water Levels Room) - 0x0A249 +158091 - 0x00C72 (Pond Room 1) - True - True +158092 - 0x0129D (Pond Room 2) - 0x00C72 - True +158093 - 0x008BB (Pond Room 3) - 0x0129D - True +158094 - 0x0078D (Pond Room 4) - 0x008BB - True +158095 - 0x18313 (Pond Room 5) - 0x0078D - True +158096 - 0x0A249 (Flood Room Entry Panel) - 0x18313 - True +Door - 0x0A24B (Flood Room Entry) - 0x0A249 Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316: 158097 - 0x1C2DF (Reduce Water Level Far Left) - True - True @@ -161,249 +163,247 @@ Desert Water Levels Room (Desert) - Desert Elevator Room - 0x0C316: 158102 - 0x1831D (Raise Water Level Far Right) - True - True 158103 - 0x1C2B1 (Raise Water Level Near Left) - True - True 158104 - 0x1831B (Raise Water Level Near Right) - True - True -158105 - 0x04D18 (Flood Reflection 1) - 0x1C260 & 0x1831C - Reflection -158106 - 0x01205 (Flood Reflection 2) - 0x04D18 & 0x1C260 & 0x1831C - Reflection -158107 - 0x181AB (Flood Reflection 3) - 0x01205 & 0x1C260 & 0x1831C - Reflection -158108 - 0x0117A (Flood Reflection 4) - 0x181AB & 0x1C260 & 0x1831C - Reflection -158109 - 0x17ECA (Flood Reflection 5) - 0x0117A & 0x1C260 & 0x1831C - Reflection -158110 - 0x18076 (Flood Reflection 6) - 0x17ECA & 0x1C260 & 0x1831C - Reflection -Door - 0x0C316 (Door to Elevator Room) - 0x18076 +158105 - 0x04D18 (Flood Room 1) - 0x1C260 & 0x1831C - True +158106 - 0x01205 (Flood Room 2) - 0x04D18 & 0x1C260 & 0x1831C - True +158107 - 0x181AB (Flood Room 3) - 0x01205 & 0x1C260 & 0x1831C - True +158108 - 0x0117A (Flood Room 4) - 0x181AB & 0x1C260 & 0x1831C - True +158109 - 0x17ECA (Flood Room 5) - 0x0117A & 0x1C260 & 0x1831C - True +158110 - 0x18076 (Flood Room 6) - 0x17ECA & 0x1C260 & 0x1831C - True +Door - 0x0C316 (Elevator Room Entry) - 0x18076 Desert Elevator Room (Desert) - Desert Lowest Level Inbetween Shortcuts - 0x012FB: -158111 - 0x17C31 (Final Transparent Reflection) - True - Reflection -158113 - 0x012D7 (Final Reflection) - 0x17C31 & 0x0A015 - Reflection -158114 - 0x0A015 (Final Reflection Control) - 0x17C31 - True -158115 - 0x0A15C (Final Bent Reflection 1) - True - Reflection -158116 - 0x09FFF (Final Bent Reflection 2) - 0x0A15C - Reflection -158117 - 0x0A15F (Final Bent Reflection 3) - 0x09FFF - Reflection -158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True -Laser - 0x012FB (Laser) - 0x03608 +158111 - 0x17C31 (Final Transparent) - True - True +158113 - 0x012D7 (Final Hexagonal) - 0x17C31 & 0x0A015 - True +158114 - 0x0A015 (Final Hexagonal Control) - 0x17C31 - True +158115 - 0x0A15C (Final Bent 1) - True - True +158116 - 0x09FFF (Final Bent 2) - 0x0A15C - True +158117 - 0x0A15F (Final Bent 3) - 0x09FFF - True Desert Lowest Level Inbetween Shortcuts (Desert): -Outside Quarry (Quarry) - Main Island - True - Quarry Between Entry Doors - 0x09D6F: -158118 - 0x09E57 (Door to Quarry 1 Panel) - True - Squares & Black/White Squares +Outside Quarry (Quarry) - Main Island - True - Quarry Between Entrys - 0x09D6F: +158118 - 0x09E57 (Entry 1 Panel) - True - Black/White Squares 158120 - 0x17CC4 (Elevator Control) - 0x0367C - Dots & Eraser 158603 - 0x17CF0 (Discard) - True - Triangles 158702 - 0x03612 (Laser Panel) - 0x0A3D0 & 0x0367C - Eraser & Shapers Laser - 0x01539 (Laser) - 0x03612 -Door - 0x09D6F (Door to Quarry 1) - 0x09E57 +Door - 0x09D6F (Entry 1) - 0x09E57 -Quarry Between Entry Doors (Quarry) - Quarry - 0x17C07: -158119 - 0x17C09 (Door to Quarry 2 Panel) - True - Shapers -Door - 0x17C07 (Door to Quarry 2) - 0x17C09 +Quarry Between Entrys (Quarry) - Quarry - 0x17C07: +158119 - 0x17C09 (Entry 2 Panel) - True - Shapers +Door - 0x17C07 (Entry 2) - 0x17C09 Quarry (Quarry) - Quarry Mill Ground Floor - 0x02010: -158121 - 0x01E5A (Door to Mill Left) - True - Squares & Black/White Squares -158122 - 0x01E59 (Door to Mill Right) - True - Dots -Door - 0x02010 (Door to Mill) - 0x01E59 & 0x01E5A +158121 - 0x01E5A (Mill Entry Left Panel) - True - Black/White Squares +158122 - 0x01E59 (Mill Entry Right Panel) - True - Dots +Door - 0x02010 (Mill Entry) - 0x01E59 & 0x01E5A Quarry Mill Ground Floor (Quarry Mill) - Quarry - 0x275FF - Quarry Mill Middle Floor - 0x03678 - Outside Quarry - 0x17CE8: -158123 - 0x275ED (Ground Floor Shortcut Door Panel) - True - True -Door - 0x275FF (Ground Floor Shortcut Door) - 0x275ED +158123 - 0x275ED (Side Exit Panel) - True - True +Door - 0x275FF (Side Exit) - 0x275ED 158124 - 0x03678 (Lower Ramp Control) - True - Dots & Eraser -158145 - 0x17CAC (Door to Outside Quarry Stairs Panel) - True - True -Door - 0x17CE8 (Door to Outside Quarry Stairs) - 0x17CAC +158145 - 0x17CAC (Roof Exit Panel) - True - True +Door - 0x17CE8 (Roof Exit) - 0x17CAC Quarry Mill Middle Floor (Quarry Mill) - Quarry Mill Ground Floor - 0x03675 - Quarry Mill Upper Floor - 0x03679: -158125 - 0x00E0C (Eraser and Dots 1) - True - Dots & Eraser -158126 - 0x01489 (Eraser and Dots 2) - 0x00E0C - Dots & Eraser -158127 - 0x0148A (Eraser and Dots 3) - 0x01489 - Dots & Eraser -158128 - 0x014D9 (Eraser and Dots 4) - 0x0148A - Dots & Eraser -158129 - 0x014E7 (Eraser and Dots 5) - 0x014D9 - Dots & Eraser -158130 - 0x014E8 (Eraser and Dots 6) - 0x014E7 - Dots & Eraser +158125 - 0x00E0C (Lower Row 1) - True - Dots & Eraser +158126 - 0x01489 (Lower Row 2) - 0x00E0C - Dots & Eraser +158127 - 0x0148A (Lower Row 3) - 0x01489 - Dots & Eraser +158128 - 0x014D9 (Lower Row 4) - 0x0148A - Dots & Eraser +158129 - 0x014E7 (Lower Row 5) - 0x014D9 - Dots & Eraser +158130 - 0x014E8 (Lower Row 6) - 0x014E7 - Dots & Eraser 158131 - 0x03679 (Lower Lift Control) - 0x014E8 - Dots & Eraser Quarry Mill Upper Floor (Quarry Mill) - Quarry Mill Middle Floor - 0x03676 & 0x03679 - Quarry Mill Ground Floor - 0x0368A: 158132 - 0x03676 (Upper Ramp Control) - True - Dots & Eraser 158133 - 0x03675 (Upper Lift Control) - True - Dots & Eraser -158134 - 0x00557 (Eraser and Squares 1) - True - Squares & Colored Squares & Eraser -158135 - 0x005F1 (Eraser and Squares 2) - 0x00557 - Squares & Colored Squares & Eraser -158136 - 0x00620 (Eraser and Squares 3) - 0x005F1 - Squares & Colored Squares & Eraser -158137 - 0x009F5 (Eraser and Squares 4) - 0x00620 - Squares & Colored Squares & Eraser -158138 - 0x0146C (Eraser and Squares 5) - 0x009F5 - Squares & Colored Squares & Eraser -158139 - 0x3C12D (Eraser and Squares 6) - 0x0146C - Squares & Colored Squares & Eraser -158140 - 0x03686 (Eraser and Squares 7) - 0x3C12D - Squares & Colored Squares & Eraser -158141 - 0x014E9 (Eraser and Squares 8) - 0x03686 - Squares & Colored Squares & Eraser -158142 - 0x03677 (Stair Control) - True - Squares & Colored Squares & Eraser +158134 - 0x00557 (Upper Row 1) - True - Colored Squares & Eraser +158135 - 0x005F1 (Upper Row 2) - 0x00557 - Colored Squares & Eraser +158136 - 0x00620 (Upper Row 3) - 0x005F1 - Colored Squares & Eraser +158137 - 0x009F5 (Upper Row 4) - 0x00620 - Colored Squares & Eraser +158138 - 0x0146C (Upper Row 5) - 0x009F5 - Colored Squares & Eraser +158139 - 0x3C12D (Upper Row 6) - 0x0146C - Colored Squares & Eraser +158140 - 0x03686 (Upper Row 7) - 0x3C12D - Colored Squares & Eraser +158141 - 0x014E9 (Upper Row 8) - 0x03686 - Colored Squares & Eraser +158142 - 0x03677 (Stair Control) - True - Colored Squares & Eraser Door - 0x0368A (Stairs) - 0x03677 -158143 - 0x3C125 (Big Squares & Dots & Eraser) - 0x0367C - Squares & Black/White Squares & Dots & Eraser -158144 - 0x0367C (Small Squares & Dots & Eraser) - 0x014E9 - Squares & Colored Squares & Dots & Eraser +158143 - 0x3C125 (Control Room Left) - 0x0367C - Black/White Squares & Dots & Eraser +158144 - 0x0367C (Control Room Right) - 0x014E9 - Colored Squares & Dots & Eraser Quarry Boathouse (Quarry Boathouse) - Quarry - True - Quarry Boathouse Upper Front - 0x03852 - Quarry Boathouse Behind Staircase - 0x2769B: -158146 - 0x034D4 (Intro Stars) - True - Stars -158147 - 0x021D5 (Intro Shapers) - True - Shapers & Rotated Shapers +158146 - 0x034D4 (Intro Left) - True - Stars +158147 - 0x021D5 (Intro Right) - True - Shapers & Rotated Shapers 158148 - 0x03852 (Ramp Height Control) - 0x034D4 & 0x021D5 - Rotated Shapers 158166 - 0x17CA6 (Boat Spawn) - True - Boat -Door - 0x2769B (Boat Staircase) - 0x17CA6 -Door - 0x27163 (Boat Staircase Invis Barrier) - 0x17CA6 +Door - 0x2769B (Dock) - 0x17CA6 +Door - 0x27163 (Dock Invis Barrier) - 0x17CA6 Quarry Boathouse Behind Staircase (Quarry Boathouse) - Boat - 0x17CA6: Quarry Boathouse Upper Front (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x17C50: -158149 - 0x021B3 (Eraser and Shapers 1) - True - Shapers & Eraser -158150 - 0x021B4 (Eraser and Shapers 2) - 0x021B3 - Shapers & Eraser -158151 - 0x021B0 (Eraser and Shapers 3) - 0x021B4 - Shapers & Eraser -158152 - 0x021AF (Eraser and Shapers 4) - 0x021B0 - Shapers & Eraser -158153 - 0x021AE (Eraser and Shapers 5) - 0x021AF - Shapers & Eraser & Broken Shapers -Door - 0x17C50 (Boathouse Barrier 1) - 0x021AE +158149 - 0x021B3 (Front Row 1) - True - Shapers & Eraser +158150 - 0x021B4 (Front Row 2) - 0x021B3 - Shapers & Eraser +158151 - 0x021B0 (Front Row 3) - 0x021B4 - Shapers & Eraser +158152 - 0x021AF (Front Row 4) - 0x021B0 - Shapers & Eraser +158153 - 0x021AE (Front Row 5) - 0x021AF - Shapers & Eraser +Door - 0x17C50 (First Barrier) - 0x021AE Quarry Boathouse Upper Middle (Quarry Boathouse) - Quarry Boathouse Upper Back - 0x03858: 158154 - 0x03858 (Ramp Horizontal Control) - True - Shapers & Eraser Quarry Boathouse Upper Back (Quarry Boathouse) - Quarry Boathouse Upper Middle - 0x3865F: -158155 - 0x38663 (Shortcut Door Panel) - True - True -Door - 0x3865F (Shortcut Door) - 0x38663 -158156 - 0x021B5 (Stars and Colored Eraser 1) - True - Stars & Stars + Same Colored Symbol & Eraser -158157 - 0x021B6 (Stars and Colored Eraser 2) - 0x021B5 - Stars & Stars + Same Colored Symbol & Eraser -158158 - 0x021B7 (Stars and Colored Eraser 3) - 0x021B6 - Stars & Stars + Same Colored Symbol & Eraser -158159 - 0x021BB (Stars and Colored Eraser 4) - 0x021B7 - Stars & Stars + Same Colored Symbol & Eraser -158160 - 0x09DB5 (Stars and Colored Eraser 5) - 0x021BB - Stars & Stars + Same Colored Symbol & Eraser -158161 - 0x09DB1 (Stars and Colored Eraser 6) - 0x09DB5 - Stars & Stars + Same Colored Symbol & Eraser -158162 - 0x3C124 (Stars and Colored Eraser 7) - 0x09DB1 - Stars & Stars + Same Colored Symbol & Eraser -158163 - 0x09DB3 (Stars & Eraser & Shapers 1) - 0x3C124 - Stars & Eraser & Shapers -158164 - 0x09DB4 (Stars & Eraser & Shapers 2) - 0x09DB3 - Stars & Eraser & Shapers +158155 - 0x38663 (Second Barrier Panel) - True - True +Door - 0x3865F (Second Barrier) - 0x38663 +158156 - 0x021B5 (Back First Row 1) - True - Stars & Stars + Same Colored Symbol & Eraser +158157 - 0x021B6 (Back First Row 2) - 0x021B5 - Stars & Stars + Same Colored Symbol & Eraser +158158 - 0x021B7 (Back First Row 3) - 0x021B6 - Stars & Stars + Same Colored Symbol & Eraser +158159 - 0x021BB (Back First Row 4) - 0x021B7 - Stars & Stars + Same Colored Symbol & Eraser +158160 - 0x09DB5 (Back First Row 5) - 0x021BB - Stars & Stars + Same Colored Symbol & Eraser +158161 - 0x09DB1 (Back First Row 6) - 0x09DB5 - Stars & Stars + Same Colored Symbol & Eraser +158162 - 0x3C124 (Back First Row 7) - 0x09DB1 - Stars & Stars + Same Colored Symbol & Eraser +158163 - 0x09DB3 (Back First Row 8) - 0x3C124 - Stars & Eraser & Shapers +158164 - 0x09DB4 (Back First Row 9) - 0x09DB3 - Stars & Eraser & Shapers 158165 - 0x275FA (Hook Control) - True - Shapers & Eraser -158167 - 0x0A3CB (Stars & Eraser & Shapers 3) - 0x09DB4 - Stars & Eraser & Shapers -158168 - 0x0A3CC (Stars & Eraser & Shapers 4) - 0x0A3CB - Stars & Eraser & Shapers -158169 - 0x0A3D0 (Stars & Eraser & Shapers 5) - 0x0A3CC - Stars & Eraser & Shapers +158167 - 0x0A3CB (Back Second Row 1) - 0x09DB4 - Stars & Eraser & Shapers +158168 - 0x0A3CC (Back Second Row 2) - 0x0A3CB - Stars & Eraser & Shapers +158169 - 0x0A3D0 (Back Second Row 3) - 0x0A3CC - Stars & Eraser & Shapers Shadows (Shadows) - Main Island - True - Shadows Ledge - 0x19B24 - Shadows Laser Room - 0x194B2 & 0x19665: 158170 - 0x334DB (Door Timer Outside) - True - True Door - 0x19B24 (Timed Door) - 0x334DB -158171 - 0x0AC74 (Lower Avoid 6) - 0x0A8DC - Shadows Avoid -158172 - 0x0AC7A (Lower Avoid 7) - 0x0AC74 - Shadows Avoid -158173 - 0x0A8E0 (Lower Avoid 8) - 0x0AC7A - Shadows Avoid -158174 - 0x386FA (Environmental Avoid 1) - 0x0A8E0 - Shadows Avoid & Environment -158175 - 0x1C33F (Environmental Avoid 2) - 0x386FA - Shadows Avoid & Environment -158176 - 0x196E2 (Environmental Avoid 3) - 0x1C33F - Shadows Avoid & Environment -158177 - 0x1972A (Environmental Avoid 4) - 0x196E2 - Shadows Avoid & Environment -158178 - 0x19809 (Environmental Avoid 5) - 0x1972A - Shadows Avoid & Environment -158179 - 0x19806 (Environmental Avoid 6) - 0x19809 - Shadows Avoid & Environment -158180 - 0x196F8 (Environmental Avoid 7) - 0x19806 - Shadows Avoid & Environment -158181 - 0x1972F (Environmental Avoid 8) - 0x196F8 - Shadows Avoid & Environment -Door - 0x194B2 (Laser Room Right Door) - 0x1972F -158182 - 0x19797 (Follow 1) - 0x0A8E0 - Shadows Follow -158183 - 0x1979A (Follow 2) - 0x19797 - Shadows Follow -158184 - 0x197E0 (Follow 3) - 0x1979A - Shadows Follow -158185 - 0x197E8 (Follow 4) - 0x197E0 - Shadows Follow -158186 - 0x197E5 (Follow 5) - 0x197E8 - Shadows Follow -Door - 0x19665 (Laser Room Left Door) - 0x197E5 +158171 - 0x0AC74 (Intro 6) - 0x0A8DC - True +158172 - 0x0AC7A (Intro 7) - 0x0AC74 - True +158173 - 0x0A8E0 (Intro 8) - 0x0AC7A - True +158174 - 0x386FA (Far 1) - 0x0A8E0 - True +158175 - 0x1C33F (Far 2) - 0x386FA - True +158176 - 0x196E2 (Far 3) - 0x1C33F - True +158177 - 0x1972A (Far 4) - 0x196E2 - True +158178 - 0x19809 (Far 5) - 0x1972A - True +158179 - 0x19806 (Far 6) - 0x19809 - True +158180 - 0x196F8 (Far 7) - 0x19806 - True +158181 - 0x1972F (Far 8) - 0x196F8 - True +Door - 0x194B2 (Laser Entry Right) - 0x1972F +158182 - 0x19797 (Near 1) - 0x0A8E0 - True +158183 - 0x1979A (Near 2) - 0x19797 - True +158184 - 0x197E0 (Near 3) - 0x1979A - True +158185 - 0x197E8 (Near 4) - 0x197E0 - True +158186 - 0x197E5 (Near 5) - 0x197E8 - True +Door - 0x19665 (Laser Entry Left) - 0x197E5 Shadows Ledge (Shadows) - Shadows - 0x1855B - Quarry - 0x19865 & 0x0A2DF: 158187 - 0x334DC (Door Timer Inside) - True - True -158188 - 0x198B5 (Lower Avoid 1) - True - Shadows Avoid -158189 - 0x198BD (Lower Avoid 2) - 0x198B5 - Shadows Avoid -158190 - 0x198BF (Lower Avoid 3) - 0x198BD & 0x334DC & 0x19B24 - Shadows Avoid -Door - 0x19865 (Barrier to Quarry) - 0x198BF -Door - 0x0A2DF (Barrier to Quarry 2) - 0x198BF -158191 - 0x19771 (Lower Avoid 4) - 0x198BF - Shadows Avoid -158192 - 0x0A8DC (Lower Avoid 5) - 0x19771 - Shadows Avoid -Door - 0x1855B (Barrier to Shadows) - 0x0A8DC -Door - 0x19ADE (Barrier to Shadows 2) - 0x0A8DC +158188 - 0x198B5 (Intro 1) - True - True +158189 - 0x198BD (Intro 2) - 0x198B5 - True +158190 - 0x198BF (Intro 3) - 0x198BD & 0x334DC & 0x19B24 - True +Door - 0x19865 (Quarry Barrier) - 0x198BF +Door - 0x0A2DF (Quarry Barrier 2) - 0x198BF +158191 - 0x19771 (Intro 4) - 0x198BF - True +158192 - 0x0A8DC (Intro 5) - 0x19771 - True +Door - 0x1855B (Ledge Barrier) - 0x0A8DC +Door - 0x19ADE (Ledge Barrier 2) - 0x0A8DC Shadows Laser Room (Shadows): -158703 - 0x19650 (Laser Panel) - True - Shadows Avoid & Shadows Follow +158703 - 0x19650 (Laser Panel) - True - True Laser - 0x181B3 (Laser) - 0x19650 Keep (Keep) - Main Island - True - Keep 2nd Maze - 0x01954 - Keep 2nd Pressure Plate - 0x01BEC: -158193 - 0x00139 (Hedge Maze 1) - True - Environment +158193 - 0x00139 (Hedge Maze 1) - True - True 158197 - 0x0A3A8 (Reset Pressure Plates 1) - True - True -158198 - 0x033EA (Pressure Plates 1) - 0x0A3A8 - Pressure Plates & Dots -Door - 0x01954 (Hedge Maze 1 Exit Door) - 0x00139 -Door - 0x01BEC (Pressure Plates 1 Exit Door) - 0x033EA +158198 - 0x033EA (Pressure Plates 1) - 0x0A3A8 - Dots +Door - 0x01954 (Hedge Maze 1 Exit) - 0x00139 +Door - 0x01BEC (Pressure Plates 1 Exit) - 0x033EA Keep 2nd Maze (Keep) - Keep - 0x018CE - Keep 3rd Maze - 0x019D8: Door - 0x018CE (Hedge Maze 2 Shortcut) - 0x00139 -158194 - 0x019DC (Hedge Maze 2) - True - Environment -Door - 0x019D8 (Hedge Maze 2 Exit Door) - 0x019DC +158194 - 0x019DC (Hedge Maze 2) - True - True +Door - 0x019D8 (Hedge Maze 2 Exit) - 0x019DC Keep 3rd Maze (Keep) - Keep - 0x019B5 - Keep 4th Maze - 0x019E6: Door - 0x019B5 (Hedge Maze 3 Shortcut) - 0x019DC -158195 - 0x019E7 (Hedge Maze 3) - True - Environment & Sound -Door - 0x019E6 (Hedge Maze 3 Exit Door) - 0x019E7 +158195 - 0x019E7 (Hedge Maze 3) - True - True +Door - 0x019E6 (Hedge Maze 3 Exit) - 0x019E7 Keep 4th Maze (Keep) - Keep - 0x0199A - Keep Tower - 0x01A0E: Door - 0x0199A (Hedge Maze 4 Shortcut) - 0x019E7 -158196 - 0x01A0F (Hedge Maze 4) - True - Environment -Door - 0x01A0E (Hedge Maze 4 Exit Door) - 0x01A0F +158196 - 0x01A0F (Hedge Maze 4) - True - True +Door - 0x01A0E (Hedge Maze 4 Exit) - 0x01A0F Keep 2nd Pressure Plate (Keep) - Keep 3rd Pressure Plate - 0x01BEA: 158199 - 0x0A3B9 (Reset Pressure Plates 2) - True - True -158200 - 0x01BE9 (Pressure Plates 2) - 0x0A3B9 - Pressure Plates & Stars & Stars + Same Colored Symbol & Squares & Black/White Squares -Door - 0x01BEA (Pressure Plates 2 Exit Door) - 0x01BE9 +158200 - 0x01BE9 (Pressure Plates 2) - 0x0A3B9 - Stars & Stars + Same Colored Symbol & Black/White Squares +Door - 0x01BEA (Pressure Plates 2 Exit) - 0x01BE9 Keep 3rd Pressure Plate (Keep) - Keep 4th Pressure Plate - 0x01CD5: 158201 - 0x0A3BB (Reset Pressure Plates 3) - True - True -158202 - 0x01CD3 (Pressure Plates 3) - 0x0A3BB - Pressure Plates & Shapers & Squares & Black/White Squares & Colored Squares -Door - 0x01CD5 (Pressure Plates 3 Exit Door) - 0x01CD3 +158202 - 0x01CD3 (Pressure Plates 3) - 0x0A3BB - Shapers & Black/White Squares & Colored Squares +Door - 0x01CD5 (Pressure Plates 3 Exit) - 0x01CD3 Keep 4th Pressure Plate (Keep) - Keep - 0x09E3D - Keep Tower - 0x01D40: 158203 - 0x0A3AD (Reset Pressure Plates 4) - True - True -158204 - 0x01D3F (Pressure Plates 4) - 0x0A3AD - Pressure Plates & Shapers & Dots & Symmetry -Door - 0x01D40 (Pressure Plates 4 Exit Door) - 0x01D3F +158204 - 0x01D3F (Pressure Plates 4) - 0x0A3AD - Shapers & Dots & Symmetry +Door - 0x01D40 (Pressure Plates 4 Exit) - 0x01D3F 158604 - 0x17D27 (Discard) - True - Triangles -158205 - 0x09E49 (Shortcut to Shadows Panel) - True - True -Door - 0x09E3D (Shortcut to Shadows) - 0x09E49 +158205 - 0x09E49 (Shadows Shortcut Panel) - True - True +Door - 0x09E3D (Shadows Shortcut) - 0x09E49 Shipwreck (Shipwreck) - Keep 3rd Pressure Plate - True: -158654 - 0x00AFB (Vault) - True - Symmetry & Sound & Sound Dots & Colored Dots +158654 - 0x00AFB (Vault) - True - Symmetry & Sound Dots & Colored Dots 158655 - 0x03535 (Vault Box) - 0x00AFB - True 158605 - 0x17D28 (Discard) - True - Triangles Keep Tower (Keep) - Keep - 0x04F8F: -158206 - 0x0361B (Tower Shortcut to Keep Panel) - True - True -Door - 0x04F8F (Tower Shortcut to Keep) - 0x0361B -158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - Environment & Sound -158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01D3F - Shapers & Squares & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Dots +158206 - 0x0361B (Tower Shortcut Panel) - True - True +Door - 0x04F8F (Tower Shortcut) - 0x0361B +158704 - 0x0360E (Laser Panel Hedges) - 0x01A0F & 0x019E7 & 0x019DC & 0x00139 - True +158705 - 0x03317 (Laser Panel Pressure Plates) - 0x01D3F - Shapers & Black/White Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Dots Laser - 0x014BB (Laser) - 0x0360E | 0x03317 Outside Monastery (Monastery) - Main Island - True - Main Island - 0x0364E - Inside Monastery - 0x0C128 & 0x0C153 - Monastery Garden - 0x03750: -158207 - 0x03713 (Shortcut Door Panel) - True - True +158207 - 0x03713 (Shortcut Panel) - True - True Door - 0x0364E (Shortcut) - 0x03713 -158208 - 0x00B10 (Door Open Left) - True - True -158209 - 0x00C92 (Door Open Right) - True - True -Door - 0x0C128 (Left Door) - 0x00B10 -Door - 0x0C153 (Right Door) - 0x00C92 -158210 - 0x00290 (Rhombic Avoid 1) - 0x09D9B - Environment -158211 - 0x00038 (Rhombic Avoid 2) - 0x09D9B & 0x00290 - Environment -158212 - 0x00037 (Rhombic Avoid 3) - 0x09D9B & 0x00038 - Environment -Door - 0x03750 (Door to Garden) - 0x00037 +158208 - 0x00B10 (Entry Left) - True - True +158209 - 0x00C92 (Entry Right) - True - True +Door - 0x0C128 (Entry Inner) - 0x00B10 +Door - 0x0C153 (Entry Outer) - 0x00C92 +158210 - 0x00290 (Outside 1) - 0x09D9B - True +158211 - 0x00038 (Outside 2) - 0x09D9B & 0x00290 - True +158212 - 0x00037 (Outside 3) - 0x09D9B & 0x00038 - True +Door - 0x03750 (Garden Entry) - 0x00037 158706 - 0x17CA4 (Laser Panel) - 0x193A6 - True Laser - 0x17C65 (Laser) - 0x17CA4 Inside Monastery (Monastery): -158213 - 0x09D9B (Overhead Door Control) - True - Dots -158214 - 0x193A7 (Branch Avoid 1) - 0x00037 - Environment -158215 - 0x193AA (Branch Avoid 2) - 0x193A7 - Environment -158216 - 0x193AB (Branch Follow 1) - 0x193AA - Environment -158217 - 0x193A6 (Branch Follow 2) - 0x193AB - Environment +158213 - 0x09D9B (Shutters Control) - True - Dots +158214 - 0x193A7 (Inside 1) - 0x00037 - True +158215 - 0x193AA (Inside 2) - 0x193A7 - True +158216 - 0x193AB (Inside 3) - 0x193AA - True +158217 - 0x193A6 (Inside 4) - 0x193AB - True Monastery Garden (Monastery): Town (Town) - Main Island - True - Boat - 0x0A054 - Town Maze Rooftop - 0x28AA2 - Town Church - True - Town Wooden Rooftop - 0x034F5 - RGB House - 0x28A61 - Windmill Interior - 0x1845B - Town Inside Cargo Box - 0x0A0C9: 158218 - 0x0A054 (Boat Spawn) - 0x17CA6 | 0x17CDF | 0x09DB8 | 0x17C95 - Boat -158219 - 0x0A0C8 (Cargo Box Panel) - True - Squares & Black/White Squares & Shapers -Door - 0x0A0C9 (Cargo Box Door) - 0x0A0C8 +158219 - 0x0A0C8 (Cargo Box Entry Panel) - True - Black/White Squares & Shapers +Door - 0x0A0C9 (Cargo Box Entry) - 0x0A0C8 158707 - 0x09F98 (Desert Laser Redirect) - True - True -158220 - 0x18590 (Tree Outlines) - True - Symmetry & Environment -158221 - 0x28AE3 (Vines Shadows Follow) - 0x18590 - Shadows Follow & Environment -158222 - 0x28938 (Four-way Apple Tree) - 0x28AE3 - Environment -158223 - 0x079DF (Triple Environmental Puzzle) - 0x28938 - Shadows Avoid & Environment & Reflection -158235 - 0x2899C (Full Dot Grid Shapers 1) - True - Rotated Shapers & Dots -158236 - 0x28A33 (Full Dot Grid Shapers 2) - 0x2899C - Shapers & Dots -158237 - 0x28ABF (Full Dot Grid Shapers 3) - 0x28A33 - Shapers & Rotated Shapers & Dots -158238 - 0x28AC0 (Full Dot Grid Shapers 4) - 0x28ABF - Rotated Shapers & Dots -158239 - 0x28AC1 (Full Dot Grid Shapers 5) - 0x28AC0 - Rotated Shapers & Dots -Door - 0x034F5 (Wooden Roof Staircase) - 0x28AC1 -158225 - 0x28998 (Tinted Door Panel) - True - Stars & Rotated Shapers -Door - 0x28A61 (Tinted Door to RGB House) - 0x28998 -158226 - 0x28A0D (Door to Church Stars Panel) - 0x28998 - Stars & RGB & Environment -Door - 0x03BB0 (Door to Church) - 0x28A0D -158228 - 0x28A79 (Maze Stair Control) - True - Environment -Door - 0x28AA2 (Maze Staircase) - 0x28A79 -158241 - 0x17F5F (Windmill Door Panel) - True - Dots -Door - 0x1845B (Windmill Door) - 0x17F5F +158220 - 0x18590 (Transparent) - True - Symmetry +158221 - 0x28AE3 (Vines) - 0x18590 - True +158222 - 0x28938 (Apple Tree) - 0x28AE3 - True +158223 - 0x079DF (Triple Exit) - 0x28938 - True +158235 - 0x2899C (Wooden Roof Lower Row 1) - True - Rotated Shapers & Dots +158236 - 0x28A33 (Wooden Roof Lower Row 2) - 0x2899C - Shapers & Dots +158237 - 0x28ABF (Wooden Roof Lower Row 3) - 0x28A33 - Shapers & Rotated Shapers & Dots +158238 - 0x28AC0 (Wooden Roof Lower Row 4) - 0x28ABF - Rotated Shapers & Dots +158239 - 0x28AC1 (Wooden Roof Lower Row 5) - 0x28AC0 - Rotated Shapers & Dots +Door - 0x034F5 (Wooden Roof Stairs) - 0x28AC1 +158225 - 0x28998 (Tinted Glass Door Panel) - True - Stars & Rotated Shapers +Door - 0x28A61 (Tinted Glass Door) - 0x28998 +158226 - 0x28A0D (Church Entry Panel) - 0x28A61 - Stars +Door - 0x03BB0 (Church Entry) - 0x28A0D +158228 - 0x28A79 (Maze Stair Control) - True - True +Door - 0x28AA2 (Maze Stairs) - 0x28A79 +158241 - 0x17F5F (Windmill Entry Panel) - True - Dots +Door - 0x1845B (Windmill Entry) - 0x17F5F Town Inside Cargo Box (Town): 158606 - 0x17D01 (Cargo Box Discard) - True - Triangles @@ -413,34 +413,34 @@ Town Maze Rooftop (Town) - Town Red Rooftop - 0x2896A: Town Red Rooftop (Town): 158607 - 0x17C71 (Rooftop Discard) - True - Triangles -158230 - 0x28AC7 (Symmetry Squares 1) - True - Symmetry & Squares & Black/White Squares -158231 - 0x28AC8 (Symmetry Squares 2) - 0x28AC7 - Symmetry & Squares & Black/White Squares -158232 - 0x28ACA (Symmetry Squares 3 + Dots) - 0x28AC8 - Symmetry & Squares & Black/White Squares & Dots -158233 - 0x28ACB (Symmetry Squares 4 + Dots) - 0x28ACA - Symmetry & Squares & Black/White Squares & Dots -158234 - 0x28ACC (Symmetry Squares 5 + Dots) - 0x28ACB - Symmetry & Squares & Black/White Squares & Dots -158224 - 0x28B39 (Hexagonal Reflection) - 0x079DF - Reflection +158230 - 0x28AC7 (Red Rooftop 1) - True - Symmetry & Black/White Squares +158231 - 0x28AC8 (Red Rooftop 2) - 0x28AC7 - Symmetry & Black/White Squares +158232 - 0x28ACA (Red Rooftop 3) - 0x28AC8 - Symmetry & Black/White Squares & Dots +158233 - 0x28ACB (Red Rooftop 4) - 0x28ACA - Symmetry & Black/White Squares & Dots +158234 - 0x28ACC (Red Rooftop 5) - 0x28ACB - Symmetry & Black/White Squares & Dots +158224 - 0x28B39 (Tall Hexagonal) - 0x079DF - True Town Wooden Rooftop (Town): -158240 - 0x28AD9 (Shapers & Dots & Eraser) - 0x28AC1 - Rotated Shapers & Dots & Eraser +158240 - 0x28AD9 (Wooden Rooftop) - 0x28AC1 - Rotated Shapers & Dots & Eraser Town Church (Town): -158227 - 0x28A69 (Church Lattice) - 0x03BB0 - Environment +158227 - 0x28A69 (Church Lattice) - 0x03BB0 - True RGB House (Town) - RGB Room - 0x2897B: -158242 - 0x034E4 (Sound Room Left) - True - Sound & Sound Waves -158243 - 0x034E3 (Sound Room Right) - True - Sound & Sound Dots -Door - 0x2897B (RGB House Staircase) - 0x034E4 & 0x034E3 +158242 - 0x034E4 (Sound Room Left) - True - True +158243 - 0x034E3 (Sound Room Right) - True - Sound Dots +Door - 0x2897B (RGB House Stairs) - 0x034E4 & 0x034E3 RGB Room (Town): -158244 - 0x334D8 (RGB Control) - True - Rotated Shapers & RGB & Squares & Colored Squares -158245 - 0x03C0C (RGB Squares) - 0x334D8 - RGB & Squares & Colored Squares & Black/White Squares -158246 - 0x03C08 (RGB Stars) - 0x334D8 - RGB & Stars +158244 - 0x334D8 (RGB Control) - True - Rotated Shapers & Colored Squares +158245 - 0x03C0C (RGB Room Left) - 0x334D8 - Colored Squares & Black/White Squares +158246 - 0x03C08 (RGB Room Right) - 0x334D8 - Stars Town Tower (Town Tower) - Town - True - Town Tower Top - 0x27798 & 0x27799 & 0x2779A & 0x2779C: -Door - 0x27798 (Blue Panels Door) - 0x28ACC -Door - 0x27799 (Church Lattice Door) - 0x28A69 -Door - 0x2779A (Environmental Set Door) - 0x28B39 -Door - 0x2779C (Eraser Set Door) - 0x28AD9 +Door - 0x27798 (First Door) - 0x28ACC +Door - 0x2779C (Second Door) - 0x28AD9 +Door - 0x27799 (Third Door) - 0x28A69 +Door - 0x2779A (Fourth Door) - 0x28B39 Town Tower Top (Town): 158708 - 0x032F5 (Laser Panel) - True - True @@ -448,8 +448,8 @@ Laser - 0x032F9 (Laser) - 0x032F5 Windmill Interior (Windmill) - Theater - 0x17F88: 158247 - 0x17D02 (Turn Control) - True - Dots -158248 - 0x17F89 (Door to Front of Theater Panel) - True - Squares & Black/White Squares -Door - 0x17F88 (Door to Front of Theater) - 0x17F89 +158248 - 0x17F89 (Theater Entry Panel) - True - Black/White Squares +Door - 0x17F88 (Theater Entry) - 0x17F89 Theater (Theater) - Town - 0x0A16D | 0x3CCDF: 158656 - 0x00815 (Video Input) - True - True @@ -459,73 +459,73 @@ Theater (Theater) - Town - 0x0A16D | 0x3CCDF: 158660 - 0x03549 (Challenge Video) - 0x00815 & 0x0356B - True 158661 - 0x0354F (Shipwreck Video) - 0x00815 & 0x03535 - True 158662 - 0x03545 (Mountain Video) - 0x00815 & 0x03542 - True -158249 - 0x0A168 (Door to Cargo Box Left Panel) - True - Squares & Black/White Squares & Eraser -158250 - 0x33AB2 (Door to Cargo Box Right Panel) - True - Squares & Black/White Squares & Shapers -Door - 0x0A16D (Door to Cargo Box Left) - 0x0A168 -Door - 0x3CCDF (Door to Cargo Box Right) - 0x33AB2 +158249 - 0x0A168 (Exit Left Panel) - True - Black/White Squares & Eraser +158250 - 0x33AB2 (Exit Right Panel) - True - Black/White Squares & Shapers +Door - 0x0A16D (Exit Left) - 0x0A168 +Door - 0x3CCDF (Exit Right) - 0x33AB2 158608 - 0x17CF7 (Discard) - True - Triangles Jungle (Jungle) - Main Island - True - Outside Jungle River - 0x3873B - Boat - 0x17CDF: 158251 - 0x17CDF (Shore Boat Spawn) - True - Boat 158609 - 0x17F9B (Discard) - True - Triangles -158252 - 0x002C4 (Waves 1) - True - Sound & Sound Waves -158253 - 0x00767 (Waves 2) - 0x002C4 - Sound & Sound Waves -158254 - 0x002C6 (Waves 3) - 0x00767 - Sound & Sound Waves -158255 - 0x0070E (Waves 4) - 0x002C6 - Sound & Sound Waves -158256 - 0x0070F (Waves 5) - 0x0070E - Sound & Sound Waves -158257 - 0x0087D (Waves 6) - 0x0070F - Sound & Sound Waves -158258 - 0x002C7 (Waves 7) - 0x0087D - Sound & Sound Waves +158252 - 0x002C4 (First Row 1) - True - True +158253 - 0x00767 (First Row 2) - 0x002C4 - True +158254 - 0x002C6 (First Row 3) - 0x00767 - True +158255 - 0x0070E (Second Row 1) - 0x002C6 - True +158256 - 0x0070F (Second Row 2) - 0x0070E - True +158257 - 0x0087D (Second Row 3) - 0x0070F - True +158258 - 0x002C7 (Second Row 4) - 0x0087D - True 158259 - 0x17CAB (Popup Wall Control) - 0x002C7 - True Door - 0x1475B (Popup Wall) - 0x17CAB -158260 - 0x0026D (Popup Wall 1) - 0x1475B - Sound & Sound Dots -158261 - 0x0026E (Popup Wall 2) - 0x0026D - Sound & Sound Dots -158262 - 0x0026F (Popup Wall 3) - 0x0026E - Sound & Sound Dots -158263 - 0x00C3F (Popup Wall 4) - 0x0026F - Sound & Sound Dots -158264 - 0x00C41 (Popup Wall 5) - 0x00C3F - Sound & Sound Dots -158265 - 0x014B2 (Popup Wall 6) - 0x00C41 - Sound & Sound Dots +158260 - 0x0026D (Popup Wall 1) - 0x1475B - Sound Dots +158261 - 0x0026E (Popup Wall 2) - 0x0026D - Sound Dots +158262 - 0x0026F (Popup Wall 3) - 0x0026E - Sound Dots +158263 - 0x00C3F (Popup Wall 4) - 0x0026F - Sound Dots +158264 - 0x00C41 (Popup Wall 5) - 0x00C3F - Sound Dots +158265 - 0x014B2 (Popup Wall 6) - 0x00C41 - Sound Dots 158709 - 0x03616 (Laser Panel) - 0x014B2 - True Laser - 0x00274 (Laser) - 0x03616 -158266 - 0x337FA (Shortcut to River Panel) - True - True -Door - 0x3873B (Shortcut to River) - 0x337FA +158266 - 0x337FA (Laser Shortcut Panel) - True - True +Door - 0x3873B (Laser Shortcut) - 0x337FA Outside Jungle River (River) - Main Island - True - Monastery Garden - 0x0CF2A: -158267 - 0x17CAA (Rhombic Avoid to Monastery Garden) - True - Environment -Door - 0x0CF2A (Shortcut to Monastery Garden) - 0x17CAA -158663 - 0x15ADD (Vault) - True - Environment & Black/White Squares & Dots +158267 - 0x17CAA (Monastery Shortcut Panel) - True - True +Door - 0x0CF2A (Monastery Shortcut) - 0x17CAA +158663 - 0x15ADD (Vault) - True - Black/White Squares & Dots 158664 - 0x03702 (Vault Box) - 0x15ADD - True -Outside Bunker (Bunker) - Main Island - True - Inside Bunker - 0x0C2A4: -158268 - 0x17C2E (Bunker Entry Panel) - True - Squares & Black/White Squares & Colored Squares -Door - 0x0C2A4 (Bunker Entry Door) - 0x17C2E +Outside Bunker (Bunker) - Main Island - True - Bunker - 0x0C2A4: +158268 - 0x17C2E (Entry Panel) - True - Black/White Squares & Colored Squares +Door - 0x0C2A4 (Entry) - 0x17C2E -Inside Bunker (Bunker) - Inside Bunker Glass Room - 0x17C79: -158269 - 0x09F7D (Drawn Squares 1) - True - Squares & Colored Squares -158270 - 0x09FDC (Drawn Squares 2) - 0x09F7D - Squares & Colored Squares & Black/White Squares -158271 - 0x09FF7 (Drawn Squares 3) - 0x09FDC - Squares & Colored Squares & Black/White Squares -158272 - 0x09F82 (Drawn Squares 4) - 0x09FF7 - Squares & Colored Squares & Black/White Squares -158273 - 0x09FF8 (Drawn Squares 5) - 0x09F82 - Squares & Colored Squares & Black/White Squares -158274 - 0x09D9F (Drawn Squares 6) - 0x09FF8 - Squares & Colored Squares & Black/White Squares -158275 - 0x09DA1 (Drawn Squares 7) - 0x09D9F - Squares & Colored Squares -158276 - 0x09DA2 (Drawn Squares 8) - 0x09DA1 - Squares & Colored Squares -158277 - 0x09DAF (Drawn Squares 9) - 0x09DA2 - Squares & Colored Squares -158278 - 0x0A099 (Door to Bunker Proper Panel) - 0x09DAF - True -Door - 0x17C79 (Door to Bunker Proper) - 0x0A099 +Bunker (Bunker) - Bunker Glass Room - 0x17C79: +158269 - 0x09F7D (Intro Left 1) - True - Colored Squares +158270 - 0x09FDC (Intro Left 2) - 0x09F7D - Colored Squares & Black/White Squares +158271 - 0x09FF7 (Intro Left 3) - 0x09FDC - Colored Squares & Black/White Squares +158272 - 0x09F82 (Intro Left 4) - 0x09FF7 - Colored Squares & Black/White Squares +158273 - 0x09FF8 (Intro Left 5) - 0x09F82 - Colored Squares & Black/White Squares +158274 - 0x09D9F (Intro Back 1) - 0x09FF8 - Colored Squares & Black/White Squares +158275 - 0x09DA1 (Intro Back 2) - 0x09D9F - Colored Squares +158276 - 0x09DA2 (Intro Back 3) - 0x09DA1 - Colored Squares +158277 - 0x09DAF (Intro Back 4) - 0x09DA2 - Colored Squares +158278 - 0x0A099 (Tinted Glass Door Panel) - 0x09DAF - True +Door - 0x17C79 (Tinted Glass Door) - 0x0A099 -Inside Bunker Glass Room (Bunker) - Inside Bunker Ultraviolet Room - 0x0C2A3: -158279 - 0x0A010 (Drawn Squares through Tinted Glass 1) - True - Squares & Colored Squares & RGB & Environment -158280 - 0x0A01B (Drawn Squares through Tinted Glass 2) - 0x0A010 - Squares & Colored Squares & Black/White Squares & RGB & Environment -158281 - 0x0A01F (Drawn Squares through Tinted Glass 3) - 0x0A01B - Squares & Colored Squares & Black/White Squares & RGB & Environment -Door - 0x0C2A3 (Door to Ultraviolet Room) - 0x0A01F +Bunker Glass Room (Bunker) - Bunker Ultraviolet Room - 0x0C2A3: +158279 - 0x0A010 (Glass Room 1) - True - Colored Squares +158280 - 0x0A01B (Glass Room 2) - 0x0A010 - Colored Squares & Black/White Squares +158281 - 0x0A01F (Glass Room 3) - 0x0A01B - Colored Squares & Black/White Squares +Door - 0x0C2A3 (UV Room Entry) - 0x0A01F -Inside Bunker Ultraviolet Room (Bunker) - Inside Bunker Elevator Section - 0x0A08D: +Bunker Ultraviolet Room (Bunker) - Bunker Elevator Section - 0x0A08D: 158282 - 0x34BC5 (Drop-Down Door Open) - True - True 158283 - 0x34BC6 (Drop-Down Door Close) - 0x34BC5 - True -158284 - 0x17E63 (Drop-Down Door Squares 1) - 0x34BC5 - Squares & Colored Squares & RGB & Environment -158285 - 0x17E67 (Drop-Down Door Squares 2) - 0x17E63 & 0x34BC6 - Squares & Colored Squares & Black/White Squares & RGB -Door - 0x0A08D (Door to Elevator) - 0x17E67 +158284 - 0x17E63 (UV Room 1) - 0x34BC5 - Colored Squares +158285 - 0x17E67 (UV Room 2) - 0x17E63 & 0x34BC6 - Colored Squares & Black/White Squares +Door - 0x0A08D (Elevator Room Entry) - 0x17E67 -Inside Bunker Elevator Section (Bunker) - Bunker Laser Platform - 0x0A079: -158286 - 0x0A079 (Elevator Control) - True - Squares & Colored Squares & Black/White Squares & RGB +Bunker Elevator Section (Bunker) - Bunker Laser Platform - 0x0A079: +158286 - 0x0A079 (Elevator Control) - True - Colored Squares & Black/White Squares Bunker Laser Platform (Bunker): 158710 - 0x09DE0 (Laser Panel) - True - True @@ -533,76 +533,76 @@ Laser - 0x0C2B2 (Laser) - 0x09DE0 Outside Swamp (Swamp) - Swamp Entry Area - 0x00C1C - Main Island - True: 158287 - 0x0056E (Entry Panel) - True - Shapers -Door - 0x00C1C (Entry Door) - 0x0056E +Door - 0x00C1C (Entry) - 0x0056E Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay: -158288 - 0x00469 (Seperatable Shapers 1) - True - Shapers -158289 - 0x00472 (Seperatable Shapers 2) - 0x00469 - Shapers -158290 - 0x00262 (Seperatable Shapers 3) - 0x00472 - Shapers -158291 - 0x00474 (Seperatable Shapers 4) - 0x00262 - Shapers -158292 - 0x00553 (Seperatable Shapers 5) - 0x00474 - Shapers -158293 - 0x0056F (Seperatable Shapers 6) - 0x00553 - Shapers -158294 - 0x00390 (Combinable Shapers 1) - 0x0056F - Shapers -158295 - 0x010CA (Combinable Shapers 2) - 0x00390 - Shapers -158296 - 0x00983 (Combinable Shapers 3) - 0x010CA - Shapers -158297 - 0x00984 (Combinable Shapers 4) - 0x00983 - Shapers -158298 - 0x00986 (Combinable Shapers 5) - 0x00984 - Shapers -158299 - 0x00985 (Combinable Shapers 6) - 0x00986 - Shapers -158300 - 0x00987 (Combinable Shapers 7) - 0x00985 - Shapers -158301 - 0x181A9 (Combinable Shapers 8) - 0x00987 - Shapers +158288 - 0x00469 (Intro Front 1) - True - Shapers +158289 - 0x00472 (Intro Front 2) - 0x00469 - Shapers +158290 - 0x00262 (Intro Front 3) - 0x00472 - Shapers +158291 - 0x00474 (Intro Front 4) - 0x00262 - Shapers +158292 - 0x00553 (Intro Front 5) - 0x00474 - Shapers +158293 - 0x0056F (Intro Front 6) - 0x00553 - Shapers +158294 - 0x00390 (Intro Back 1) - 0x0056F - Shapers +158295 - 0x010CA (Intro Back 2) - 0x00390 - Shapers +158296 - 0x00983 (Intro Back 3) - 0x010CA - Shapers +158297 - 0x00984 (Intro Back 4) - 0x00983 - Shapers +158298 - 0x00986 (Intro Back 5) - 0x00984 - Shapers +158299 - 0x00985 (Intro Back 6) - 0x00986 - Shapers +158300 - 0x00987 (Intro Back 7) - 0x00985 - Shapers +158301 - 0x181A9 (Intro Back 8) - 0x00987 - Shapers Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609: 158302 - 0x00609 (Sliding Bridge) - True - Shapers -Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Broken Shapers - 0x184B7 - Swamp Sliding Bridge - TrueOneWay: -158313 - 0x00982 (Platform Shapers 1) - True - Shapers -158314 - 0x0097F (Platform Shapers 2) - 0x00982 - Shapers -158315 - 0x0098F (Platform Shapers 3) - 0x0097F - Shapers -158316 - 0x00990 (Platform Shapers 4) - 0x0098F - Shapers -Door - 0x184B7 (Door to Broken Shapers) - 0x00990 +Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay: +158313 - 0x00982 (Platform Row 1) - True - Shapers +158314 - 0x0097F (Platform Row 2) - 0x00982 - Shapers +158315 - 0x0098F (Platform Row 3) - 0x0097F - Shapers +158316 - 0x00990 (Platform Row 4) - 0x0098F - Shapers +Door - 0x184B7 (Between Bridges First Door) - 0x00990 158317 - 0x17C0D (Platform Shortcut Left Panel) - True - Shapers 158318 - 0x17C0E (Platform Shortcut Right Panel) - True - Shapers Door - 0x38AE6 (Platform Shortcut Door) - 0x17C0E Door - 0x04B7F (Cyan Water Pump) - 0x00006 Swamp Cyan Underwater (Swamp): -158307 - 0x00002 (Cyan Underwater Negative Shapers 1) - True - Shapers & Negative Shapers -158308 - 0x00004 (Cyan Underwater Negative Shapers 2) - 0x00002 - Shapers & Negative Shapers -158309 - 0x00005 (Cyan Underwater Negative Shapers 3) - 0x00004 - Shapers & Negative Shapers -158310 - 0x013E6 (Cyan Underwater Negative Shapers 4) - 0x00005 - Shapers & Negative Shapers -158311 - 0x00596 (Cyan Underwater Negative Shapers 5) - 0x013E6 - Shapers & Negative Shapers +158307 - 0x00002 (Cyan Underwater 1) - True - Shapers & Negative Shapers +158308 - 0x00004 (Cyan Underwater 2) - 0x00002 - Shapers & Negative Shapers +158309 - 0x00005 (Cyan Underwater 3) - 0x00004 - Shapers & Negative Shapers +158310 - 0x013E6 (Cyan Underwater 4) - 0x00005 - Shapers & Negative Shapers +158311 - 0x00596 (Cyan Underwater 5) - 0x013E6 - Shapers & Negative Shapers 158312 - 0x18488 (Cyan Underwater Sliding Bridge Control) - True - Shapers -Swamp Broken Shapers (Swamp) - Swamp Rotated Shapers - 0x18507: -158303 - 0x00999 (Broken Shapers 1) - 0x00990 - Shapers & Broken Shapers -158304 - 0x0099D (Broken Shapers 2) - 0x00999 - Shapers & Broken Shapers -158305 - 0x009A0 (Broken Shapers 3) - 0x0099D - Shapers & Broken Shapers -158306 - 0x009A1 (Broken Shapers 4) - 0x009A0 - Shapers & Broken Shapers -Door - 0x18507 (Door to Rotated Shapers) - 0x009A1 +Swamp Between Bridges Near (Swamp) - Swamp Between Bridges Far - 0x18507: +158303 - 0x00999 (Between Bridges Near Row 1) - 0x00990 - Shapers +158304 - 0x0099D (Between Bridges Near Row 2) - 0x00999 - Shapers +158305 - 0x009A0 (Between Bridges Near Row 3) - 0x0099D - Shapers +158306 - 0x009A1 (Between Bridges Near Row 4) - 0x009A0 - Shapers +Door - 0x18507 (Between Bridges Second Door) - 0x009A1 -Swamp Rotated Shapers (Swamp) - Swamp Red Underwater - 0x183F2 - Swamp Rotating Bridge - TrueOneWay: -158319 - 0x00007 (Rotated Shapers 1) - 0x009A1 - Rotated Shapers -158320 - 0x00008 (Rotated Shapers 2) - 0x00007 - Rotated Shapers & Shapers -158321 - 0x00009 (Rotated Shapers 3) - 0x00008 - Rotated Shapers -158322 - 0x0000A (Rotated Shapers 4) - 0x00009 - Rotated Shapers +Swamp Between Bridges Far (Swamp) - Swamp Red Underwater - 0x183F2 - Swamp Rotating Bridge - TrueOneWay: +158319 - 0x00007 (Between Bridges Far Row 1) - 0x009A1 - Rotated Shapers +158320 - 0x00008 (Between Bridges Far Row 2) - 0x00007 - Rotated Shapers & Shapers +158321 - 0x00009 (Between Bridges Far Row 3) - 0x00008 - Rotated Shapers +158322 - 0x0000A (Between Bridges Far Row 4) - 0x00009 - Rotated Shapers Door - 0x183F2 (Red Water Pump) - 0x00596 -Swamp Red Underwater (Swamp) - Swamp Maze - 0x014D1: -158323 - 0x00001 (Red Underwater Negative Shapers 1) - True - Shapers & Negative Shapers -158324 - 0x014D2 (Red Underwater Negative Shapers 2) - True - Shapers & Negative Shapers -158325 - 0x014D4 (Red Underwater Negative Shapers 3) - True - Shapers & Negative Shapers -158326 - 0x014D1 (Red Underwater Negative Shapers 4) - True - Shapers & Negative Shapers +Swamp Red Underwater (Swamp) - Swamp Maze - 0x305D5: +158323 - 0x00001 (Red Underwater 1) - True - Shapers & Negative Shapers +158324 - 0x014D2 (Red Underwater 2) - True - Shapers & Negative Shapers +158325 - 0x014D4 (Red Underwater 3) - True - Shapers & Negative Shapers +158326 - 0x014D1 (Red Underwater 4) - True - Shapers & Negative Shapers Door - 0x305D5 (Red Underwater Exit) - 0x014D1 -Swamp Rotating Bridge (Swamp) - Swamp Rotated Shapers - 0x181F5 - Swamp Near Boat - 0x181F5 - Swamp Purple Area - 0x181F5: +Swamp Rotating Bridge (Swamp) - Swamp Between Bridges Far - 0x181F5 - Swamp Near Boat - 0x181F5 - Swamp Purple Area - 0x181F5: 158327 - 0x181F5 (Rotating Bridge) - True - Rotated Shapers & Shapers Swamp Near Boat (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Blue Underwater - 0x18482: 158328 - 0x09DB8 (Boat Spawn) - True - Boat -158329 - 0x003B2 (More Rotated Shapers 1) - 0x0000A - Rotated Shapers -158330 - 0x00A1E (More Rotated Shapers 2) - 0x003B2 - Rotated Shapers -158331 - 0x00C2E (More Rotated Shapers 3) - 0x00A1E - Rotated Shapers -158332 - 0x00E3A (More Rotated Shapers 4) - 0x00C2E - Rotated Shapers +158329 - 0x003B2 (Beyond Rotating Bridge 1) - 0x0000A - Rotated Shapers +158330 - 0x00A1E (Beyond Rotating Bridge 2) - 0x003B2 - Rotated Shapers +158331 - 0x00C2E (Beyond Rotating Bridge 3) - 0x00A1E - Rotated Shapers +158332 - 0x00E3A (Beyond Rotating Bridge 4) - 0x00C2E - Rotated Shapers 158339 - 0x17E2B (Long Bridge Control) - True - Rotated Shapers & Shapers Door - 0x18482 (Blue Water Pump) - 0x00E3A @@ -610,25 +610,25 @@ Swamp Purple Area (Swamp) - Swamp Rotating Bridge - TrueOneWay - Swamp Purple Un Door - 0x0A1D6 (Purple Water Pump) - 0x00E3A Swamp Purple Underwater (Swamp): -158333 - 0x009A6 (Underwater Back Optional) - True - Shapers +158333 - 0x009A6 (Purple Underwater) - True - Shapers Swamp Blue Underwater (Swamp): -158334 - 0x009AB (Blue Underwater Negative Shapers 1) - True - Shapers & Negative Shapers -158335 - 0x009AD (Blue Underwater Negative Shapers 2) - 0x009AB - Shapers & Negative Shapers -158336 - 0x009AE (Blue Underwater Negetive Shapers 3) - 0x009AD - Shapers & Negative Shapers -158337 - 0x009AF (Blue Underwater Negative Shapers 4) - 0x009AE - Shapers & Negative Shapers -158338 - 0x00006 (Blue Underwater Negative Shapers 5) - 0x009AF - Shapers & Negative Shapers & Broken Negative Shapers +158334 - 0x009AB (Blue Underwater 1) - True - Shapers & Negative Shapers +158335 - 0x009AD (Blue Underwater 2) - 0x009AB - Shapers & Negative Shapers +158336 - 0x009AE (Blue Underwater 3) - 0x009AD - Shapers & Negative Shapers +158337 - 0x009AF (Blue Underwater 4) - 0x009AE - Shapers & Negative Shapers +158338 - 0x00006 (Blue Underwater 5) - 0x009AF - Shapers & Negative Shapers Swamp Maze (Swamp) - Swamp Laser Area - 0x17C0A & 0x17E07: -158340 - 0x17C0A (Maze Control) - True - Shapers & Negative Shapers & Rotated Shapers & Environment -158112 - 0x17E07 (Maze Control Other Side) - True - Shapers & Negative Shapers & Rotated Shapers & Environment +158340 - 0x17C0A (Maze Control) - True - Shapers & Negative Shapers & Rotated Shapers +158112 - 0x17E07 (Maze Control Other Side) - True - Shapers & Negative Shapers & Rotated Shapers Swamp Laser Area (Swamp) - Outside Swamp - 0x2D880: 158711 - 0x03615 (Laser Panel) - True - True Laser - 0x00BF6 (Laser) - 0x03615 -158341 - 0x17C05 (Near Laser Shortcut Left Panel) - True - Rotated Shapers -158342 - 0x17C02 (Near Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Rotated Shapers -Door - 0x2D880 (Near Laser Shortcut) - 0x17C02 +158341 - 0x17C05 (Laser Shortcut Left Panel) - True - Rotated Shapers +158342 - 0x17C02 (Laser Shortcut Right Panel) - 0x17C05 - Shapers & Negative Shapers & Rotated Shapers +Door - 0x2D880 (Laser Shortcut) - 0x17C02 Treehouse Entry Area (Treehouse) - Treehouse Between Doors - 0x0C309: 158343 - 0x17C95 (Boat Spawn) - True - Boat @@ -651,8 +651,8 @@ Treehouse Yellow Bridge (Treehouse) - Treehouse After Yellow Bridge - 0x17DC4: 158354 - 0x17DC4 (Yellow Bridge 9) - 0x17DC2 - Stars Treehouse After Yellow Bridge (Treehouse) - Treehouse Junction - 0x0A181: -158355 - 0x0A182 (Beyond Yellow Bridge Door Panel) - True - Stars -Door - 0x0A181 (Beyond Yellow Bridge Door) - 0x0A182 +158355 - 0x0A182 (Third Door Panel) - True - Stars +Door - 0x0A181 (Third Door) - 0x0A182 Treehouse Junction (Treehouse) - Treehouse Right Orange Bridge - True - Treehouse First Purple Bridge - True - Treehouse Green Bridge - True: 158356 - 0x2700B (Laser House Door Timer Outside Control) - True - True @@ -668,7 +668,7 @@ Treehouse Right Orange Bridge (Treehouse) - Treehouse Bridge Platform - 0x17DA2: 158391 - 0x17D88 (Right Orange Bridge 1) - True - Stars 158392 - 0x17DB4 (Right Orange Bridge 2) - 0x17D88 - Stars 158393 - 0x17D8C (Right Orange Bridge 3) - 0x17DB4 - Stars -158394 - 0x17CE3 (Right Orange Bridge 4 & Directional) - 0x17D8C - Stars & Environment +158394 - 0x17CE3 (Right Orange Bridge 4 & Directional) - 0x17D8C - Stars 158395 - 0x17DCD (Right Orange Bridge 5) - 0x17CE3 - Stars 158396 - 0x17DB2 (Right Orange Bridge 6) - 0x17DCD - Stars 158397 - 0x17DCC (Right Orange Bridge 7) - 0x17DB2 - Stars @@ -683,246 +683,248 @@ Treehouse Bridge Platform (Treehouse) - Main Island - 0x0C32D: Door - 0x0C32D (Drawbridge) - 0x037FF Treehouse Second Purple Bridge (Treehouse) - Treehouse Left Orange Bridge - 0x17DC6: -158362 - 0x17D9B (Second Purple Bridge 1) - True - Stars & Squares & Black/White Squares -158363 - 0x17D99 (Second Purple Bridge 2) - 0x17D9B - Stars & Squares & Black/White Squares -158364 - 0x17DAA (Second Purple Bridge 3) - 0x17D99 - Stars & Squares & Black/White Squares -158365 - 0x17D97 (Second Purple Bridge 4) - 0x17DAA - Stars & Squares & Black/White Squares & Colored Squares -158366 - 0x17BDF (Second Purple Bridge 5) - 0x17D97 - Stars & Squares & Colored Squares -158367 - 0x17D91 (Second Purple Bridge 6) - 0x17BDF - Stars & Squares & Colored Squares -158368 - 0x17DC6 (Second Purple Bridge 7) - 0x17D91 - Stars & Squares & Colored Squares +158362 - 0x17D9B (Second Purple Bridge 1) - True - Stars & Black/White Squares +158363 - 0x17D99 (Second Purple Bridge 2) - 0x17D9B - Stars & Black/White Squares +158364 - 0x17DAA (Second Purple Bridge 3) - 0x17D99 - Stars & Black/White Squares +158365 - 0x17D97 (Second Purple Bridge 4) - 0x17DAA - Stars & Black/White Squares & Colored Squares +158366 - 0x17BDF (Second Purple Bridge 5) - 0x17D97 - Stars & Colored Squares +158367 - 0x17D91 (Second Purple Bridge 6) - 0x17BDF - Stars & Colored Squares +158368 - 0x17DC6 (Second Purple Bridge 7) - 0x17D91 - Stars & Colored Squares Treehouse Left Orange Bridge (Treehouse) - Treehouse Laser Room Front Platform - 0x17DDB - Treehouse Laser Room Back Platform - 0x17DDB: -158376 - 0x17DB3 (Left Orange Bridge 1) - True - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -158377 - 0x17DB5 (Left Orange Bridge 2) - 0x17DB3 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -158378 - 0x17DB6 (Left Orange Bridge 3) - 0x17DB5 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -158379 - 0x17DC0 (Left Orange Bridge 4) - 0x17DB6 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -158380 - 0x17DD7 (Left Orange Bridge 5) - 0x17DC0 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol -158381 - 0x17DD9 (Left Orange Bridge 6) - 0x17DD7 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol -158382 - 0x17DB8 (Left Orange Bridge 7) - 0x17DD9 - Stars & Squares & Black/White Squares & Colored Squares & Stars + Same Colored Symbol -158383 - 0x17DDC (Left Orange Bridge 8) - 0x17DB8 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol -158384 - 0x17DD1 (Left Orange Bridge 9 & Directional) - 0x17DDC - Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Environment -158385 - 0x17DDE (Left Orange Bridge 10) - 0x17DD1 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol -158386 - 0x17DE3 (Left Orange Bridge 11) - 0x17DDE - Stars & Squares & Colored Squares & Stars + Same Colored Symbol -158387 - 0x17DEC (Left Orange Bridge 12) - 0x17DE3 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -158388 - 0x17DAE (Left Orange Bridge 13) - 0x17DEC - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -158389 - 0x17DB0 (Left Orange Bridge 14) - 0x17DAE - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -158390 - 0x17DDB (Left Orange Bridge 15) - 0x17DB0 - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol +158376 - 0x17DB3 (Left Orange Bridge 1) - True - Stars & Black/White Squares & Stars + Same Colored Symbol +158377 - 0x17DB5 (Left Orange Bridge 2) - 0x17DB3 - Stars & Black/White Squares & Stars + Same Colored Symbol +158378 - 0x17DB6 (Left Orange Bridge 3) - 0x17DB5 - Stars & Black/White Squares & Stars + Same Colored Symbol +158379 - 0x17DC0 (Left Orange Bridge 4) - 0x17DB6 - Stars & Black/White Squares & Stars + Same Colored Symbol +158380 - 0x17DD7 (Left Orange Bridge 5) - 0x17DC0 - Stars & Black/White Squares & Colored Squares & Stars + Same Colored Symbol +158381 - 0x17DD9 (Left Orange Bridge 6) - 0x17DD7 - Stars & Black/White Squares & Colored Squares & Stars + Same Colored Symbol +158382 - 0x17DB8 (Left Orange Bridge 7) - 0x17DD9 - Stars & Black/White Squares & Colored Squares & Stars + Same Colored Symbol +158383 - 0x17DDC (Left Orange Bridge 8) - 0x17DB8 - Stars & Colored Squares & Stars + Same Colored Symbol +158384 - 0x17DD1 (Left Orange Bridge 9 & Directional) - 0x17DDC - Stars & Colored Squares & Stars + Same Colored Symbol +158385 - 0x17DDE (Left Orange Bridge 10) - 0x17DD1 - Stars & Colored Squares & Stars + Same Colored Symbol +158386 - 0x17DE3 (Left Orange Bridge 11) - 0x17DDE - Stars & Colored Squares & Stars + Same Colored Symbol +158387 - 0x17DEC (Left Orange Bridge 12) - 0x17DE3 - Stars & Black/White Squares & Stars + Same Colored Symbol +158388 - 0x17DAE (Left Orange Bridge 13) - 0x17DEC - Stars & Black/White Squares & Stars + Same Colored Symbol +158389 - 0x17DB0 (Left Orange Bridge 14) - 0x17DAE - Stars & Black/White Squares & Stars + Same Colored Symbol +158390 - 0x17DDB (Left Orange Bridge 15) - 0x17DB0 - Stars & Black/White Squares & Stars + Same Colored Symbol Treehouse Green Bridge (Treehouse): 158369 - 0x17E3C (Green Bridge 1) - True - Stars & Shapers 158370 - 0x17E4D (Green Bridge 2) - 0x17E3C - Stars & Shapers 158371 - 0x17E4F (Green Bridge 3) - 0x17E4D - Stars & Shapers & Rotated Shapers -158372 - 0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Rotated Shapers & Environment -158373 - 0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Colored Shapers & Stars + Same Colored Symbol -158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Colored Shapers & Negative Shapers & Colored Negative Shapers & Stars + Same Colored Symbol +158372 - 0x17E52 (Green Bridge 4 & Directional) - 0x17E4F - Stars & Rotated Shapers +158373 - 0x17E5B (Green Bridge 5) - 0x17E52 - Stars & Shapers & Stars + Same Colored Symbol +158374 - 0x17E5F (Green Bridge 6) - 0x17E5B - Stars & Shapers & Negative Shapers & Stars + Same Colored Symbol 158375 - 0x17E61 (Green Bridge 7) - 0x17E5F - Stars & Shapers & Rotated Shapers 158610 - 0x17FA9 (Green Bridge Discard) - 0x17E61 - Triangles Treehouse Laser Room Front Platform (Treehouse) - Treehouse Laser Room - 0x0C323: -Door - 0x0C323 (Door to Laser House) - 0x17DA2 & 0x2700B & 0x17DDB +Door - 0x0C323 (Laser House Entry) - 0x17DA2 & 0x2700B & 0x17DDB Treehouse Laser Room Back Platform (Treehouse): -158611 - 0x17FA0 (Burnt House Discard) - True - Triangles +158611 - 0x17FA0 (Laser Discard) - True - Triangles Treehouse Laser Room (Treehouse): 158712 - 0x03613 (Laser Panel) - True - True -158403 - 0x17CBC (Laser House Door Timer Inside Control) - True - True +158403 - 0x17CBC (Laser House Door Timer Inside) - True - True Laser - 0x028A4 (Laser) - 0x03613 -Mountaintop (Mountaintop) - Main Island - True - Inside Mountain Top Layer - 0x17C34: +Mountainside (Mountainside) - Main Island - True - Mountaintop - True: +158612 - 0x17C42 (Discard) - True - Triangles +158665 - 0x002A6 (Vault) - True - Symmetry & Colored Dots & Black/White Squares & Dots +158666 - 0x03542 (Vault Box) - 0x002A6 - True + +Mountaintop (Mountaintop) - Mountain Top Layer - 0x17C34: 158405 - 0x0042D (River Shape) - True - True 158406 - 0x09F7F (Box Short) - 7 Lasers - True -158407 - 0x17C34 (Trap Door Triple Exit) - 0x09F7F - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -158612 - 0x17C42 (Discard) - True - Triangles -158665 - 0x002A6 (Vault) - True - Symmetry & Colored Dots & Squares & Black/White Squares & Dots -158666 - 0x03542 (Vault Box) - 0x002A6 - True +158407 - 0x17C34 (Trap Door Triple Exit) - 0x09F7F - Stars & Black/White Squares & Stars + Same Colored Symbol 158800 - 0xFFF00 (Box Long) - 7 Lasers & 11 Lasers & 0x17C34 - True -Inside Mountain Top Layer (Inside Mountain) - Inside Mountain Top Layer Bridge - 0x09E39: -158408 - 0x09E39 (Light Bridge Controller) - True - Squares & Black/White Squares & Colored Squares & Eraser & Colored Eraser +Mountain Top Layer (Mountain Floor 1) - Mountain Top Layer Bridge - 0x09E39: +158408 - 0x09E39 (Light Bridge Controller) - True - Black/White Squares & Colored Squares & Eraser -Inside Mountain Top Layer Bridge (Inside Mountain) - Inside Mountain Second Layer - 0x09E54: -158409 - 0x09E7A (Obscured Vision 1) - True - Obscured & Squares & Black/White Squares & Dots -158410 - 0x09E71 (Obscured Vision 2) - 0x09E7A - Obscured & Squares & Black/White Squares & Dots -158411 - 0x09E72 (Obscured Vision 3) - 0x09E71 - Obscured & Squares & Black/White Squares & Shapers & Dots -158412 - 0x09E69 (Obscured Vision 4) - 0x09E72 - Obscured & Squares & Black/White Squares & Dots -158413 - 0x09E7B (Obscured Vision 5) - 0x09E69 - Obscured & Squares & Black/White Squares & Dots -158414 - 0x09E73 (Moving Background 1) - True - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -158415 - 0x09E75 (Moving Background 2) - 0x09E73 - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -158416 - 0x09E78 (Moving Background 3) - 0x09E75 - Moving & Shapers -158417 - 0x09E79 (Moving Background 4) - 0x09E78 - Moving & Shapers & Rotated Shapers -158418 - 0x09E6C (Moving Background 5) - 0x09E79 - Moving & Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -158419 - 0x09E6F (Moving Background 6) - 0x09E6C - Moving & Stars & Rotated Shapers & Shapers -158420 - 0x09E6B (Moving Background 7) - 0x09E6F - Moving & Stars & Dots -158421 - 0x33AF5 (Physically Obstructed 1) - True - Squares & Black/White Squares & Environment & Symmetry -158422 - 0x33AF7 (Physically Obstructed 2) - 0x33AF5 - Squares & Black/White Squares & Stars & Environment -158423 - 0x09F6E (Physically Obstructed 3) - 0x33AF7 - Symmetry & Dots & Environment -158424 - 0x09EAD (Angled Inside Trash 1) - True - Squares & Black/White Squares & Shapers & Angled -158425 - 0x09EAF (Angled Inside Trash 2) - 0x09EAD - Squares & Black/White Squares & Shapers & Angled -Door - 0x09E54 (Door to Second Layer) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B +Mountain Top Layer Bridge (Mountain Floor 1) - Mountain Floor 2 - 0x09E54: +158409 - 0x09E7A (Right Row 1) - True - Black/White Squares & Dots +158410 - 0x09E71 (Right Row 2) - 0x09E7A - Black/White Squares & Dots +158411 - 0x09E72 (Right Row 3) - 0x09E71 - Black/White Squares & Shapers & Dots +158412 - 0x09E69 (Right Row 4) - 0x09E72 - Black/White Squares & Dots +158413 - 0x09E7B (Right Row 5) - 0x09E69 - Black/White Squares & Dots +158414 - 0x09E73 (Left Row 1) - True - Stars & Black/White Squares & Stars + Same Colored Symbol +158415 - 0x09E75 (Left Row 2) - 0x09E73 - Stars & Black/White Squares & Stars + Same Colored Symbol +158416 - 0x09E78 (Left Row 3) - 0x09E75 - Shapers +158417 - 0x09E79 (Left Row 4) - 0x09E78 - Shapers & Rotated Shapers +158418 - 0x09E6C (Left Row 5) - 0x09E79 - Stars & Black/White Squares & Stars + Same Colored Symbol +158419 - 0x09E6F (Left Row 6) - 0x09E6C - Stars & Rotated Shapers & Shapers +158420 - 0x09E6B (Left Row 7) - 0x09E6F - Stars & Dots +158421 - 0x33AF5 (Back Row 1) - True - Black/White Squares & Symmetry +158422 - 0x33AF7 (Back Row 2) - 0x33AF5 - Black/White Squares & Stars +158423 - 0x09F6E (Back Row 3) - 0x33AF7 - Symmetry & Dots +158424 - 0x09EAD (Trash Pillar 1) - True - Black/White Squares & Shapers +158425 - 0x09EAF (Trash Pillar 2) - 0x09EAD - Black/White Squares & Shapers +Door - 0x09E54 (Exit) - 0x09EAF & 0x09F6E & 0x09E6B & 0x09E7B -Inside Mountain Second Layer (Inside Mountain) - Inside Mountain Second Layer Light Bridge Room Near - 0x09FFB - Inside Mountain Second Layer Blue Bridge - 0x09E86: -158426 - 0x09FD3 (Color Cycle 1) - True - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol -158427 - 0x09FD4 (Color Cycle 2) - 0x09FD3 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol -158428 - 0x09FD6 (Color Cycle 3) - 0x09FD4 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol -158429 - 0x09FD7 (Color Cycle 4) - 0x09FD6 - Color Cycle & RGB & Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Shapers & Colored Shapers -158430 - 0x09FD8 (Color Cycle 5) - 0x09FD7 - Color Cycle & RGB & Squares & Colored Squares & Symmetry & Colored Dots +Mountain Floor 2 (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Near - 0x09FFB - Mountain Floor 2 Blue Bridge - 0x09E86: +158426 - 0x09FD3 (Near Row 1) - True - Stars & Colored Squares & Stars + Same Colored Symbol +158427 - 0x09FD4 (Near Row 2) - 0x09FD3 - Stars & Colored Squares & Stars + Same Colored Symbol +158428 - 0x09FD6 (Near Row 3) - 0x09FD4 - Stars & Colored Squares & Stars + Same Colored Symbol +158429 - 0x09FD7 (Near Row 4) - 0x09FD6 - Stars & Colored Squares & Stars + Same Colored Symbol & Shapers +158430 - 0x09FD8 (Near Row 5) - 0x09FD7 - Colored Squares & Symmetry & Colored Dots Door - 0x09FFB (Staircase Near) - 0x09FD8 -Inside Mountain Second Layer Blue Bridge (Inside Mountain) - Inside Mountain Second Layer Beyond Bridge - TrueOneWay - Inside Mountain Second Layer At Door - TrueOneWay: +Mountain Floor 2 Blue Bridge (Mountain Floor 2) - Mountain Floor 2 Beyond Bridge - TrueOneWay - Mountain Floor 2 At Door - TrueOneWay: -Inside Mountain Second Layer At Door (Inside Mountain) - Inside Mountain Second Layer Elevator Room - 0x09EDD: -Door - 0x09EDD (Door to Elevator) - 0x09ED8 & 0x09E86 +Mountain Floor 2 At Door (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EDD: +Door - 0x09EDD (Elevator Room Entry) - 0x09ED8 & 0x09E86 -Inside Mountain Second Layer Light Bridge Room Near (Inside Mountain): -158431 - 0x09E86 (Light Bridge Controller 2) - True - Stars & Stars + Same Colored Symbol & Colored Rotated Shapers & Rotated Shapers & Eraser & Two Lines +Mountain Floor 2 Light Bridge Room Near (Mountain Floor 2): +158431 - 0x09E86 (Light Bridge Controller Near) - True - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser -Inside Mountain Second Layer Beyond Bridge (Inside Mountain) - Inside Mountain Second Layer Light Bridge Room Far - 0x09E07: -158432 - 0x09FCC (Same Solution 1) - True - Dots & Same Solution -158433 - 0x09FCE (Same Solution 2) - 0x09FCC - Squares & Black/White Squares & Same Solution -158434 - 0x09FCF (Same Solution 3) - 0x09FCE - Stars & Same Solution -158435 - 0x09FD0 (Same Solution 4) - 0x09FCF - Rotated Shapers & Same Solution -158436 - 0x09FD1 (Same Solution 5) - 0x09FD0 - Stars & Squares & Colored Squares & Stars + Same Colored Symbol & Same Solution -158437 - 0x09FD2 (Same Solution 6) - 0x09FD1 - Shapers & Same Solution +Mountain Floor 2 Beyond Bridge (Mountain Floor 2) - Mountain Floor 2 Light Bridge Room Far - 0x09E07: +158432 - 0x09FCC (Far Row 1) - True - Dots +158433 - 0x09FCE (Far Row 2) - 0x09FCC - Black/White Squares +158434 - 0x09FCF (Far Row 3) - 0x09FCE - Stars +158435 - 0x09FD0 (Far Row 4) - 0x09FCF - Rotated Shapers +158436 - 0x09FD1 (Far Row 5) - 0x09FD0 - Stars & Colored Squares & Stars + Same Colored Symbol +158437 - 0x09FD2 (Far Row 6) - 0x09FD1 - Shapers Door - 0x09E07 (Staircase Far) - 0x09FD2 -Inside Mountain Second Layer Light Bridge Room Far (Inside Mountain): -158438 - 0x09ED8 (Light Bridge Controller 3) - True - Stars & Stars + Same Colored Symbol & Colored Rotated Shapers & Rotated Shapers & Eraser & Two Lines +Mountain Floor 2 Light Bridge Room Far (Mountain Floor 2): +158438 - 0x09ED8 (Light Bridge Controller Far) - True - Stars & Stars + Same Colored Symbol & Rotated Shapers & Eraser -Inside Mountain Second Layer Elevator Room (Inside Mountain) - Inside Mountain Second Layer Elevator - TrueOneWay: +Mountain Floor 2 Elevator Room (Mountain Floor 2) - Mountain Floor 2 Elevator - TrueOneWay: 158613 - 0x17F93 (Elevator Discard) - True - Triangles -Inside Mountain Second Layer Elevator (Inside Mountain) - Inside Mountain Second Layer Elevator Room - 0x09EEB - Inside Mountain Third Layer - 0x09EEB: +Mountain Floor 2 Elevator (Mountain Floor 2) - Mountain Floor 2 Elevator Room - 0x09EEB - Mountain Third Layer - 0x09EEB: 158439 - 0x09EEB (Elevator Control Panel) - True - Dots -Inside Mountain Third Layer (Inside Mountain) - Inside Mountain Second Layer Elevator - TrueOneWay - Inside Mountain Bottom Layer - 0x09F89: +Mountain Third Layer (Mountain Bottom Floor) - Mountain Floor 2 Elevator - TrueOneWay - Mountain Bottom Floor - 0x09F89: 158440 - 0x09FC1 (Giant Puzzle Bottom Left) - True - Shapers & Eraser 158441 - 0x09F8E (Giant Puzzle Bottom Right) - True - Shapers & Eraser 158442 - 0x09F01 (Giant Puzzle Top Right) - True - Rotated Shapers 158443 - 0x09EFF (Giant Puzzle Top Left) - True - Shapers & Eraser 158444 - 0x09FDA (Giant Puzzle) - 0x09FC1 & 0x09F8E & 0x09F01 & 0x09EFF - Shapers & Symmetry -Door - 0x09F89 (Glass Door) - 0x09FDA +Door - 0x09F89 (Exit) - 0x09FDA -Inside Mountain Bottom Layer (Inside Mountain) - Inside Mountain Bottom Layer Rock - 0x17FA2 - Final Room - 0x0C141: -158614 - 0x17FA2 (Bottom Layer Discard) - 0xFFF00 - Triangles & Environment -158445 - 0x01983 (Door to Final Room Left) - True - Shapers & Stars -158446 - 0x01987 (Door to Final Room Right) - True - Squares & Colored Squares & Dots -Door - 0x0C141 (Door to Final Room) - 0x01983 & 0x01987 +Mountain Bottom Floor (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x17FA2 - Final Room - 0x0C141: +158614 - 0x17FA2 (Discard) - 0xFFF00 - Triangles +158445 - 0x01983 (Final Room Entry Left) - True - Shapers & Stars +158446 - 0x01987 (Final Room Entry Right) - True - Colored Squares & Dots +Door - 0x0C141 (Final Room Entry) - 0x01983 & 0x01987 -Inside Mountain Bottom Layer Rock (Inside Mountain) - Inside Mountain Bottom Layer - 0x17F33 - Inside Mountain Path to Secret Area - 0x17F33: -Door - 0x17F33 (Bottom Layer Rock Open) - True +Mountain Bottom Floor Rock (Mountain Bottom Floor) - Mountain Bottom Floor - 0x17F33 - Mountain Path to Caves - 0x17F33: +Door - 0x17F33 (Rock Open) - True -Inside Mountain Path to Secret Area (Inside Mountain) - Inside Mountain Bottom Layer Rock - 0x334E1 - Inside Mountain Caves - 0x2D77D: -158447 - 0x00FF8 (Secret Area Entry Panel) - True - Triangles & Black/White Squares & Squares -Door - 0x2D77D (Door to Secret Area) - 0x00FF8 +Mountain Path to Caves (Mountain Bottom Floor) - Mountain Bottom Floor Rock - 0x334E1 - Caves - 0x2D77D: +158447 - 0x00FF8 (Caves Entry Panel) - True - Triangles & Black/White Squares +Door - 0x2D77D (Caves Entry) - 0x00FF8 158448 - 0x334E1 (Rock Control) - True - True -Inside Mountain Caves (Inside Mountain Caves) - Main Island - 0x2D73F - Main Island - 0x2D859 - Path to Challenge - 0x019A5: -158451 - 0x335AB (Elevator Inside Control) - True - Dots & Squares & Black/White Squares -158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Squares & Black/White Squares -158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Squares & Black/White Squares & Dots -158454 - 0x00190 (Dot Grid Triangles 1) - True - Dots & Triangles -158455 - 0x00558 (Dot Grid Triangles 2) - 0x00190 - Dots & Triangles -158456 - 0x00567 (Dot Grid Triangles 3) - 0x00558 - Dots & Triangles -158457 - 0x006FE (Dot Grid Triangles 4) - 0x00567 - Dots & Triangles -158458 - 0x01A0D (Symmetry Triangles) - True - Symmetry & Triangles -158459 - 0x008B8 (Squares and Triangles) - True - Squares & Black/White Squares & Triangles -158460 - 0x00973 (Stars and Triangles) - 0x008B8 - Stars & Triangles -158461 - 0x0097B (Stars and Triangles of same color) - 0x00973 - Stars & Triangles & Stars and Triangles of same color & Stars + Same Colored Symbol -158462 - 0x0097D (Stars & Squares and Triangles) - 0x0097B - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol & Triangles -158463 - 0x0097E (Stars & Squares and Triangles 2) - 0x0097D - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol & Stars and Triangles of same color -158464 - 0x00994 (Rotated Shapers and Triangles 1) - True - Rotated Shapers & Triangles -158465 - 0x334D5 (Rotated Shapers and Triangles 2) - 0x00994 - Rotated Shapers & Triangles -158466 - 0x00995 (Rotated Shapers and Triangles 3) - 0x334D5 - Rotated Shapers & Triangles -158467 - 0x00996 (Shapers and Triangles 1) - 0x00995 - Shapers & Triangles -158468 - 0x00998 (Shapers and Triangles 2) - 0x00996 - Shapers & Triangles -158469 - 0x009A4 (Broken Shapers) - True - Shapers & Broken Shapers -158470 - 0x018A0 (Symmetry Shapers) - True - Shapers & Symmetry -158471 - 0x00A72 (Broken and Negative Shapers) - True - Shapers & Broken Shapers & Negative Shapers -158472 - 0x32962 (Rotated Broken Shapers) - True - Rotated Shapers & Broken Rotated Shapers -158473 - 0x32966 (Stars and Squares) - True - Stars & Squares & Black/White Squares & Stars + Same Colored Symbol -158474 - 0x01A31 (Rainbow Squares) - True - Color Cycle & RGB & Squares & Colored Squares -158475 - 0x00B71 (Squares & Stars and Colored Eraser) - True - Colored Eraser & Squares & Colored Squares & Stars & Stars + Same Colored Symbol & Eraser -158478 - 0x288EA (Wooden Beam Shapers) - True - Environment & Shapers -158479 - 0x288FC (Wooden Beam Squares and Shapers) - True - Environment & Squares & Black/White Squares & Shapers & Rotated Shapers -158480 - 0x289E7 (Wooden Beam Stars and Squares) - True - Environment & Stars & Squares & Black/White Squares -158481 - 0x288AA (Wooden Beam Shapers and Stars) - True - Environment & Stars & Shapers -158482 - 0x17FB9 (Upstairs Dot Grid Negative Shapers) - True - Shapers & Dots & Negative Shapers -158483 - 0x0A16B (Upstairs Dot Grid Gap Dots) - True - Dots -158484 - 0x0A2CE (Upstairs Dot Grid Stars) - 0x0A16B - Stars & Dots -158485 - 0x0A2D7 (Upstairs Dot Grid Stars & Squares) - 0x0A2CE - Dots & Black/White Squares & Stars + Same Colored Symbol & Stars -158486 - 0x0A2DD (Upstairs Dot Grid Shapers) - 0x0A2D7 - Shapers & Dots -158487 - 0x0A2EA (Upstairs Dot Grid Rotated Shapers) - 0x0A2DD - Rotated Shapers & Dots -158488 - 0x0008F (Upstairs Invisible Dots 1) - True - Dots & Invisible Dots -158489 - 0x0006B (Upstairs Invisible Dots 2) - 0x0008F - Dots & Invisible Dots -158490 - 0x0008B (Upstairs Invisible Dots 3) - 0x0006B - Dots & Invisible Dots -158491 - 0x0008C (Upstairs Invisible Dots 4) - 0x0008B - Dots & Invisible Dots -158492 - 0x0008A (Upstairs Invisible Dots 5) - 0x0008C - Dots & Invisible Dots -158493 - 0x00089 (Upstairs Invisible Dots 6) - 0x0008A - Dots & Invisible Dots -158494 - 0x0006A (Upstairs Invisible Dots 7) - 0x00089 - Dots & Invisible Dots -158495 - 0x0006C (Upstairs Invisible Dots 8) - 0x0006A - Dots & Invisible Dots -158496 - 0x00027 (Upstairs Invisible Dot Symmetry 1) - True - Dots & Invisible Dots & Symmetry -158497 - 0x00028 (Upstairs Invisible Dot Symmetry 2) - 0x00027 - Dots & Invisible Dots & Symmetry -158498 - 0x00029 (Upstairs Invisible Dot Symmetry 3) - 0x00028 - Dots & Invisible Dots & Symmetry -158476 - 0x09DD5 (Lone Pillar) - True - Pillar & Triangles -Door - 0x019A5 (Secret Black Door to Challenge) - 0x09DD5 -158449 - 0x021D7 (Shortcut to Mountain Panel) - True - Triangles & Stars & Stars + Same Colored Symbol & Colored Triangles -Door - 0x2D73F (Shortcut to Mountain Door) - 0x021D7 -158450 - 0x17CF2 (Shortcut to Swamp Panel) - True - Triangles -Door - 0x2D859 (Shortcut to Swamp Door) - 0x17CF2 +Caves (Caves) - Main Island - 0x2D73F - Main Island - 0x2D859 - Path to Challenge - 0x019A5: +158451 - 0x335AB (Elevator Inside Control) - True - Dots & Black/White Squares +158452 - 0x335AC (Elevator Upper Outside Control) - 0x335AB - Black/White Squares +158453 - 0x3369D (Elevator Lower Outside Control) - 0x335AB - Black/White Squares & Dots +158454 - 0x00190 (Blue Tunnel Right First 1) - True - Dots & Triangles +158455 - 0x00558 (Blue Tunnel Right First 2) - 0x00190 - Dots & Triangles +158456 - 0x00567 (Blue Tunnel Right First 3) - 0x00558 - Dots & Triangles +158457 - 0x006FE (Blue Tunnel Right First 4) - 0x00567 - Dots & Triangles +158458 - 0x01A0D (Blue Tunnel Left First 1) - True - Symmetry & Triangles +158459 - 0x008B8 (Blue Tunnel Left Second 1) - True - Black/White Squares & Triangles +158460 - 0x00973 (Blue Tunnel Left Second 2) - 0x008B8 - Stars & Triangles +158461 - 0x0097B (Blue Tunnel Left Second 3) - 0x00973 - Stars & Triangles & Stars + Same Colored Symbol +158462 - 0x0097D (Blue Tunnel Left Second 4) - 0x0097B - Stars & Black/White Squares & Stars + Same Colored Symbol & Triangles +158463 - 0x0097E (Blue Tunnel Left Second 5) - 0x0097D - Stars & Black/White Squares & Stars + Same Colored Symbol +158464 - 0x00994 (Blue Tunnel Right Second 1) - True - Rotated Shapers & Triangles +158465 - 0x334D5 (Blue Tunnel Right Second 2) - 0x00994 - Rotated Shapers & Triangles +158466 - 0x00995 (Blue Tunnel Right Second 3) - 0x334D5 - Rotated Shapers & Triangles +158467 - 0x00996 (Blue Tunnel Right Second 4) - 0x00995 - Shapers & Triangles +158468 - 0x00998 (Blue Tunnel Right Second 5) - 0x00996 - Shapers & Triangles +158469 - 0x009A4 (Blue Tunnel Left Third 1) - True - Shapers +158470 - 0x018A0 (Blue Tunnel Right Third 1) - True - Shapers & Symmetry +158471 - 0x00A72 (Blue Tunnel Left Fourth 1) - True - Shapers & Negative Shapers +158472 - 0x32962 (First Floor Left) - True - Rotated Shapers +158473 - 0x32966 (First Floor Grounded) - True - Stars & Black/White Squares & Stars + Same Colored Symbol +158474 - 0x01A31 (First Floor Middle) - True - Colored Squares +158475 - 0x00B71 (First Floor Right) - True - Colored Squares & Stars & Stars + Same Colored Symbol & Eraser +158478 - 0x288EA (First Wooden Beam) - True - Shapers +158479 - 0x288FC (Second Wooden Beam) - True - Black/White Squares & Shapers & Rotated Shapers +158480 - 0x289E7 (Third Wooden Beam) - True - Stars & Black/White Squares +158481 - 0x288AA (Fourth Wooden Beam) - True - Stars & Shapers +158482 - 0x17FB9 (Left Upstairs Single) - True - Shapers & Dots & Negative Shapers +158483 - 0x0A16B (Left Upstairs Left Row 1) - True - Dots +158484 - 0x0A2CE (Left Upstairs Left Row 2) - 0x0A16B - Stars & Dots +158485 - 0x0A2D7 (Left Upstairs Left Row 3) - 0x0A2CE - Dots & Black/White Squares & Stars + Same Colored Symbol & Stars +158486 - 0x0A2DD (Left Upstairs Left Row 4) - 0x0A2D7 - Shapers & Dots +158487 - 0x0A2EA (Left Upstairs Left Row 5) - 0x0A2DD - Rotated Shapers & Dots +158488 - 0x0008F (Right Upstairs Left Row 1) - True - Dots & Invisible Dots +158489 - 0x0006B (Right Upstairs Left Row 2) - 0x0008F - Dots & Invisible Dots +158490 - 0x0008B (Right Upstairs Left Row 3) - 0x0006B - Dots & Invisible Dots +158491 - 0x0008C (Right Upstairs Left Row 4) - 0x0008B - Dots & Invisible Dots +158492 - 0x0008A (Right Upstairs Left Row 5) - 0x0008C - Dots & Invisible Dots +158493 - 0x00089 (Right Upstairs Left Row 6) - 0x0008A - Dots & Invisible Dots +158494 - 0x0006A (Right Upstairs Left Row 7) - 0x00089 - Dots & Invisible Dots +158495 - 0x0006C (Right Upstairs Left Row 8) - 0x0006A - Dots & Invisible Dots +158496 - 0x00027 (Right Upstairs Right Row 1) - True - Dots & Invisible Dots & Symmetry +158497 - 0x00028 (Right Upstairs Right Row 2) - 0x00027 - Dots & Invisible Dots & Symmetry +158498 - 0x00029 (Right Upstairs Right Row 3) - 0x00028 - Dots & Invisible Dots & Symmetry +158476 - 0x09DD5 (Lone Pillar) - True - Triangles +Door - 0x019A5 (Pillar Door) - 0x09DD5 +158449 - 0x021D7 (Mountain Shortcut Panel) - True - Triangles & Stars & Stars + Same Colored Symbol +Door - 0x2D73F (Mountain Shortcut Door) - 0x021D7 +158450 - 0x17CF2 (Swamp Shortcut Panel) - True - Triangles +Door - 0x2D859 (Swamp Shortcut Door) - 0x17CF2 -Path to Challenge (Inside Mountain Caves) - Challenge - 0x0A19A: -158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Shapers & Colored Shapers & Stars + Same Colored Symbol -Door - 0x0A19A (Challenge Entry Door) - 0x0A16E +Path to Challenge (Caves) - Challenge - 0x0A19A: +158477 - 0x0A16E (Challenge Entry Panel) - True - Stars & Shapers & Stars + Same Colored Symbol +Door - 0x0A19A (Challenge Entry) - 0x0A16E -Challenge (Challenge) - Theater Walkway - 0x0348A: +Challenge (Challenge) - Tunnels - 0x0348A: 158499 - 0x0A332 (Start Timer) - 11 Lasers - True 158500 - 0x0088E (Small Basic) - 0x0A332 - True 158501 - 0x00BAF (Big Basic) - 0x0088E - True -158502 - 0x00BF3 (Square) - 0x00BAF - Squares & Black/White Squares +158502 - 0x00BF3 (Square) - 0x00BAF - Black/White Squares 158503 - 0x00C09 (Maze Map) - 0x00BF3 - Dots 158504 - 0x00CDB (Stars and Dots) - 0x00C09 - Stars & Dots 158505 - 0x0051F (Symmetry) - 0x00CDB - Symmetry & Colored Dots & Dots 158506 - 0x00524 (Stars and Shapers) - 0x0051F - Stars & Shapers 158507 - 0x00CD4 (Big Basic 2) - 0x00524 - True -158508 - 0x00CB9 (Choice Squares Right) - 0x00CD4 - Squares & Black/White Squares -158509 - 0x00CA1 (Choice Squares Middle) - 0x00CD4 - Squares & Black/White Squares -158510 - 0x00C80 (Choice Squares Left) - 0x00CD4 - Squares & Black/White Squares -158511 - 0x00C68 (Choice Squares 2 Right) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares -158512 - 0x00C59 (Choice Squares 2 Middle) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares -158513 - 0x00C22 (Choice Squares 2 Left) - 0x00CB9 | 0x00CA1 | 0x00C80 - Squares & Black/White Squares & Colored Squares +158508 - 0x00CB9 (Choice Squares Right) - 0x00CD4 - Black/White Squares +158509 - 0x00CA1 (Choice Squares Middle) - 0x00CD4 - Black/White Squares +158510 - 0x00C80 (Choice Squares Left) - 0x00CD4 - Black/White Squares +158511 - 0x00C68 (Choice Squares 2 Right) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares +158512 - 0x00C59 (Choice Squares 2 Middle) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares +158513 - 0x00C22 (Choice Squares 2 Left) - 0x00CB9 | 0x00CA1 | 0x00C80 - Black/White Squares & Colored Squares 158514 - 0x034F4 (Maze Hidden 1) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles 158515 - 0x034EC (Maze Hidden 2) - 0x00C68 | 0x00C59 | 0x00C22 - Triangles -158516 - 0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry & Pillar -158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Squares & Black/White Squares & Symmetry & Pillar +158516 - 0x1C31A (Dots Pillar) - 0x034F4 & 0x034EC - Dots & Symmetry +158517 - 0x1C319 (Squares Pillar) - 0x034F4 & 0x034EC - Black/White Squares & Symmetry 158667 - 0x0356B (Vault Box) - 0x1C31A & 0x1C319 - True -158518 - 0x039B4 (Door to Theater Walkway Panel) - True - Triangles -Door - 0x0348A (Door to Theater Walkway) - 0x039B4 +158518 - 0x039B4 (Tunnels Entry Panel) - True - Triangles +Door - 0x0348A (Tunnels Entry) - 0x039B4 -Theater Walkway (Theater Walkway) - Windmill Interior - 0x27739 - Desert Lowest Level Inbetween Shortcuts - 0x27263 - Town - 0x09E87: +Tunnels (Tunnels) - Windmill Interior - 0x27739 - Desert Lowest Level Inbetween Shortcuts - 0x27263 - Town - 0x09E87: 158668 - 0x2FAF6 (Vault Box) - True - True 158519 - 0x27732 (Theater Shortcut Panel) - True - True -Door - 0x27739 (Door to Windmill Interior) - 0x27732 +Door - 0x27739 (Theater Shortcut) - 0x27732 158520 - 0x2773D (Desert Shortcut Panel) - True - True -Door - 0x27263 (Door to Desert Elevator Room) - 0x2773D +Door - 0x27263 (Desert Shortcut) - 0x2773D 158521 - 0x09E85 (Town Shortcut Panel) - True - Triangles -Door - 0x09E87 (Door to Town) - 0x09E85 +Door - 0x09E87 (Town Shortcut) - 0x09E85 -Final Room (Inside Mountain Final Room) - Elevator - 0x339BB & 0x33961: -158522 - 0x0383A (Right Pillar 1) - True - Stars & Pillar -158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Stars & Dots & Pillar -158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots & Pillar -158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Dots & Symmetry & Pillar -158526 - 0x0383D (Left Pillar 1) - True - Dots & Pillar -158527 - 0x0383F (Left Pillar 2) - 0x0383D - Squares & Black/White Squares & Pillar -158528 - 0x03859 (Left Pillar 3) - 0x0383F - Shapers & Pillar -158529 - 0x339BB (Left Pillar 4) - 0x03859 - Squares & Black/White Squares & Stars & Symmetry & Pillar +Final Room (Mountain Final Room) - Elevator - 0x339BB & 0x33961: +158522 - 0x0383A (Right Pillar 1) - True - Stars +158523 - 0x09E56 (Right Pillar 2) - 0x0383A - Stars & Dots +158524 - 0x09E5A (Right Pillar 3) - 0x09E56 - Dots +158525 - 0x33961 (Right Pillar 4) - 0x09E5A - Dots & Symmetry +158526 - 0x0383D (Left Pillar 1) - True - Dots +158527 - 0x0383F (Left Pillar 2) - 0x0383D - Black/White Squares +158528 - 0x03859 (Left Pillar 3) - 0x0383F - Shapers +158529 - 0x339BB (Left Pillar 4) - 0x03859 - Black/White Squares & Stars & Symmetry -Elevator (Inside Mountain Final Room): +Elevator (Mountain Final Room): 158530 - 0x3D9A6 (Elevator Door Closer Left) - True - True 158531 - 0x3D9A7 (Elevator Door Close Right) - True - True -158532 - 0x3C113 (Elevator Door Open Left) - 0x3D9A6 | 0x3D9A7 - True -158533 - 0x3C114 (Elevator Door Open Right) - 0x3D9A6 | 0x3D9A7 - True +158532 - 0x3C113 (Elevator Entry Left) - 0x3D9A6 | 0x3D9A7 - True +158533 - 0x3C114 (Elevator Entry Right) - 0x3D9A6 | 0x3D9A7 - True 158534 - 0x3D9AA (Back Wall Left) - 0x3D9A6 | 0x3D9A7 - True 158535 - 0x3D9A8 (Back Wall Right) - 0x3D9A6 | 0x3D9A7 - True 158536 - 0x3D9A9 (Elevator Start) - 0x3D9AA | 0x3D9A8 - True diff --git a/worlds/witness/__init__.py b/worlds/witness/__init__.py index a6a9f9f6f8..0f8f0d75c0 100644 --- a/worlds/witness/__init__.py +++ b/worlds/witness/__init__.py @@ -36,7 +36,7 @@ class WitnessWorld(World): """ game = "The Witness" topology_present = False - data_version = 5 + data_version = 7 static_logic = StaticWitnessLogic() static_locat = StaticWitnessLocations() diff --git a/worlds/witness/locations.py b/worlds/witness/locations.py index 9b7d60ea16..ea0728b16c 100644 --- a/worlds/witness/locations.py +++ b/worlds/witness/locations.py @@ -30,17 +30,17 @@ class StaticWitnessLocations: "Outside Tutorial Vault Box", "Outside Tutorial Discard", - "Outside Tutorial Dots Introduction 5", - "Outside Tutorial Squares Introduction 9", + "Outside Tutorial Shed Row 5", + "Outside Tutorial Tree Row 9", "Glass Factory Discard", - "Glass Factory Vertical Symmetry 5", - "Glass Factory Rotational Symmetry 3", + "Glass Factory Back Wall 5", + "Glass Factory Front 3", "Glass Factory Melting 3", - "Symmetry Island Black Dots 5", - "Symmetry Island Colored Dots 6", - "Symmetry Island Fading Lines 7", + "Symmetry Island Right 5", + "Symmetry Island Back 6", + "Symmetry Island Left 7", "Symmetry Island Scenery Outlines 5", "Symmetry Island Laser Panel", @@ -48,26 +48,28 @@ class StaticWitnessLocations: "Desert Vault Box", "Desert Discard", - "Desert Sun Reflection 8", - "Desert Artificial Light Reflection 3", - "Desert Pond Reflection 5", - "Desert Flood Reflection 6", + "Desert Surface 8", + "Desert Light Room 3", + "Desert Pond Room 5", + "Desert Flood Room 6", + "Desert Final Bent 3", + "Desert Final Hexagonal", "Desert Laser Panel", - "Quarry Mill Eraser and Dots 6", - "Quarry Mill Eraser and Squares 8", - "Quarry Mill Small Squares & Dots & Eraser", - "Quarry Boathouse Intro Shapers", - "Quarry Boathouse Intro Stars", - "Quarry Boathouse Eraser and Shapers 5", - "Quarry Boathouse Stars & Eraser & Shapers 2", - "Quarry Boathouse Stars & Eraser & Shapers 5", + "Quarry Mill Lower Row 6", + "Quarry Mill Upper Row 8", + "Quarry Mill Control Room Right", + "Quarry Boathouse Intro Right", + "Quarry Boathouse Intro Left", + "Quarry Boathouse Front Row 5", + "Quarry Boathouse Back First Row 9", + "Quarry Boathouse Back Second Row 3", "Quarry Discard", "Quarry Laser Panel", - "Shadows Lower Avoid 8", - "Shadows Environmental Avoid 8", - "Shadows Follow 5", + "Shadows Intro 8", + "Shadows Far 8", + "Shadows Near 5", "Shadows Laser Panel", "Keep Hedge Maze 4", @@ -79,44 +81,44 @@ class StaticWitnessLocations: "Shipwreck Vault Box", "Shipwreck Discard", - "Monastery Rhombic Avoid 3", - "Monastery Branch Follow 2", + "Monastery Outside 3", + "Monastery Inside 4", "Monastery Laser Panel", "Town Cargo Box Discard", - "Town Hexagonal Reflection", + "Town Tall Hexagonal", "Town Church Lattice", "Town Rooftop Discard", - "Town Symmetry Squares 5 + Dots", - "Town Full Dot Grid Shapers 5", - "Town Shapers & Dots & Eraser", + "Town Red Rooftop 5", + "Town Wooden Roof Lower Row 5", + "Town Wooden Rooftop", "Town Laser Panel", "Theater Discard", "Jungle Discard", - "Jungle Waves 3", - "Jungle Waves 7", + "Jungle First Row 3", + "Jungle Second Row 4", "Jungle Popup Wall 6", "Jungle Laser Panel", "River Vault Box", - "Bunker Drawn Squares 5", - "Bunker Drawn Squares 9", - "Bunker Drawn Squares through Tinted Glass 3", - "Bunker Drop-Down Door Squares 2", + "Bunker Intro Left 5", + "Bunker Intro Back 4", + "Bunker Glass Room 3", + "Bunker UV Room 2", "Bunker Laser Panel", - "Swamp Seperatable Shapers 6", - "Swamp Combinable Shapers 8", - "Swamp Broken Shapers 4", - "Swamp Cyan Underwater Negative Shapers 5", - "Swamp Platform Shapers 4", - "Swamp Rotated Shapers 4", - "Swamp Red Underwater Negative Shapers 4", - "Swamp More Rotated Shapers 4", - "Swamp Blue Underwater Negative Shapers 5", + "Swamp Intro Front 6", + "Swamp Intro Back 8", + "Swamp Between Bridges Near Row 4", + "Swamp Cyan Underwater 5", + "Swamp Platform Row 4", + "Swamp Between Bridges Far Row 4", + "Swamp Red Underwater 4", + "Swamp Beyond Rotating Bridge 4", + "Swamp Blue Underwater 5", "Swamp Laser Panel", "Treehouse Yellow Bridge 9", @@ -125,73 +127,77 @@ class StaticWitnessLocations: "Treehouse Green Bridge 7", "Treehouse Green Bridge Discard", "Treehouse Left Orange Bridge 15", - "Treehouse Burnt House Discard", + "Treehouse Laser Discard", "Treehouse Right Orange Bridge 12", "Treehouse Laser Panel", - "Mountaintop Discard", - "Mountaintop Vault Box", - } + "Mountainside Discard", + "Mountainside Vault Box", - UNCOMMON_LOCATIONS = { "Mountaintop River Shape", "Tutorial Patio Floor", - "Quarry Mill Big Squares & Dots & Eraser", + "Quarry Mill Control Room Left", "Theater Tutorial Video", "Theater Desert Video", "Theater Jungle Video", "Theater Shipwreck Video", "Theater Mountain Video", - "Town RGB Squares", - "Town RGB Stars", - "Swamp Underwater Back Optional", + "Town RGB Room Left", + "Town RGB Room Right", + "Swamp Purple Underwater", } CAVES_LOCATIONS = { - "Inside Mountain Caves Dot Grid Triangles 4", - "Inside Mountain Caves Symmetry Triangles", - "Inside Mountain Caves Stars & Squares and Triangles 2", - "Inside Mountain Caves Shapers and Triangles 2", - "Inside Mountain Caves Symmetry Shapers", - "Inside Mountain Caves Broken and Negative Shapers", - "Inside Mountain Caves Broken Shapers", + "Caves Blue Tunnel Right First 4", + "Caves Blue Tunnel Left First 1", + "Caves Blue Tunnel Left Second 5", + "Caves Blue Tunnel Right Second 5", + "Caves Blue Tunnel Right Third 1", + "Caves Blue Tunnel Left Fourth 1", + "Caves Blue Tunnel Left Third 1", - "Inside Mountain Caves Rainbow Squares", - "Inside Mountain Caves Squares & Stars and Colored Eraser", - "Inside Mountain Caves Rotated Broken Shapers", - "Inside Mountain Caves Stars and Squares", - "Inside Mountain Caves Lone Pillar", - "Inside Mountain Caves Wooden Beam Shapers", - "Inside Mountain Caves Wooden Beam Squares and Shapers", - "Inside Mountain Caves Wooden Beam Stars and Squares", - "Inside Mountain Caves Wooden Beam Shapers and Stars", - "Inside Mountain Caves Upstairs Invisible Dots 8", - "Inside Mountain Caves Upstairs Invisible Dot Symmetry 3", - "Inside Mountain Caves Upstairs Dot Grid Negative Shapers", - "Inside Mountain Caves Upstairs Dot Grid Rotated Shapers", + "Caves First Floor Middle", + "Caves First Floor Right", + "Caves First Floor Left", + "Caves First Floor Grounded", + "Caves Lone Pillar", + "Caves First Wooden Beam", + "Caves Second Wooden Beam", + "Caves Third Wooden Beam", + "Caves Fourth Wooden Beam", + "Caves Right Upstairs Left Row 8", + "Caves Right Upstairs Right Row 3", + "Caves Left Upstairs Single", + "Caves Left Upstairs Left Row 5", - "Theater Walkway Vault Box", - "Inside Mountain Bottom Layer Discard", + "Tunnels Vault Box", + "Mountain Bottom Floor Discard", "Theater Challenge Video", } MOUNTAIN_UNREACHABLE_FROM_BEHIND = { "Mountaintop Trap Door Triple Exit", - "Inside Mountain Obscured Vision 5", - "Inside Mountain Moving Background 7", - "Inside Mountain Physically Obstructed 3", - "Inside Mountain Angled Inside Trash 2", - "Inside Mountain Color Cycle 5", - "Inside Mountain Same Solution 6", + "Mountain Floor 1 Right Row 5", + "Mountain Floor 1 Left Row 7", + "Mountain Floor 1 Back Row 3", + "Mountain Floor 1 Trash Pillar 2", + "Mountain Floor 2 Near Row 5", + "Mountain Floor 2 Far Row 6", } MOUNTAIN_REACHABLE_FROM_BEHIND = { - "Inside Mountain Elevator Discard", - "Inside Mountain Giant Puzzle", + "Mountain Floor 2 Elevator Discard", + "Mountain Bottom Floor Giant Puzzle", - "Inside Mountain Final Room Left Pillar 4", - "Inside Mountain Final Room Right Pillar 4", + "Mountain Final Room Left Pillar 4", + "Mountain Final Room Right Pillar 4", + } + + MOUNTAIN_EXTRAS = { + "Challenge Vault Box", + "Theater Challenge Video", + "Mountain Bottom Floor Discard" } ALL_LOCATIONS_TO_ID = dict() @@ -241,37 +247,44 @@ class WitnessPlayerLocations: StaticWitnessLocations.GENERAL_LOCATIONS ) - doors = get_option_value(world, player, "shuffle_doors") + doors = get_option_value(world, player, "shuffle_doors") >= 2 earlyutm = is_option_enabled(world, player, "early_secret_area") victory = get_option_value(world, player, "victory_condition") - lasers = get_option_value(world, player, "challenge_lasers") + mount_lasers = get_option_value(world, player, "mountain_lasers") + chal_lasers = get_option_value(world, player, "challenge_lasers") laser_shuffle = get_option_value(world, player, "shuffle_lasers") postgame = set() postgame = postgame | StaticWitnessLocations.CAVES_LOCATIONS postgame = postgame | StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND postgame = postgame | StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND + postgame = postgame | StaticWitnessLocations.MOUNTAIN_EXTRAS self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | postgame - if earlyutm or doors >= 2 or (victory == 1 and (lasers <= 11 or laser_shuffle)): + mountain_enterable_from_top = victory == 0 or victory == 1 or (victory == 3 and chal_lasers > mount_lasers) + + if earlyutm or doors: # in non-doors, there is no way to get symbol-locked by the final pillars (currently) postgame -= StaticWitnessLocations.CAVES_LOCATIONS - if doors >= 2: + if (doors or earlyutm) and (victory == 0 or (victory == 2 and mount_lasers > chal_lasers)): + postgame -= {"Challenge Vault Box", "Theater Challenge Video"} + + if doors or mountain_enterable_from_top: postgame -= StaticWitnessLocations.MOUNTAIN_REACHABLE_FROM_BEHIND - if victory != 2: + if mountain_enterable_from_top: postgame -= StaticWitnessLocations.MOUNTAIN_UNREACHABLE_FROM_BEHIND + if (victory == 0 and doors) or victory == 1 or (victory == 2 and mount_lasers > chal_lasers and doors): + postgame -= {"Mountain Bottom Floor Discard"} + if is_option_enabled(world, player, "shuffle_discarded_panels"): self.PANEL_TYPES_TO_SHUFFLE.add("Discard") if is_option_enabled(world, player, "shuffle_vault_boxes"): self.PANEL_TYPES_TO_SHUFFLE.add("Vault") - if is_option_enabled(world, player, "shuffle_uncommon"): - self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | StaticWitnessLocations.UNCOMMON_LOCATIONS - self.CHECK_LOCATIONS = self.CHECK_LOCATIONS | player_logic.ADDED_CHECKS if not is_option_enabled(world, player, "shuffle_postgame"): diff --git a/worlds/witness/player_logic.py b/worlds/witness/player_logic.py index 6fe45b107f..4840ea0a5d 100644 --- a/worlds/witness/player_logic.py +++ b/worlds/witness/player_logic.py @@ -60,7 +60,10 @@ class WitnessPlayerLogic: for dependentItem in door_items: all_options.add(items_option.union(dependentItem)) - return frozenset(all_options) + if panel_hex != "0x28A0D": + return frozenset(all_options) + else: # 0x28A0D depends on another entity for *non-power* reasons -> This dependency needs to be preserved + these_items = all_options these_panels = self.DEPENDENT_REQUIREMENTS_BY_HEX[panel_hex]["panels"] @@ -321,7 +324,7 @@ class WitnessPlayerLogic: self.VICTORY_LOCATION = "0x0356B" self.EVENT_ITEM_NAMES = { "0x01A0F": "Keep Laser Panel (Hedge Mazes) Activates", - "0x09D9B": "Monastery Overhead Doors Open", + "0x09D9B": "Monastery Shutters Open", "0x193A6": "Monastery Laser Panel Activates", "0x00037": "Monastery Branch Panels Activate", "0x0A079": "Access to Bunker Laser", @@ -332,24 +335,24 @@ class WitnessPlayerLogic: "0x01D3F": "Keep Laser Panel (Pressure Plates) Activates", "0x09F7F": "Mountain Access", "0x0367C": "Quarry Laser Mill Requirement Met", - "0x009A1": "Swamp Rotated Shapers 1 Activates", + "0x009A1": "Swamp Between Bridges Far 1 Activates", "0x00006": "Swamp Cyan Water Drains", - "0x00990": "Swamp Broken Shapers 1 Activates", - "0x0A8DC": "Lower Avoid 6 Activates", - "0x0000A": "Swamp More Rotated Shapers 1 Access", - "0x09E86": "Inside Mountain Second Layer Blue Bridge Access", - "0x09ED8": "Inside Mountain Second Layer Yellow Bridge Access", + "0x00990": "Swamp Between Bridges Near Row 1 Activates", + "0x0A8DC": "Intro 6 Activates", + "0x0000A": "Swamp Beyond Rotating Bridge 1 Access", + "0x09E86": "Mountain Floor 2 Blue Bridge Access", + "0x09ED8": "Mountain Floor 2 Yellow Bridge Access", "0x0A3D0": "Quarry Laser Boathouse Requirement Met", "0x00596": "Swamp Red Water Drains", "0x00E3A": "Swamp Purple Water Drains", "0x0343A": "Door to Symmetry Island Powers On", - "0xFFF00": "Inside Mountain Bottom Layer Discard Turns On", + "0xFFF00": "Mountain Bottom Floor Discard Turns On", "0x17CA6": "All Boat Panels Turn On", "0x17CDF": "All Boat Panels Turn On", "0x09DB8": "All Boat Panels Turn On", "0x17C95": "All Boat Panels Turn On", "0x03BB0": "Town Church Lattice Vision From Outside", - "0x28AC1": "Town Shapers & Dots & Eraser Turns On", + "0x28AC1": "Town Wooden Rooftop Turns On", "0x28A69": "Town Tower 1st Door Opens", "0x28ACC": "Town Tower 2nd Door Opens", "0x28AD9": "Town Tower 3rd Door Opens", @@ -357,9 +360,9 @@ class WitnessPlayerLogic: "0x03675": "Quarry Mill Ramp Activation From Above", "0x03679": "Quarry Mill Lift Lowering While Standing On It", "0x2FAF6": "Tutorial Gate Secret Solution Knowledge", - "0x079DF": "Town Hexagonal Reflection Turns On", + "0x079DF": "Town Tall Hexagonal Turns On", "0x17DA2": "Right Orange Bridge Fully Extended", - "0x19B24": "Shadows Lower Avoid Patterns Visible", + "0x19B24": "Shadows Intro Patterns Visible", "0x2700B": "Open Door to Treehouse Laser House", "0x00055": "Orchard Apple Trees 4 Turns On", "0x17DDB": "Left Orange Bridge Fully Extended", @@ -369,6 +372,8 @@ class WitnessPlayerLogic: "0x03481": "Tutorial Video Pattern Knowledge", "0x03702": "Jungle Video Pattern Knowledge", "0x0356B": "Challenge Video Pattern Knowledge", + "0x0A15F": "Desert Laser Panel Shutters Open (1)", + "0x012D7": "Desert Laser Panel Shutters Open (2)", } self.ALWAYS_EVENT_NAMES_BY_HEX = { diff --git a/worlds/witness/regions.py b/worlds/witness/regions.py index b5ee31b8ca..143f3e77e5 100644 --- a/worlds/witness/regions.py +++ b/worlds/witness/regions.py @@ -73,7 +73,7 @@ class WitnessRegions: all_locations = all_locations | set(locations_for_this_region) world.regions += [ - create_region(world, player, region_name, self.locat,locations_for_this_region) + create_region(world, player, region_name, self.locat, locations_for_this_region) ] for region_name, region in StaticWitnessLogic.ALL_REGIONS_BY_NAME.items(): diff --git a/worlds/witness/settings/Disable_Unrandomized.txt b/worlds/witness/settings/Disable_Unrandomized.txt index f03f5496c2..4f26e3136a 100644 --- a/worlds/witness/settings/Disable_Unrandomized.txt +++ b/worlds/witness/settings/Disable_Unrandomized.txt @@ -25,84 +25,84 @@ Disabled Locations: 0x00055 (Orchard Apple Tree 3) 0x032F7 (Orchard Apple Tree 4) 0x032FF (Orchard Apple Tree 5) -0x198B5 (Shadows Lower Avoid 1) -0x198BD (Shadows Lower Avoid 2) -0x198BF (Shadows Lower Avoid 3) -0x19771 (Shadows Lower Avoid 4) -0x0A8DC (Shadows Lower Avoid 5) -0x0AC74 (Shadows Lower Avoid 6) -0x0AC7A (Shadows Lower Avoid 7) -0x0A8E0 (Shadows Lower Avoid 8) -0x386FA (Shadows Environmental Avoid 1) -0x1C33F (Shadows Environmental Avoid 2) -0x196E2 (Shadows Environmental Avoid 3) -0x1972A (Shadows Environmental Avoid 4) -0x19809 (Shadows Environmental Avoid 5) -0x19806 (Shadows Environmental Avoid 6) -0x196F8 (Shadows Environmental Avoid 7) -0x1972F (Shadows Environmental Avoid 8) -0x19797 (Shadows Follow 1) -0x1979A (Shadows Follow 2) -0x197E0 (Shadows Follow 3) -0x197E8 (Shadows Follow 4) -0x197E5 (Shadows Follow 5) +0x198B5 (Shadows Intro 1) +0x198BD (Shadows Intro 2) +0x198BF (Shadows Intro 3) +0x19771 (Shadows Intro 4) +0x0A8DC (Shadows Intro 5) +0x0AC74 (Shadows Intro 6) +0x0AC7A (Shadows Intro 7) +0x0A8E0 (Shadows Intro 8) +0x386FA (Shadows Far 1) +0x1C33F (Shadows Far 2) +0x196E2 (Shadows Far 3) +0x1972A (Shadows Far 4) +0x19809 (Shadows Far 5) +0x19806 (Shadows Far 6) +0x196F8 (Shadows Far 7) +0x1972F (Shadows Far 8) +0x19797 (Shadows Near 1) +0x1979A (Shadows Near 2) +0x197E0 (Shadows Near 3) +0x197E8 (Shadows Near 4) +0x197E5 (Shadows Near 5) 0x19650 (Shadows Laser) 0x00139 (Keep Hedge Maze 1) 0x019DC (Keep Hedge Maze 2) 0x019E7 (Keep Hedge Maze 3) 0x01A0F (Keep Hedge Maze 4) 0x0360E (Laser Hedges) -0x00B10 (Monastery Door Open Left) -0x00C92 (Monastery Door Open Right) -0x00290 (Monastery Rhombic Avoid 1) -0x00038 (Monastery Rhombic Avoid 2) -0x00037 (Monastery Rhombic Avoid 3) -0x193A7 (Monastery Branch Avoid 1) -0x193AA (Monastery Branch Avoid 2) -0x193AB (Monastery Branch Follow 1) -0x193A6 (Monastery Branch Follow 2) +0x00B10 (Monastery Entry Left) +0x00C92 (Monastery Entry Right) +0x00290 (Monastery Outside 1) +0x00038 (Monastery Outside 2) +0x00037 (Monastery Outside 3) +0x193A7 (Monastery Inside 1) +0x193AA (Monastery Inside 2) +0x193AB (Monastery Inside 3) +0x193A6 (Monastery Inside 4) 0x17CA4 (Monastery Laser) -0x18590 (Tree Outlines) - True - Symmetry & Environment -0x28AE3 (Vines Shadows Follow) - 0x18590 - Shadows Follow & Environment -0x28938 (Four-way Apple Tree) - 0x28AE3 - Environment -0x079DF (Triple Environmental Puzzle) - 0x28938 - Shadows Avoid & Environment & Reflection -0x28B39 (Hexagonal Reflection) - 0x079DF & 0x2896A - Reflection +0x18590 (Transparent) - True - Symmetry & Environment +0x28AE3 (Vines) - 0x18590 - Shadows Follow & Environment +0x28938 (Apple Tree) - 0x28AE3 - Environment +0x079DF (Triple Exit) - 0x28938 - Shadows Avoid & Environment & Reflection +0x28B39 (Tall Hexagonal) - 0x079DF & 0x2896A - Reflection 0x03553 (Theater Tutorial Video) 0x03552 (Theater Desert Video) 0x0354E (Theater Jungle Video) 0x03549 (Theater Challenge Video) 0x0354F (Theater Shipwreck Video) 0x03545 (Theater Mountain Video) -0x002C4 (Waves 1) -0x00767 (Waves 2) -0x002C6 (Waves 3) -0x0070E (Waves 4) -0x0070F (Waves 5) -0x0087D (Waves 6) -0x002C7 (Waves 7) -0x15ADD (River Rhombic Avoid Vault) +0x002C4 (First Row 1) +0x00767 (First Row 2) +0x002C6 (First Row 3) +0x0070E (Second Row 1) +0x0070F (Second Row 2) +0x0087D (Second Row 3) +0x002C7 (Second Row 4) +0x15ADD (River Outside Vault) 0x03702 (River Vault Box) -0x17CAA (Rhombic Avoid to Monastery Garden) +0x17CAA (Monastery Shortcut Panel) 0x17C2E (Door to Bunker) -0x09F7D (Bunker Drawn Squares 1) -0x09FDC (Bunker Drawn Squares 2) -0x09FF7 (Bunker Drawn Squares 3) -0x09F82 (Bunker Drawn Squares 4) -0x09FF8 (Bunker Drawn Squares 5) -0x09D9F (Bunker Drawn Squares 6) -0x09DA1 (Bunker Drawn Squares 7) -0x09DA2 (Bunker Drawn Squares 8) -0x09DAF (Bunker Drawn Squares 9) -0x0A010 (Bunker Drawn Squares through Tinted Glass 1) -0x0A01B (Bunker Drawn Squares through Tinted Glass 2) -0x0A01F (Bunker Drawn Squares through Tinted Glass 3) -0x0A099 (Door to Bunker Proper) +0x09F7D (Bunker Intro Left 1) +0x09FDC (Bunker Intro Left 2) +0x09FF7 (Bunker Intro Left 3) +0x09F82 (Bunker Intro Left 4) +0x09FF8 (Bunker Intro Left 5) +0x09D9F (Bunker Intro Back 1) +0x09DA1 (Bunker Intro Back 2) +0x09DA2 (Bunker Intro Back 3) +0x09DAF (Bunker Intro Back 4) +0x0A010 (Bunker Glass Room 1) +0x0A01B (Bunker Glass Room 2) +0x0A01F (Bunker Glass Room 3) +0x0A099 (Tinted Glass Door) 0x34BC5 (Bunker Drop-Down Door Open) 0x34BC6 (Bunker Drop-Down Door Close) -0x17E63 (Bunker Drop-Down Door Squares 1) -0x17E67 (Bunker Drop-Down Door Squares 2) +0x17E63 (Bunker UV Room 1) +0x17E67 (Bunker UV Room 2) 0x09DE0 (Bunker Laser) 0x0A079 (Bunker Elevator Control) 0x0042D (Mountaintop River Shape) -0x17CAA (River Door to Garden Panel) \ No newline at end of file +0x17CAA (River Garden Entry Panel) diff --git a/worlds/witness/settings/Door_Panel_Shuffle.txt b/worlds/witness/settings/Door_Panel_Shuffle.txt index d6982f52e3..745c0b4a45 100644 --- a/worlds/witness/settings/Door_Panel_Shuffle.txt +++ b/worlds/witness/settings/Door_Panel_Shuffle.txt @@ -1,31 +1,31 @@ Items: -Glass Factory Entry Door (Panel) -Door to Symmetry Island Lower (Panel) -Door to Symmetry Island Upper (Panel) -Door to Desert Flood Light Room (Panel) -Desert Flood Room Flood Controls (Panel) -Quarry Door to Mill (Panel) +Glass Factory Entry (Panel) +Symmetry Island Lower (Panel) +Symmetry Island Upper (Panel) +Desert Light Room Entry (Panel) +Desert Flood Controls (Panel) +Quarry Mill Entry (Panel) Quarry Mill Ramp Controls (Panel) -Quarry Mill Elevator Controls (Panel) +Quarry Mill Lift Controls (Panel) Quarry Boathouse Ramp Height Control (Panel) Quarry Boathouse Ramp Horizontal Control (Panel) Shadows Door Timer (Panel) -Monastery Entry Door Left (Panel) -Monastery Entry Door Right (Panel) -Town Door to RGB House (Panel) -Town Door to Church (Panel) +Monastery Entry Left (Panel) +Monastery Entry Right (Panel) +Town Tinted Glass Door (Panel) +Town Church Entry (Panel) Town Maze Panel (Drop-Down Staircase) (Panel) -Windmill Door (Panel) +Windmill Entry (Panel) Treehouse First & Second Doors (Panel) Treehouse Third Door (Panel) Treehouse Laser House Door Timer (Panel) -Treehouse Shortcut Drop-Down Bridge (Panel) +Treehouse Drawbridge (Panel) Jungle Popup Wall (Panel) -Bunker Entry Door (Panel) -Inside Bunker Door to Bunker Proper (Panel) +Bunker Entry (Panel) +Bunker Tinted Glass Door (Panel) Bunker Elevator Control (Panel) -Swamp Entry Door (Panel) +Swamp Entry (Panel) Swamp Sliding Bridge (Panel) Swamp Rotating Bridge (Panel) Swamp Maze Control (Panel) -Boat +Boat \ No newline at end of file diff --git a/worlds/witness/settings/Doors_Complex.txt b/worlds/witness/settings/Doors_Complex.txt index c62562e32a..9d883cad2f 100644 --- a/worlds/witness/settings/Doors_Complex.txt +++ b/worlds/witness/settings/Doors_Complex.txt @@ -1,128 +1,128 @@ Items: -Outside Tutorial Optional Door -Outside Tutorial Outpost Entry Door -Outside Tutorial Outpost Exit Door -Glass Factory Entry Door -Glass Factory Back Wall -Symmetry Island Lower Door -Symmetry Island Upper Door -Orchard Middle Gate -Orchard Final Gate -Desert Door to Flood Light Room -Desert Door to Pond Room -Desert Door to Water Levels Room -Desert Door to Elevator Room -Quarry Main Entry 1 -Quarry Main Entry 2 -Quarry Door to Mill -Quarry Mill Side Door -Quarry Mill Rooftop Shortcut -Quarry Mill Stairs -Quarry Boathouse Boat Staircase -Quarry Boathouse First Barrier -Quarry Boathouse Shortcut -Shadows Timed Door -Shadows Laser Room Right Door -Shadows Laser Room Left Door -Shadows Barrier to Quarry -Shadows Barrier to Ledge -Keep Hedge Maze 1 Exit Door -Keep Pressure Plates 1 Exit Door -Keep Hedge Maze 2 Shortcut -Keep Hedge Maze 2 Exit Door -Keep Hedge Maze 3 Shortcut -Keep Hedge Maze 3 Exit Door -Keep Hedge Maze 4 Shortcut -Keep Hedge Maze 4 Exit Door -Keep Pressure Plates 2 Exit Door -Keep Pressure Plates 3 Exit Door -Keep Pressure Plates 4 Exit Door -Keep Shortcut to Shadows -Keep Tower Shortcut -Monastery Shortcut -Monastery Inner Door -Monastery Outer Door -Monastery Door to Garden -Town Cargo Box Door -Town Wooden Roof Staircase -Town Tinted Door to RGB House -Town Door to Church -Town Maze Staircase -Town Windmill Door -Town RGB House Staircase -Town Tower Blue Panels Door -Town Tower Lattice Door -Town Tower Environmental Set Door -Town Tower Wooden Roof Set Door -Theater Entry Door -Theater Exit Door Left -Theater Exit Door Right -Jungle Bamboo Shortcut to River -Jungle Popup Wall -River Shortcut to Monastery Garden -Bunker Bunker Entry Door -Bunker Tinted Glass Door -Bunker Door to Ultraviolet Room -Bunker Door to Elevator -Swamp Entry Door -Swamp Door to Broken Shapers +Outside Tutorial Outpost Path (Door) +Outside Tutorial Outpost Entry (Door) +Outside Tutorial Outpost Exit (Door) +Glass Factory Entry (Door) +Glass Factory Back Wall (Door) +Symmetry Island Lower (Door) +Symmetry Island Upper (Door) +Orchard First Gate (Door) +Orchard Second Gate (Door) +Desert Light Room Entry (Door) +Desert Pond Room Entry (Door) +Desert Flood Room Entry (Door) +Desert Elevator Room Entry (Door) +Quarry Entry 1 (Door) +Quarry Entry 2 (Door) +Quarry Mill Entry (Door) +Quarry Mill Side Exit (Door) +Quarry Mill Roof Exit (Door) +Quarry Mill Stairs (Door) +Quarry Boathouse Dock (Door) +Quarry Boathouse First Barrier (Door) +Quarry Boathouse Second Barrier (Door) +Shadows Timed Door (Door) +Shadows Laser Entry Right (Door) +Shadows Laser Entry Left (Door) +Shadows Quarry Barrier (Door) +Shadows Ledge Barrier (Door) +Keep Hedge Maze 1 Exit (Door) +Keep Pressure Plates 1 Exit (Door) +Keep Hedge Maze 2 Shortcut (Door) +Keep Hedge Maze 2 Exit (Door) +Keep Hedge Maze 3 Shortcut (Door) +Keep Hedge Maze 3 Exit (Door) +Keep Hedge Maze 4 Shortcut (Door) +Keep Hedge Maze 4 Exit (Door) +Keep Pressure Plates 2 Exit (Door) +Keep Pressure Plates 3 Exit (Door) +Keep Pressure Plates 4 Exit (Door) +Keep Shadows Shortcut (Door) +Keep Tower Shortcut (Door) +Monastery Shortcut (Door) +Monastery Entry Inner (Door) +Monastery Entry Outer (Door) +Monastery Garden Entry (Door) +Town Cargo Box Entry (Door) +Town Wooden Roof Stairs (Door) +Town Tinted Glass Door (Door) +Town Church Entry (Door) +Town Maze Stairs (Door) +Town Windmill Entry (Door) +Town RGB House Stairs (Door) +Town Tower First Door (Door) +Town Tower Third Door (Door) +Town Tower Fourth Door (Door) +Town Tower Second Door (Door) +Theater Entry (Door) +Theater Exit Left (Door) +Theater Exit Right (Door) +Jungle Bamboo Laser Shortcut (Door) +Jungle Popup Wall (Door) +River Monastery Shortcut (Door) +Bunker Entry (Door) +Bunker Tinted Glass Door (Door) +Bunker UV Room Entry (Door) +Bunker Elevator Room Entry (Door) +Swamp Entry (Door) +Swamp Between Bridges First Door Swamp Platform Shortcut Door -Swamp Cyan Water Pump -Swamp Door to Rotated Shapers -Swamp Red Water Pump -Swamp Red Underwater Exit -Swamp Blue Water Pump -Swamp Purple Water Pump -Swamp Near Laser Shortcut -Treehouse First Door -Treehouse Second Door -Treehouse Beyond Yellow Bridge Door -Treehouse Drawbridge -Treehouse Timed Door to Laser House -Inside Mountain First Layer Exit Door -Inside Mountain Second Layer Staircase Near -Inside Mountain Second Layer Exit Door -Inside Mountain Second Layer Staircase Far -Inside Mountain Giant Puzzle Exit Door -Inside Mountain Door to Final Room -Inside Mountain Bottom Layer Rock -Inside Mountain Door to Secret Area -Caves Pillar Door -Caves Mountain Shortcut -Caves Swamp Shortcut -Challenge Entry Door -Challenge Door to Theater Walkway -Theater Walkway Door to Windmill Interior -Theater Walkway Door to Desert Elevator Room -Theater Walkway Door to Town +Swamp Cyan Water Pump (Door) +Swamp Between Bridges Second Door +Swamp Red Water Pump (Door) +Swamp Red Underwater Exit (Door) +Swamp Blue Water Pump (Door) +Swamp Purple Water Pump (Door) +Swamp Laser Shortcut (Door) +Treehouse First Door (Door) +Treehouse Second Door (Door) +Treehouse Third Door (Door) +Treehouse Drawbridge (Door) +Treehouse Laser House Entry (Door) +Mountain Floor 1 Exit (Door) +Mountain Floor 2 Staircase Near (Door) +Mountain Floor 2 Exit (Door) +Mountain Floor 2 Staircase Far (Door) +Mountain Bottom Floor Giant Puzzle Exit (Door) +Mountain Bottom Floor Final Room Entry (Door) +Mountain Bottom Floor Rock (Door) +Caves Entry (Door) +Caves Pillar Door (Door) +Caves Mountain Shortcut (Door) +Caves Swamp Shortcut (Door) +Challenge Entry (Door) +Challenge Tunnels Entry (Door) +Tunnels Theater Shortcut (Door) +Tunnels Desert Shortcut (Door) +Tunnels Town Shortcut (Door) Added Locations: -Outside Tutorial Door to Outpost Panel -Outside Tutorial Exit Door from Outpost Panel -Glass Factory Entry Door Panel -Glass Factory Vertical Symmetry 5 -Symmetry Island Door to Symmetry Island Lower Panel -Symmetry Island Door to Symmetry Island Upper Panel +Outside Tutorial Outpost Entry Panel +Outside Tutorial Outpost Exit Panel +Glass Factory Entry Panel +Glass Factory Back Wall 5 +Symmetry Island Lower Panel +Symmetry Island Upper Panel Orchard Apple Tree 3 Orchard Apple Tree 5 -Desert Door to Desert Flood Light Room Panel -Desert Artificial Light Reflection 3 -Desert Door to Water Levels Room Panel -Desert Flood Reflection 6 -Quarry Door to Quarry 1 Panel -Quarry Door to Quarry 2 Panel -Quarry Door to Mill Right -Quarry Door to Mill Left -Quarry Mill Ground Floor Shortcut Door Panel -Quarry Mill Door to Outside Quarry Stairs Panel +Desert Light Room Entry Panel +Desert Light Room 3 +Desert Flood Room Entry Panel +Desert Flood Room 6 +Quarry Entry 1 Panel +Quarry Entry 2 Panel +Quarry Mill Entry Right Panel +Quarry Mill Entry Left Panel +Quarry Mill Side Exit Panel +Quarry Mill Roof Exit Panel Quarry Mill Stair Control -Quarry Boathouse Shortcut Door Panel +Quarry Boathouse Second Barrier Panel Shadows Door Timer Inside Shadows Door Timer Outside -Shadows Environmental Avoid 8 -Shadows Follow 5 -Shadows Lower Avoid 3 -Shadows Lower Avoid 5 +Shadows Far 8 +Shadows Near 5 +Shadows Intro 3 +Shadows Intro 5 Keep Hedge Maze 1 Keep Pressure Plates 1 Keep Hedge Maze 2 @@ -131,71 +131,70 @@ Keep Hedge Maze 4 Keep Pressure Plates 2 Keep Pressure Plates 3 Keep Pressure Plates 4 -Keep Shortcut to Shadows Panel -Keep Tower Shortcut to Keep Panel -Monastery Shortcut Door Panel -Monastery Door Open Left -Monastery Door Open Right -Monastery Rhombic Avoid 3 -Town Cargo Box Panel -Town Full Dot Grid Shapers 5 -Town Tinted Door Panel -Town Door to Church Stars Panel +Keep Shadows Shortcut Panel +Keep Tower Shortcut Panel +Monastery Shortcut Panel +Monastery Entry Left +Monastery Entry Right +Monastery Outside 3 +Town Cargo Box Entry Panel +Town Wooden Roof Lower Row 5 +Town Tinted Glass Door Panel +Town Church Entry Panel Town Maze Stair Control -Town Windmill Door Panel -Town Sound Room Left +Town Windmill Entry Panel Town Sound Room Right -Town Symmetry Squares 5 + Dots +Town Red Rooftop 5 Town Church Lattice -Town Hexagonal Reflection -Town Shapers & Dots & Eraser -Windmill Door to Front of Theater Panel -Theater Door to Cargo Box Left Panel -Theater Door to Cargo Box Right Panel -Jungle Shortcut to River Panel +Town Tall Hexagonal +Town Wooden Rooftop +Windmill Theater Entry Panel +Theater Exit Left Panel +Theater Exit Right Panel +Jungle Laser Shortcut Panel Jungle Popup Wall Control -River Rhombic Avoid to Monastery Garden -Bunker Bunker Entry Panel -Bunker Door to Bunker Proper Panel -Bunker Drawn Squares through Tinted Glass 3 -Bunker Drop-Down Door Squares 2 +River Monastery Shortcut Panel +Bunker Entry Panel +Bunker Tinted Glass Door Panel +Bunker Glass Room 3 +Bunker UV Room 2 Swamp Entry Panel -Swamp Platform Shapers 4 +Swamp Platform Row 4 Swamp Platform Shortcut Right Panel -Swamp Blue Underwater Negative Shapers 5 -Swamp Broken Shapers 4 -Swamp Cyan Underwater Negative Shapers 5 -Swamp Red Underwater Negative Shapers 4 -Swamp More Rotated Shapers 4 -Swamp More Rotated Shapers 4 -Swamp Near Laser Shortcut Right Panel +Swamp Blue Underwater 5 +Swamp Between Bridges Near Row 4 +Swamp Cyan Underwater 5 +Swamp Red Underwater 4 +Swamp Beyond Rotating Bridge 4 +Swamp Beyond Rotating Bridge 4 +Swamp Laser Shortcut Right Panel Treehouse First Door Panel Treehouse Second Door Panel -Treehouse Beyond Yellow Bridge Door Panel +Treehouse Third Door Panel Treehouse Bridge Control Treehouse Left Orange Bridge 15 Treehouse Right Orange Bridge 12 Treehouse Laser House Door Timer Outside Control -Treehouse Laser House Door Timer Inside Control -Inside Mountain Moving Background 7 -Inside Mountain Obscured Vision 5 -Inside Mountain Physically Obstructed 3 -Inside Mountain Angled Inside Trash 2 -Inside Mountain Color Cycle 5 -Inside Mountain Light Bridge Controller 2 -Inside Mountain Light Bridge Controller 3 -Inside Mountain Same Solution 6 -Inside Mountain Giant Puzzle -Inside Mountain Door to Final Room Left -Inside Mountain Door to Final Room Right -Inside Mountain Bottom Layer Discard -Inside Mountain Rock Control -Inside Mountain Secret Area Entry Panel -Inside Mountain Caves Lone Pillar -Inside Mountain Caves Shortcut to Mountain Panel -Inside Mountain Caves Shortcut to Swamp Panel -Inside Mountain Caves Challenge Entry Panel -Challenge Door to Theater Walkway Panel -Theater Walkway Theater Shortcut Panel -Theater Walkway Desert Shortcut Panel -Theater Walkway Town Shortcut Panel \ No newline at end of file +Treehouse Laser House Door Timer Inside +Mountain Floor 1 Left Row 7 +Mountain Floor 1 Right Row 5 +Mountain Floor 1 Back Row 3 +Mountain Floor 1 Trash Pillar 2 +Mountain Floor 2 Near Row 5 +Mountain Floor 2 Light Bridge Controller Near +Mountain Floor 2 Light Bridge Controller Far +Mountain Floor 2 Far Row 6 +Mountain Bottom Floor Giant Puzzle +Mountain Bottom Floor Final Room Entry Left +Mountain Bottom Floor Final Room Entry Right +Mountain Bottom Floor Discard +Mountain Bottom Floor Rock Control +Mountain Bottom Floor Caves Entry Panel +Caves Lone Pillar +Caves Mountain Shortcut Panel +Caves Swamp Shortcut Panel +Caves Challenge Entry Panel +Challenge Tunnels Entry Panel +Tunnels Theater Shortcut Panel +Tunnels Desert Shortcut Panel +Tunnels Town Shortcut Panel \ No newline at end of file diff --git a/worlds/witness/settings/Doors_Max.txt b/worlds/witness/settings/Doors_Max.txt index ec0a56a597..739000426c 100644 --- a/worlds/witness/settings/Doors_Max.txt +++ b/worlds/witness/settings/Doors_Max.txt @@ -1,104 +1,104 @@ Items: -Outside Tutorial Optional Door -Outside Tutorial Outpost Entry Door -Outside Tutorial Outpost Exit Door -Glass Factory Entry Door -Glass Factory Back Wall -Symmetry Island Lower Door -Symmetry Island Upper Door -Orchard Middle Gate -Orchard Final Gate -Desert Door to Flood Light Room -Desert Door to Pond Room -Desert Door to Water Levels Room -Desert Door to Elevator Room -Quarry Main Entry 1 -Quarry Main Entry 2 -Quarry Door to Mill -Quarry Mill Side Door -Quarry Mill Rooftop Shortcut -Quarry Mill Stairs -Quarry Boathouse Boat Staircase -Quarry Boathouse First Barrier -Quarry Boathouse Shortcut -Shadows Timed Door -Shadows Laser Room Right Door -Shadows Laser Room Left Door -Shadows Barrier to Quarry -Shadows Barrier to Ledge -Keep Hedge Maze 1 Exit Door -Keep Pressure Plates 1 Exit Door -Keep Hedge Maze 2 Shortcut -Keep Hedge Maze 2 Exit Door -Keep Hedge Maze 3 Shortcut -Keep Hedge Maze 3 Exit Door -Keep Hedge Maze 4 Shortcut -Keep Hedge Maze 4 Exit Door -Keep Pressure Plates 2 Exit Door -Keep Pressure Plates 3 Exit Door -Keep Pressure Plates 4 Exit Door -Keep Shortcut to Shadows -Keep Tower Shortcut -Monastery Shortcut -Monastery Inner Door -Monastery Outer Door -Monastery Door to Garden -Town Cargo Box Door -Town Wooden Roof Staircase -Town Tinted Door to RGB House -Town Door to Church -Town Maze Staircase -Town Windmill Door -Town RGB House Staircase -Town Tower Blue Panels Door -Town Tower Lattice Door -Town Tower Environmental Set Door -Town Tower Wooden Roof Set Door -Theater Entry Door -Theater Exit Door Left -Theater Exit Door Right -Jungle Bamboo Shortcut to River -Jungle Popup Wall -River Shortcut to Monastery Garden -Bunker Bunker Entry Door -Bunker Tinted Glass Door -Bunker Door to Ultraviolet Room -Bunker Door to Elevator -Swamp Entry Door -Swamp Door to Broken Shapers +Outside Tutorial Outpost Path (Door) +Outside Tutorial Outpost Entry (Door) +Outside Tutorial Outpost Exit (Door) +Glass Factory Entry (Door) +Glass Factory Back Wall (Door) +Symmetry Island Lower (Door) +Symmetry Island Upper (Door) +Orchard First Gate (Door) +Orchard Second Gate (Door) +Desert Light Room Entry (Door) +Desert Pond Room Entry (Door) +Desert Flood Room Entry (Door) +Desert Elevator Room Entry (Door) +Quarry Entry 1 (Door) +Quarry Entry 2 (Door) +Quarry Mill Entry (Door) +Quarry Mill Side Exit (Door) +Quarry Mill Roof Exit (Door) +Quarry Mill Stairs (Door) +Quarry Boathouse Dock (Door) +Quarry Boathouse First Barrier (Door) +Quarry Boathouse Second Barrier (Door) +Shadows Timed Door (Door) +Shadows Laser Entry Right (Door) +Shadows Laser Entry Left (Door) +Shadows Quarry Barrier (Door) +Shadows Ledge Barrier (Door) +Keep Hedge Maze 1 Exit (Door) +Keep Pressure Plates 1 Exit (Door) +Keep Hedge Maze 2 Shortcut (Door) +Keep Hedge Maze 2 Exit (Door) +Keep Hedge Maze 3 Shortcut (Door) +Keep Hedge Maze 3 Exit (Door) +Keep Hedge Maze 4 Shortcut (Door) +Keep Hedge Maze 4 Exit (Door) +Keep Pressure Plates 2 Exit (Door) +Keep Pressure Plates 3 Exit (Door) +Keep Pressure Plates 4 Exit (Door) +Keep Shadows Shortcut (Door) +Keep Tower Shortcut (Door) +Monastery Shortcut (Door) +Monastery Entry Inner (Door) +Monastery Entry Outer (Door) +Monastery Garden Entry (Door) +Town Cargo Box Entry (Door) +Town Wooden Roof Stairs (Door) +Town Tinted Glass Door (Door) +Town Church Entry (Door) +Town Maze Stairs (Door) +Town Windmill Entry (Door) +Town RGB House Stairs (Door) +Town Tower First Door (Door) +Town Tower Third Door (Door) +Town Tower Fourth Door (Door) +Town Tower Second Door (Door) +Theater Entry (Door) +Theater Exit Left (Door) +Theater Exit Right (Door) +Jungle Bamboo Laser Shortcut (Door) +Jungle Popup Wall (Door) +River Monastery Shortcut (Door) +Bunker Entry (Door) +Bunker Tinted Glass Door (Door) +Bunker UV Room Entry (Door) +Bunker Elevator Room Entry (Door) +Swamp Entry (Door) +Swamp Between Bridges First Door Swamp Platform Shortcut Door -Swamp Cyan Water Pump -Swamp Door to Rotated Shapers -Swamp Red Water Pump -Swamp Red Underwater Exit -Swamp Blue Water Pump -Swamp Purple Water Pump -Swamp Near Laser Shortcut -Treehouse First Door -Treehouse Second Door -Treehouse Beyond Yellow Bridge Door -Treehouse Drawbridge -Treehouse Timed Door to Laser House -Inside Mountain First Layer Exit Door -Inside Mountain Second Layer Staircase Near -Inside Mountain Second Layer Exit Door -Inside Mountain Second Layer Staircase Far -Inside Mountain Giant Puzzle Exit Door -Inside Mountain Door to Final Room -Inside Mountain Bottom Layer Rock -Inside Mountain Door to Secret Area -Caves Pillar Door -Caves Mountain Shortcut -Caves Swamp Shortcut -Challenge Entry Door -Challenge Door to Theater Walkway -Theater Walkway Door to Windmill Interior -Theater Walkway Door to Desert Elevator Room -Theater Walkway Door to Town +Swamp Cyan Water Pump (Door) +Swamp Between Bridges Second Door +Swamp Red Water Pump (Door) +Swamp Red Underwater Exit (Door) +Swamp Blue Water Pump (Door) +Swamp Purple Water Pump (Door) +Swamp Laser Shortcut (Door) +Treehouse First Door (Door) +Treehouse Second Door (Door) +Treehouse Third Door (Door) +Treehouse Drawbridge (Door) +Treehouse Laser House Entry (Door) +Mountain Floor 1 Exit (Door) +Mountain Floor 2 Staircase Near (Door) +Mountain Floor 2 Exit (Door) +Mountain Floor 2 Staircase Far (Door) +Mountain Bottom Floor Giant Puzzle Exit (Door) +Mountain Bottom Floor Final Room Entry (Door) +Mountain Bottom Floor Rock (Door) +Caves Entry (Door) +Caves Pillar Door (Door) +Caves Mountain Shortcut (Door) +Caves Swamp Shortcut (Door) +Challenge Entry (Door) +Challenge Tunnels Entry (Door) +Tunnels Theater Shortcut (Door) +Tunnels Desert Shortcut (Door) +Tunnels Town Shortcut (Door) -Desert Flood Room Flood Controls (Panel) +Desert Flood Controls (Panel) Quarry Mill Ramp Controls (Panel) -Quarry Mill Elevator Controls (Panel) +Quarry Mill Lift Controls (Panel) Quarry Boathouse Ramp Height Control (Panel) Quarry Boathouse Ramp Horizontal Control (Panel) Bunker Elevator Control (Panel) @@ -108,32 +108,32 @@ Swamp Maze Control (Panel) Boat Added Locations: -Outside Tutorial Door to Outpost Panel -Outside Tutorial Exit Door from Outpost Panel -Glass Factory Entry Door Panel -Glass Factory Vertical Symmetry 5 -Symmetry Island Door to Symmetry Island Lower Panel -Symmetry Island Door to Symmetry Island Upper Panel +Outside Tutorial Outpost Entry Panel +Outside Tutorial Outpost Exit Panel +Glass Factory Entry Panel +Glass Factory Back Wall 5 +Symmetry Island Lower Panel +Symmetry Island Upper Panel Orchard Apple Tree 3 Orchard Apple Tree 5 -Desert Door to Desert Flood Light Room Panel -Desert Artificial Light Reflection 3 -Desert Door to Water Levels Room Panel -Desert Flood Reflection 6 -Quarry Door to Quarry 1 Panel -Quarry Door to Quarry 2 Panel -Quarry Door to Mill Right -Quarry Door to Mill Left -Quarry Mill Ground Floor Shortcut Door Panel -Quarry Mill Door to Outside Quarry Stairs Panel +Desert Light Room Entry Panel +Desert Light Room 3 +Desert Flood Room Entry Panel +Desert Flood Room 6 +Quarry Entry 1 Panel +Quarry Entry 2 Panel +Quarry Mill Entry Right Panel +Quarry Mill Entry Left Panel +Quarry Mill Side Exit Panel +Quarry Mill Roof Exit Panel Quarry Mill Stair Control -Quarry Boathouse Shortcut Door Panel +Quarry Boathouse Second Barrier Panel Shadows Door Timer Inside Shadows Door Timer Outside -Shadows Environmental Avoid 8 -Shadows Follow 5 -Shadows Lower Avoid 3 -Shadows Lower Avoid 5 +Shadows Far 8 +Shadows Near 5 +Shadows Intro 3 +Shadows Intro 5 Keep Hedge Maze 1 Keep Pressure Plates 1 Keep Hedge Maze 2 @@ -142,71 +142,70 @@ Keep Hedge Maze 4 Keep Pressure Plates 2 Keep Pressure Plates 3 Keep Pressure Plates 4 -Keep Shortcut to Shadows Panel -Keep Tower Shortcut to Keep Panel -Monastery Shortcut Door Panel -Monastery Door Open Left -Monastery Door Open Right -Monastery Rhombic Avoid 3 -Town Cargo Box Panel -Town Full Dot Grid Shapers 5 -Town Tinted Door Panel -Town Door to Church Stars Panel +Keep Shadows Shortcut Panel +Keep Tower Shortcut Panel +Monastery Shortcut Panel +Monastery Entry Left +Monastery Entry Right +Monastery Outside 3 +Town Cargo Box Entry Panel +Town Wooden Roof Lower Row 5 +Town Tinted Glass Door Panel +Town Church Entry Panel Town Maze Stair Control -Town Windmill Door Panel -Town Sound Room Left +Town Windmill Entry Panel Town Sound Room Right -Town Symmetry Squares 5 + Dots +Town Red Rooftop 5 Town Church Lattice -Town Hexagonal Reflection -Town Shapers & Dots & Eraser -Windmill Door to Front of Theater Panel -Theater Door to Cargo Box Left Panel -Theater Door to Cargo Box Right Panel -Jungle Shortcut to River Panel +Town Tall Hexagonal +Town Wooden Rooftop +Windmill Theater Entry Panel +Theater Exit Left Panel +Theater Exit Right Panel +Jungle Laser Shortcut Panel Jungle Popup Wall Control -River Rhombic Avoid to Monastery Garden -Bunker Bunker Entry Panel -Bunker Door to Bunker Proper Panel -Bunker Drawn Squares through Tinted Glass 3 -Bunker Drop-Down Door Squares 2 +River Monastery Shortcut Panel +Bunker Entry Panel +Bunker Tinted Glass Door Panel +Bunker Glass Room 3 +Bunker UV Room 2 Swamp Entry Panel -Swamp Platform Shapers 4 +Swamp Platform Row 4 Swamp Platform Shortcut Right Panel -Swamp Blue Underwater Negative Shapers 5 -Swamp Broken Shapers 4 -Swamp Cyan Underwater Negative Shapers 5 -Swamp Red Underwater Negative Shapers 4 -Swamp More Rotated Shapers 4 -Swamp More Rotated Shapers 4 -Swamp Near Laser Shortcut Right Panel +Swamp Blue Underwater 5 +Swamp Between Bridges Near Row 4 +Swamp Cyan Underwater 5 +Swamp Red Underwater 4 +Swamp Beyond Rotating Bridge 4 +Swamp Beyond Rotating Bridge 4 +Swamp Laser Shortcut Right Panel Treehouse First Door Panel Treehouse Second Door Panel -Treehouse Beyond Yellow Bridge Door Panel +Treehouse Third Door Panel Treehouse Bridge Control Treehouse Left Orange Bridge 15 Treehouse Right Orange Bridge 12 Treehouse Laser House Door Timer Outside Control -Treehouse Laser House Door Timer Inside Control -Inside Mountain Moving Background 7 -Inside Mountain Obscured Vision 5 -Inside Mountain Physically Obstructed 3 -Inside Mountain Angled Inside Trash 2 -Inside Mountain Color Cycle 5 -Inside Mountain Light Bridge Controller 2 -Inside Mountain Light Bridge Controller 3 -Inside Mountain Same Solution 6 -Inside Mountain Giant Puzzle -Inside Mountain Door to Final Room Left -Inside Mountain Door to Final Room Right -Inside Mountain Bottom Layer Discard -Inside Mountain Rock Control -Inside Mountain Secret Area Entry Panel -Inside Mountain Caves Lone Pillar -Inside Mountain Caves Shortcut to Mountain Panel -Inside Mountain Caves Shortcut to Swamp Panel -Inside Mountain Caves Challenge Entry Panel -Challenge Door to Theater Walkway Panel -Theater Walkway Theater Shortcut Panel -Theater Walkway Desert Shortcut Panel -Theater Walkway Town Shortcut Panel \ No newline at end of file +Treehouse Laser House Door Timer Inside +Mountain Floor 1 Left Row 7 +Mountain Floor 1 Right Row 5 +Mountain Floor 1 Back Row 3 +Mountain Floor 1 Trash Pillar 2 +Mountain Floor 2 Near Row 5 +Mountain Floor 2 Light Bridge Controller Near +Mountain Floor 2 Light Bridge Controller Far +Mountain Floor 2 Far Row 6 +Mountain Bottom Floor Giant Puzzle +Mountain Bottom Floor Final Room Entry Left +Mountain Bottom Floor Final Room Entry Right +Mountain Bottom Floor Discard +Mountain Bottom Floor Rock Control +Mountain Bottom Floor Caves Entry Panel +Caves Lone Pillar +Caves Mountain Shortcut Panel +Caves Swamp Shortcut Panel +Caves Challenge Entry Panel +Challenge Tunnels Entry Panel +Tunnels Theater Shortcut Panel +Tunnels Desert Shortcut Panel +Tunnels Town Shortcut Panel \ No newline at end of file diff --git a/worlds/witness/settings/Doors_Simple.txt b/worlds/witness/settings/Doors_Simple.txt index 1335456d95..8cda2ee64b 100644 --- a/worlds/witness/settings/Doors_Simple.txt +++ b/worlds/witness/settings/Doors_Simple.txt @@ -1,73 +1,73 @@ Items: -Glass Factory Back Wall -Quarry Boathouse Boat Staircase +Glass Factory Back Wall (Door) +Quarry Boathouse Dock (Door) Outside Tutorial Outpost Doors -Glass Factory Entry Door +Glass Factory Entry (Door) Symmetry Island Doors Orchard Gates Desert Doors Quarry Main Entry -Quarry Door to Mill +Quarry Mill Entry (Door) Quarry Mill Shortcuts Quarry Boathouse Barriers -Shadows Timed Door +Shadows Timed Door (Door) Shadows Laser Room Door Shadows Barriers Keep Hedge Maze Doors Keep Pressure Plates Doors Keep Shortcuts -Monastery Entry Door +Monastery Entry Monastery Shortcuts Town Doors Town Tower Doors -Theater Entry Door -Theater Exit Door +Theater Entry (Door) +Theater Exit Jungle & River Shortcuts -Jungle Popup Wall +Jungle Popup Wall (Door) Bunker Doors Swamp Doors -Swamp Near Laser Shortcut +Swamp Laser Shortcut (Door) Swamp Water Pumps Treehouse Entry Doors -Treehouse Drawbridge -Treehouse Timed Door to Laser House -Inside Mountain First Layer Exit Door -Inside Mountain Second Layer Stairs & Doors -Inside Mountain Giant Puzzle Exit Door -Inside Mountain Door to Final Room -Inside Mountain Bottom Layer Doors to Caves +Treehouse Drawbridge (Door) +Treehouse Laser House Entry (Door) +Mountain Floor 1 Exit (Door) +Mountain Floor 2 Stairs & Doors +Mountain Bottom Floor Giant Puzzle Exit (Door) +Mountain Bottom Floor Final Room Entry (Door) +Mountain Bottom Floor Doors to Caves Caves Doors to Challenge Caves Exits to Main Island -Challenge Door to Theater Walkway -Theater Walkway Doors +Challenge Tunnels Entry (Door) +Tunnels Doors Added Locations: -Outside Tutorial Door to Outpost Panel -Outside Tutorial Exit Door from Outpost Panel -Glass Factory Entry Door Panel -Glass Factory Vertical Symmetry 5 -Symmetry Island Door to Symmetry Island Lower Panel -Symmetry Island Door to Symmetry Island Upper Panel +Outside Tutorial Outpost Entry Panel +Outside Tutorial Outpost Exit Panel +Glass Factory Entry Panel +Glass Factory Back Wall 5 +Symmetry Island Lower Panel +Symmetry Island Upper Panel Orchard Apple Tree 3 Orchard Apple Tree 5 -Desert Door to Desert Flood Light Room Panel -Desert Artificial Light Reflection 3 -Desert Door to Water Levels Room Panel -Desert Flood Reflection 6 -Quarry Door to Quarry 1 Panel -Quarry Door to Quarry 2 Panel -Quarry Door to Mill Right -Quarry Door to Mill Left -Quarry Mill Ground Floor Shortcut Door Panel -Quarry Mill Door to Outside Quarry Stairs Panel +Desert Light Room Entry Panel +Desert Light Room 3 +Desert Flood Room Entry Panel +Desert Flood Room 6 +Quarry Entry 1 Panel +Quarry Entry 2 Panel +Quarry Mill Entry Right Panel +Quarry Mill Entry Left Panel +Quarry Mill Side Exit Panel +Quarry Mill Roof Exit Panel Quarry Mill Stair Control -Quarry Boathouse Shortcut Door Panel +Quarry Boathouse Second Barrier Panel Shadows Door Timer Inside Shadows Door Timer Outside -Shadows Environmental Avoid 8 -Shadows Follow 5 -Shadows Lower Avoid 3 -Shadows Lower Avoid 5 +Shadows Far 8 +Shadows Near 5 +Shadows Intro 3 +Shadows Intro 5 Keep Hedge Maze 1 Keep Pressure Plates 1 Keep Hedge Maze 2 @@ -76,71 +76,70 @@ Keep Hedge Maze 4 Keep Pressure Plates 2 Keep Pressure Plates 3 Keep Pressure Plates 4 -Keep Shortcut to Shadows Panel -Keep Tower Shortcut to Keep Panel -Monastery Shortcut Door Panel -Monastery Door Open Left -Monastery Door Open Right -Monastery Rhombic Avoid 3 -Town Cargo Box Panel -Town Full Dot Grid Shapers 5 -Town Tinted Door Panel -Town Door to Church Stars Panel +Keep Shadows Shortcut Panel +Keep Tower Shortcut Panel +Monastery Shortcut Panel +Monastery Entry Left +Monastery Entry Right +Monastery Outside 3 +Town Cargo Box Entry Panel +Town Wooden Roof Lower Row 5 +Town Tinted Glass Door Panel +Town Church Entry Panel Town Maze Stair Control -Town Windmill Door Panel -Town Sound Room Left +Town Windmill Entry Panel Town Sound Room Right -Town Symmetry Squares 5 + Dots +Town Red Rooftop 5 Town Church Lattice -Town Hexagonal Reflection -Town Shapers & Dots & Eraser -Windmill Door to Front of Theater Panel -Theater Door to Cargo Box Left Panel -Theater Door to Cargo Box Right Panel -Jungle Shortcut to River Panel +Town Tall Hexagonal +Town Wooden Rooftop +Windmill Theater Entry Panel +Theater Exit Left Panel +Theater Exit Right Panel +Jungle Laser Shortcut Panel Jungle Popup Wall Control -River Rhombic Avoid to Monastery Garden -Bunker Bunker Entry Panel -Bunker Door to Bunker Proper Panel -Bunker Drawn Squares through Tinted Glass 3 -Bunker Drop-Down Door Squares 2 +River Monastery Shortcut Panel +Bunker Entry Panel +Bunker Tinted Glass Door Panel +Bunker Glass Room 3 +Bunker UV Room 2 Swamp Entry Panel -Swamp Platform Shapers 4 +Swamp Platform Row 4 Swamp Platform Shortcut Right Panel -Swamp Blue Underwater Negative Shapers 5 -Swamp Broken Shapers 4 -Swamp Cyan Underwater Negative Shapers 5 -Swamp Red Underwater Negative Shapers 4 -Swamp More Rotated Shapers 4 -Swamp More Rotated Shapers 4 -Swamp Near Laser Shortcut Right Panel +Swamp Blue Underwater 5 +Swamp Between Bridges Near Row 4 +Swamp Cyan Underwater 5 +Swamp Red Underwater 4 +Swamp Beyond Rotating Bridge 4 +Swamp Beyond Rotating Bridge 4 +Swamp Laser Shortcut Right Panel Treehouse First Door Panel Treehouse Second Door Panel -Treehouse Beyond Yellow Bridge Door Panel +Treehouse Third Door Panel Treehouse Bridge Control Treehouse Left Orange Bridge 15 Treehouse Right Orange Bridge 12 Treehouse Laser House Door Timer Outside Control -Treehouse Laser House Door Timer Inside Control -Inside Mountain Moving Background 7 -Inside Mountain Obscured Vision 5 -Inside Mountain Physically Obstructed 3 -Inside Mountain Angled Inside Trash 2 -Inside Mountain Color Cycle 5 -Inside Mountain Light Bridge Controller 2 -Inside Mountain Light Bridge Controller 3 -Inside Mountain Same Solution 6 -Inside Mountain Giant Puzzle -Inside Mountain Door to Final Room Left -Inside Mountain Door to Final Room Right -Inside Mountain Bottom Layer Discard -Inside Mountain Rock Control -Inside Mountain Secret Area Entry Panel -Inside Mountain Caves Lone Pillar -Inside Mountain Caves Shortcut to Mountain Panel -Inside Mountain Caves Shortcut to Swamp Panel -Inside Mountain Caves Challenge Entry Panel -Challenge Door to Theater Walkway Panel -Theater Walkway Theater Shortcut Panel -Theater Walkway Desert Shortcut Panel -Theater Walkway Town Shortcut Panel \ No newline at end of file +Treehouse Laser House Door Timer Inside +Mountain Floor 1 Left Row 7 +Mountain Floor 1 Right Row 5 +Mountain Floor 1 Back Row 3 +Mountain Floor 1 Trash Pillar 2 +Mountain Floor 2 Near Row 5 +Mountain Floor 2 Light Bridge Controller Near +Mountain Floor 2 Light Bridge Controller Far +Mountain Floor 2 Far Row 6 +Mountain Bottom Floor Giant Puzzle +Mountain Bottom Floor Final Room Entry Left +Mountain Bottom Floor Final Room Entry Right +Mountain Bottom Floor Discard +Mountain Bottom Floor Rock Control +Mountain Bottom Floor Caves Entry Panel +Caves Lone Pillar +Caves Mountain Shortcut Panel +Caves Swamp Shortcut Panel +Caves Challenge Entry Panel +Challenge Tunnels Entry Panel +Tunnels Theater Shortcut Panel +Tunnels Desert Shortcut Panel +Tunnels Town Shortcut Panel \ No newline at end of file diff --git a/worlds/witness/settings/Early_UTM.txt b/worlds/witness/settings/Early_UTM.txt index 893f29d8bb..b04aa3d339 100644 --- a/worlds/witness/settings/Early_UTM.txt +++ b/worlds/witness/settings/Early_UTM.txt @@ -5,5 +5,5 @@ Starting Inventory: Caves Exits to Main Island Remove Items: -Caves Mountain Shortcut -Caves Swamp Shortcut \ No newline at end of file +Caves Mountain Shortcut (Door) +Caves Swamp Shortcut (Door) \ No newline at end of file From 1c0a93acaded42995853b718207d476bf2f2feb9 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 18 Sep 2022 00:00:54 +0200 Subject: [PATCH 79/91] doc: update use of relative/absolute imports it matters for apworlds to function --- docs/apworld specification.md | 7 +++++++ docs/world api.md | 18 +++++++++++------- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/docs/apworld specification.md b/docs/apworld specification.md index 2dcc3f0bef..9b37fd831f 100644 --- a/docs/apworld specification.md +++ b/docs/apworld specification.md @@ -23,3 +23,10 @@ No metadata is specified yet. ## Extra Data The zip can contain arbitrary files in addition what was specified above. + + +## Caveats + +Imports from other files inside the apworld have to use relative imports. + +Imports from AP base have to use absolute imports, e.g. Options.py and worlds/AutoWorld.py. diff --git a/docs/world api.md b/docs/world api.md index fd0a3711f3..5c83ae42da 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -188,7 +188,7 @@ the `/worlds` directory. The starting point for the package is `__init.py__`. Conventionally, your world class is placed in that file. World classes must inherit from the `World` class in `/worlds/AutoWorld.py`, -which can be imported as `..AutoWorld.World` from your package. +which can be imported as `worlds.AutoWorld.World` from your package. AP will pick up your world automatically due to the `AutoWorld` implementation. @@ -209,6 +209,10 @@ e.g. `from .Options import mygame_options` from your `__init__.py` will load When imported names pile up it may be easier to use `from . import Options` and access the variable as `Options.mygame_options`. +Imports from directories outside your world should use absolute imports. +Correct use of relative / absolute imports is required for zipped worlds to +function, see [apworld specification.md](apworld%20specification.md). + ### Your Item Type Each world uses its own subclass of `BaseClasses.Item`. The constuctor can be @@ -321,7 +325,7 @@ mygame_options: typing.Dict[str, type(Option)] = { ```python # __init__.py -from ..AutoWorld import World +from worlds.AutoWorld import World from .Options import mygame_options # import the options dict class MyGameWorld(World): @@ -350,7 +354,7 @@ more natural. These games typically have been edited to 'bake in' the items. from .Options import mygame_options # the options we defined earlier from .Items import mygame_items # data used below to add items to the World from .Locations import mygame_locations # same as above -from ..AutoWorld import World +from worlds.AutoWorld import World from BaseClasses import Region, Location, Entrance, Item, RegionType, ItemClassification from Utils import get_options, output_path @@ -551,7 +555,7 @@ def generate_basic(self) -> None: ### Setting Rules ```python -from ..generic.Rules import add_rule, set_rule, forbid_item +from worlds.generic.Rules import add_rule, set_rule, forbid_item from Items import get_item_type def set_rules(self) -> None: @@ -601,7 +605,7 @@ implement more complex logic in logic mixins, even if there is no need to add properties to the `BaseClasses.CollectionState` state object. When importing a file that defines a class that inherits from -`..AutoWorld.LogicMixin` the state object's class is automatically extended by +`worlds.AutoWorld.LogicMixin` the state object's class is automatically extended by the mixin's members. These members should be prefixed with underscore following the name of the implementing world. This is due to sharing a namespace with all other logic mixins. @@ -620,7 +624,7 @@ Please do this with caution and only when neccessary. ```python # Logic.py -from ..AutoWorld import LogicMixin +from worlds.AutoWorld import LogicMixin class MyGameLogic(LogicMixin): def _mygame_has_key(self, world: MultiWorld, player: int): @@ -631,7 +635,7 @@ class MyGameLogic(LogicMixin): ```python # __init__.py -from ..generic.Rules import set_rule +from worlds.generic.Rules import set_rule import .Logic # apply the mixin by importing its file class MyGameWorld(World): From 0215e1fa28b4f1bdde43ae500c84bf8f03829638 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 18 Sep 2022 12:40:35 +0200 Subject: [PATCH 80/91] SC2: always show uncollected locations (#1007) --- Starcraft2Client.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/Starcraft2Client.py b/Starcraft2Client.py index 7f4bb8f404..09e4db8b36 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -295,34 +295,37 @@ class SC2Context(CommonContext): category_panel.add_widget( Label(text=category, size_hint_y=None, height=50, outline_width=1)) - # Map is completed for mission in categories[category]: - text = mission - tooltip = "" + text: str = mission + tooltip: str = "" # Map has uncollected locations if mission in unfinished_missions: text = f"[color=6495ED]{text}[/color]" - tooltip = f"Uncollected locations:\n" - tooltip += "\n".join([self.ctx.location_names[loc] for loc in - self.ctx.locations_for_mission(mission) - if loc in self.ctx.missing_locations]) elif mission in available_missions: text = f"[color=FFFFFF]{text}[/color]" # Map requirements not met else: text = f"[color=a9a9a9]{text}[/color]" tooltip = f"Requires: " - if len(self.ctx.mission_req_table[mission].required_world) > 0: + if self.ctx.mission_req_table[mission].required_world: tooltip += ", ".join(list(self.ctx.mission_req_table)[req_mission - 1] for req_mission in self.ctx.mission_req_table[mission].required_world) - if self.ctx.mission_req_table[mission].number > 0: + if self.ctx.mission_req_table[mission].number: tooltip += " and " - if self.ctx.mission_req_table[mission].number > 0: + if self.ctx.mission_req_table[mission].number: tooltip += f"{self.ctx.mission_req_table[mission].number} missions completed" + remaining_location_names: typing.List[str] = [ + self.ctx.location_names[loc] for loc in self.ctx.locations_for_mission(mission) + if loc in self.ctx.missing_locations] + if remaining_location_names: + if tooltip: + tooltip += "\n" + tooltip += f"Uncollected locations:\n" + tooltip += "\n".join(remaining_location_names) mission_button = MissionButton(text=text, size_hint_y=None, height=50) mission_button.tooltip_text = tooltip From 58f66e0f427bd9d204de27cff92acd13ff016e44 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 18 Sep 2022 13:02:05 +0200 Subject: [PATCH 81/91] autoworld: don't load files/folders starting with '.' (#1030) * autoworld: don't load files/folders starting with '.' The imports fail if the folder has a '.' in the name, with a somewhat obscure error, and adding a '.' in front of it is what a linux user might expect to use when disabling a world temporarily. * autoworld: use tuple to filter .* and _* --- worlds/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/worlds/__init__.py b/worlds/__init__.py index 46b383b303..e36eb275a3 100644 --- a/worlds/__init__.py +++ b/worlds/__init__.py @@ -27,7 +27,8 @@ class WorldSource(typing.NamedTuple): world_sources: typing.List[WorldSource] = [] file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly for file in os.scandir(folder): - if not file.name.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders + # prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "." + if not file.name.startswith(("_", ".")): if file.is_dir(): world_sources.append(WorldSource(file.name)) elif file.is_file() and file.name.endswith(".apworld"): From c2d69cb05eaaa1514b324d1ef4fbd1ff38444130 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Sun, 18 Sep 2022 14:30:43 +0200 Subject: [PATCH 82/91] Core: add generic interface to add ER data to hints (#1014) --- BaseClasses.py | 7 ++++++ Main.py | 53 ++++++++++++++-------------------------- worlds/AutoWorld.py | 5 ++++ worlds/alttp/Regions.py | 4 +++ worlds/alttp/__init__.py | 18 +++++++++++++- 5 files changed, 52 insertions(+), 35 deletions(-) diff --git a/BaseClasses.py b/BaseClasses.py index 7a7abc8bad..df8ac02071 100644 --- a/BaseClasses.py +++ b/BaseClasses.py @@ -955,6 +955,13 @@ class Region: return True return False + def get_connecting_entrance(self, is_main_entrance: typing.Callable[[Entrance], bool]) -> Entrance: + for entrance in self.entrances: + if is_main_entrance(entrance): + return entrance + 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 __repr__(self): return self.__str__() diff --git a/Main.py b/Main.py index acff74595a..bbd0c805df 100644 --- a/Main.py +++ b/Main.py @@ -12,7 +12,7 @@ from typing import Dict, Tuple, Optional, Set from BaseClasses import MultiWorld, CollectionState, Region, RegionType, LocationProgressType, Location from worlds.alttp.Items import item_name_groups -from worlds.alttp.Regions import lookup_vanilla_location_to_entrance +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 SHOP_ID_START, total_shop_slots, FillDisabledShopSlots from Utils import output_path, get_options, __version__, version_tuple @@ -249,24 +249,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No output_file_futures.append( pool.submit(AutoWorld.call_single, world, "generate_output", player, temp_dir)) - def get_entrance_to_region(region: Region): - for entrance in region.entrances: - if entrance.parent_region.type in (RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic): - return entrance - for entrance in region.entrances: # BFS might be better here, trying DFS for now. - return get_entrance_to_region(entrance.parent_region) - # collect ER hint info - er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if - world.shuffle[player] != "vanilla" or world.retro_caves[player]} - - for region in world.regions: - if region.player in er_hint_data and region.locations: - main_entrance = get_entrance_to_region(region) - for location in region.locations: - if type(location.address) == int: # skips events and crystals - if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name: - er_hint_data[region.player][location.address] = main_entrance.name + er_hint_data: Dict[int, Dict[int, str]] = {} + AutoWorld.call_all(world, 'extend_hint_information', er_hint_data) checks_in_area = {player: {area: list() for area in ordered_areas} for player in range(1, world.players + 1)} @@ -276,22 +261,23 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for location in world.get_filled_locations(): if type(location.address) is int: - main_entrance = get_entrance_to_region(location.parent_region) if location.game != "A Link to the Past": checks_in_area[location.player]["Light World"].append(location.address) - elif location.parent_region.dungeon: - dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower', - 'Inverted Ganons Tower': 'Ganons Tower'} \ - .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) - checks_in_area[location.player][dungeonname].append(location.address) - elif location.parent_region.type == RegionType.LightWorld: - checks_in_area[location.player]["Light World"].append(location.address) - elif location.parent_region.type == RegionType.DarkWorld: - checks_in_area[location.player]["Dark World"].append(location.address) - elif main_entrance.parent_region.type == RegionType.LightWorld: - checks_in_area[location.player]["Light World"].append(location.address) - elif main_entrance.parent_region.type == RegionType.DarkWorld: - checks_in_area[location.player]["Dark World"].append(location.address) + else: + main_entrance = location.parent_region.get_connecting_entrance(is_main_entrance) + if location.parent_region.dungeon: + dungeonname = {'Inverted Agahnims Tower': 'Agahnims Tower', + 'Inverted Ganons Tower': 'Ganons Tower'} \ + .get(location.parent_region.dungeon.name, location.parent_region.dungeon.name) + checks_in_area[location.player][dungeonname].append(location.address) + elif location.parent_region.type == RegionType.LightWorld: + checks_in_area[location.player]["Light World"].append(location.address) + elif location.parent_region.type == RegionType.DarkWorld: + checks_in_area[location.player]["Dark World"].append(location.address) + elif main_entrance.parent_region.type == RegionType.LightWorld: + checks_in_area[location.player]["Light World"].append(location.address) + elif main_entrance.parent_region.type == RegionType.DarkWorld: + checks_in_area[location.player]["Dark World"].append(location.address) checks_in_area[location.player]["Total"] += 1 oldmancaves = [] @@ -305,7 +291,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No player = region.player location_id = SHOP_ID_START + total_shop_slots + index - main_entrance = get_entrance_to_region(region) + main_entrance = region.get_connecting_entrance(is_main_entrance) if main_entrance.parent_region.type == RegionType.LightWorld: checks_in_area[player]["Light World"].append(location_id) else: @@ -340,7 +326,6 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No for player, world_precollected in world.precollected_items.items()} precollected_hints = {player: set() for player in range(1, world.players + 1 + len(world.groups))} - for slot in world.player_ids: slot_data[slot] = world.worlds[slot].fill_slot_data() diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index 959bc858a0..db72ca6a95 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -240,6 +240,11 @@ class World(metaclass=AutoWorldRegister): """Fill in the slot_data field in the Connected network package.""" return {} + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): + """Fill in additional entrance information text into locations, which is displayed when hinted. + structure is {player_id: {location_id: text}} You will need to insert your own player_id.""" + pass + def modify_multidata(self, multidata: Dict[str, Any]) -> None: # TODO: TypedDict for multidata? """For deeper modification of server multidata.""" pass diff --git a/worlds/alttp/Regions.py b/worlds/alttp/Regions.py index 80c4767d23..5f8bd0a43d 100644 --- a/worlds/alttp/Regions.py +++ b/worlds/alttp/Regions.py @@ -4,6 +4,10 @@ import typing from BaseClasses import Region, Entrance, RegionType +def is_main_entrance(entrance: Entrance) -> bool: + return entrance.parent_region.type in {RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic} + + def create_regions(world, player): world.regions += [ diff --git a/worlds/alttp/__init__.py b/worlds/alttp/__init__.py index 2aeeec3951..bbdd941127 100644 --- a/worlds/alttp/__init__.py +++ b/worlds/alttp/__init__.py @@ -12,7 +12,8 @@ from .InvertedRegions import create_inverted_regions, mark_dark_world_regions from .ItemPool import generate_itempool, difficulties from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem from .Options import alttp_options, smallkey_shuffle -from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions +from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \ + is_main_entrance from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \ get_hash_string, get_base_rom_path, LttPDeltaPatch from .Rules import set_rules @@ -24,6 +25,7 @@ lttp_logger = logging.getLogger("A Link to the Past") extras_list = sum(difficulties['normal'].extras[0:5], []) + class ALTTPWeb(WebWorld): setup_en = Tutorial( "Multiworld Setup Tutorial", @@ -410,6 +412,20 @@ class ALTTPWorld(World): finally: self.rom_name_available_event.set() # make sure threading continues and errors are collected + @classmethod + def stage_extend_hint_information(cls, world, hint_data: typing.Dict[int, typing.Dict[int, str]]): + er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if + world.shuffle[player] != "vanilla" or world.retro_caves[player]} + + for region in world.regions: + if region.player in er_hint_data and region.locations: + main_entrance = region.get_connecting_entrance(is_main_entrance) + for location in region.locations: + if type(location.address) == int: # skips events and crystals + if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name: + er_hint_data[region.player][location.address] = main_entrance.name + hint_data.update(er_hint_data) + def modify_multidata(self, multidata: dict): import base64 # wait for self.rom_name to be available. From 101dab0ea408bac06b4fed5973ad87321603cd13 Mon Sep 17 00:00:00 2001 From: SoldierofOrder <107806872+SoldierofOrder@users.noreply.github.com> Date: Sun, 18 Sep 2022 10:48:36 -0400 Subject: [PATCH 83/91] SC2: Add helpful feedback when failing to locate SC2 (#1032) * SC2: The client now throws a descriptive error when ExecuteInfo.txt exists but is empty, and offers more helpful suggestions when the file doesn't exist. * SC2: Replaced the new RuntimeError with a warning in the logger to keep things consistent. * Removed communism Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Starcraft2Client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Starcraft2Client.py b/Starcraft2Client.py index 09e4db8b36..d91adffb08 100644 --- a/Starcraft2Client.py +++ b/Starcraft2Client.py @@ -793,7 +793,12 @@ def check_game_install_path() -> bool: with open(einfo) as f: content = f.read() if content: - base = re.search(r" = (.*)Versions", content).group(1) + try: + base = re.search(r" = (.*)Versions", content).group(1) + except AttributeError: + sc2_logger.warning(f"Found {einfo}, but it was empty. Run SC2 through the Blizzard launcher, then " + f"try again.") + return False if os.path.exists(base): executable = sc2.paths.latest_executeble(Path(base).expanduser() / "Versions") @@ -810,7 +815,8 @@ def check_game_install_path() -> bool: else: sc2_logger.warning(f"{einfo} pointed to {base}, but we could not find an SC2 install there.") else: - sc2_logger.warning(f"Couldn't find {einfo}. Please run /set_path with your SC2 install directory.") + sc2_logger.warning(f"Couldn't find {einfo}. Run SC2 through the Blizzard launcher, then try again. " + f"If that fails, please run /set_path with your SC2 install directory.") return False From 4686881566c17c9875c6f10c05633675299da47e Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Sun, 18 Sep 2022 11:49:31 +0200 Subject: [PATCH 84/91] WebHost: CustomServer: use defaultdicts also change non_hintable to defaultdict in MultiServer and add some typing --- MultiServer.py | 3 ++- WebHostLib/customserver.py | 17 +++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index b4ddb936d6..b0307cb85c 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -126,6 +126,7 @@ class Context: location_names: typing.Dict[int, str] = Utils.KeyedDefaultDict(lambda code: f'Unknown location (ID:{code})') all_item_and_group_names: typing.Dict[str, typing.Set[str]] forced_auto_forfeits: typing.Dict[str, bool] + non_hintable_names: typing.Dict[str, typing.Set[str]] def __init__(self, host: str, port: int, server_password: str, password: str, location_check_points: int, hint_cost: int, item_cheat: bool, forfeit_mode: str = "disabled", collect_mode="disabled", @@ -196,7 +197,7 @@ class Context: self.item_name_groups = {} self.all_item_and_group_names = {} self.forced_auto_forfeits = collections.defaultdict(lambda: False) - self.non_hintable_names = {} + self.non_hintable_names = collections.defaultdict(frozenset) self._load_game_data() self._init_game_data() diff --git a/WebHostLib/customserver.py b/WebHostLib/customserver.py index da7b54ba6d..6272633f4e 100644 --- a/WebHostLib/customserver.py +++ b/WebHostLib/customserver.py @@ -1,15 +1,16 @@ from __future__ import annotations -import functools -import websockets import asyncio +import collections +import datetime +import functools +import logging +import pickle +import random import socket import threading import time -import random -import pickle -import logging -import datetime +import websockets import Utils from .models import db_session, Room, select, commit, Command, db @@ -49,6 +50,8 @@ class DBCommandProcessor(ServerCommandProcessor): class WebHostContext(Context): + room_id: int + def __init__(self, static_server_data: dict): # static server data is used during _load_game_data to load required data, # without needing to import worlds system, which takes quite a bit of memory @@ -62,6 +65,8 @@ class WebHostContext(Context): def _load_game_data(self): for key, value in self.static_server_data.items(): setattr(self, key, value) + self.forced_auto_forfeits = collections.defaultdict(lambda: False, self.forced_auto_forfeits) + self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names) def listen_to_db_commands(self): cmdprocessor = DBCommandProcessor(self) From 267d9234e5d45157f4493e843eb10463880a0fff Mon Sep 17 00:00:00 2001 From: alwaysintreble Date: Mon, 19 Sep 2022 15:40:15 -0500 Subject: [PATCH 85/91] core: fix options with "random" as default value not generating (#1033) * core: fix options with "random" as default value not generating when option is missing from the player yaml, Using this in #893 and tested there. * remove if * OptionSets default to frozenset so handle that * range had some specific instances of assuming default as a valid value so change this here to call the from_any * isinstance instead of type Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com> --- Generate.py | 2 +- Options.py | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Generate.py b/Generate.py index 763471e90b..f048e54383 100644 --- a/Generate.py +++ b/Generate.py @@ -455,7 +455,7 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, else: player_option.verify(AutoWorldRegister.world_types[ret.game], ret.name, plando_options) else: - setattr(ret, option_key, option(option.default)) + setattr(ret, option_key, option.from_any(option.default)) # call the from_any here to support default "random" def roll_settings(weights: dict, plando_options: PlandoSettings = PlandoSettings.bosses): diff --git a/Options.py b/Options.py index 11ec46f2e9..49f044d8cd 100644 --- a/Options.py +++ b/Options.py @@ -483,7 +483,7 @@ class Range(NumericOption): if text.startswith("random"): return cls.weighted_range(text) elif text == "default" and hasattr(cls, "default"): - return cls(cls.default) + return cls.from_any(cls.default) elif text == "high": return cls(cls.range_end) elif text == "low": @@ -494,7 +494,7 @@ class Range(NumericOption): and text in ("true", "false"): # these are the conditions where "true" and "false" make sense if text == "true": - return cls(cls.default) + return cls.from_any(cls.default) else: # "false" return cls(0) return cls(int(text)) @@ -698,10 +698,7 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys): @classmethod def from_any(cls, data: typing.Any): - if type(data) == list: - cls.verify_keys(data) - return cls(data) - elif type(data) == set: + if isinstance(data, (list, set, frozenset)): cls.verify_keys(data) return cls(data) return cls.from_text(str(data)) From a95d0ce9ef08fcbdec04ba2a53b25b1a70135c89 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 20 Sep 2022 09:09:13 +0200 Subject: [PATCH 86/91] Doc: clarify requirements.txt in world api.md --- docs/world api.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/world api.md b/docs/world api.md index 5c83ae42da..514cd9ac93 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -195,8 +195,10 @@ AP will pick up your world automatically due to the `AutoWorld` implementation. ### Requirements If your world needs specific python packages, they can be listed in -`world/[world_name]/requirements.txt`. -See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format) +`world/[world_name]/requirements.txt`. ModuleUpdate.py will automatically +pick up and install them. + +See [pip documentation](https://pip.pypa.io/en/stable/cli/pip_install/#requirements-file-format). ### Relative Imports From 2d5ec6ce22e7f7755525f0b1775514f740846c0c Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 20 Sep 2022 08:08:43 +0200 Subject: [PATCH 87/91] Doc: item/location name must not be numeric --- docs/world api.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/world api.md b/docs/world api.md index 514cd9ac93..0471cf1b68 100644 --- a/docs/world api.md +++ b/docs/world api.md @@ -103,8 +103,9 @@ or boss drops for RPG-like games but could also be progress in a research tree. Each location has a `name` and an `id` (a.k.a. "code" or "address"), is placed in a Region and has access rules. -The name needs to be unique in each game, the ID needs to be unique across all -games and is best in the same range as the item IDs. +The name needs to be unique in each game and must not be numeric (has to +contain least 1 letter or symbol). The ID needs to be unique across all games +and is best in the same range as the item IDs. World-specific IDs are 1 to 253-1, IDs ≤ 0 are global and reserved. Special locations with ID `None` can hold events. @@ -121,6 +122,9 @@ their world. Progression items will be assigned to locations with higher priority and moved around to meet defined rules and accomplish progression balancing. +The name needs to be unique in each game, meaning a duplicate item has the +same ID. Name must not be numeric (has to contain at least 1 letter or symbol). + Special items with ID `None` can mark events (read below). Other classifications include From 809bda02d1cc79e438171286aadb77603c582428 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 20 Sep 2022 08:09:08 +0200 Subject: [PATCH 88/91] Test: item/location name must not be numeric --- test/general/TestNames.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 test/general/TestNames.py diff --git a/test/general/TestNames.py b/test/general/TestNames.py new file mode 100644 index 0000000000..6dae53240d --- /dev/null +++ b/test/general/TestNames.py @@ -0,0 +1,20 @@ +import unittest +from worlds.AutoWorld import AutoWorldRegister + + +class TestNames(unittest.TestCase): + def testItemNamesFormat(self): + """Item names must not be all numeric in order to differentiate between ID and name in !hint""" + for gamename, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=gamename): + for item_name in world_type.item_name_to_id: + self.assertFalse(item_name.isnumeric(), + f"Item name \"{item_name}\" is invalid. It must not be numeric.") + + def testLocationNameFormat(self): + """Location names must not be all numeric in order to differentiate between ID and name in !hint_location""" + for gamename, world_type in AutoWorldRegister.world_types.items(): + with self.subTest(game=gamename): + for location_name in world_type.location_name_to_id: + self.assertFalse(location_name.isnumeric(), + f"Location name \"{location_name}\" is invalid. It must not be numeric.") From 6d5ddf3cadd579265319d3a60dd6e8b05fca6153 Mon Sep 17 00:00:00 2001 From: black-sliver <59490463+black-sliver@users.noreply.github.com> Date: Tue, 20 Sep 2022 01:31:08 +0200 Subject: [PATCH 89/91] MultiServer: allow using IDs for hints --- MultiServer.py | 181 ++++++++++++++++++++++++++++++------------------- 1 file changed, 111 insertions(+), 70 deletions(-) diff --git a/MultiServer.py b/MultiServer.py index b0307cb85c..9f0865d425 100644 --- a/MultiServer.py +++ b/MultiServer.py @@ -222,11 +222,11 @@ class Context: self.all_item_and_group_names[game_name] = \ set(game_package["item_name_to_id"]) | set(self.item_name_groups[game_name]) - def item_names_for_game(self, game: str) -> typing.Dict[str, int]: - return self.gamespackage[game]["item_name_to_id"] + def item_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]: + return self.gamespackage[game]["item_name_to_id"] if game in self.gamespackage else None - def location_names_for_game(self, game: str) -> typing.Dict[str, int]: - return self.gamespackage[game]["location_name_to_id"] + def location_names_for_game(self, game: str) -> typing.Optional[typing.Dict[str, int]]: + return self.gamespackage[game]["location_name_to_id"] if game in self.gamespackage else None # General networking async def send_msgs(self, endpoint: Endpoint, msgs: typing.Iterable[dict]) -> bool: @@ -901,14 +901,14 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi ctx.save() -def collect_hints(ctx: Context, team: int, slot: int, item_name: str) -> typing.List[NetUtils.Hint]: +def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str]) -> typing.List[NetUtils.Hint]: hints = [] slots: typing.Set[int] = {slot} for group_id, group in ctx.groups.items(): if slot in group: slots.add(group_id) - seeked_item_id = ctx.item_names_for_game(ctx.games[slot])[item_name] + seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item] for finding_player, check_data in ctx.locations.items(): for location_id, (item_id, receiving_player, item_flags) in check_data.items(): if receiving_player in slots and item_id == seeked_item_id: @@ -1336,13 +1336,33 @@ class ClientMessageProcessor(CommonCommandProcessor): self.output(f"A hint costs {self.ctx.get_hint_cost(self.client.slot)} points. " f"You have {points_available} points.") return True + + elif input_text.isnumeric(): + game = self.ctx.games[self.client.slot] + hint_id = int(input_text) + hint_name = self.ctx.item_names[hint_id] \ + if not for_location and hint_id in self.ctx.item_names \ + else self.ctx.location_names[hint_id] \ + if for_location and hint_id in self.ctx.location_names \ + else None + if hint_name in self.ctx.non_hintable_names[game]: + self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") + hints = [] + elif not for_location: + hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id) + else: + hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id) + else: game = self.ctx.games[self.client.slot] + if game not in self.ctx.all_item_and_group_names: + self.output("Can't look up item/location for unknown game. Hint for ID instead.") + return False names = self.ctx.location_names_for_game(game) \ if for_location else \ self.ctx.all_item_and_group_names[game] - hint_name, usable, response = get_intended_text(input_text, - names) + hint_name, usable, response = get_intended_text(input_text, names) + if usable: if hint_name in self.ctx.non_hintable_names[game]: self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.") @@ -1356,63 +1376,65 @@ class ClientMessageProcessor(CommonCommandProcessor): hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name) else: # location name hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name) - cost = self.ctx.get_hint_cost(self.client.slot) - if hints: - new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot] - old_hints = set(hints) - new_hints - if old_hints: - notify_hints(self.ctx, self.client.team, list(old_hints)) - if not new_hints: - self.output("Hint was previously used, no points deducted.") - if new_hints: - found_hints = [hint for hint in new_hints if hint.found] - not_found_hints = [hint for hint in new_hints if not hint.found] - if not not_found_hints: # everything's been found, no need to pay - can_pay = 1000 - elif cost: - can_pay = int((points_available // cost) > 0) # limit to 1 new hint per call - else: - can_pay = 1000 - - self.ctx.random.shuffle(not_found_hints) - # By popular vote, make hints prefer non-local placements - not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player)) - - hints = found_hints - while can_pay > 0: - if not not_found_hints: - break - hint = not_found_hints.pop() - hints.append(hint) - can_pay -= 1 - self.ctx.hints_used[self.client.team, self.client.slot] += 1 - points_available = get_client_points(self.ctx, self.client) - - if not_found_hints: - if hints and cost and int((points_available // cost) == 0): - self.output( - f"There may be more hintables, however, you cannot afford to pay for any more. " - f" You have {points_available} and need at least " - f"{self.ctx.get_hint_cost(self.client.slot)}.") - elif hints: - self.output( - "There may be more hintables, you can rerun the command to find more.") - else: - self.output(f"You can't afford the hint. " - f"You have {points_available} points and need at least " - f"{self.ctx.get_hint_cost(self.client.slot)}.") - notify_hints(self.ctx, self.client.team, hints) - self.ctx.save() - return True - - else: - self.output("Nothing found. Item/Location may not exist.") - return False else: self.output(response) return False + if hints: + cost = self.ctx.get_hint_cost(self.client.slot) + new_hints = set(hints) - self.ctx.hints[self.client.team, self.client.slot] + old_hints = set(hints) - new_hints + if old_hints: + notify_hints(self.ctx, self.client.team, list(old_hints)) + if not new_hints: + self.output("Hint was previously used, no points deducted.") + if new_hints: + found_hints = [hint for hint in new_hints if hint.found] + not_found_hints = [hint for hint in new_hints if not hint.found] + + if not not_found_hints: # everything's been found, no need to pay + can_pay = 1000 + elif cost: + can_pay = int((points_available // cost) > 0) # limit to 1 new hint per call + else: + can_pay = 1000 + + self.ctx.random.shuffle(not_found_hints) + # By popular vote, make hints prefer non-local placements + not_found_hints.sort(key=lambda hint: int(hint.receiving_player != hint.finding_player)) + + hints = found_hints + while can_pay > 0: + if not not_found_hints: + break + hint = not_found_hints.pop() + hints.append(hint) + can_pay -= 1 + self.ctx.hints_used[self.client.team, self.client.slot] += 1 + points_available = get_client_points(self.ctx, self.client) + + if not_found_hints: + if hints and cost and int((points_available // cost) == 0): + self.output( + f"There may be more hintables, however, you cannot afford to pay for any more. " + f" You have {points_available} and need at least " + f"{self.ctx.get_hint_cost(self.client.slot)}.") + elif hints: + self.output( + "There may be more hintables, you can rerun the command to find more.") + else: + self.output(f"You can't afford the hint. " + f"You have {points_available} points and need at least " + f"{self.ctx.get_hint_cost(self.client.slot)}.") + notify_hints(self.ctx, self.client.team, hints) + self.ctx.save() + return True + + else: + self.output("Nothing found. Item/Location may not exist.") + return False + @mark_raw def _cmd_hint(self, item_name: str = "") -> bool: """Use !hint {item_name}, @@ -1860,17 +1882,25 @@ class ServerCommandProcessor(CommonCommandProcessor): seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) if usable: team, slot = self.ctx.player_name_lookup[seeked_player] - item_name = " ".join(item_name) game = self.ctx.games[slot] - item_name, usable, response = get_intended_text(item_name, self.ctx.all_item_and_group_names[game]) + full_name = " ".join(item_name) + + if full_name.isnumeric(): + item, usable, response = int(full_name), True, None + elif game in self.ctx.all_item_and_group_names: + item, usable, response = get_intended_text(full_name, self.ctx.all_item_and_group_names[game]) + else: + self.output("Can't look up item for unknown game. Hint for ID instead.") + return False + if usable: - if item_name in self.ctx.item_name_groups[game]: + if game in self.ctx.item_name_groups and item in self.ctx.item_name_groups[game]: hints = [] - for item_name_from_group in self.ctx.item_name_groups[game][item_name]: + for item_name_from_group in self.ctx.item_name_groups[game][item]: if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group)) - else: # item name - hints = collect_hints(self.ctx, team, slot, item_name) + else: # item name or id + hints = collect_hints(self.ctx, team, slot, item) if hints: notify_hints(self.ctx, team, hints) @@ -1891,11 +1921,22 @@ class ServerCommandProcessor(CommonCommandProcessor): seeked_player, usable, response = get_intended_text(player_name, self.ctx.player_names.values()) if usable: team, slot = self.ctx.player_name_lookup[seeked_player] - location_name = " ".join(location_name) - location_name, usable, response = get_intended_text(location_name, - self.ctx.location_names_for_game(self.ctx.games[slot])) + game = self.ctx.games[slot] + full_name = " ".join(location_name) + + if full_name.isnumeric(): + location, usable, response = int(full_name), True, None + elif self.ctx.location_names_for_game(game) is not None: + location, usable, response = get_intended_text(full_name, self.ctx.location_names_for_game(game)) + else: + self.output("Can't look up location for unknown game. Hint for ID instead.") + return False + if usable: - hints = collect_hint_location_name(self.ctx, team, slot, location_name) + if isinstance(location, int): + hints = collect_hint_location_id(self.ctx, team, slot, location) + else: + hints = collect_hint_location_name(self.ctx, team, slot, location) if hints: notify_hints(self.ctx, team, hints) else: From be1158ad7886e46ba3fec8c53b80604e4a105aa9 Mon Sep 17 00:00:00 2001 From: Fabian Dill Date: Wed, 21 Sep 2022 17:53:33 +0200 Subject: [PATCH 90/91] Windows: update VC Redistributable to 14.32.31332 from 14.29.30037 --- inno_setup.iss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inno_setup.iss b/inno_setup.iss index ff2da1211a..cfdfec7ba8 100644 --- a/inno_setup.iss +++ b/inno_setup.iss @@ -196,7 +196,7 @@ begin begin // Is the installed version at least the packaged one ? Log('VC Redist x64 Version : found ' + strVersion); - Result := (CompareStr(strVersion, 'v14.29.30037') < 0); + Result := (CompareStr(strVersion, 'v14.32.31332') < 0); end else begin From 813ee5ee3bb70f6c8fe30948d60b08594838e4bf Mon Sep 17 00:00:00 2001 From: CaitSith2 Date: Sat, 24 Sep 2022 02:43:00 -0700 Subject: [PATCH 91/91] Factorio: Add explicit support for factory-levels mod. (#1050) * Factorio: Add explicit support for factory-levels mod. * Fix inconsistent space/tabs --- worlds/factorio/Mod.py | 3 ++- worlds/factorio/data/mod/info.json | 7 ++++--- .../factorio/data/mod_template/data-final-fixes.lua | 12 ++++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/worlds/factorio/Mod.py b/worlds/factorio/Mod.py index 9889e58bf3..89666ffbdd 100644 --- a/worlds/factorio/Mod.py +++ b/worlds/factorio/Mod.py @@ -34,7 +34,8 @@ base_info = { "factorio_version": "1.1", "dependencies": [ "base >= 1.1.0", - "? science-not-invited" + "? science-not-invited", + "? factory-levels" ] } diff --git a/worlds/factorio/data/mod/info.json b/worlds/factorio/data/mod/info.json index b93686d060..70a9518344 100644 --- a/worlds/factorio/data/mod/info.json +++ b/worlds/factorio/data/mod/info.json @@ -7,7 +7,8 @@ "description": "Integration client for the Archipelago Randomizer", "factorio_version": "1.1", "dependencies": [ - "base >= 1.1.0", - "? science-not-invited" - ] + "base >= 1.1.0", + "? science-not-invited", + "? factory-levels" + ] } diff --git a/worlds/factorio/data/mod_template/data-final-fixes.lua b/worlds/factorio/data/mod_template/data-final-fixes.lua index cc813b2fff..70bc1eac0a 100644 --- a/worlds/factorio/data/mod_template/data-final-fixes.lua +++ b/worlds/factorio/data/mod_template/data-final-fixes.lua @@ -183,6 +183,18 @@ end data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories) data.raw["assembling-machine"]["assembling-machine-2"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories) data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes) +if mods["factory-levels"] then + -- Factory-Levels allows the assembling machines to get faster (and depending on settings), more productive at crafting products, the more the + -- assembling machine crafts the product. If the machine crafts enough, it may auto-upgrade to the next tier. + for i = 1, 25, 1 do + data.raw["assembling-machine"]["assembling-machine-1-level-" .. i].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories) + data.raw["assembling-machine"]["assembling-machine-1-level-" .. i].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes) + end + for i = 1, 50, 1 do + data.raw["assembling-machine"]["assembling-machine-2-level-" .. i].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories) + end +end + data.raw["ammo"]["artillery-shell"].stack_size = 10 {# each randomized tech gets set to be invisible, with new nodes added that trigger those #}